diff options
author | 2011-06-15 16:59:54 +0000 | |
---|---|---|
committer | 2011-06-15 16:59:54 +0000 | |
commit | 2590d96369d0217e31dc2812690dde61dac417b5 (patch) | |
tree | 82276f787b08a28548e342c7921486f1acefab9f /storage | |
parent | first commit (diff) | |
download | anaconda-2590d96369d0217e31dc2812690dde61dac417b5.tar.gz anaconda-2590d96369d0217e31dc2812690dde61dac417b5.tar.bz2 anaconda-2590d96369d0217e31dc2812690dde61dac417b5.zip |
Initial import from Sabayon (ver 0.9.9.56)
Diffstat (limited to 'storage')
35 files changed, 16911 insertions, 0 deletions
diff --git a/storage/Makefile.am b/storage/Makefile.am new file mode 100644 index 0000000..7347694 --- /dev/null +++ b/storage/Makefile.am @@ -0,0 +1,26 @@ +# storage/Makefile.am for anaconda +# +# Copyright (C) 2009 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Author: David Cantrell <dcantrell@redhat.com> + +pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME) +storagedir = $(pkgpyexecdir)/storage +storage_PYTHON = *.py + +SUBDIRS = devicelibs formats + +MAINTAINERCLEANFILES = Makefile.in diff --git a/storage/__init__.py b/storage/__init__.py new file mode 100644 index 0000000..5e35958 --- /dev/null +++ b/storage/__init__.py @@ -0,0 +1,2238 @@ +# __init__.py +# Entry point for anaconda's storage configuration module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +import os +import time +import stat +import errno +import sys +import statvfs + +import nss.nss +import parted + +import isys +import iutil +from constants import * +from pykickstart.constants import * +from flags import flags + +import storage_log +from errors import * +from devices import * +from devicetree import DeviceTree +from deviceaction import * +from formats import getFormat +from formats import get_device_format_class +from formats import get_default_filesystem_type +from devicelibs.lvm import safeLvmName +from devicelibs.dm import name_from_dm_node +from devicelibs.crypto import generateBackupPassphrase +from devicelibs.mpath import MultipathConfigWriter +from devicelibs.edd import get_edd_dict +from udev import * +import iscsi +import fcoe +import zfcp +import dasd + +import shelve +import contextlib + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + +def storageInitialize(anaconda): + storage = anaconda.storage + + storage.shutdown() + + if anaconda.dir == DISPATCH_BACK: + return + + # touch /dev/.in_sysinit so that /lib/udev/rules.d/65-md-incremental.rules + # does not mess with any mdraid sets + open("/dev/.in_sysinit", "w") + + # XXX I don't understand why I have to do this, but this is needed to + # populate the udev db + udev_trigger(subsystem="block", action="change") + + # Before we set up the storage system, we need to know which disks to + # ignore, etc. Luckily that's all in the kickstart data. + if anaconda.ksdata: + anaconda.storage.zeroMbr = anaconda.ksdata.zerombr.zerombr + anaconda.storage.ignoredDisks = anaconda.ksdata.ignoredisk.ignoredisk + anaconda.storage.exclusiveDisks = anaconda.ksdata.ignoredisk.onlyuse + + if anaconda.ksdata.clearpart.type is not None: + anaconda.storage.clearPartType = anaconda.ksdata.clearpart.type + anaconda.storage.clearPartDisks = anaconda.ksdata.clearpart.drives + if anaconda.ksdata.clearpart.initAll: + anaconda.storage.reinitializeDisks = anaconda.ksdata.clearpart.initAll + + anaconda.intf.resetInitializeDiskQuestion() + anaconda.intf.resetReinitInconsistentLVMQuestion() + + # Set up the protected partitions list now. + if anaconda.protected: + storage.protectedDevSpecs.extend(anaconda.protected) + storage.reset() + + if not flags.livecdInstall and not storage.protectedDevices: + if anaconda.upgrade: + return + else: + anaconda.intf.messageWindow(_("Unknown Device"), + _("The installation source given by device %s " + "could not be found. Please check your " + "parameters and try again.") % devspec, + type="custom", custom_buttons = [_("_Exit installer")]) + sys.exit(1) + else: + storage.reset() + +# dispatch.py helper function +def storageComplete(anaconda): + if anaconda.dir == DISPATCH_BACK: + rc = anaconda.intf.messageWindow(_("Installation cannot continue."), + _("The storage configuration you have " + "chosen has already been activated. You " + "can no longer return to the disk editing " + "screen. Would you like to continue with " + "the installation process?"), + type = "yesno") + if rc == 0: + sys.exit(0) + return DISPATCH_FORWARD + + devs = anaconda.storage.devicetree.getDevicesByType("luks/dm-crypt") + existing_luks = False + new_luks = False + for dev in devs: + if dev.exists: + existing_luks = True + else: + new_luks = True + + if (anaconda.storage.encryptedAutoPart or new_luks) and \ + not anaconda.storage.encryptionPassphrase: + while True: + (passphrase, retrofit) = anaconda.intf.getLuksPassphrase(preexist=existing_luks) + if passphrase: + anaconda.storage.encryptionPassphrase = passphrase + anaconda.storage.encryptionRetrofit = retrofit + break + else: + rc = anaconda.intf.messageWindow(_("Encrypt device?"), + _("You specified block device encryption " + "should be enabled, but you have not " + "supplied a passphrase. If you do not " + "go back and provide a passphrase, " + "block device encryption will be " + "disabled."), + type="custom", + custom_buttons=[_("Back"), _("Continue")], + default=0) + if rc == 1: + log.info("user elected to not encrypt any devices.") + undoEncryption(anaconda.storage) + anaconda.storage.encryptedAutoPart = False + break + + if anaconda.storage.encryptionPassphrase: + for dev in anaconda.storage.devices: + if dev.format.type == "luks" and not dev.format.exists: + dev.format.passphrase = anaconda.storage.encryptionPassphrase + + if anaconda.ksdata: + return + + rc = anaconda.intf.messageWindow(_("Writing storage configuration to disk"), + _("The partitioning options you have selected " + "will now be written to disk. Any " + "data on deleted or reformatted partitions " + "will be lost."), + type = "custom", custom_icon="warning", + custom_buttons=[_("Go _back"), + _("_Write changes to disk")], + default = 0) + + # Make sure that all is down, even the disks that we setup after popluate. + anaconda.storage.devicetree.teardownAll() + + if rc == 0: + return DISPATCH_BACK + +def writeEscrowPackets(anaconda): + escrowDevices = filter(lambda d: d.format.type == "luks" and \ + d.format.escrow_cert, + anaconda.storage.devices) + + if not escrowDevices: + return + + log.debug("escrow: writeEscrowPackets start") + + wait_win = anaconda.intf.waitWindow(_("Running..."), + _("Storing encryption keys")) + + nss.nss.nss_init_nodb() # Does nothing if NSS is already initialized + + backupPassphrase = generateBackupPassphrase() + try: + for device in escrowDevices: + log.debug("escrow: device %s: %s" % + (repr(device.path), repr(device.format.type))) + device.format.escrow(anaconda.rootPath + "/root", + backupPassphrase) + + wait_win.pop() + except (IOError, RuntimeError) as e: + wait_win.pop() + anaconda.intf.messageWindow(_("Error"), + _("Error storing an encryption key: " + "%s\n") % str(e), type="custom", + custom_icon="error", + custom_buttons=[_("_Exit installer")]) + sys.exit(1) + + log.debug("escrow: writeEscrowPackets done") + + +def undoEncryption(storage): + for device in storage.devicetree.getDevicesByType("luks/dm-crypt"): + if device.exists: + continue + + slave = device.slave + format = device.format + + # set any devices that depended on the luks device to now depend on + # the former slave device + for child in storage.devicetree.getChildren(device): + child.parents.remove(device) + device.removeChild() + child.parents.append(slave) + + storage.devicetree.registerAction(ActionDestroyFormat(device)) + storage.devicetree.registerAction(ActionDestroyDevice(device)) + storage.devicetree.registerAction(ActionDestroyFormat(slave)) + storage.devicetree.registerAction(ActionCreateFormat(slave, format)) + +class Storage(object): + def __init__(self, anaconda): + self.anaconda = anaconda + + # storage configuration variables + self.ignoredDisks = [] + self.exclusiveDisks = [] + self.doAutoPart = False + self.clearPartType = None + self.clearPartDisks = [] + self.encryptedAutoPart = False + self.encryptionPassphrase = None + self.escrowCertificates = {} + self.autoPartEscrowCert = None + self.autoPartAddBackupPassphrase = False + self.encryptionRetrofit = False + self.reinitializeDisks = False + self.zeroMbr = None + self.protectedDevSpecs = [] + self.autoPartitionRequests = [] + self.eddDict = {} + + self.__luksDevs = {} + + self.iscsi = iscsi.iscsi() + self.fcoe = fcoe.fcoe() + self.zfcp = zfcp.ZFCP() + self.dasd = dasd.DASD() + + self._nextID = 0 + self.defaultFSType = get_default_filesystem_type() + self.defaultBootFSType = get_default_filesystem_type(boot=True) + self._dumpFile = "/tmp/storage.state" + + # these will both be empty until our reset method gets called + self.devicetree = DeviceTree(intf=self.anaconda.intf, + ignored=self.ignoredDisks, + exclusive=self.exclusiveDisks, + type=self.clearPartType, + clear=self.clearPartDisks, + reinitializeDisks=self.reinitializeDisks, + protected=self.protectedDevSpecs, + zeroMbr=self.zeroMbr, + passphrase=self.encryptionPassphrase, + luksDict=self.__luksDevs, + iscsi=self.iscsi, + dasd=self.dasd) + self.fsset = FSSet(self.devicetree, self.anaconda.rootPath) + + def doIt(self): + self.devicetree.processActions() + self.doEncryptionPassphraseRetrofits() + + # now set the boot partition's flag + try: + boot = self.anaconda.platform.bootDevice() + if boot.type == "mdarray": + bootDevs = boot.parents + else: + bootDevs = [boot] + except DeviceError: + bootDevs = [] + else: + for dev in bootDevs: + if hasattr(dev, "bootable"): + # Dos labels can only have one partition marked as active + # and unmarking ie the windows partition is not a good idea + skip = False + if dev.disk.format.partedDisk.type == "msdos": + for p in dev.disk.format.partedDisk.partitions: + if p.type == parted.PARTITION_NORMAL and \ + p.getFlag(parted.PARTITION_BOOT): + skip = True + break + if skip: + log.info("not setting boot flag on %s as there is" + "another active partition" % dev.name) + continue + log.info("setting boot flag on %s" % dev.name) + dev.bootable = True + dev.disk.setup() + dev.disk.format.commitToDisk() + + self.dumpState("final") + + @property + def nextID(self): + id = self._nextID + self._nextID += 1 + return id + + def shutdown(self): + try: + self.devicetree.teardownAll() + except Exception as e: + log.error("failure tearing down device tree: %s" % e) + + self.zfcp.shutdown() + + # TODO: iscsi.shutdown() + + def reset(self): + """ Reset storage configuration to reflect actual system state. + + This should rescan from scratch but not clobber user-obtained + information like passphrases, iscsi config, &c + + """ + # save passphrases for luks devices so we don't have to reprompt + self.encryptionPassphrase = None + for device in self.devices: + if device.format.type == "luks" and device.format.exists: + self.__luksDevs[device.format.uuid] = device.format._LUKS__passphrase + + w = self.anaconda.intf.waitWindow(_("Finding Devices"), + _("Finding storage devices")) + self.iscsi.startup(self.anaconda.intf) + self.fcoe.startup(self.anaconda.intf) + self.zfcp.startup() + self.dasd.startup(intf=self.anaconda.intf, zeroMbr=self.zeroMbr) + if self.anaconda.upgrade: + clearPartType = CLEARPART_TYPE_NONE + else: + clearPartType = self.clearPartType + + self.devicetree = DeviceTree(intf=self.anaconda.intf, + ignored=self.ignoredDisks, + exclusive=self.exclusiveDisks, + type=clearPartType, + clear=self.clearPartDisks, + reinitializeDisks=self.reinitializeDisks, + protected=self.protectedDevSpecs, + zeroMbr=self.zeroMbr, + passphrase=self.encryptionPassphrase, + luksDict=self.__luksDevs, + iscsi=self.iscsi, + dasd=self.dasd) + self.devicetree.populate() + self.fsset = FSSet(self.devicetree, self.anaconda.rootPath) + self.eddDict = get_edd_dict(self.partitioned) + self.anaconda.rootParts = None + self.anaconda.upgradeRoot = None + self.dumpState("initial") + w.pop() + + @property + def devices(self): + """ A list of all the devices in the device tree. """ + devices = self.devicetree.devices + devices.sort(key=lambda d: d.name) + return devices + + @property + def disks(self): + """ A list of the disks in the device tree. + + Ignored disks are not included, as are disks with no media present. + + This is based on the current state of the device tree and + does not necessarily reflect the actual on-disk state of the + system's disks. + """ + disks = [] + for device in self.devicetree.devices: + if device.isDisk: + if not device.mediaPresent: + log.info("Skipping disk: %s: No media present" % device.name) + continue + disks.append(device) + disks.sort(key=lambda d: d.name, cmp=self.compareDisks) + return disks + + @property + def partitioned(self): + """ A list of the partitioned devices in the device tree. + + Ignored devices are not included, nor disks with no media present. + + Devices of types for which partitioning is not supported are also + not included. + + This is based on the current state of the device tree and + does not necessarily reflect the actual on-disk state of the + system's disks. + """ + partitioned = [] + for device in self.devicetree.devices: + if not device.partitioned: + continue + + if not device.mediaPresent: + log.info("Skipping device: %s: No media present" % device.name) + continue + + partitioned.append(device) + + partitioned.sort(key=lambda d: d.name) + return partitioned + + @property + def partitions(self): + """ A list of the partitions in the device tree. + + This is based on the current state of the device tree and + does not necessarily reflect the actual on-disk state of the + system's disks. + """ + partitions = self.devicetree.getDevicesByInstance(PartitionDevice) + partitions.sort(key=lambda d: d.name) + return partitions + + @property + def vgs(self): + """ A list of the LVM Volume Groups in the device tree. + + This is based on the current state of the device tree and + does not necessarily reflect the actual on-disk state of the + system's disks. + """ + vgs = self.devicetree.getDevicesByType("lvmvg") + vgs.sort(key=lambda d: d.name) + return vgs + + @property + def lvs(self): + """ A list of the LVM Logical Volumes in the device tree. + + This is based on the current state of the device tree and + does not necessarily reflect the actual on-disk state of the + system's disks. + """ + lvs = self.devicetree.getDevicesByType("lvmlv") + lvs.sort(key=lambda d: d.name) + return lvs + + @property + def pvs(self): + """ A list of the LVM Physical Volumes in the device tree. + + This is based on the current state of the device tree and + does not necessarily reflect the actual on-disk state of the + system's disks. + """ + devices = self.devicetree.devices + pvs = [d for d in devices if d.format.type == "lvmpv"] + pvs.sort(key=lambda d: d.name) + return pvs + + def unusedPVs(self, vg=None): + unused = [] + for pv in self.pvs: + used = False + for _vg in self.vgs: + if _vg.dependsOn(pv) and _vg != vg: + used = True + break + elif _vg == vg: + break + if not used: + unused.append(pv) + return unused + + @property + def mdarrays(self): + """ A list of the MD arrays in the device tree. + + This is based on the current state of the device tree and + does not necessarily reflect the actual on-disk state of the + system's disks. + """ + arrays = self.devicetree.getDevicesByType("mdarray") + arrays.sort(key=lambda d: d.name) + return arrays + + @property + def mdcontainers(self): + """ A list of the MD containers in the device tree. """ + arrays = self.devicetree.getDevicesByType("mdcontainer") + arrays.sort(key=lambda d: d.name) + return arrays + + @property + def mdmembers(self): + """ A list of the MD member devices in the device tree. + + This is based on the current state of the device tree and + does not necessarily reflect the actual on-disk state of the + system's disks. + """ + devices = self.devicetree.devices + members = [d for d in devices if d.format.type == "mdmember"] + members.sort(key=lambda d: d.name) + return members + + def unusedMDMembers(self, array=None): + unused = [] + for member in self.mdmembers: + used = False + for _array in self.mdarrays + self.mdcontainers: + if _array.dependsOn(member) and _array != array: + used = True + break + elif _array == array: + break + if not used: + unused.append(member) + return unused + + @property + def unusedMDMinors(self): + """ Return a list of unused minors for use in RAID. """ + raidMinors = range(0,32) + for array in self.mdarrays + self.mdcontainers: + if array.minor is not None and array.minor in raidMinors: + raidMinors.remove(array.minor) + return raidMinors + + @property + def swaps(self): + """ A list of the swap devices in the device tree. + + This is based on the current state of the device tree and + does not necessarily reflect the actual on-disk state of the + system's disks. + """ + devices = self.devicetree.devices + swaps = [d for d in devices if d.format.type == "swap"] + swaps.sort(key=lambda d: d.name) + return swaps + + @property + def protectedDevices(self): + devices = self.devicetree.devices + protected = [d for d in devices if d.protected] + protected.sort(key=lambda d: d.name) + return protected + + def exceptionDisks(self): + """ Return a list of removable devices to save exceptions to. + + FIXME: This raises the problem that the device tree can be + in a state that does not reflect that actual current + state of the system at any given point. + + We need a way to provide direct scanning of disks, + partitions, and filesystems without relying on the + larger objects' correctness. + + Also, we need to find devices that have just been made + available for the purpose of storing the exception + report. + """ + # When a usb is connected from before the start of the installation, + # it is not correctly detected. + udev_trigger(subsystem="block", action="change") + self.reset() + + dests = [] + + for disk in self.disks: + if not disk.removable and \ + disk.format is not None and \ + disk.format.mountable: + dests.append([disk.path, disk.name]) + + for part in self.partitions: + if not part.disk.removable: + continue + + elif part.partedPartition.active and \ + not part.partedPartition.getFlag(parted.PARTITION_RAID) and \ + not part.partedPartition.getFlag(parted.PARTITION_LVM) and \ + part.format is not None and part.format.mountable: + dests.append([part.path, part.name]) + + return dests + + def deviceImmutable(self, device, ignoreProtected=False): + """ Return any reason the device cannot be modified/removed. + + Return False if the device can be removed. + + Devices that cannot be removed include: + + - protected partitions + - devices that are part of an md array or lvm vg + - extended partition containing logical partitions that + meet any of the above criteria + + """ + if not isinstance(device, Device): + raise ValueError("arg1 (%s) must be a Device instance" % device) + + if not ignoreProtected and device.protected: + return _("This partition is holding the data for the hard " + "drive install.") + elif isinstance(device, PartitionDevice) and device.isProtected: + # LDL formatted DASDs always have one partition, you'd have to + # reformat the DASD in CDL mode to get rid of it + return _("You cannot delete a partition of a LDL formatted " + "DASD.") + elif device.format.type == "mdmember": + for array in self.mdarrays + self.mdcontainers: + if array.dependsOn(device): + if array.minor is not None: + return _("This device is part of the RAID " + "device %s.") % (array.path,) + else: + return _("This device is part of a RAID device.") + elif device.format.type == "lvmpv": + for vg in self.vgs: + if vg.dependsOn(device): + if vg.name is not None: + return _("This device is part of the LVM " + "volume group '%s'.") % (vg.name,) + else: + return _("This device is part of a LVM volume " + "group.") + elif device.format.type == "luks": + try: + luksdev = self.devicetree.getChildren(device)[0] + except IndexError: + pass + else: + return self.deviceImmutable(luksdev) + elif isinstance(device, PartitionDevice) and device.isExtended: + reasons = {} + for dep in self.deviceDeps(device): + reason = self.deviceImmutable(dep) + if reason: + reasons[dep.path] = reason + if reasons: + msg = _("This device is an extended partition which " + "contains logical partitions that cannot be " + "deleted:\n\n") + for dev in reasons: + msg += "%s: %s" % (dev, reasons[dev]) + return msg + + for i in self.devicetree.immutableDevices: + if i[0] == device.name: + return i[1] + + return False + + def deviceDeps(self, device): + return self.devicetree.getDependentDevices(device) + + def newPartition(self, *args, **kwargs): + """ Return a new PartitionDevice instance for configuring. """ + if kwargs.has_key("fmt_type"): + kwargs["format"] = getFormat(kwargs.pop("fmt_type"), + mountpoint=kwargs.pop("mountpoint", + None), + **kwargs.pop("fmt_args", {})) + + if kwargs.has_key("disks"): + parents = kwargs.pop("disks") + if isinstance(parents, Device): + kwargs["parents"] = [parents] + else: + kwargs["parents"] = parents + + if kwargs.has_key("name"): + name = kwargs.pop("name") + else: + name = "req%d" % self.nextID + + return PartitionDevice(name, *args, **kwargs) + + def newMDArray(self, *args, **kwargs): + """ Return a new MDRaidArrayDevice instance for configuring. """ + if kwargs.has_key("fmt_type"): + kwargs["format"] = getFormat(kwargs.pop("fmt_type"), + mountpoint=kwargs.pop("mountpoint", + None)) + + if kwargs.has_key("minor"): + kwargs["minor"] = int(kwargs["minor"]) + else: + kwargs["minor"] = self.unusedMDMinors[0] + + if kwargs.has_key("name"): + name = kwargs.pop("name") + else: + name = "md%d" % kwargs["minor"] + + return MDRaidArrayDevice(name, *args, **kwargs) + + def newVG(self, *args, **kwargs): + """ Return a new LVMVolumeGroupDevice instance. """ + pvs = kwargs.pop("pvs", []) + for pv in pvs: + if pv not in self.devices: + raise ValueError("pv is not in the device tree") + + if kwargs.has_key("name"): + name = kwargs.pop("name") + else: + name = self.createSuggestedVGName(self.anaconda.network) + + if name in [d.name for d in self.devices]: + raise ValueError("name already in use") + + return LVMVolumeGroupDevice(name, pvs, *args, **kwargs) + + def newLV(self, *args, **kwargs): + """ Return a new LVMLogicalVolumeDevice instance. """ + if kwargs.has_key("vg"): + vg = kwargs.pop("vg") + + mountpoint = kwargs.pop("mountpoint", None) + if kwargs.has_key("fmt_type"): + kwargs["format"] = getFormat(kwargs.pop("fmt_type"), + mountpoint=mountpoint) + + if kwargs.has_key("name"): + name = kwargs.pop("name") + else: + if kwargs.get("format") and kwargs["format"].type == "swap": + swap = True + else: + swap = False + name = self.createSuggestedLVName(vg, + swap=swap, + mountpoint=mountpoint) + + if name in [d.name for d in self.devices]: + raise ValueError("name already in use") + + return LVMLogicalVolumeDevice(name, vg, *args, **kwargs) + + def createDevice(self, device): + """ Schedule creation of a device. + + TODO: We could do some things here like assign the next + available raid minor if one isn't already set. + """ + self.devicetree.registerAction(ActionCreateDevice(device)) + if device.format.type: + self.devicetree.registerAction(ActionCreateFormat(device)) + + def destroyDevice(self, device): + """ Schedule destruction of a device. """ + if device.format.exists and device.format.type: + # schedule destruction of any formatting while we're at it + self.devicetree.registerAction(ActionDestroyFormat(device)) + + action = ActionDestroyDevice(device) + self.devicetree.registerAction(action) + + def formatDevice(self, device, format): + """ Schedule formatting of a device. """ + self.devicetree.registerAction(ActionDestroyFormat(device)) + self.devicetree.registerAction(ActionCreateFormat(device, format)) + + def formatByDefault(self, device): + """Return whether the device should be reformatted by default.""" + formatlist = ['/boot', '/var', '/tmp', '/usr'] + exceptlist = ['/home', '/usr/local', '/opt', '/var/www'] + + if not device.format.linuxNative: + return False + + if device.format.mountable: + if not device.format.mountpoint: + return False + + if device.format.mountpoint == "/" or \ + device.format.mountpoint in formatlist: + return True + + for p in formatlist: + if device.format.mountpoint.startswith(p): + for q in exceptlist: + if device.format.mountpoint.startswith(q): + return False + return True + elif device.format.type == "swap": + return True + + # be safe for anything else and default to off + return False + + def extendedPartitionsSupported(self): + """ Return whether any disks support extended partitions.""" + for disk in self.partitioned: + if disk.format.partedDisk.supportsFeature(parted.DISK_TYPE_EXTENDED): + return True + return False + + def createSuggestedVGName(self, network): + """ Return a reasonable, unused VG name. """ + # try to create a volume group name incorporating the hostname + hn = network.hostname + vgnames = [vg.name for vg in self.vgs] + if hn is not None and hn != '': + if hn == 'localhost' or hn == 'localhost.localdomain': + vgtemplate = "VolGroup" + elif hn.find('.') != -1: + template = "vg_%s" % (hn.split('.')[0].lower(),) + vgtemplate = safeLvmName(template) + else: + template = "vg_%s" % (hn.lower(),) + vgtemplate = safeLvmName(template) + else: + vgtemplate = "VolGroup" + + if vgtemplate not in vgnames and \ + vgtemplate not in lvm.lvm_vg_blacklist: + return vgtemplate + else: + i = 0 + while 1: + tmpname = "%s%02d" % (vgtemplate, i,) + if not tmpname in vgnames and \ + tmpname not in lvm.lvm_vg_blacklist: + break + + i += 1 + if i > 99: + tmpname = "" + + return tmpname + + def createSuggestedLVName(self, vg, swap=None, mountpoint=None): + """ Return a suitable, unused name for a new logical volume. """ + # FIXME: this is not at all guaranteed to work + if mountpoint: + # try to incorporate the mountpoint into the name + if mountpoint == '/': + lvtemplate = 'lv_root' + else: + if mountpoint.startswith("/"): + template = "lv_%s" % mountpoint[1:] + else: + template = "lv_%s" % (mountpoint,) + + lvtemplate = safeLvmName(template) + else: + if swap: + if len([s for s in self.swaps if s in vg.lvs]): + idx = len([s for s in self.swaps if s in vg.lvs]) + while True: + lvtemplate = "lv_swap%02d" % idx + if lvtemplate in [lv.lvname for lv in vg.lvs]: + idx += 1 + else: + break + else: + lvtemplate = "lv_swap" + else: + idx = len(vg.lvs) + while True: + lvtemplate = "LogVol%02d" % idx + if lvtemplate in [l.lvname for l in vg.lvs]: + idx += 1 + else: + break + + return lvtemplate + + def doEncryptionPassphraseRetrofits(self): + """ Add the global passphrase to all preexisting LUKS devices. + + This establishes a common passphrase for all encrypted devices + in the system so that users only have to enter one passphrase + during system boot. + """ + if not self.encryptionRetrofit: + return + + for device in self.devices: + if device.format.type == "luks" and \ + device.format._LUKS__passphrase != self.encryptionPassphrase: + log.info("adding new passphrase to preexisting encrypted " + "device %s" % device.path) + try: + device.format.addPassphrase(self.encryptionPassphrase) + except CryptoError: + log.error("failed to add new passphrase to existing " + "device %s" % device.path) + + def sanityCheck(self): + """ Run a series of tests to verify the storage configuration. + + This function is called at the end of partitioning so that + we can make sure you don't have anything silly (like no /, + a really small /, etc). Returns (errors, warnings) where + each is a list of strings. + """ + checkSizes = [('/usr', 250), ('/tmp', 50), ('/var', 384), + ('/home', 100), ('/boot', 75)] + warnings = [] + errors = [] + + mustbeonlinuxfs = ['/', '/var', '/tmp', '/usr', '/home', '/usr/share', '/usr/lib'] + mustbeonroot = ['/bin','/dev','/sbin','/etc','/lib','/root', '/mnt', 'lost+found', '/proc'] + + filesystems = self.mountpoints + root = self.fsset.rootDevice + swaps = self.fsset.swapDevices + try: + boot = self.anaconda.platform.bootDevice() + except DeviceError: + boot = None + + if not root: + errors.append(_("You have not defined a root partition (/), " + "which is required for installation of %s " + "to continue.") % (productName,)) + + if root and root.size < 250: + warnings.append(_("Your root partition is less than 250 " + "megabytes which is usually too small to " + "install %s.") % (productName,)) + + if (root and + root.size < self.anaconda.backend.getMinimumSizeMB("/")): + errors.append(_("Your / partition is less than %(min)s " + "MB which is lower than recommended " + "for a normal %(productName)s install.") + % {'min': self.anaconda.backend.getMinimumSizeMB("/"), + 'productName': productName}) + + # livecds have to have the rootfs type match up + if (root and + self.anaconda.backend.rootFsType and + root.format.type != self.anaconda.backend.rootFsType): + errors.append(_("Your / partition does not match the " + "the live image you are installing from. " + "It must be formatted as %s.") + % (self.anaconda.backend.rootFsType,)) + + for (mount, size) in checkSizes: + if mount in filesystems and filesystems[mount].size < size: + warnings.append(_("Your %(mount)s partition is less than " + "%(size)s megabytes which is lower than " + "recommended for a normal %(productName)s " + "install.") + % {'mount': mount, 'size': size, + 'productName': productName}) + + usb_disks = [] + firewire_disks = [] + for disk in self.disks: + if isys.driveUsesModule(disk.name, ["usb-storage", "ub"]): + usb_disks.append(disk) + elif isys.driveUsesModule(disk.name, ["sbp2", "firewire-sbp2"]): + firewire_disks.append(disk) + + uses_usb = False + uses_firewire = False + for device in filesystems.values(): + for disk in usb_disks: + if device.dependsOn(disk): + uses_usb = True + break + + for disk in firewire_disks: + if device.dependsOn(disk): + uses_firewire = True + break + + if uses_usb: + warnings.append(_("Installing on a USB device. This may " + "or may not produce a working system.")) + if uses_firewire: + warnings.append(_("Installing on a FireWire device. This may " + "or may not produce a working system.")) + + errors.extend(self.anaconda.platform.checkBootRequest(boot)) + + if not swaps: + if iutil.memInstalled() < isys.EARLY_SWAP_RAM: + errors.append(_("You have not specified a swap partition. " + "Due to the amount of memory present, a " + "swap partition is required to complete " + "installation.")) + else: + warnings.append(_("You have not specified a swap partition. " + "Although not strictly required in all cases, " + "it will significantly improve performance " + "for most installations.")) + + for (mountpoint, dev) in filesystems.items(): + if mountpoint in mustbeonroot: + errors.append(_("This mount point is invalid. The %s directory must " + "be on the / file system.") % mountpoint) + + if mountpoint in mustbeonlinuxfs and (not dev.format.mountable or not dev.format.linuxNative): + errors.append(_("The mount point %s must be on a linux file system.") % mountpoint) + + return (errors, warnings) + + def isProtected(self, device): + """ Return True is the device is protected. """ + return device.protected + + def checkNoDisks(self): + """Check that there are valid disk devices.""" + if not self.disks: + self.anaconda.intf.messageWindow(_("No Drives Found"), + _("An error has occurred - no valid devices were " + "found on which to create new file systems. " + "Please check your hardware for the cause " + "of this problem.")) + return True + return False + + def dumpState(self, suffix): + """ Dump the current device list to the storage shelf. """ + key = "devices.%d.%s" % (time.time(), suffix) + with contextlib.closing(shelve.open(self._dumpFile)) as shelf: + shelf[key] = [d.dict for d in self.devices] + + def write(self, instPath): + self.fsset.write(instPath) + self.iscsi.write(instPath, self.anaconda) + self.fcoe.write(instPath, self.anaconda) + self.zfcp.write(instPath) + self.dasd.write(instPath) + + def writeKS(self, f): + def useExisting(lst): + foundCreateDevice = False + foundCreateFormat = False + + for l in lst: + if isinstance(l, ActionCreateDevice): + foundCreateDevice = True + elif isinstance(l, ActionCreateFormat): + foundCreateFormat = True + + return (foundCreateFormat and not foundCreateDevice) + + log.warning("Storage.writeKS not completely implemented") + f.write("# The following is the partition information you requested\n") + f.write("# Note that any partitions you deleted are not expressed\n") + f.write("# here so unless you clear all partitions first, this is\n") + f.write("# not guaranteed to work\n") + + # clearpart + if self.clearPartType is None or self.clearPartType == CLEARPART_TYPE_NONE: + args = ["--none"] + elif self.clearPartType == CLEARPART_TYPE_LINUX: + args = ["--linux"] + else: + args = ["--all"] + + if self.clearPartDisks: + args += ["--drives=%s" % ",".join(self.clearPartDisks)] + if self.reinitializeDisks: + args += ["--initlabel"] + + f.write("#clearpart %s\n" % " ".join(args)) + + # ignoredisks + if self.ignoredDisks: + f.write("#ignoredisk --drives=%s\n" % ",".join(self.ignoredDisks)) + elif self.exclusiveDisks: + f.write("#ignoredisk --only-use=%s\n" % ",".join(self.exclusiveDisks)) + + # the various partitioning commands + dict = {} + actions = filter(lambda x: x.device.format.type != "luks", + self.devicetree.findActions(type="create")) + + for action in actions: + if dict.has_key(action.device.path): + dict[action.device.path].append(action) + else: + dict[action.device.path] = [action] + + for device in self.devices: + # If there's no action for the given device, it must be one + # we are reusing. + if not dict.has_key(device.path): + noformat = True + preexisting = True + else: + noformat = False + preexisting = useExisting(dict[device.path]) + + device.writeKS(f, preexisting=preexisting, noformat=noformat) + f.write("\n") + + self.iscsi.writeKS(f) + self.fcoe.writeKS(f) + self.zfcp.writeKS(f) + + def turnOnSwap(self, upgrading=None): + self.fsset.turnOnSwap(self.anaconda, upgrading=upgrading) + + def mountFilesystems(self, raiseErrors=None, readOnly=None, skipRoot=False): + self.fsset.mountFilesystems(self.anaconda, raiseErrors=raiseErrors, + readOnly=readOnly, skipRoot=skipRoot) + + def umountFilesystems(self, ignoreErrors=True, swapoff=True): + self.fsset.umountFilesystems(ignoreErrors=ignoreErrors, swapoff=swapoff) + + def parseFSTab(self): + self.fsset.parseFSTab() + + def mkDevRoot(self): + self.fsset.mkDevRoot() + + def createSwapFile(self, device, size): + self.fsset.createSwapFile(device, size) + + @property + def fsFreeSpace(self): + return self.fsset.fsFreeSpace() + + @property + def mtab(self): + return self.fsset.mtab() + + @property + def mountpoints(self): + return self.fsset.mountpoints + + @property + def migratableDevices(self): + return self.fsset.migratableDevices + + @property + def rootDevice(self): + return self.fsset.rootDevice + + def compareDisks(self, first, second): + if self.eddDict.has_key(first) and self.eddDict.has_key(second): + one = self.eddDict[first] + two = self.eddDict[second] + if (one < two): + return -1 + elif (one > two): + return 1 + + # if one is in the BIOS and the other not prefer the one in the BIOS + if self.eddDict.has_key(first): + return -1 + if self.eddDict.has_key(second): + return 1 + + if first.startswith("hd"): + type1 = 0 + elif first.startswith("sd"): + type1 = 1 + elif (first.startswith("vd") or first.startswith("xvd")): + type1 = -1 + else: + type1 = 2 + + if second.startswith("hd"): + type2 = 0 + elif second.startswith("sd"): + type2 = 1 + elif (second.startswith("vd") or second.startswith("xvd")): + type2 = -1 + else: + type2 = 2 + + if (type1 < type2): + return -1 + elif (type1 > type2): + return 1 + else: + len1 = len(first) + len2 = len(second) + + if (len1 < len2): + return -1 + elif (len1 > len2): + return 1 + else: + if (first < second): + return -1 + elif (first > second): + return 1 + + return 0 + +def getReleaseString(mountpoint): + relName = None + relVer = None + + filename = "%s/etc/system-release" % mountpoint + if os.access(filename, os.R_OK): + with open(filename) as f: + relstr = f.readline().strip() + relName = ' '.join(relstr.split(" ")[:2]) + relVer = relstr.split()[-1] + + return (relName, relVer) + +def findExistingRootDevices(anaconda, upgradeany=False): + """ Return a list of all root filesystems in the device tree. """ + rootDevs = [] + + if not os.path.exists(anaconda.rootPath): + iutil.mkdirChain(anaconda.rootPath) + + roots = [] + for device in anaconda.storage.devicetree.leaves: + if not device.format.linuxNative or not device.format.mountable: + continue + + if device.protected: + # can't upgrade the part holding hd: media so why look at it? + continue + + try: + device.setup() + except Exception as e: + log.warning("setup of %s failed: %s" % (device.name, e)) + continue + + try: + device.format.mount(options="ro", mountpoint=anaconda.rootPath) + except Exception as e: + log.warning("mount of %s as %s failed: %s" % (device.name, + device.format.type, + e)) + device.teardown() + continue + + if os.access(anaconda.rootPath + "/etc/fstab", os.R_OK): + (product, version) = getReleaseString(anaconda.rootPath) + if upgradeany or \ + anaconda.instClass.productUpgradable(product, version): + rootDevs.append((device, "%s %s" % (product, version))) + else: + log.info("product %s version %s found on %s is not upgradable" + % (product, version, device.name)) + + # this handles unmounting the filesystem + device.teardown(recursive=True) + + return rootDevs + +def mountExistingSystem(anaconda, rootEnt, + allowDirty=None, warnDirty=None, + readOnly=None): + """ Mount filesystems specified in rootDevice's /etc/fstab file. """ + rootDevice = rootEnt[0] + rootPath = anaconda.rootPath + fsset = anaconda.storage.fsset + if readOnly: + readOnly = "ro" + else: + readOnly = "" + + if rootDevice.protected and os.path.ismount("/mnt/isodir"): + isys.mount("/mnt/isodir", + rootPath, + fstype=rootDevice.format.type, + bindMount=True) + else: + rootDevice.setup() + rootDevice.format.mount(chroot=rootPath, + mountpoint="/", + options=readOnly) + + fsset.parseFSTab() + + # check for dirty filesystems + dirtyDevs = [] + for device in fsset.devices: + if not hasattr(device.format, "isDirty"): + continue + + try: + device.setup() + except DeviceError as e: + # we'll catch this in the main loop + continue + + if device.format.isDirty: + log.info("%s contains a dirty %s filesystem" % (device.path, + device.format.type)) + dirtyDevs.append(device.path) + + messageWindow = anaconda.intf.messageWindow + if not allowDirty and dirtyDevs: + messageWindow(_("Dirty File Systems"), + _("The following file systems for your Linux system " + "were not unmounted cleanly. Please boot your " + "Linux installation, let the file systems be " + "checked and shut down cleanly to upgrade.\n" + "%s") % "\n".join(dirtyDevs)) + anaconda.storage.devicetree.teardownAll() + sys.exit(0) + elif warnDirty and dirtyDevs: + rc = messageWindow(_("Dirty File Systems"), + _("The following file systems for your Linux " + "system were not unmounted cleanly. Would " + "you like to mount them anyway?\n" + "%s") % "\n".join(dirtyDevs), + type = "yesno") + if rc == 0: + return -1 + + fsset.mountFilesystems(anaconda, readOnly=readOnly, skipRoot=True) + + +class BlkidTab(object): + """ Dictionary-like interface to blkid.tab with device path keys """ + def __init__(self, chroot=""): + self.chroot = chroot + self.devices = {} + + def parse(self): + path = "%s/etc/blkid/blkid.tab" % self.chroot + log.debug("parsing %s" % path) + with open(path) as f: + for line in f.readlines(): + # this is pretty ugly, but an XML parser is more work than + # is justifiable for this purpose + if not line.startswith("<device "): + continue + + line = line[len("<device "):-len("</device>\n")] + (data, sep, device) = line.partition(">") + if not device: + continue + + self.devices[device] = {} + for pair in data.split(): + try: + (key, value) = pair.split("=") + except ValueError: + continue + + self.devices[device][key] = value[1:-1] # strip off quotes + + def __getitem__(self, key): + return self.devices[key] + + def get(self, key, default=None): + return self.devices.get(key, default) + + +class CryptTab(object): + """ Dictionary-like interface to crypttab entries with map name keys """ + def __init__(self, devicetree, blkidTab=None, chroot=""): + self.devicetree = devicetree + self.blkidTab = blkidTab + self.chroot = chroot + self.mappings = {} + + def parse(self, chroot=""): + """ Parse /etc/crypttab from an existing installation. """ + if not chroot or not os.path.isdir(chroot): + chroot = "" + + path = "%s/etc/crypttab" % chroot + log.debug("parsing %s" % path) + with open(path) as f: + if not self.blkidTab: + try: + self.blkidTab = BlkidTab(chroot=chroot) + self.blkidTab.parse() + except Exception: + self.blkidTab = None + + for line in f.readlines(): + (line, pound, comment) = line.partition("#") + fields = line.split() + if not 2 <= len(fields) <= 4: + continue + elif len(fields) == 2: + fields.extend(['none', '']) + elif len(fields) == 3: + fields.append('') + + (name, devspec, keyfile, options) = fields + + # resolve devspec to a device in the tree + device = self.devicetree.resolveDevice(devspec, + blkidTab=self.blkidTab) + if device: + self.mappings[name] = {"device": device, + "keyfile": keyfile, + "options": options} + + def populate(self): + """ Populate the instance based on the device tree's contents. """ + for device in self.devicetree.devices: + # XXX should we put them all in there or just the ones that + # are part of a device containing swap or a filesystem? + # + # Put them all in here -- we can filter from FSSet + if device.format.type != "luks": + continue + + key_file = device.format.keyFile + if not key_file: + key_file = "none" + + options = device.format.options + if not options: + options = "" + + self.mappings[device.format.mapName] = {"device": device, + "keyfile": key_file, + "options": options} + + def crypttab(self): + """ Write out /etc/crypttab """ + crypttab = "" + for name in self.mappings: + entry = self[name] + crypttab += "%s UUID=%s %s %s\n" % (name, + entry['device'].format.uuid, + entry['keyfile'], + entry['options']) + return crypttab + + def __getitem__(self, key): + return self.mappings[key] + + def get(self, key, default=None): + return self.mappings.get(key, default) + +def get_containing_device(path, devicetree): + """ Return the device that a path resides on. """ + if not os.path.exists(path): + return None + + st = os.stat(path) + major = os.major(st.st_dev) + minor = os.minor(st.st_dev) + link = "/sys/dev/block/%s:%s" % (major, minor) + if not os.path.exists(link): + return None + + try: + device_name = os.path.basename(os.readlink(link)) + except Exception: + return None + + if device_name.startswith("dm-"): + # have I told you lately that I love you, device-mapper? + device_name = name_from_dm_node(device_name) + + return devicetree.getDeviceByName(device_name) + + +class FSSet(object): + """ A class to represent a set of filesystems. """ + def __init__(self, devicetree, rootpath): + self.devicetree = devicetree + self.rootpath = rootpath + self.cryptTab = None + self.blkidTab = None + self.origFStab = None + self.active = False + self._dev = None + self._devpts = None + self._sysfs = None + self._proc = None + self._devshm = None + self.preserveLines = [] # lines we just ignore and preserve + + @property + def sysfs(self): + if not self._sysfs: + self._sysfs = NoDevice(format=getFormat("sysfs", + device="sys", + mountpoint="/sys")) + return self._sysfs + + @property + def dev(self): + if not self._dev: + self._dev = DirectoryDevice("/dev", format=getFormat("bind", + device="/dev", + mountpoint="/dev", + exists=True), + exists=True) + + return self._dev + + @property + def devpts(self): + if not self._devpts: + self._devpts = NoDevice(format=getFormat("devpts", + device="devpts", + mountpoint="/dev/pts")) + return self._devpts + + @property + def proc(self): + if not self._proc: + self._proc = NoDevice(format=getFormat("proc", + device="proc", + mountpoint="/proc")) + return self._proc + + @property + def devshm(self): + if not self._devshm: + self._devshm = NoDevice(format=getFormat("tmpfs", + device="tmpfs", + mountpoint="/dev/shm")) + return self._devshm + + @property + def devices(self): + return sorted(self.devicetree.devices, key=lambda d: d.path) + + @property + def mountpoints(self): + filesystems = {} + for device in self.devices: + if device.format.mountable and device.format.mountpoint: + filesystems[device.format.mountpoint] = device + return filesystems + + def _parseOneLine(self, (devspec, mountpoint, fstype, options, dump, passno)): + # find device in the tree + device = self.devicetree.resolveDevice(devspec, + cryptTab=self.cryptTab, + blkidTab=self.blkidTab) + if device: + # fall through to the bottom of this block + pass + elif devspec.startswith("/dev/loop"): + # FIXME: create devices.LoopDevice + log.warning("completely ignoring your loop mount") + elif ":" in devspec and fstype.startswith("nfs"): + # NFS -- preserve but otherwise ignore + device = NFSDevice(devspec, + format=getFormat(fstype, + device=devspec)) + elif devspec.startswith("/") and fstype == "swap": + # swap file + device = FileDevice(devspec, + parents=get_containing_device(devspec, self.devicetree), + format=getFormat(fstype, + device=devspec, + exists=True), + exists=True) + elif fstype == "bind" or "bind" in options: + # bind mount... set fstype so later comparison won't + # turn up false positives + fstype = "bind" + + # This is probably not going to do anything useful, so we'll + # make sure to try again from FSSet.mountFilesystems. The bind + # mount targets should be accessible by the time we try to do + # the bind mount from there. + parents = get_containing_device(devspec, self.devicetree) + device = DirectoryDevice(devspec, parents=parents, exists=True) + device.format = getFormat("bind", + device=device.path, + exists=True) + elif mountpoint in ("/proc", "/sys", "/dev/shm", "/dev/pts"): + # drop these now -- we'll recreate later + return None + else: + # nodev filesystem -- preserve or drop completely? + format = getFormat(fstype) + if devspec == "none" or \ + isinstance(format, get_device_format_class("nodev")): + device = NoDevice(format=format) + else: + device = StorageDevice(devspec, format=format) + + if device is None: + log.error("failed to resolve %s (%s) from fstab" % (devspec, + fstype)) + raise UnrecognizedFSTabEntryError() + + if device.format.type is None: + log.info("Unrecognized filesystem type for %s (%s)" + % (device.name, fstype)) + raise UnrecognizedFSTabEntryError() + + # make sure, if we're using a device from the tree, that + # the device's format we found matches what's in the fstab + fmt = getFormat(fstype, device=device.path) + if fmt.type != device.format.type: + raise StorageError("scanned format (%s) differs from fstab " + "format (%s)" % (device.format.type, fstype)) + + if device.format.mountable: + device.format.mountpoint = mountpoint + device.format.mountopts = options + + # is this useful? + try: + device.format.options = options + except AttributeError: + pass + + return device + + def parseFSTab(self, chroot=None): + """ parse /etc/fstab + + preconditions: + all storage devices have been scanned, including filesystems + postconditions: + + FIXME: control which exceptions we raise + + XXX do we care about bind mounts? + how about nodev mounts? + loop mounts? + """ + if not chroot or not os.path.isdir(chroot): + chroot = self.rootpath + + path = "%s/etc/fstab" % chroot + if not os.access(path, os.R_OK): + # XXX should we raise an exception instead? + log.info("cannot open %s for read" % path) + return + + blkidTab = BlkidTab(chroot=chroot) + try: + blkidTab.parse() + log.debug("blkid.tab devs: %s" % blkidTab.devices.keys()) + except Exception as e: + log.info("error parsing blkid.tab: %s" % e) + blkidTab = None + + cryptTab = CryptTab(self.devicetree, blkidTab=blkidTab, chroot=chroot) + try: + cryptTab.parse(chroot=chroot) + log.debug("crypttab maps: %s" % cryptTab.mappings.keys()) + except Exception as e: + log.info("error parsing crypttab: %s" % e) + cryptTab = None + + self.blkidTab = blkidTab + self.cryptTab = cryptTab + + with open(path) as f: + log.debug("parsing %s" % path) + + lines = f.readlines() + + # save the original file + self.origFStab = ''.join(lines) + + for line in lines: + # strip off comments + (line, pound, comment) = line.partition("#") + fields = line.split() + + if not 4 <= len(fields) <= 6: + continue + elif len(fields) == 4: + fields.extend([0, 0]) + elif len(fields) == 5: + fields.append(0) + + (devspec, mountpoint, fstype, options, dump, passno) = fields + + try: + device = self._parseOneLine((devspec, mountpoint, fstype, options, dump, passno)) + except UnrecognizedFSTabEntryError: + # just write the line back out as-is after upgrade + self.preserveLines.append(line) + continue + except Exception as e: + raise Exception("fstab entry %s is malformed: %s" % (devspec, e)) + + if not device: + continue + + if device not in self.devicetree.devices: + try: + self.devicetree._addDevice(device) + except ValueError: + # just write duplicates back out post-install + self.preserveLines.append(line) + + def fsFreeSpace(self, chroot=None): + if not chroot: + chroot = self.rootpath + + space = [] + for device in self.devices: + if not device.format.mountable or \ + not device.format.mountpoint or \ + not device.format.status: + continue + + path = "%s/%s" % (chroot, device.format.mountpoint) + + ST_RDONLY = 1 # this should be in python's posix module + if os.statvfs(path)[statvfs.F_FLAG] & ST_RDONLY: + continue + + try: + space.append((device.format.mountpoint, + isys.pathSpaceAvailable(path))) + except SystemError: + log.error("failed to calculate free space for %s" % (device.format.mountpoint,)) + + space.sort(key=lambda s: s[1]) + return space + + def mtab(self): + format = "%s %s %s %s 0 0\n" + mtab = "" + devices = self.mountpoints.values() + self.swapDevices + devices.extend([self.devshm, self.devpts, self.sysfs, self.proc]) + devices.sort(key=lambda d: getattr(d.format, "mountpoint", None)) + for device in devices: + if not device.format.status: + continue + if not device.format.mountable: + continue + if device.format.mountpoint: + options = device.format.mountopts + if options: + options = options.replace("defaults,", "") + options = options.replace("defaults", "") + + if options: + options = "rw," + options + else: + options = "rw" + mtab = mtab + format % (device.path, + device.format.mountpoint, + device.format.type, + options) + return mtab + + def turnOnSwap(self, anaconda, upgrading=None): + def swapErrorDialog(msg, device): + if not anaconda.intf: + sys.exit(0) + + buttons = [_("Skip"), _("Format"), _("_Exit")] + ret = anaconda.intf.messageWindow(_("Error"), msg, type="custom", + custom_buttons=buttons, + custom_icon="warning") + + if ret == 0: + self.devicetree._removeDevice(device) + return False + elif ret == 1: + device.format.create(force=True) + return True + else: + sys.exit(0) + + for device in self.swapDevices: + if isinstance(device, FileDevice): + # set up FileDevices' parents now that they are accessible + targetDir = "%s/%s" % (anaconda.rootPath, device.path) + parent = get_containing_device(targetDir, self.devicetree) + if not parent: + log.error("cannot determine which device contains " + "directory %s" % device.path) + device.parents = [] + self.devicetree._removeDevice(device) + continue + else: + device.parents = [parent] + + while True: + try: + device.setup() + device.format.setup() + except OldSwapError: + msg = _("The swap device:\n\n %s\n\n" + "is an old-style Linux swap partition. If " + "you want to use this device for swap space, " + "you must reformat as a new-style Linux swap " + "partition.") \ + % device.path + + if swapErrorDialog(msg, device): + continue + except SuspendError: + if upgrading: + msg = _("The swap device:\n\n %s\n\n" + "in your /etc/fstab file is currently in " + "use as a software suspend device, " + "which means your system is hibernating. " + "To perform an upgrade, please shut down " + "your system rather than hibernating it.") \ + % device.path + else: + msg = _("The swap device:\n\n %s\n\n" + "in your /etc/fstab file is currently in " + "use as a software suspend device, " + "which means your system is hibernating. " + "If you are performing a new install, " + "make sure the installer is set " + "to format all swap devices.") \ + % device.path + + if swapErrorDialog(msg, device): + continue + except UnknownSwapError: + msg = _("The swap device:\n\n %s\n\n" + "does not contain a supported swap volume. In " + "order to continue installation, you will need " + "to format the device or skip it.") \ + % device.path + + if swapErrorDialog(msg, device): + continue + except DeviceError as (msg, name): + if anaconda.intf: + if upgrading: + err = _("Error enabling swap device %(name)s: " + "%(msg)s\n\n" + "The /etc/fstab on your upgrade partition " + "does not reference a valid swap " + "device.\n\nPress OK to exit the " + "installer") % {'name': name, 'msg': msg} + else: + err = _("Error enabling swap device %(name)s: " + "%(msg)s\n\n" + "This most likely means this swap " + "device has not been initialized.\n\n" + "Press OK to exit the installer.") % \ + {'name': name, 'msg': msg} + anaconda.intf.messageWindow(_("Error"), err) + sys.exit(0) + + break + + def mountFilesystems(self, anaconda, raiseErrors=None, readOnly=None, + skipRoot=False): + intf = anaconda.intf + devices = self.mountpoints.values() + self.swapDevices + devices.extend([self.dev, self.devshm, self.devpts, self.sysfs, self.proc]) + devices.sort(key=lambda d: getattr(d.format, "mountpoint", None)) + + for device in devices: + if not device.format.mountable or not device.format.mountpoint: + continue + + if skipRoot and device.format.mountpoint == "/": + continue + + options = device.format.options + if "noauto" in options.split(","): + continue + + if device.format.type == "bind" and device != self.dev: + # set up the DirectoryDevice's parents now that they are + # accessible + # + # -- bind formats' device and mountpoint are always both + # under the chroot. no exceptions. none, damn it. + targetDir = "%s/%s" % (anaconda.rootPath, device.path) + parent = get_containing_device(targetDir, self.devicetree) + if not parent: + log.error("cannot determine which device contains " + "directory %s" % device.path) + device.parents = [] + self.devicetree._removeDevice(device) + continue + else: + device.parents = [parent] + + try: + device.setup() + except Exception as msg: + # FIXME: need an error popup + continue + + if readOnly: + options = "%s,%s" % (options, readOnly) + + try: + device.format.setup(options=options, + chroot=anaconda.rootPath) + except OSError as e: + log.error("OSError: (%d) %s" % (e.errno, e.strerror)) + + if intf: + if e.errno == errno.EEXIST: + intf.messageWindow(_("Invalid mount point"), + _("An error occurred when trying " + "to create %s. Some element of " + "this path is not a directory. " + "This is a fatal error and the " + "install cannot continue.\n\n" + "Press <Enter> to exit the " + "installer.") + % (device.format.mountpoint,)) + else: + na = {'mountpoint': device.format.mountpoint, + 'msg': e.strerror} + intf.messageWindow(_("Invalid mount point"), + _("An error occurred when trying " + "to create %(mountpoint)s: " + "%(msg)s. This is " + "a fatal error and the install " + "cannot continue.\n\n" + "Press <Enter> to exit the " + "installer.") % na) + sys.exit(0) + except SystemError as (num, msg): + log.error("SystemError: (%d) %s" % (num, msg) ) + + if raiseErrors: + raise + if intf and not device.format.linuxNative: + na = {'path': device.path, + 'mountpoint': device.format.mountpoint} + ret = intf.messageWindow(_("Unable to mount filesystem"), + _("An error occurred mounting " + "device %(path)s as " + "%(mountpoint)s. You may " + "continue installation, but " + "there may be problems.") % na, + type="custom", + custom_icon="warning", + custom_buttons=[_("_Exit installer"), + _("_Continue")]) + + if ret == 0: + sys.exit(0) + else: + continue + + sys.exit(0) + except FSError as msg: + log.error("FSError: %s" % msg) + + if intf: + na = {'path': device.path, + 'mountpoint': device.format.mountpoint, + 'msg': msg} + intf.messageWindow(_("Unable to mount filesystem"), + _("An error occurred mounting " + "device %(path)s as %(mountpoint)s: " + "%(msg)s. This is " + "a fatal error and the install " + "cannot continue.\n\n" + "Press <Enter> to exit the " + "installer.") % na) + sys.exit(0) + + self.active = True + + def umountFilesystems(self, ignoreErrors=True, swapoff=True): + devices = self.mountpoints.values() + self.swapDevices + devices.extend([self.dev, self.devshm, self.devpts, self.sysfs, self.proc]) + devices.sort(key=lambda d: getattr(d.format, "mountpoint", None)) + devices.reverse() + for device in devices: + if not device.format.mountable and \ + (device.format.type != "swap" or swapoff): + continue + + device.format.teardown() + device.teardown() + + self.active = False + + def createSwapFile(self, device, size, rootPath=None): + """ Create and activate a swap file under rootPath. """ + if not rootPath: + rootPath = self.rootpath + + filename = "/SWAP" + count = 0 + basedir = os.path.normpath("%s/%s" % (rootPath, + device.format.mountpoint)) + while os.path.exists("%s/%s" % (basedir, filename)) or \ + self.devicetree.getDeviceByName(filename): + file = os.path.normpath("%s/%s" % (basedir, filename)) + count += 1 + filename = "/SWAP-%d" % count + + dev = FileDevice(filename, + size=size, + parents=[device], + format=getFormat("swap", device=filename)) + dev.create() + dev.setup() + dev.format.create() + dev.format.setup() + # nasty, nasty + self.devicetree._addDevice(dev) + + def mkDevRoot(self, instPath=None): + if not instPath: + instPath = self.rootpath + + root = self.rootDevice + if root is None: + # cannot create root device ! + return + dev = "%s/%s" % (instPath, root.path) + if not os.path.exists("%s/dev/root" %(instPath,)) and os.path.exists(dev): + rdev = os.stat(dev).st_rdev + os.mknod("%s/dev/root" % (instPath,), stat.S_IFBLK | 0600, rdev) + + @property + def swapDevices(self): + swaps = [] + for device in self.devices: + if device.format.type == "swap": + swaps.append(device) + return swaps + + @property + def rootDevice(self): + for path in ["/", self.rootpath]: + for device in self.devices: + try: + mountpoint = device.format.mountpoint + except AttributeError: + mountpoint = None + + if mountpoint == path: + return device + + @property + def migratableDevices(self): + """ List of devices whose filesystems can be migrated. """ + migratable = [] + for device in self.devices: + if device.format.migratable and device.format.exists: + migratable.append(device) + + return migratable + + def write(self, instPath=None): + """ write out all config files based on the set of filesystems """ + if not instPath: + instPath = self.rootpath + + # /etc/fstab + fstab_path = os.path.normpath("%s/etc/fstab" % instPath) + fstab = self.fstab() + open(fstab_path, "w").write(fstab) + + # /etc/crypttab + crypttab_path = os.path.normpath("%s/etc/crypttab" % instPath) + crypttab = self.crypttab() + open(crypttab_path, "w").write(crypttab) + + # /etc/mdadm.conf + mdadm_path = os.path.normpath("%s/etc/mdadm.conf" % instPath) + mdadm_conf = self.mdadmConf() + if mdadm_conf: + with open(mdadm_path, "w") as md_f: + md_f.write(mdadm_conf) + + # /etc/multipath.conf + multipath_path = os.path.normpath("%s/etc/multipath.conf" % instPath) + multipath_conf = self.multipathConf() + if multipath_conf: + with open(multipath_path, "w") as mp_f: + mp_f.write(multipath_conf) + + def crypttab(self): + # if we are upgrading, do we want to update crypttab? + # gut reaction says no, but plymouth needs the names to be very + # specific for passphrase prompting + if not self.cryptTab: + self.cryptTab = CryptTab(self.devicetree) + self.cryptTab.populate() + + devices = self.mountpoints.values() + self.swapDevices + + # prune crypttab -- only mappings required by one or more entries + for name in self.cryptTab.mappings.keys(): + keep = False + mapInfo = self.cryptTab[name] + cryptoDev = mapInfo['device'] + for device in devices: + if device == cryptoDev or device.dependsOn(cryptoDev): + keep = True + break + + if not keep: + del self.cryptTab.mappings[name] + + return self.cryptTab.crypttab() + + def mdadmConf(self): + """ Return the contents of mdadm.conf. """ + arrays = self.devicetree.getDevicesByType("mdarray") + arrays.extend(self.devicetree.getDevicesByType("mdbiosraidarray")) + arrays.extend(self.devicetree.getDevicesByType("mdcontainer")) + # Sort it, this not only looks nicer, but this will also put + # containers (which get md0, md1, etc.) before their members + # (which get md127, md126, etc.). and lame as it is mdadm will not + # assemble the whole stack in one go unless listed in the proper order + # in mdadm.conf + arrays.sort(key=lambda d: d.path) + + conf = "# mdadm.conf written out by anaconda\n" + conf += "MAILADDR root\n" + conf += "AUTO +imsm +1.x -all\n" + devices = self.mountpoints.values() + self.swapDevices + for array in arrays: + for device in devices: + if device == array or device.dependsOn(array): + conf += array.mdadmConfEntry + break + + return conf + + def multipathConf(self): + """ Return the contents of multipath.conf. """ + mpaths = self.devicetree.getDevicesByType("dm-multipath") + if not mpaths: + return None + mpaths.sort(key=lambda d: d.name) + config = MultipathConfigWriter() + whitelist = [] + for mpath in mpaths: + config.addMultipathDevice(mpath) + whitelist.append(mpath.name) + whitelist.extend([d.name for d in mpath.parents]) + + # blacklist everything we're not using and let the + # sysadmin sort it out. + for d in self.devicetree.devices: + if not d.name in whitelist: + config.addBlacklistDevice(d) + + return config.write() + + def fstab (self): + format = "%-23s %-23s %-7s %-15s %d %d\n" + fstab = """ +# +# /etc/fstab +# Created by anaconda on %s +# +# Accessible filesystems, by reference, are maintained under '/dev/disk' +# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info +# +""" % time.asctime() + + devices = sorted(self.mountpoints.values(), + key=lambda d: d.format.mountpoint) + devices += self.swapDevices + devices.extend([self.devshm, self.devpts, self.sysfs, self.proc]) + netdevs = self.devicetree.getDevicesByInstance(NetworkStorageDevice) + for device in devices: + # why the hell do we put swap in the fstab, anyway? + if not device.format.mountable and device.format.type != "swap": + continue + + # Don't write out lines for optical devices, either. + if isinstance(device, OpticalDevice): + continue + + fstype = getattr(device.format, "mountType", device.format.type) + if fstype == "swap": + mountpoint = "swap" + options = device.format.options + else: + mountpoint = device.format.mountpoint + options = device.format.options + if not mountpoint: + log.warning("%s filesystem on %s has no mountpoint" % \ + (fstype, + device.path)) + continue + + options = options or "defaults" + for netdev in netdevs: + if device.dependsOn(netdev): + options = options + ",_netdev" + break + devspec = device.fstabSpec + dump = device.format.dump + if device.format.check and mountpoint == "/": + passno = 1 + elif device.format.check: + passno = 2 + else: + passno = 0 + fstab = fstab + device.fstabComment + fstab = fstab + format % (devspec, mountpoint, fstype, + options, dump, passno) + + # now, write out any lines we were unable to process because of + # unrecognized filesystems or unresolveable device specifications + for line in self.preserveLines: + fstab += line + + return fstab diff --git a/storage/dasd.py b/storage/dasd.py new file mode 100644 index 0000000..2052843 --- /dev/null +++ b/storage/dasd.py @@ -0,0 +1,220 @@ +# +# dasd.py - DASD class +# +# Copyright (C) 2009 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. +# +# Red Hat Author(s): David Cantrell <dcantrell@redhat.com> +# + +import iutil +import sys +import os +from storage.devices import deviceNameToDiskByPath +from constants import * +from flags import flags + +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) + +def getDasdPorts(): + """ Return comma delimited string of valid DASD ports. """ + ports = [] + + f = open("/proc/dasd/devices", "r") + lines = map(lambda x: x.strip(), f.readlines()) + f.close() + + for line in lines: + if "unknown" in line: + continue + + if "(FBA )" in line or "(ECKD)" in line: + ports.append(line.split('(')[0]) + + return ','.join(ports) + +class DASD: + """ Controlling class for DASD interaction before the storage code in + anaconda has initialized. + + The DASD class can determine if any DASD devices on the system are + unformatted and can perform a dasdfmt on them. + """ + + def __init__(self): + self._dasdlist = [] + self._devices = [] # list of DASDDevice objects + self._totalCylinders = 0 + self._completedCylinders = 0.0 + self._maxFormatJobs = 0 + self.started = False + + def startup(self, *args, **kwargs): + """ Look for any unformatted DASDs in the system and offer the user + the option for format them with dasdfmt or exit the installer. + """ + if self.started: + return + + self.started = True + + if not iutil.isS390(): + return + + intf = kwargs.get("intf") + zeroMbr = kwargs.get("zeroMbr") + + log.info("Checking for unformatted DASD devices:") + + for device in os.listdir("/sys/block"): + if not device.startswith("dasd"): + continue + + statusfile = "/sys/block/%s/device/status" % (device,) + if not os.path.isfile(statusfile): + continue + + f = open(statusfile, "r") + status = f.read().strip() + f.close() + + if status == "unformatted": + log.info(" %s is an unformatted DASD" % (device,)) + self._dasdlist.append(device) + + if not len(self._dasdlist): + log.info(" no unformatted DASD devices found") + return + + askUser = True + + if zeroMbr: + askUser = False + elif not intf and not zeroMbr: + log.info(" non-interactive kickstart install without zerombr " + "command, unable to run dasdfmt, exiting installer") + sys.exit(0) + + tmplist = map(lambda s: "/dev/" + s, self._dasdlist) + self._dasdlist = map(lambda s: deviceNameToDiskByPath(s), tmplist) + c = len(self._dasdlist) + + if intf and askUser: + title = P_("Unformatted DASD Device Found", + "Unformatted DASD Devices Found", c) + msg = P_("Format uninitialized DASD device?\n\n" + "There is %d uninitialized DASD device on this " + "system. To continue installation, the device must " + "be formatted. Formatting will remove any data on " + "this device." % c, + "Format uninitialized DASD devices?\n\n" + "There are %d uninitialized DASD devices on this " + "system. To continue installation, the devices must " + "be formatted. Formatting will remove any data on " + "these devices." % c, + c) + + devs = '' + for dasd in self._dasdlist: + devs += "%s\n" % (dasd,) + + icon = "/usr/share/icons/gnome/32x32/status/dialog-error.png" + buttons = [_("_Format"), _("_Exit installer")] + rc = intf.detailedMessageWindow(title, msg, devs.strip(), + type="custom", + custom_icon=icon, + custom_buttons=buttons) + if rc == 1: + log.info(" not running dasdfmt, exiting installer") + sys.exit(0) + + argv = ["-y", "-P", "-d", "cdl", "-b", "4096"] + + if intf: + title = P_("Formatting DASD Device", "Formatting DASD Devices", c) + msg = P_("Preparing %d DASD device for use with Linux..." % c, + "Preparing %d DASD devices for use with Linux..." % c, c) + pw = intf.progressWindow(title, msg, 1.0) + + for dasd in self._dasdlist: + log.info("Running dasdfmt on %s" % (dasd,)) + iutil.execWithCallback("/sbin/dasdfmt", argv + [dasd], + stdout="/dev/tty5", stderr="/dev/tty5", + callback=self._updateProgressWindow, + callback_data=pw, echo=False) + + pw.pop() + else: + for dasd in self._dasdlist: + log.info("Running dasdfmt on %s" % (dasd,)) + iutil.execWithRedirect("/sbin/dasdfmt", argv + [dasd], + stdout="/dev/tty5", stderr="/dev/tty5") + + def addDASD(self, dasd): + """ Adds a DASDDevice to the internal list of DASDs. """ + if dasd: + self._devices.append(dasd) + + def write(self, instPath): + """ Write /etc/dasd.conf to target system for all DASD devices + configured during installation. + """ + if self._devices == []: + return + + f = open(os.path.realpath(instPath + "/etc/dasd.conf"), "w") + for dasd in self._devices: + fields = [dasd.busid] + dasd.getOpts() + f.write("%s\n" % (" ".join(fields),)) + f.close() + + def _updateProgressWindow(self, data, callback_data=None): + """ Reads progress output from dasdfmt and collects the number of + cylinders completed so the progress window can update. + """ + if not callback_data: + return + + if data == '\n': + # each newline we see in this output means one more cylinder done + self._completedCylinders += 1.0 + callback_data.set(self._completedCylinders / self.totalCylinders) + + @property + def totalCylinders(self): + """ Total number of cylinders of all unformatted DASD devices. """ + if self._totalCylinders: + return self._totalCylinders + + argv = ["-t", "-v", "-y", "-d", "cdl", "-b", "4096"] + for dasd in self._dasdlist: + buf = iutil.execWithCapture("/sbin/dasdfmt", argv + [dasd], + stderr="/dev/tty5") + for line in buf.splitlines(): + if line.startswith("Drive Geometry: "): + # line will look like this: + # Drive Geometry: 3339 Cylinders * 15 Heads = 50085 Tracks + cyls = long(filter(lambda s: s, line.split(' '))[2]) + self._totalCylinders += cyls + break + + return self._totalCylinders + +# vim:tw=78:ts=4:et:sw=4 diff --git a/storage/deviceaction.py b/storage/deviceaction.py new file mode 100644 index 0000000..4524e7c --- /dev/null +++ b/storage/deviceaction.py @@ -0,0 +1,376 @@ +# deviceaction.py +# Device modification action classes for anaconda's storage configuration +# module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +from udev import * + +from devices import StorageDevice, PartitionDevice +from formats import getFormat +from errors import * +from parted import partitionFlag, PARTITION_LBA + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + + +# The values are just hints as to the ordering. +# Eg: fsmod and devmod ordering depends on the mod (shrink -v- grow) +ACTION_TYPE_NONE = 0 +ACTION_TYPE_DESTROY = 1000 +ACTION_TYPE_RESIZE = 500 +ACTION_TYPE_MIGRATE = 250 +ACTION_TYPE_CREATE = 100 + +action_strings = {ACTION_TYPE_NONE: "None", + ACTION_TYPE_DESTROY: "Destroy", + ACTION_TYPE_RESIZE: "Resize", + ACTION_TYPE_MIGRATE: "Migrate", + ACTION_TYPE_CREATE: "Create"} + +ACTION_OBJECT_NONE = 0 +ACTION_OBJECT_FORMAT = 1 +ACTION_OBJECT_DEVICE = 2 + +object_strings = {ACTION_OBJECT_NONE: "None", + ACTION_OBJECT_FORMAT: "Format", + ACTION_OBJECT_DEVICE: "Device"} + +RESIZE_SHRINK = 88 +RESIZE_GROW = 89 + +resize_strings = {RESIZE_SHRINK: "Shrink", + RESIZE_GROW: "Grow"} + +def action_type_from_string(type_string): + if type_string is None: + return None + + for (k,v) in action_strings.items(): + if v.lower() == type_string.lower(): + return k + + return resize_type_from_string(type_string) + +def action_object_from_string(type_string): + if type_string is None: + return None + + for (k,v) in object_strings.items(): + if v.lower() == type_string.lower(): + return k + +def resize_type_from_string(type_string): + if type_string is None: + return None + + for (k,v) in resize_strings.items(): + if v.lower() == type_string.lower(): + return k + +class DeviceAction(object): + """ An action that will be carried out in the future on a Device. + + These classes represent actions to be performed on devices or + filesystems. + + The operand Device instance will be modified according to the + action, but no changes will be made to the underlying device or + filesystem until the DeviceAction instance's execute method is + called. The DeviceAction instance's cancel method should reverse + any modifications made to the Device instance's attributes. + + If the Device instance represents a pre-existing device, the + constructor should call any methods or set any attributes that the + action will eventually change. Device/DeviceFormat classes should verify + that the requested modifications are reasonable and raise an + exception if not. + + Only one action of any given type/object pair can exist for any + given device at any given time. This is enforced by the + DeviceTree. + + Basic usage: + + a = DeviceAction(dev) + a.execute() + + OR + + a = DeviceAction(dev) + a.cancel() + + + XXX should we back up the device with a deep copy for forcibly + cancelling actions? + + The downside is that we lose any checking or verification that + would get done when resetting the Device instance's attributes to + their original values. + + The upside is that we would be guaranteed to achieve a total + reversal. No chance of, eg: resizes ending up altering Device + size due to rounding or other miscalculation. +""" + type = ACTION_TYPE_NONE + obj = ACTION_OBJECT_NONE + + def __init__(self, device): + if not isinstance(device, StorageDevice): + raise ValueError("arg 1 must be a StorageDevice instance") + self.device = device + + + def execute(self, intf=None): + """ perform the action """ + pass + + def cancel(self): + """ cancel the action """ + pass + + def isDestroy(self): + return self.type == ACTION_TYPE_DESTROY + + def isCreate(self): + return self.type == ACTION_TYPE_CREATE + + def isMigrate(self): + return self.type == ACTION_TYPE_MIGRATE + + def isResize(self): + return self.type == ACTION_TYPE_RESIZE + + def isShrink(self): + return (self.type == ACTION_TYPE_RESIZE and self.dir == RESIZE_SHRINK) + + def isGrow(self): + return (self.type == ACTION_TYPE_RESIZE and self.dir == RESIZE_GROW) + + def isDevice(self): + return self.obj == ACTION_OBJECT_DEVICE + + def isFormat(self): + return self.obj == ACTION_OBJECT_FORMAT + + @property + def format(self): + return self.device.format + + def __str__(self): + s = "%s %s" % (action_strings[self.type], object_strings[self.obj]) + if self.isResize(): + s += " (%s)" % resize_strings[self.dir] + if self.isFormat(): + s += " %s on" % self.format.type + if self.isMigrate(): + s += " to %s" % self.format.migrationTarget + s += " %s %s (id %d)" % (self.device.type, self.device.name, + self.device.id) + return s + +class ActionCreateDevice(DeviceAction): + """ Action representing the creation of a new device. """ + type = ACTION_TYPE_CREATE + obj = ACTION_OBJECT_DEVICE + + def __init__(self, device): + # FIXME: assert device.fs is None + DeviceAction.__init__(self, device) + + def execute(self, intf=None): + self.device.create(intf=intf) + + +class ActionDestroyDevice(DeviceAction): + """ An action representing the deletion of an existing device. """ + type = ACTION_TYPE_DESTROY + obj = ACTION_OBJECT_DEVICE + + def __init__(self, device): + # XXX should we insist that device.fs be None? + DeviceAction.__init__(self, device) + if device.exists: + device.teardown() + + def execute(self, intf=None): + self.device.destroy() + + # Make sure libparted does not keep cached info for this device + # and returns it when we create a new device with the same name + if self.device.partedDevice: + self.device.partedDevice.removeFromCache() + + +class ActionResizeDevice(DeviceAction): + """ An action representing the resizing of an existing device. """ + type = ACTION_TYPE_RESIZE + obj = ACTION_OBJECT_DEVICE + + def __init__(self, device, newsize): + if device.currentSize == newsize: + raise ValueError("new size same as old size") + + if not device.resizable: + raise ValueError("device is not resizable") + + DeviceAction.__init__(self, device) + if newsize > device.currentSize: + self.dir = RESIZE_GROW + else: + self.dir = RESIZE_SHRINK + self.origsize = device.targetSize + self.device.targetSize = newsize + + def execute(self, intf=None): + self.device.resize(intf=intf) + + def cancel(self): + self.device.targetSize = self.origsize + + +class ActionCreateFormat(DeviceAction): + """ An action representing creation of a new filesystem. """ + type = ACTION_TYPE_CREATE + obj = ACTION_OBJECT_FORMAT + + def __init__(self, device, format=None): + DeviceAction.__init__(self, device) + if format: + self.origFormat = device.format + if self.device.format.exists: + self.device.format.teardown() + self.device.format = format + else: + self.origFormat = getFormat(None) + + def execute(self, intf=None): + self.device.setup() + + if isinstance(self.device, PartitionDevice): + for flag in partitionFlag.keys(): + # Keep the LBA flag on pre-existing partitions + if flag in [ PARTITION_LBA, self.format.partedFlag ]: + continue + self.device.unsetFlag(flag) + + if self.format.partedFlag is not None: + self.device.setFlag(self.format.partedFlag) + + if self.format.partedSystem is not None: + self.device.partedPartition.system = self.format.partedSystem + + self.device.disk.format.commitToDisk() + + self.device.format.create(intf=intf, + device=self.device.path, + options=self.device.formatArgs) + # Get the UUID now that the format is created + udev_settle() + self.device.updateSysfsPath() + info = udev_get_block_device(self.device.sysfsPath) + self.device.format.uuid = udev_device_get_uuid(info) + + def cancel(self): + self.device.format = self.origFormat + + +class ActionDestroyFormat(DeviceAction): + """ An action representing the removal of an existing filesystem. + + XXX this seems unnecessary + """ + type = ACTION_TYPE_DESTROY + obj = ACTION_OBJECT_FORMAT + + def __init__(self, device): + DeviceAction.__init__(self, device) + self.origFormat = self.device.format + if device.format.exists: + device.format.teardown() + self.device.format = None + + def execute(self, intf=None): + """ wipe the filesystem signature from the device """ + if self.origFormat: + self.device.setup(orig=True) + self.origFormat.destroy() + udev_settle() + self.device.teardown() + + def cancel(self): + self.device.format = self.origFormat + + @property + def format(self): + return self.origFormat + + +class ActionResizeFormat(DeviceAction): + """ An action representing the resizing of an existing filesystem. + + XXX Do we even want to support resizing of a filesystem without + also resizing the device it resides on? + """ + type = ACTION_TYPE_RESIZE + obj = ACTION_OBJECT_FORMAT + + def __init__(self, device, newsize): + if device.format.targetSize == newsize: + raise ValueError("new size same as old size") + + DeviceAction.__init__(self, device) + if newsize > device.format.currentSize: + self.dir = RESIZE_GROW + else: + self.dir = RESIZE_SHRINK + self.origSize = self.device.format.targetSize + self.device.format.targetSize = newsize + + def execute(self, intf=None): + self.device.setup(orig=True) + self.device.format.doResize(intf=intf) + + def cancel(self): + self.device.format.targetSize = self.origSize + +class ActionMigrateFormat(DeviceAction): + """ An action representing the migration of an existing filesystem. """ + type = ACTION_TYPE_MIGRATE + obj = ACTION_OBJECT_FORMAT + + def __init__(self, device): + if not device.format.migratable or not device.format.exists: + raise ValueError("device format is not migratable") + + DeviceAction.__init__(self, device) + self.device.format.migrate = True + + def execute(self, intf=None): + self.device.setup(orig=True) + self.device.format.doMigrate(intf=intf) + + def cancel(self): + self.device.format.migrate = False + diff --git a/storage/devicelibs/Makefile.am b/storage/devicelibs/Makefile.am new file mode 100644 index 0000000..86a7d5e --- /dev/null +++ b/storage/devicelibs/Makefile.am @@ -0,0 +1,24 @@ +# storage/devicelibs/Makefile.am for anaconda +# +# Copyright (C) 2009 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Author: David Cantrell <dcantrell@redhat.com> + +pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME) +storagedevicelibsdir = $(pkgpyexecdir)/storage/devicelibs +storagedevicelibs_PYTHON = *.py + +MAINTAINERCLEANFILES = Makefile.in diff --git a/storage/devicelibs/__init__.py b/storage/devicelibs/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/storage/devicelibs/__init__.py diff --git a/storage/devicelibs/crypto.py b/storage/devicelibs/crypto.py new file mode 100644 index 0000000..136435d --- /dev/null +++ b/storage/devicelibs/crypto.py @@ -0,0 +1,193 @@ +# +# crypto.py +# +# Copyright (C) 2009 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. +# +# Author(s): Dave Lehman <dlehman@redhat.com> +# Martin Sivak <msivak@redhat.com> +# + +import os +from pycryptsetup import CryptSetup +import iutil + +from ..errors import * + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +# Keep the character set size a power of two to make sure all characters are +# equally likely +GENERATED_PASSPHRASE_CHARSET = ("0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "./") +# 20 chars * 6 bits per char = 120 "bits of security" +GENERATED_PASSPHRASE_LENGTH = 20 + +def generateBackupPassphrase(): + rnd = os.urandom(GENERATED_PASSPHRASE_LENGTH) + cs = GENERATED_PASSPHRASE_CHARSET + raw = "".join([cs[ord(c) % len(cs)] for c in rnd]) + + # Make the result easier to read + parts = [] + for i in xrange(0, len(raw), 5): + parts.append(raw[i : i + 5]) + return "-".join(parts) + +def askyes(question): + return True + +def dolog(priority, text): + pass + +def is_luks(device): + cs = CryptSetup(yesDialog = askyes, logFunc = dolog) + return cs.isLuks(device) + +def luks_uuid(device): + cs = CryptSetup(yesDialog = askyes, logFunc = dolog) + return cs.luksUUID(device).strip() + +def luks_status(name): + """True means active, False means inactive (or non-existent)""" + cs = CryptSetup(yesDialog = askyes, logFunc = dolog) + return cs.luksStatus(name)!=0 + +def luks_format(device, + passphrase=None, key_file=None, + cipher=None, key_size=None): + cs = CryptSetup(yesDialog = askyes, logFunc = dolog) + key_file_unlink = False + + if passphrase: + key_file = cs.prepare_passphrase_file(passphrase) + key_file_unlink = True + elif key_file and os.path.isfile(key_file): + pass + else: + raise ValueError("luks_format requires either a passphrase or a key file") + + #None is not considered as default value and pycryptsetup doesn't accept it + #so we need to filter out all Nones + kwargs = {} + kwargs["device"] = device + if cipher: kwargs["cipher"] = cipher + if key_file: kwargs["keyfile"] = key_file + if key_size: kwargs["keysize"] = key_size + + rc = cs.luksFormat(**kwargs) + if key_file_unlink: os.unlink(key_file) + + if rc: + raise CryptoError("luks_format failed for '%s'" % device) + +def luks_open(device, name, passphrase=None, key_file=None): + cs = CryptSetup(yesDialog = askyes, logFunc = dolog) + key_file_unlink = False + + if passphrase: + key_file = cs.prepare_passphrase_file(passphrase) + key_file_unlink = True + elif key_file and os.path.isfile(key_file): + pass + else: + raise ValueError("luks_open requires either a passphrase or a key file") + + rc = cs.luksOpen(device = device, name = name, keyfile = key_file) + if key_file_unlink: os.unlink(key_file) + if rc: + raise CryptoError("luks_open failed for %s (%s)" % (device, name)) + +def luks_close(name): + cs = CryptSetup(yesDialog = askyes, logFunc = dolog) + rc = cs.luksClose(name) + if rc: + raise CryptoError("luks_close failed for %s" % name) + +def luks_add_key(device, + new_passphrase=None, new_key_file=None, + passphrase=None, key_file=None): + + params = ["-q"] + + p = os.pipe() + if passphrase: + os.write(p[1], "%s\n" % passphrase) + elif key_file and os.path.isfile(key_file): + params.extend(["--key-file", key_file]) + else: + raise CryptoError("luks_add_key requires either a passphrase or a key file") + + params.extend(["luksAddKey", device]) + + if new_passphrase: + os.write(p[1], "%s\n" % new_passphrase) + elif new_key_file and os.path.isfile(new_key_file): + params.append("%s" % new_key_file) + else: + raise CryptoError("luks_add_key requires either a passphrase or a key file to add") + + os.close(p[1]) + + rc = iutil.execWithRedirect("cryptsetup", params, + stdin = p[0], + stdout = "/dev/tty5", + stderr = "/dev/tty5") + + os.close(p[0]) + if rc: + raise CryptoError("luks add key failed with errcode %d" % (rc,)) + +def luks_remove_key(device, + del_passphrase=None, del_key_file=None, + passphrase=None, key_file=None): + + params = [] + + p = os.pipe() + if del_passphrase: #the first question is about the key we want to remove + os.write(p[1], "%s\n" % del_passphrase) + + if passphrase: + os.write(p[1], "%s\n" % passphrase) + elif key_file and os.path.isfile(key_file): + params.extend(["--key-file", key_file]) + else: + raise CryptoError("luks_remove_key requires either a passphrase or a key file") + + params.extend(["luksRemoveKey", device]) + + if del_passphrase: + pass + elif del_key_file and os.path.isfile(del_key_file): + params.append("%s" % del_key_file) + else: + raise CryptoError("luks_remove_key requires either a passphrase or a key file to remove") + + os.close(p[1]) + + rc = iutil.execWithRedirect("cryptsetup", params, + stdin = p[0], + stdout = "/dev/tty5", + stderr = "/dev/tty5") + + os.close(p[0]) + if rc: + raise CryptoError("luks_remove_key failed with errcode %d" % (rc,)) + + diff --git a/storage/devicelibs/dm.py b/storage/devicelibs/dm.py new file mode 100644 index 0000000..02745e0 --- /dev/null +++ b/storage/devicelibs/dm.py @@ -0,0 +1,130 @@ +# +# dm.py +# device-mapper functions +# +# Copyright (C) 2009 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. +# +# Author(s): Dave Lehman <dlehman@redhat.com> +# + +import os + +import block +import iutil +from ..errors import * + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + +def name_from_dm_node(dm_node): + name = block.getNameFromDmNode(dm_node) + if name is not None: + return name + + st = os.stat("/dev/%s" % dm_node) + major = os.major(st.st_rdev) + minor = os.minor(st.st_rdev) + name = iutil.execWithCapture("dmsetup", + ["info", "--columns", + "--noheadings", "-o", "name", + "-j", str(major), "-m", str(minor)], + stderr="/dev/tty5") + log.debug("name_from_dm(%s) returning '%s'" % (dm_node, name.strip())) + return name.strip() + +def dm_node_from_name(map_name): + dm_node = block.getDmNodeFromName(map_name) + if dm_node is not None: + return dm_node + + devnum = iutil.execWithCapture("dmsetup", + ["info", "--columns", + "--noheadings", + "-o", "devno", + map_name], + stderr="/dev/tty5") + (major, sep, minor) = devnum.strip().partition(":") + if not sep: + raise DMError("dm device does not exist") + + dm_node = "dm-%d" % int(minor) + log.debug("dm_node_from_name(%s) returning '%s'" % (map_name, dm_node)) + return dm_node + +def dm_is_multipath(info): + major = None + minor = None + + if info.has_key('MAJOR'): + major = info['MAJOR'] + elif info.has_key('DM_MAJOR'): + major = info['DM_MAJOR'] + if info.has_key('MINOR'): + minor = info['MINOR'] + elif info.has_key('DM_MINOR'): + minor = info['DM_MINOR'] + + if major is None or minor is None: + return False + + for map in block.dm.maps(): + dev = map.dev + if dev.major == int(major) and dev.minor == int(minor): + for table in map.table: + if table.type == 'multipath': + return True + +def _get_backing_devnums_from_map(map_name): + ret = [] + buf = iutil.execWithCapture("dmsetup", + ["info", "--columns", + "--noheadings", + "-o", "devnos_used", + map_name], + stderr="/dev/tty5") + dev_nums = buf.split() + for dev_num in dev_nums: + (major, colon, minor) = dev_num.partition(":") + ret.append((int(major), int(minor))) + + return ret + +def get_backing_devnums(dm_node): + #dm_node = dm_node_from_name(map_name) + if not dm_node: + return None + + top_dir = "/sys/block" + backing_devs = os.listdir("%s/%s/slaves/" % (top_dir, dm_node)) + dev_nums = [] + for backing_dev in backing_devs: + dev_num = open("%s/%s/dev" % (top_dir, backing_dev)).read().strip() + (_major, _minor) = dev_num.split(":") + dev_nums.append((int(_major), int(_minor))) + + return dev_nums + +def get_backing_devs_from_name(map_name): + dm_node = dm_node_from_name(map_name) + if not dm_node: + return None + + slave_devs = os.listdir("/sys/block/virtual/%s" % dm_node) + return slave_devs + diff --git a/storage/devicelibs/edd.py b/storage/devicelibs/edd.py new file mode 100644 index 0000000..da03914 --- /dev/null +++ b/storage/devicelibs/edd.py @@ -0,0 +1,97 @@ +# +# edd.py +# BIOS EDD data parsing functions +# +# Copyright (C) 2010 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. +# +# Author(s): Hans de Goede <hdegoede@redhat.com> +# + +import os +import struct + +import logging +log = logging.getLogger("storage") + +def get_edd_dict(devices): + """Given an array of devices return a dict with the BIOS ID for them.""" + edd_dict = {} + + for biosdev in range(80, 80 + 15): + sysfspath = "/sys/firmware/edd/int13_dev%d" % biosdev + if not os.path.exists(sysfspath): + break # We are done + + sysfspath = "/sys/firmware/edd/int13_dev%d/mbr_signature" % biosdev + if not os.path.exists(sysfspath): + log.warning("No mbrsig for biosdev: %d" % biosdev) + continue + + try: + file = open(sysfspath, "r") + eddsig = file.read() + file.close() + except (IOError, OSError) as e: + log.warning("Error reading EDD mbrsig for %d: %s" % + (biosdev, str(e))) + continue + + sysfspath = "/sys/firmware/edd/int13_dev%d/sectors" % biosdev + try: + file = open(sysfspath, "r") + eddsize = file.read() + file.close() + except (IOError, OSError) as e: + eddsize = None + + found = [] + for dev in devices: + try: + fd = os.open(dev.path, os.O_RDONLY) + os.lseek(fd, 440, 0) + mbrsig = struct.unpack('I', os.read(fd, 4)) + os.close(fd) + except OSError as e: + log.warning("Error reading mbrsig from disk %s: %s" % + (dev.name, str(e))) + continue + + mbrsigStr = "0x%08x\n" % mbrsig + if mbrsigStr == eddsig: + if eddsize: + sysfspath = "/sys%s/size" % dev.sysfsPath + try: + file = open(sysfspath, "r") + size = file.read() + file.close() + except (IOError, OSError) as e: + log.warning("Error getting size for: %s" % dev.name) + continue + if eddsize != size: + continue + found.append(dev.name) + + if not found: + log.error("No matching mbr signature found for biosdev %d" % + biosdev) + elif len(found) > 1: + log.error("Multiple signature matches found for biosdev %d: %s" % + (biosdev, str(found))) + else: + log.info("Found %s for biosdev %d" %(found[0], biosdev)) + edd_dict[found[0]] = biosdev + + return edd_dict diff --git a/storage/devicelibs/lvm.py b/storage/devicelibs/lvm.py new file mode 100644 index 0000000..fd74f56 --- /dev/null +++ b/storage/devicelibs/lvm.py @@ -0,0 +1,419 @@ +# +# lvm.py +# lvm functions +# +# Copyright (C) 2009 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. +# +# Author(s): Dave Lehman <dlehman@redhat.com> +# + +import os +import math +import re + +import iutil + +from ..errors import * +from constants import * + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +MAX_LV_SLOTS = 256 + +def has_lvm(): + has_lvm = False + for path in os.environ["PATH"].split(":"): + if os.access("%s/lvm" % path, os.X_OK): + has_lvm = True + break + + if has_lvm: + has_lvm = False + for line in open("/proc/devices").readlines(): + if "device-mapper" in line.split(): + has_lvm = True + break + + return has_lvm + +# Start config_args handling code +# +# Theoretically we can handle all that can be handled with the LVM --config +# argument. For every time we call an lvm_cc (lvm compose config) funciton +# we regenerate the config_args with all global info. +config_args = [] # Holds the final argument list +config_args_data = { "filterRejects": [], # regular expressions to reject. + "filterAccepts": [] } # regexp to accept + +def _composeConfig(): + """lvm command accepts lvm.conf type arguments preceded by --config. """ + global config_args, config_args_data + config_args = [] + + filter_string = "" + rejects = config_args_data["filterRejects"] + # we don't need the accept for now. + # accepts = config_args_data["filterAccepts"] + # if len(accepts) > 0: + # for i in range(len(rejects)): + # filter_string = filter_string + ("\"a|%s|\", " % accpets[i]) + + if len(rejects) > 0: + for i in range(len(rejects)): + filter_string = filter_string + ("\"r|%s|\"," % rejects[i]) + + + filter_string = " filter=[%s] " % filter_string.strip(",") + + # As we add config strings we should check them all. + if filter_string == "": + # Nothing was really done. + return + + # devices_string can have (inside the brackets) "dir", "scan", + # "preferred_names", "filter", "cache_dir", "write_cache_state", + # "types", "sysfs_scan", "md_component_detection". see man lvm.conf. + devices_string = " devices {%s} " % (filter_string) # strings can be added + config_string = devices_string # more strings can be added. + config_args = ["--config", config_string] + +def lvm_cc_addFilterRejectRegexp(regexp): + """ Add a regular expression to the --config string.""" + global config_args_data + config_args_data["filterRejects"].append(regexp) + + # compoes config once more. + _composeConfig() + +def lvm_cc_resetFilter(): + global config_args, config_args_data + config_args_data["filterRejects"] = [] + config_args_data["filterAccepts"] = [] + config_args = [] +# End config_args handling code. + +# Names that should not be used int the creation of VGs +lvm_vg_blacklist = [] +def blacklistVG(name): + global lvm_vg_blacklist + lvm_vg_blacklist.append(name) + +def getPossiblePhysicalExtents(floor=0): + """Returns a list of integers representing the possible values for + the physical extent of a volume group. Value is in KB. + + floor - size (in KB) of smallest PE we care about. + """ + + possiblePE = [] + curpe = 8 + while curpe <= 16384*1024: + if curpe >= floor: + possiblePE.append(curpe) + curpe = curpe * 2 + + return possiblePE + +def getMaxLVSize(): + """ Return the maximum size (in MB) of a logical volume. """ + if iutil.getArch() in ("x86_64", "ppc64", "alpha", "ia64", "s390", "sparc"): #64bit architectures + return (8*1024*1024*1024*1024) #Max is 8EiB (very large number..) + else: + return (16*1024*1024) #Max is 16TiB + +# LVM sources set the maximum length limit on VG and LV names at 128. Set +# our default to 2 below that to account for 0 through 99 entries we may +# make with this name as a prefix. LVM doesn't seem to impose a limit of +# 99, but we do in anaconda. +def safeLvmName(name, maxlen=126): + tmp = name.strip() + tmp = tmp.replace("/", "_") + tmp = re.sub("[^0-9a-zA-Z._]", "", tmp) + tmp = tmp.lstrip("_") + + if len(tmp) > maxlen: + tmp = tmp[:maxlen] + + return tmp + +def clampSize(size, pesize, roundup=None): + if roundup: + round = math.ceil + else: + round = math.floor + + return long(round(float(size)/float(pesize)) * pesize) + +def lvm(args, progress=None): + rc = iutil.execWithPulseProgress("lvm", args, + stdout = "/dev/tty5", + stderr = "/dev/tty5", + progress=progress) + if not rc: + return + + try: + # grab the last line of program.log and strip off the timestamp + msg = open("/tmp/program.log").readlines()[-1] + msg = msg.split("program: ", 1)[1].strip() + except Exception: + msg = "" + + raise LVMError(msg) + +def pvcreate(device, progress=None): + args = ["pvcreate"] + \ + config_args + \ + [device] + + try: + lvm(args, progress=progress) + except LVMError as msg: + raise LVMError("pvcreate failed for %s: %s" % (device, msg)) + +def pvresize(device, size): + args = ["pvresize"] + \ + ["--setphysicalvolumesize", ("%dm" % size)] + \ + config_args + \ + [device] + + try: + lvm(args) + except LVMError as msg: + raise LVMError("pvresize failed for %s: %s" % (device, msg)) + +def pvremove(device): + args = ["pvremove"] + \ + config_args + \ + [device] + + try: + lvm(args) + except LVMError as msg: + raise LVMError("pvremove failed for %s: %s" % (device, msg)) + +def pvinfo(device): + """ + If the PV was created with '--metadacopies 0', lvm will do some + scanning of devices to determine from their metadata which VG + this PV belongs to. + + pvs -o pv_name,pv_mda_count,vg_name,vg_uuid --config \ + 'devices { scan = "/dev" filter = ["a/loop0/", "r/.*/"] }' + """ + #cfg = "'devices { scan = \"/dev\" filter = [\"a/%s/\", \"r/.*/\"] }'" + args = ["pvs", "--noheadings"] + \ + ["--units", "m"] + \ + ["-o", "pv_name,pv_mda_count,vg_name,vg_uuid"] + \ + config_args + \ + [device] + + rc = iutil.execWithCapture("lvm", args, + stderr = "/dev/tty5") + vals = rc.split() + if not vals: + raise LVMError("pvinfo failed for %s" % device) + + # don't raise an exception if pv is not a part of any vg + pv_name = vals[0] + try: + vg_name, vg_uuid = vals[2], vals[3] + except IndexError: + vg_name, vg_uuid = "", "" + + info = {'pv_name': pv_name, + 'vg_name': vg_name, + 'vg_uuid': vg_uuid} + + return info + +def vgcreate(vg_name, pv_list, pe_size, progress=None): + argv = ["vgcreate"] + if pe_size: + argv.extend(["-s", "%dm" % pe_size]) + argv.extend(config_args) + argv.append(vg_name) + argv.extend(pv_list) + + try: + lvm(argv, progress=progress) + except LVMError as msg: + raise LVMError("vgcreate failed for %s: %s" % (vg_name, msg)) + +def vgremove(vg_name): + args = ["vgremove", "--force"] + \ + config_args +\ + [vg_name] + + try: + lvm(args) + except LVMError as msg: + raise LVMError("vgremove failed for %s: %s" % (vg_name, msg)) + +def vgactivate(vg_name): + args = ["vgchange", "-a", "y"] + \ + config_args + \ + [vg_name] + + try: + lvm(args) + except LVMError as msg: + raise LVMError("vgactivate failed for %s: %s" % (vg_name, msg)) + +def vgdeactivate(vg_name): + args = ["vgchange", "-a", "n"] + \ + config_args + \ + [vg_name] + + try: + lvm(args) + except LVMError as msg: + raise LVMError("vgdeactivate failed for %s: %s" % (vg_name, msg)) + +def vgreduce(vg_name, pv_list, rm=False): + """ Reduce a VG. + + rm -> with RemoveMissing option. + Use pv_list when rm=False, otherwise ignore pv_list and call vgreduce with + the --removemissing option. + """ + args = ["vgreduce"] + if rm: + args.extend(["--removemissing", vg_name]) + else: + args.extend([vg_name] + pv_list) + + try: + lvm(args) + except LVMError as msg: + raise LVMError("vgreduce failed for %s: %s" % (vg_name, msg)) + +def vginfo(vg_name): + args = ["vgs", "--noheadings", "--nosuffix"] + \ + ["--units", "m"] + \ + ["-o", "uuid,size,free,extent_size,extent_count,free_count,pv_count"] + \ + config_args + \ + [vg_name] + + buf = iutil.execWithCapture("lvm", + args, + stderr="/dev/tty5") + info = buf.split() + if len(info) != 7: + raise LVMError(_("vginfo failed for %s" % vg_name)) + + d = {} + (d['uuid'],d['size'],d['free'],d['pe_size'], + d['pe_count'],d['pe_free'],d['pv_count']) = info + return d + +def lvs(vg_name): + args = ["lvs", "--noheadings", "--nosuffix"] + \ + ["--units", "m"] + \ + ["-o", "lv_name,lv_uuid,lv_size,lv_attr"] + \ + config_args + \ + [vg_name] + + buf = iutil.execWithCapture("lvm", + args, + stderr="/dev/tty5") + + lvs = {} + for line in buf.splitlines(): + line = line.strip() + if not line: + continue + (name, uuid, size, attr) = line.split() + lvs[name] = {"size": size, + "uuid": uuid, + "attr": attr} + + if not lvs: + raise LVMError(_("lvs failed for %s" % vg_name)) + + return lvs + +def lvorigin(vg_name, lv_name): + args = ["lvs", "--noheadings", "-o", "origin"] + \ + config_args + \ + ["%s/%s" % (vg_name, lv_name)] + + buf = iutil.execWithCapture("lvm", + args, + stderr="/dev/tty5") + + try: + origin = buf.splitlines()[0].strip() + except IndexError: + origin = '' + + return origin + +def lvcreate(vg_name, lv_name, size, progress=None): + args = ["lvcreate"] + \ + ["-L", "%dm" % size] + \ + ["-n", lv_name] + \ + config_args + \ + [vg_name] + + try: + lvm(args, progress=progress) + except LVMError as msg: + raise LVMError("lvcreate failed for %s/%s: %s" % (vg_name, lv_name, msg)) + +def lvremove(vg_name, lv_name): + args = ["lvremove"] + \ + config_args + \ + ["%s/%s" % (vg_name, lv_name)] + + try: + lvm(args) + except LVMError as msg: + raise LVMError("lvremove failed for %s: %s" % (lv_name, msg)) + +def lvresize(vg_name, lv_name, size): + args = ["lvresize"] + \ + ["--force", "-L", "%dm" % size] + \ + config_args + \ + ["%s/%s" % (vg_name, lv_name)] + + try: + lvm(args) + except LVMError as msg: + raise LVMError("lvresize failed for %s: %s" % (lv_name, msg)) + +def lvactivate(vg_name, lv_name): + # see if lvchange accepts paths of the form 'mapper/$vg-$lv' + args = ["lvchange", "-a", "y"] + \ + config_args + \ + ["%s/%s" % (vg_name, lv_name)] + + try: + lvm(args) + except LVMError as msg: + raise LVMError("lvactivate failed for %s: %s" % (lv_name, msg)) + +def lvdeactivate(vg_name, lv_name): + args = ["lvchange", "-a", "n"] + \ + config_args + \ + ["%s/%s" % (vg_name, lv_name)] + + try: + lvm(args) + except LVMError as msg: + raise LVMError("lvdeactivate failed for %s: %s" % (lv_name, msg)) + diff --git a/storage/devicelibs/mdraid.py b/storage/devicelibs/mdraid.py new file mode 100644 index 0000000..4e5a5f7 --- /dev/null +++ b/storage/devicelibs/mdraid.py @@ -0,0 +1,234 @@ +# +# mdraid.py +# mdraid functions +# +# Copyright (C) 2009 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. +# +# Author(s): Dave Lehman <dlehman@redhat.com> +# + +import os + +import iutil +from ..errors import * + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + +# raidlevels constants +RAID10 = 10 +RAID6 = 6 +RAID5 = 5 +RAID4 = 4 +RAID1 = 1 +RAID0 = 0 + +def getRaidLevels(): + mdstat_descriptors = { + RAID10: ("[RAID10]", "[raid10]"), + RAID6: ("[RAID6]", "[raid6]"), + RAID5: ("[RAID5]", "[raid5]"), + RAID4: ("[RAID4]", "[raid4]"), + RAID1: ("[RAID1]", "[raid1]"), + RAID0: ("[RAID0]", "[raid0]"), + } + avail = [] + try: + f = open("/proc/mdstat", "r") + except IOError: + pass + else: + for l in f.readlines(): + if not l.startswith("Personalities"): + continue + + lst = l.split() + + for level in mdstat_descriptors: + for d in mdstat_descriptors[level]: + if d in lst: + avail.append(level) + break + + f.close() + + avail.sort() + return avail + +raid_levels = getRaidLevels() + +def raidLevel(descriptor): + for level in raid_levels: + if isRaid(level, descriptor): + return level + else: + raise ValueError, "invalid raid level descriptor %s" % descriptor + +def isRaid(raid, raidlevel): + """Return whether raidlevel is a valid descriptor of raid""" + raid_descriptors = {RAID10: ("RAID10", "raid10", "10", 10), + RAID6: ("RAID6", "raid6", "6", 6), + RAID5: ("RAID5", "raid5", "5", 5), + RAID4: ("RAID4", "raid4", "4", 4), + RAID1: ("mirror", "RAID1", "raid1", "1", 1), + RAID0: ("stripe", "RAID0", "raid0", "0", 0)} + + if raid in raid_descriptors: + return raidlevel in raid_descriptors[raid] + else: + raise ValueError, "invalid raid level %d" % raid + +def get_raid_min_members(raidlevel): + """Return the minimum number of raid members required for raid level""" + raid_min_members = {RAID10: 2, + RAID6: 4, + RAID5: 3, + RAID4: 3, + RAID1: 2, + RAID0: 2} + + for raid, min_members in raid_min_members.items(): + if isRaid(raid, raidlevel): + return min_members + + raise ValueError, "invalid raid level %d" % raidlevel + +def get_raid_max_spares(raidlevel, nummembers): + """Return the maximum number of raid spares for raidlevel.""" + raid_max_spares = {RAID10: lambda: max(0, nummembers - get_raid_min_members(RAID10)), + RAID6: lambda: max(0, nummembers - get_raid_min_members(RAID6)), + RAID5: lambda: max(0, nummembers - get_raid_min_members(RAID5)), + RAID4: lambda: max(0, nummembers - get_raid_min_members(RAID4)), + RAID1: lambda: max(0, nummembers - get_raid_min_members(RAID1)), + RAID0: lambda: 0} + + for raid, max_spares_func in raid_max_spares.items(): + if isRaid(raid, raidlevel): + return max_spares_func() + + raise ValueError, "invalid raid level %d" % raidlevel + +def mdadm(args, progress=None): + rc = iutil.execWithPulseProgress("mdadm", args, + stdout = "/dev/tty5", + stderr = "/dev/tty5", + progress=progress) + if not rc: + return + + try: + # grab the last line of program.log and strip off the timestamp + msg = open("/tmp/program.log").readlines()[-1] + msg = msg.split("program: ", 1)[1].strip() + except Exception: + msg = "" + + raise MDRaidError(msg) + +def mdcreate(device, level, disks, spares=0, metadataVer=None, bitmap=False, + progress=None): + argv = ["--create", device, "--run", "--level=%s" % level] + raid_devs = len(disks) - spares + argv.append("--raid-devices=%d" % raid_devs) + if spares: + argv.append("--spare-devices=%d" % spares) + if metadataVer: + argv.append("--metadata=%s" % metadataVer) + if bitmap: + argv.append("--bitmap=internal") + argv.extend(disks) + + try: + mdadm(argv, progress=progress) + except MDRaidError as msg: + raise MDRaidError("mdcreate failed for %s: %s" % (device, msg)) + +def mddestroy(device): + args = ["--zero-superblock", device] + + try: + mdadm(args) + except MDRaidError as msg: + raise MDRaidError("mddestroy failed for %s: %s" % (device, msg)) + +def mdadd(device): + args = ["--incremental", "--quiet"] + args.append(device) + + try: + mdadm(args) + except MDRaidError as msg: + raise MDRaidError("mdadd failed for %s: %s" % (device, msg)) + +def mdactivate(device, members=[], super_minor=None, update_super_minor=False, + uuid=None): + if super_minor is None and not uuid: + raise ValueError("mdactivate requires either a uuid or a super-minor") + + if uuid: + identifier = "--uuid=%s" % uuid + elif super_minor is not None: + identifier = "--super-minor=%d" % super_minor + else: + identifier = "" + + if update_super_minor: + extra_args = ["--update=super-minor"] + else: + extra_args = [ ] + + args = ["--assemble", device, identifier, "--run", "--auto=md"] + args += extra_args + args += members + + try: + mdadm(args) + except MDRaidError as msg: + raise MDRaidError("mdactivate failed for %s: %s" % (device, msg)) + +def mddeactivate(device): + args = ["--stop", device] + + try: + mdadm(args) + except MDRaidError as msg: + raise MDRaidError("mddeactivate failed for %s: %s" % (device, msg)) + +def mdexamine(device): + vars = iutil.execWithCapture("mdadm", + ["--examine", "--brief", device], + stderr="/dev/tty5").split() + + info = {} + if vars: + try: + info["device"] = vars[1] + vars = vars[2:] + except IndexError: + return {} + + for var in vars: + (name, equals, value) = var.partition("=") + if not equals: + continue + + info[name.lower()] = value.strip() + + return info + diff --git a/storage/devicelibs/mpath.py b/storage/devicelibs/mpath.py new file mode 100644 index 0000000..adb2644 --- /dev/null +++ b/storage/devicelibs/mpath.py @@ -0,0 +1,228 @@ +from ..udev import * +import iutil + +def parseMultipathOutput(output): + # this function parses output from "multipath -d", so we can use its + # logic for our topology. + # The input looks like: + # create: mpathb (1ATA ST3120026AS 5M) undef ATA,ST3120026AS + # size=112G features='0' hwhandler='0' wp=undef + # `-+- policy='round-robin 0' prio=1 status=undef + # `- 2:0:0:0 sda 8:0 undef ready running + # create: mpatha (36006016092d21800703762872c60db11) undef DGC,RAID 5 + # size=10G features='1 queue_if_no_path' hwhandler='1 emc' wp=undef + # `-+- policy='round-robin 0' prio=2 status=undef + # |- 6:0:0:0 sdb 8:16 undef ready running + # `- 7:0:0:0 sdc 8:32 undef ready running + # + # (In anaconda, the first one there won't be included because we blacklist + # "ATA" as a vendor.) + # + # It returns a structure like: + # [ {'mpatha':['sdb','sdc']}, ... ] + mpaths = {} + if output is None: + return mpaths + + name = None + devices = [] + + lines = output.split('\n') + for line in lines: + lexemes = line.split() + if not lexemes: + break + if lexemes[0] == 'create:': + if name and devices: + mpaths[name] = devices + name = None + devices = [] + name = lexemes[1] + elif lexemes[0].startswith('size='): + pass + elif lexemes[0] == '`-+-': + pass + elif lexemes[0] in ['|-','`-']: + devices.append(lexemes[2].replace('!', '/')) + + if name and devices: + mpaths[name] = devices + + return mpaths + +def identifyMultipaths(devices): + # this function does a couple of things + # 1) identifies multipath disks + # 2) sets their ID_FS_TYPE to multipath_member + # 3) removes the individual members of an mpath's partitions + # sample input with multipath pair [sdb,sdc] + # [sr0, sda, sda1, sdb, sdb1, sdb2, sdc, sdc1, sdd, sdd1, sdd2] + # sample output: + # [sda, sdd], [[sdb, sdc]], [sr0, sda1, sdd1, sdd2]] + log.info("devices to scan for multipath: %s" % [d['name'] for d in devices]) + + topology = parseMultipathOutput(iutil.execWithCapture("multipath", ["-d",])) + # find the devices that aren't in topology, and add them into it... + topodevs = reduce(lambda x,y: x.union(y), topology.values(), set()) + for name in set([d['name'] for d in devices]).difference(topodevs): + topology[name] = [name] + + devmap = {} + non_disk_devices = {} + for d in devices: + if not udev_device_is_disk(d): + non_disk_devices[d['name']] = d + log.info("adding %s to non_disk_device list" % (d['name'],)) + continue + devmap[d['name']] = d + + singlepath_disks = [] + multipaths = [] + + for name, disks in topology.items(): + if len(disks) == 1: + if not non_disk_devices.has_key(disks[0]): + log.info("adding %s to singlepath_disks" % (disks[0],)) + singlepath_disks.append(devmap[disks[0]]) + else: + # some usb cardreaders use multiple lun's (for different slots) + # and report a fake disk serial which is the same for all the + # lun's (#517603) + all_usb = True + # see if we've got any non-disk devices on our mpath list. + # If so, they're probably false-positives. + non_disks = False + for disk in disks: + d = devmap[disk] + if d.get("ID_USB_DRIVER") != "usb-storage": + all_usb = False + if (not devmap.has_key(disk)) and non_disk_devices.has_key(disk): + log.warning("non-disk device %s is part of an mpath" % + (disk,)) + non_disks = True + + if all_usb: + log.info("adding multi lun usb mass storage device to singlepath_disks: %s" % + (disks,)) + singlepath_disks.extend([devmap[d] for d in disks]) + continue + + if non_disks: + for disk in disks: + if devmap.has_key(disk): + del devmap[disk] + if topology.has_key(disk): + del topology[disk] + continue + + log.info("found multipath set: %s" % (disks,)) + for disk in disks: + d = devmap[disk] + log.info("adding %s to multipath_disks" % (disk,)) + d["ID_FS_TYPE"] = "multipath_member" + d["ID_MPATH_NAME"] = name + + multipaths.append([devmap[d] for d in disks]) + + non_disk_serials = {} + for name,device in non_disk_devices.items(): + serial = udev_device_get_serial(device) + non_disk_serials.setdefault(serial, []) + non_disk_serials[serial].append(device) + + for mpath in multipaths: + for serial in [d.get('ID_SERIAL_SHORT') for d in mpath]: + if non_disk_serials.has_key(serial): + log.info("filtering out non disk devices [%s]" % [d['name'] for d in non_disk_serials[serial]]) + for name in [d['name'] for d in non_disk_serials[serial]]: + if non_disk_devices.has_key(name): + del non_disk_devices[name] + + partition_devices = [] + for device in non_disk_devices.values(): + partition_devices.append(device) + + # this is the list of devices we want to keep from the original + # device list, but we want to maintain its original order. + singlepath_disks = filter(lambda d: d in devices, singlepath_disks) + #multipaths = filter(lambda d: d in devices, multipaths) + partition_devices = filter(lambda d: d in devices, partition_devices) + + mpathStr = "[" + for mpath in multipaths: + mpathStr += str([d['name'] for d in mpath]) + mpathStr += "]" + + s = "(%s, %s, %s)" % ([d['name'] for d in singlepath_disks], \ + mpathStr, \ + [d['name'] for d in partition_devices]) + log.info("devices post multipath scan: %s" % s) + return (singlepath_disks, multipaths, partition_devices) + +class MultipathConfigWriter: + def __init__(self): + self.blacklist_devices = [] + self.mpaths = [] + + def addBlacklistDevice(self, device): + self.blacklist_devices.append(device) + + def addMultipathDevice(self, mpath): + self.mpaths.append(mpath) + + def write(self): + # if you add anything here, be sure and also add it to anaconda's + # multipath.conf + ret = '' + ret += """\ +# multipath.conf written by anaconda + +defaults { + user_friendly_names yes +} +blacklist { + devnode "^(ram|raw|loop|fd|md|dm-|sr|scd|st)[0-9]*" + devnode "^hd[a-z]" + devnode "^dcssblk[0-9]*" + device { + vendor "DGC" + product "LUNZ" + } + device { + vendor "IBM" + product "S/390.*" + } + # don't count normal SATA devices as multipaths + device { + vendor "ATA" + } + # don't count 3ware devices as multipaths + device { + vendor "3ware" + } + device { + vendor "AMCC" + } + # nor highpoint devices + device { + vendor "HPT" + } +""" + for device in self.blacklist_devices: + if device.serial: + ret += '\twwid %s\n' % device.serial + elif device.vendor and device.model: + ret += '\tdevice {\n' + ret += '\t\tvendor %s\n' % device.vendor + ret += '\t\tproduct %s\n' % device.model + ret += '\t}\n' + ret += '}\n' + ret += 'multipaths {\n' + for mpath in self.mpaths: + ret += '\tmultipath {\n' + for k,v in mpath.config.items(): + ret += '\t\t%s %s\n' % (k, v) + ret += '\t}\n' + ret += '}\n' + + return ret diff --git a/storage/devicelibs/swap.py b/storage/devicelibs/swap.py new file mode 100644 index 0000000..92dfe93 --- /dev/null +++ b/storage/devicelibs/swap.py @@ -0,0 +1,125 @@ +# swap.py +# Python module for managing swap devices. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +import resource + +import iutil +import os + +from ..errors import * +from . import dm + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + + +def mkswap(device, label='', progress=None): + # We use -f to force since mkswap tends to refuse creation on lvs with + # a message about erasing bootbits sectors on whole disks. Bah. + argv = ["-f"] + if label: + argv.extend(["-L", label]) + argv.append(device) + + rc = iutil.execWithPulseProgress("mkswap", argv, + stderr = "/dev/tty5", + stdout = "/dev/tty5", + progress=progress) + + if rc: + raise SwapError("mkswap failed for '%s'" % device) + +def swapon(device, priority=None): + pagesize = resource.getpagesize() + buf = None + sig = None + + if pagesize > 2048: + num = pagesize + else: + num = 2048 + + try: + fd = os.open(device, os.O_RDONLY) + buf = os.read(fd, num) + except OSError: + pass + finally: + try: + os.close(fd) + except (OSError, UnboundLocalError): + pass + + if buf is not None and len(buf) == pagesize: + sig = buf[pagesize - 10:] + if sig == 'SWAP-SPACE': + raise OldSwapError + if sig == 'S1SUSPEND\x00' or sig == 'S2SUSPEND\x00': + raise SuspendError + + if sig != 'SWAPSPACE2': + raise UnknownSwapError + + argv = [] + if isinstance(priority, int) and 0 <= priority <= 32767: + argv.extend(["-p", "%d" % priority]) + argv.append(device) + + rc = iutil.execWithRedirect("swapon", + argv, + stderr = "/dev/tty5", + stdout = "/dev/tty5") + + if rc: + raise SwapError("swapon failed for '%s'" % device) + +def swapoff(device): + rc = iutil.execWithRedirect("swapoff", [device], + stderr = "/dev/tty5", + stdout = "/dev/tty5") + + if rc: + raise SwapError("swapoff failed for '%s'" % device) + +def swapstatus(device): + alt_dev = None + if device.startswith("/dev/mapper/"): + # get the real device node for device-mapper devices since the ones + # with meaningful names are just symlinks + try: + alt_dev = "/dev/%s" % dm.dm_node_from_name(device.split("/")[-1]) + except DMError: + alt_dev = None + + lines = open("/proc/swaps").readlines() + status = False + for line in lines: + if not line.strip(): + continue + + swap_dev = line.split()[0] + if swap_dev in [device, alt_dev]: + status = True + break + + return status + diff --git a/storage/devices.py b/storage/devices.py new file mode 100644 index 0000000..f310cef --- /dev/null +++ b/storage/devices.py @@ -0,0 +1,3576 @@ +# devices.py +# Device classes for anaconda's storage configuration module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + + +""" + Device classes for use by anaconda. + + This is the hierarchy of device objects that anaconda will use for + managing storage devices in the system. These classes will + individually make use of external support modules as needed to + perform operations specific to the type of device they represent. + + TODO: + - see how to do network devices (NetworkManager may help) + - perhaps just a wrapper here + - document return values of all methods/functions + - find out what other kinds of wild and crazy devices we need to + represent here (iseries? xen? more mainframe? mac? ps?) + - PReP + - this is a prime candidate for a PseudoDevice + - DASD + - ZFCP + - XEN + + What specifications do we allow? new existing + partitions + usage + + + filesystem, partition type are implicit + mountpoint + + + size + exact + - + range + - + resize - + + format - + + encryption + + + + disk + exact + - + set + - + how will we specify this? + partition w/ multiple parents cannot otherwise occur + primary + - + + mdraid sets + filesystem (*) + + + mountpoint + + + size? + format - + + encryption + + + + level + ? + device minor + ? + member devices + ? + spares + ? + name? + bitmap? (boolean) + - + + volume groups + name + - + member pvs + + + pesize + ? + + logical volumes + filesystem + + + mountpoint + + + size + exact + ? + format - + + encryption + + + + name + ? + vgname + ? + + +""" + +import os +import math +import copy +import time + +# device backend modules +from devicelibs import mdraid +from devicelibs import lvm +from devicelibs import dm +import parted +import _ped +import block + +from errors import * +from iutil import notify_kernel, numeric_type +from .storage_log import log_method_call +from udev import * +from formats import get_device_format_class, getFormat, DeviceFormat + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + +def get_device_majors(): + majors = {} + for line in open("/proc/devices").readlines(): + try: + (major, device) = line.split() + except ValueError: + continue + try: + majors[int(major)] = device + except ValueError: + continue + return majors +device_majors = get_device_majors() + + +def devicePathToName(devicePath): + if devicePath.startswith("/dev/"): + name = devicePath[5:] + else: + name = devicePath + + if name.startswith("mapper/"): + name = name[7:] + + return name + + +def deviceNameToDiskByPath(deviceName=None): + bypath = '/dev/disk/by-path' + + if not deviceName: + return "" + + if not os.path.isdir(bypath): + return "" + + deviceName = os.path.basename(deviceName) + + for path in os.listdir(bypath): + entry = bypath + '/' + path + + if os.path.islink(entry): + target = os.path.basename(os.readlink(entry)) + else: + target = os.path.basename(entry) + + if target == deviceName: + return entry + + return "" + + +class Device(object): + """ A generic device. + + Device instances know which devices they depend upon (parents + attribute). They do not know which devices depend upon them, but + they do know whether or not they have any dependent devices + (isleaf attribute). + + A Device's setup method should set up all parent devices as well + as the device itself. It should not run the resident format's + setup method. + + Which Device types rely on their parents' formats being active? + DMCryptDevice + + A Device's teardown method should accept the keyword argument + recursive, which takes a boolean value and indicates whether or + not to recursively close parent devices. + + A Device's create method should create all parent devices as well + as the device itself. It should also run the Device's setup method + after creating the device. The create method should not create a + device's resident format. + + Which device type rely on their parents' formats to be created + before they can be created/assembled? + VolumeGroup + DMCryptDevice + + A Device's destroy method should destroy any resident format + before destroying the device itself. + + """ + + # This is a counter for generating unique ids for Devices. + _id = 0 + + _type = "generic device" + _packages = [] + + def __init__(self, name, parents=None): + """ Create a Device instance. + + Arguments: + + name -- the device name (generally a device node's basename) + + Keyword Arguments: + + parents -- a list of required Device instances + + """ + self._name = name + if parents is None: + parents = [] + elif not isinstance(parents, list): + raise ValueError("parents must be a list of Device instances") + self.parents = parents + self.kids = 0 + + # Set this instance's id and increment the counter. + self.id = Device._id + Device._id += 1 + + for parent in self.parents: + parent.addChild() + + def __deepcopy__(self, memo): + """ Create a deep copy of a Device instance. + + We can't do copy.deepcopy on parted objects, which is okay. + For these parted objects, we just do a shallow copy. + """ + new = self.__class__.__new__(self.__class__) + memo[id(self)] = new + dont_copy_attrs = ('_raidSet',) + shallow_copy_attrs = ('_partedDevice', '_partedPartition') + for (attr, value) in self.__dict__.items(): + if attr in dont_copy_attrs: + setattr(new, attr, value) + elif attr in shallow_copy_attrs: + setattr(new, attr, copy.copy(value)) + else: + setattr(new, attr, copy.deepcopy(value, memo)) + + return new + + def __str__(self): + s = ("%(type)s instance (%(id)s) --\n" + " name = %(name)s status = %(status)s" + " parents = %(parents)s\n" + " kids = %(kids)s\n" + " id = %(dev_id)s\n" % + {"type": self.__class__.__name__, "id": "%#x" % id(self), + "name": self.name, "parents": self.parents, "kids": self.kids, + "status": self.status, "dev_id": self.id}) + return s + + @property + def dict(self): + d = {"type": self.type, "name": self.name, + "parents": [p.name for p in self.parents]} + return d + + def writeKS(self, f, preexisting=False, noformat=False, s=None): + return + + def removeChild(self): + log_method_call(self, name=self.name, kids=self.kids) + self.kids -= 1 + + def addChild(self): + log_method_call(self, name=self.name, kids=self.kids) + self.kids += 1 + + def setup(self, intf=None): + """ Open, or set up, a device. """ + raise NotImplementedError("setup method not defined for Device") + + def teardown(self, recursive=None): + """ Close, or tear down, a device. """ + raise NotImplementedError("teardown method not defined for Device") + + def create(self, intf=None): + """ Create the device. """ + raise NotImplementedError("create method not defined for Device") + + def destroy(self): + """ Destroy the device. """ + raise NotImplementedError("destroy method not defined for Device") + + def setupParents(self, orig=False): + """ Run setup method of all parent devices. """ + log_method_call(self, name=self.name, orig=orig, kids=self.kids) + for parent in self.parents: + parent.setup(orig=orig) + + def teardownParents(self, recursive=None): + """ Run teardown method of all parent devices. """ + for parent in self.parents: + parent.teardown(recursive=recursive) + + def createParents(self): + """ Run create method of all parent devices. """ + log.info("NOTE: recursive device creation disabled") + for parent in self.parents: + if not parent.exists: + raise DeviceError("parent device does not exist", self.name) + #parent.create() + + def dependsOn(self, dep): + """ Return True if this device depends on dep. """ + # XXX does a device depend on itself? + if dep in self.parents: + return True + + for parent in self.parents: + if parent.dependsOn(dep): + return True + + return False + + def dracutSetupString(self): + return "" + + @property + def status(self): + """ This device's status. + + For now, this should return a boolean: + True the device is open and ready for use + False the device is not open + """ + return False + + @property + def name(self): + """ This device's name. """ + return self._name + + @property + def isleaf(self): + """ True if this device has no children. """ + return self.kids == 0 + + @property + def typeDescription(self): + """ String describing the device type. """ + return self._type + + @property + def type(self): + """ Device type. """ + return self._type + + @property + def packages(self): + """ List of packages required to manage devices of this type. + + This list includes the packages required by its parent devices. + """ + packages = self._packages + for parent in self.parents: + for package in parent.packages: + if package not in packages: + packages.append(package) + + return packages + + @property + def mediaPresent(self): + return True + + +class NetworkStorageDevice(object): + """ Virtual base class for network backed storage devices """ + + def __init__(self, host_address=None, nic=None): + """ Create a NetworkStorage Device instance. Note this class is only + to be used as a baseclass and then only with multiple inheritance. + The only correct use is: + class MyStorageDevice(StorageDevice, NetworkStorageDevice): + + The sole purpose of this class is to: + 1) Be able to check if a StorageDevice is network backed + (using isinstance). + 2) To be able to get the host address of the host (server) backing + the storage *or* the NIC through which the storage is connected + + Arguments: + + host_address -- host address of the backing server + nic -- nic to which the storage is bound + """ + self.host_address = host_address + self.nic = nic + + +class StorageDevice(Device): + """ A generic storage device. + + A fully qualified path to the device node can be obtained via the + path attribute, although it is not guaranteed to be useful, or + even present, unless the StorageDevice's setup method has been + run. + + StorageDevice instances can optionally contain a filesystem, + represented by an FS instance. A StorageDevice's create method + should create a filesystem if one has been specified. + """ + _type = "storage device" + _devDir = "/dev" + sysfsBlockDir = "class/block" + _resizable = False + _partitionable = False + _isDisk = False + + def __init__(self, device, format=None, + size=None, major=None, minor=None, + sysfsPath='', parents=None, exists=None, serial=None, + vendor="", model="", bus=""): + """ Create a StorageDevice instance. + + Arguments: + + device -- the device name (generally a device node's basename) + + Keyword Arguments: + + size -- the device's size (units/format TBD) + major -- the device major + minor -- the device minor + sysfsPath -- sysfs device path + format -- a DeviceFormat instance + parents -- a list of required Device instances + serial -- the ID_SERIAL_SHORT for this device + vendor -- the manufacturer of this Device + model -- manufacturer's device model string + bus -- the interconnect this device uses + + """ + # allow specification of individual parents + if isinstance(parents, Device): + parents = [parents] + + self.exists = exists + Device.__init__(self, device, parents=parents) + + self.uuid = None + self._format = None + self._size = numeric_type(size) + self.major = numeric_type(major) + self.minor = numeric_type(minor) + self.sysfsPath = sysfsPath + self._serial = serial + self._vendor = vendor + self._model = model + self.bus = bus + + self.protected = False + + self.format = format + self.originalFormat = self.format + self.fstabComment = "" + self._targetSize = self._size + + self._partedDevice = None + + @property + def packages(self): + """ List of packages required to manage devices of this type. + + This list includes the packages required by this device's + format type as well those required by all of its parent + devices. + """ + packages = super(StorageDevice, self).packages + packages.extend(self.format.packages) + for parent in self.parents: + for package in parent.format.packages: + if package not in packages: + packages.append(package) + + return packages + + @property + def partedDevice(self): + if self.exists and self.status and not self._partedDevice: + log.debug("looking up parted Device: %s" % self.path) + + # We aren't guaranteed to be able to get a device. In + # particular, built-in USB flash readers show up as devices but + # do not always have any media present, so parted won't be able + # to find a device. + try: + self._partedDevice = parted.Device(path=self.path) + except (_ped.IOException, _ped.DeviceException): + pass + + return self._partedDevice + + def _getTargetSize(self): + return self._targetSize + + def _setTargetSize(self, newsize): + self._targetSize = newsize + + targetSize = property(lambda s: s._getTargetSize(), + lambda s, v: s._setTargetSize(v), + doc="Target size of this device") + + def __str__(self): + s = Device.__str__(self) + s += (" uuid = %(uuid)s format = %(format)r size = %(size)s\n" + " major = %(major)s minor = %(minor)r exists = %(exists)s\n" + " sysfs path = %(sysfs)s partedDevice = %(partedDevice)r\n" + " target size = %(targetSize)s path = %(path)s\n" + " format args = %(formatArgs)s originalFormat = %(origFmt)s" % + {"uuid": self.uuid, "format": self.format, "size": self.size, + "major": self.major, "minor": self.minor, "exists": self.exists, + "sysfs": self.sysfsPath, "partedDevice": self.partedDevice, + "targetSize": self.targetSize, "path": self.path, + "formatArgs": self.formatArgs, "origFmt": self.originalFormat}) + return s + + @property + def dict(self): + d = super(StorageDevice, self).dict + d.update({"uuid": self.uuid, "size": self.size, + "format": self.format.dict, "removable": self.removable, + "major": self.major, "minor": self.minor, + "exists": self.exists, "sysfs": self.sysfsPath, + "targetSize": self.targetSize, "path": self.path}) + return d + + @property + def path(self): + """ Device node representing this device. """ + return "%s/%s" % (self._devDir, self.name) + + def updateSysfsPath(self): + """ Update this device's sysfs path. """ + log_method_call(self, self.name, status=self.status) + sysfsName = self.name.replace("/", "!") + path = os.path.join("/sys", self.sysfsBlockDir, sysfsName) + self.sysfsPath = os.path.realpath(path)[4:] + log.debug("%s sysfsPath set to %s" % (self.name, self.sysfsPath)) + + @property + def formatArgs(self): + """ Device-specific arguments to format creation program. """ + return [] + + @property + def resizable(self): + """ Can this type of device be resized? """ + return self._resizable and self.exists and \ + ((self.format and self.format.resizable) or not self.format) + + def notifyKernel(self): + """ Send a 'change' uevent to the kernel for this device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + log.debug("not sending change uevent for non-existent device") + return + + if not self.status: + log.debug("not sending change uevent for inactive device") + return + + path = os.path.normpath("/sys/%s" % self.sysfsPath) + try: + notify_kernel(path, action="change") + except Exception, e: + log.warning("failed to notify kernel of change: %s" % e) + + @property + def fstabSpec(self): + spec = self.path + if self.format and self.format.uuid: + spec = "UUID=%s" % self.format.uuid + return spec + + def resize(self, intf=None): + """ Resize the device. + + New size should already be set. + """ + raise NotImplementedError("resize method not defined for StorageDevice") + + def setup(self, intf=None, orig=False): + """ Open, or set up, a device. """ + log_method_call(self, self.name, orig=orig, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + self.setupParents(orig=orig) + for parent in self.parents: + if orig: + parent.originalFormat.setup() + else: + parent.format.setup() + + def teardown(self, recursive=None): + """ Close, or tear down, a device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists and not recursive: + raise DeviceError("device has not been created", self.name) + + if self.status: + if self.originalFormat.exists: + self.originalFormat.teardown() + if self.format.exists: + self.format.teardown() + udev_settle() + + if recursive: + self.teardownParents(recursive=recursive) + + def _getSize(self): + """ Get the device's size in MB, accounting for pending changes. """ + if self.exists and not self.mediaPresent: + return 0 + + if self.exists and self.partedDevice: + self._size = self.currentSize + + size = self._size + if self.exists and self.resizable and self.targetSize != size: + size = self.targetSize + + return size + + def _setSize(self, newsize): + """ Set the device's size to a new value. """ + if newsize > self.maxSize: + raise DeviceError("device cannot be larger than %s MB" % + (self.maxSize(),), self.name) + self._size = newsize + + size = property(lambda x: x._getSize(), + lambda x, y: x._setSize(y), + doc="The device's size in MB, accounting for pending changes") + + @property + def currentSize(self): + """ The device's actual size. """ + size = 0 + if self.exists and self.partedDevice: + size = self.partedDevice.getSize() + elif self.exists: + size = self._size + return size + + @property + def minSize(self): + """ The minimum size this device can be. """ + if self.format.minSize: + return self.format.minSize + else: + return self.size + + @property + def maxSize(self): + """ The maximum size this device can be. """ + if self.format.maxSize > self.currentSize: + return self.currentSize + else: + return self.format.maxSize + + @property + def status(self): + """ This device's status. + + For now, this should return a boolean: + True the device is open and ready for use + False the device is not open + """ + if not self.exists: + return False + return os.access(self.path, os.W_OK) + + def _setFormat(self, format): + """ Set the Device's format. """ + if not format: + format = getFormat(None, device=self.path, exists=self.exists) + log_method_call(self, self.name, type=format.type, + current=getattr(self._format, "type", None)) + if self._format and self._format.status: + # FIXME: self.format.status doesn't mean much + raise DeviceError("cannot replace active format", self.name) + + self._format = format + + def _getFormat(self): + return self._format + + format = property(lambda d: d._getFormat(), + lambda d,f: d._setFormat(f), + doc="The device's formatting.") + + def create(self, intf=None): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + if self.exists: + raise DeviceError("device has already been created", self.name) + + self.createParents() + self.setupParents() + self.exists = True + self.setup() + + def destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + if not self.isleaf: + raise DeviceError("Cannot destroy non-leaf device", self.name) + + self.exists = False + # we already did this in DeviceTree._removeDevice + #for parent in self.parents: + # parent.removeChild() + + @property + def removable(self): + devpath = os.path.normpath("/sys/%s" % self.sysfsPath) + remfile = os.path.normpath("%s/removable" % devpath) + rem_f = None + try: + rem_f = open(remfile, "r") + except IOError as err: + if err.errno != 2: + raise + return False + try: + return (self.sysfsPath and os.path.exists(devpath) and + os.access(remfile, os.R_OK) and + rem_f.readline().strip() == "1") + finally: + rem_f.close() + + @property + def isDisk(self): + return self._isDisk + + @property + def partitionable(self): + return self._partitionable + + @property + def partitioned(self): + return self.format.type == "disklabel" and self.partitionable + + @property + def serial(self): + return self._serial + + @property + def model(self): + if not self._model: + self._model = getattr(self.partedDevice, "model", "") + return self._model + + @property + def vendor(self): + return self._vendor + +class DiskDevice(StorageDevice): + """ A disk """ + _type = "disk" + _partitionable = True + _isDisk = True + + def __init__(self, device, format=None, + size=None, major=None, minor=None, sysfsPath='', + parents=None, serial=None, vendor="", model="", bus="", + exists=True): + """ Create a DiskDevice instance. + + Arguments: + + device -- the device name (generally a device node's basename) + + Keyword Arguments: + + size -- the device's size (units/format TBD) + major -- the device major + minor -- the device minor + sysfsPath -- sysfs device path + format -- a DeviceFormat instance + parents -- a list of required Device instances + removable -- whether or not this is a removable device + serial -- the ID_SERIAL_SHORT for this device + vendor -- the manufacturer of this Device + model -- manufacturer's device model string + bus -- the interconnect this device uses + + + DiskDevices always exist. + """ + StorageDevice.__init__(self, device, format=format, size=size, + major=major, minor=minor, exists=exists, + sysfsPath=sysfsPath, parents=parents, + serial=serial, model=model, + vendor=vendor, bus=bus) + + def __str__(self): + s = StorageDevice.__str__(self) + s += (" removable = %(removable)s partedDevice = %(partedDevice)r" % + {"removable": self.removable, "partedDevice": self.partedDevice}) + return s + + @property + def mediaPresent(self): + if not self.partedDevice: + return False + + # Some drivers (cpqarray <blegh>) make block device nodes for + # controllers with no disks attached and then report a 0 size, + # treat this as no media present + return self.partedDevice.getSize() != 0 + + @property + def description(self): + return self.model + + @property + def size(self): + """ The disk's size in MB """ + return super(DiskDevice, self).size + #size = property(StorageDevice._getSize) + + def probe(self): + """ Probe for any missing information about this device. + + pyparted should be able to tell us anything we want to know. + size, disklabel type, maybe even partition layout + """ + log_method_call(self, self.name, size=self.size, partedDevice=self.partedDevice) + + def destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + if not self.mediaPresent: + raise DeviceError("cannot destroy disk with no media", self.name) + + self.teardown() + + def setup(self, intf=None, orig=False): + """ Open, or set up, a device. """ + log_method_call(self, self.name, orig=orig, status=self.status) + if not os.path.exists(self.path): + raise DeviceError("device does not exist", self.name) + + +class PartitionDevice(StorageDevice): + """ A disk partition. + + On types and flags... + + We don't need to deal with numerical partition types at all. The + only type we are concerned with is primary/logical/extended. Usage + specification is accomplished through the use of flags, which we + will set according to the partition's format. + """ + _type = "partition" + _resizable = True + defaultSize = 500 + + def __init__(self, name, format=None, + size=None, grow=False, maxsize=None, + major=None, minor=None, bootable=None, + sysfsPath='', parents=None, exists=None, + partType=None, primary=False, weight=0): + """ Create a PartitionDevice instance. + + Arguments: + + name -- the device name (generally a device node's basename) + + Keyword Arguments: + + exists -- indicates whether this is an existing device + format -- the device's format (DeviceFormat instance) + + For existing partitions: + + parents -- the disk that contains this partition + major -- the device major + minor -- the device minor + sysfsPath -- sysfs device path + + For new partitions: + + partType -- primary,extended,&c (as parted constant) + grow -- whether or not to grow the partition + maxsize -- max size for growable partitions (in MB) + size -- the device's size (in MB) + bootable -- whether the partition is bootable + parents -- a list of potential containing disks + weight -- an initial sorting weight to assign + """ + self.req_disks = [] + self.req_partType = None + self.req_primary = None + self.req_grow = None + self.req_bootable = None + self.req_size = 0 + self.req_base_size = 0 + self.req_max_size = 0 + self.req_base_weight = 0 + + self._bootable = False + + StorageDevice.__init__(self, name, format=format, size=size, + major=major, minor=minor, exists=exists, + sysfsPath=sysfsPath, parents=parents) + + if not exists: + # this is a request, not a partition -- it has no parents + self.req_disks = self.parents[:] + for dev in self.parents: + dev.removeChild() + self.parents = [] + + # FIXME: Validate partType, but only if this is a new partition + # Otherwise, overwrite it with the partition's type. + self._partType = None + self.partedFlags = {} + self._partedPartition = None + self._origPath = None + self._currentSize = 0 + + # FIXME: Validate size, but only if this is a new partition. + # For existing partitions we will get the size from + # parted. + + if self.exists: + log.debug("looking up parted Partition: %s" % self.path) + self._partedPartition = self.disk.format.partedDisk.getPartitionByPath(self.path) + if not self._partedPartition: + raise DeviceError("cannot find parted partition instance", self.name) + + self._origPath = self.path + # collect information about the partition from parted + self.probe() + if self.getFlag(parted.PARTITION_PREP): + # the only way to identify a PPC PReP Boot partition is to + # check the partition type/flags, so do it here. + self.format = getFormat("prepboot", device=self.path, exists=True) + else: + # XXX It might be worthwhile to create a shit-simple + # PartitionRequest class and pass one to this constructor + # for new partitions. + if not self._size: + # default size for new partition requests + self._size = self.defaultSize + self.req_name = name + self.req_partType = partType + self.req_primary = primary + self.req_max_size = numeric_type(maxsize) + self.req_grow = grow + self.req_bootable = bootable + + # req_size may be manipulated in the course of partitioning + self.req_size = self._size + + # req_base_size will always remain constant + self.req_base_size = self._size + + self.req_base_weight = weight + + def __str__(self): + s = StorageDevice.__str__(self) + s += (" grow = %(grow)s max size = %(maxsize)s bootable = %(bootable)s\n" + " part type = %(partType)s primary = %(primary)s\n" + " partedPartition = %(partedPart)r disk = %(disk)r\n" % + {"grow": self.req_grow, "maxsize": self.req_max_size, + "bootable": self.bootable, "partType": self.partType, + "primary": self.req_primary, + "partedPart": self.partedPartition, "disk": self.disk}) + + if self.partedPartition: + s += (" start = %(start)s end = %(end)s length = %(length)s\n" + " flags = %(flags)s" % + {"length": self.partedPartition.geometry.length, + "start": self.partedPartition.geometry.start, + "end": self.partedPartition.geometry.end, + "flags": self.partedPartition.getFlagsAsString()}) + + return s + + @property + def dict(self): + d = super(PartitionDevice, self).dict + d.update({"type": self.partType}) + if not self.exists: + d.update({"grow": self.req_grow, "maxsize": self.req_max_size, + "bootable": self.bootable, + "primary": self.req_primary}) + + if self.partedPartition: + d.update({"length": self.partedPartition.geometry.length, + "start": self.partedPartition.geometry.start, + "end": self.partedPartition.geometry.end, + "flags": self.partedPartition.getFlagsAsString()}) + return d + + def writeKS(self, f, preexisting=False, noformat=False, s=None): + args = [] + + if self.isExtended: + return + + if self.req_grow: + args.append("--grow") + if self.req_max_size: + args.append("--maxsize=%s" % self.req_max_size) + if self.req_primary: + args.append("--asprimary") + if self.req_size: + args.append("--size=%s" % (self.req_size or self.defaultSize)) + if preexisting: + if len(self.req_disks) == 1: + args.append("--ondisk=%s" % self.req_disks[0].name) + else: + args.append("--onpart=%s" % self.name) + if noformat: + args.append("--noformat") + + f.write("#part ") + self.format.writeKS(f) + f.write(" %s" % " ".join(args)) + if s: + f.write(" %s" % s) + + def _setTargetSize(self, newsize): + if newsize != self.currentSize: + # change this partition's geometry in-memory so that other + # partitioning operations can complete (e.g., autopart) + self._targetSize = newsize + disk = self.disk.format.partedDisk + + # resize the partition's geometry in memory + (constraint, geometry) = self._computeResize(self.partedPartition) + disk.setPartitionGeometry(partition=self.partedPartition, + constraint=constraint, + start=geometry.start, end=geometry.end) + + @property + def path(self): + """ Device node representing this device. """ + if not self.parents: + # Bogus, but code in various places compares devices by path + # So we must return something unique + return self.name + + return "%s/%s" % (self.parents[0]._devDir, self.name) + + @property + def partType(self): + """ Get the partition's type (as parted constant). """ + try: + ptype = self.partedPartition.type + except AttributeError: + ptype = self._partType + + if not self.exists and ptype is None: + ptype = self.req_partType + + return ptype + + @property + def isExtended(self): + return (self.partType is not None and + self.partType & parted.PARTITION_EXTENDED) + + @property + def isLogical(self): + return (self.partType is not None and + self.partType & parted.PARTITION_LOGICAL) + + @property + def isPrimary(self): + return (self.partType is not None and + self.partType == parted.PARTITION_NORMAL) + + @property + def isProtected(self): + return (self.partType is not None and + self.partType & parted.PARTITION_PROTECTED) + + @property + def fstabSpec(self): + spec = self.path + if self.disk and self.disk.type == 'dasd': + spec = deviceNameToDiskByPath(self.path) + elif self.format and self.format.uuid: + spec = "UUID=%s" % self.format.uuid + return spec + + def _getPartedPartition(self): + return self._partedPartition + + def _setPartedPartition(self, partition): + """ Set this PartitionDevice's parted Partition instance. """ + log_method_call(self, self.name) + if partition is None: + path = None + elif isinstance(partition, parted.Partition): + path = partition.path + else: + raise ValueError("partition must be a parted.Partition instance") + + log.debug("device %s new partedPartition %s has path %s" % (self.name, + partition, + path)) + self._partedPartition = partition + self.updateName() + + partedPartition = property(lambda d: d._getPartedPartition(), + lambda d,p: d._setPartedPartition(p)) + + def resetPartedPartition(self): + """ Re-get self.partedPartition from the original disklabel. """ + log_method_call(self, self.name) + if not self.exists: + return + + # find the correct partition on the original parted.Disk since the + # name/number we're now using may no longer match + _disklabel = self.disk.originalFormat + + if self.isExtended: + # getPartitionBySector doesn't work on extended partitions + _partition = _disklabel.extendedPartition + log.debug("extended lookup found partition %s" + % devicePathToName(getattr(_partition, "path", None))) + else: + # lookup the partition by sector to avoid the renumbering + # nonsense entirely + _sector = self.partedPartition.geometry.start + _partition = _disklabel.partedDisk.getPartitionBySector(_sector) + log.debug("sector-based lookup found partition %s" + % devicePathToName(getattr(_partition, "path", None))) + + self.partedPartition = _partition + + def _getWeight(self): + return self.req_base_weight + + def _setWeight(self, weight): + self.req_base_weight = weight + + weight = property(lambda d: d._getWeight(), + lambda d,w: d._setWeight(w)) + + def updateSysfsPath(self): + """ Update this device's sysfs path. """ + log_method_call(self, self.name, status=self.status) + if not self.parents: + self.sysfsPath = '' + + elif self.parents[0]._devDir == "/dev/mapper": + dm_node = dm.dm_node_from_name(self.name) + path = os.path.join("/sys", self.sysfsBlockDir, dm_node) + self.sysfsPath = os.path.realpath(path)[4:] + + else: + StorageDevice.updateSysfsPath(self) + + def updateName(self): + if self.partedPartition is None: + self._name = self.req_name + else: + self._name = \ + devicePathToName(self.partedPartition.getDeviceNodeName()) + + def dependsOn(self, dep): + """ Return True if this device depends on dep. """ + if isinstance(dep, PartitionDevice) and dep.isExtended and \ + self.isLogical and self.disk == dep.disk: + return True + + return Device.dependsOn(self, dep) + + def _setFormat(self, format): + """ Set the Device's format. """ + log_method_call(self, self.name) + StorageDevice._setFormat(self, format) + + def _setBootable(self, bootable): + """ Set the bootable flag for this partition. """ + if self.partedPartition: + if iutil.isS390(): + return + if self.flagAvailable(parted.PARTITION_BOOT): + if bootable: + self.setFlag(parted.PARTITION_BOOT) + else: + self.unsetFlag(parted.PARTITION_BOOT) + else: + raise DeviceError("boot flag not available for this partition", self.name) + + self._bootable = bootable + else: + self.req_bootable = bootable + + def _getBootable(self): + return self._bootable or self.req_bootable + + bootable = property(_getBootable, _setBootable) + + def flagAvailable(self, flag): + log_method_call(self, path=self.path, flag=flag) + if not self.partedPartition: + return + + return self.partedPartition.isFlagAvailable(flag) + + def getFlag(self, flag): + log_method_call(self, path=self.path, flag=flag) + if not self.partedPartition or not self.flagAvailable(flag): + return + + return self.partedPartition.getFlag(flag) + + def setFlag(self, flag): + log_method_call(self, path=self.path, flag=flag) + if not self.partedPartition or not self.flagAvailable(flag): + return + + self.partedPartition.setFlag(flag) + + def unsetFlag(self, flag): + log_method_call(self, path=self.path, flag=flag) + if not self.partedPartition or not self.flagAvailable(flag): + return + + self.partedPartition.unsetFlag(flag) + + def probe(self): + """ Probe for any missing information about this device. + + size, partition type, flags + """ + log_method_call(self, self.name, exists=self.exists) + if not self.exists: + return + + # this is in MB + self._size = self.partedPartition.getSize() + self._currentSize = self._size + self.targetSize = self._size + + self._partType = self.partedPartition.type + + self._bootable = self.getFlag(parted.PARTITION_BOOT) + + def create(self, intf=None): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + if self.exists: + raise DeviceError("device already exists", self.name) + + w = None + if intf: + w = intf.waitWindow(_("Creating"), + _("Creating device %s") % (self.path,)) + + try: + self.createParents() + self.setupParents() + + self.disk.format.addPartition(self.partedPartition) + + try: + self.disk.format.commit() + except DiskLabelCommitError: + part = self.disk.format.partedDisk.getPartitionByPath(self.path) + self.disk.format.removePartition(part) + raise + + # Ensure old metadata which lived in freespace so did not get + # explictly destroyed by a destroyformat action gets wiped + DeviceFormat(device=self.path, exists=True).destroy() + except Exception: + raise + else: + self.partedPartition = self.disk.format.partedDisk.getPartitionByPath(self.path) + + self.exists = True + self._currentSize = self.partedPartition.getSize() + self.setup() + finally: + if w: + w.pop() + + def _computeResize(self, partition): + log_method_call(self, self.name, status=self.status) + + # compute new size for partition + currentGeom = partition.geometry + currentDev = currentGeom.device + newLen = long(self.targetSize * 1024 * 1024) / currentDev.sectorSize + newGeometry = parted.Geometry(device=currentDev, + start=currentGeom.start, + length=newLen) + # and align the end sector + newGeometry.end = self.disk.format.endAlignment.alignDown(newGeometry, + newGeometry.end) + constraint = parted.Constraint(exactGeom=newGeometry) + + return (constraint, newGeometry) + + def resize(self, intf=None): + """ Resize the device. + + self.targetSize must be set to the new size. + """ + log_method_call(self, self.name, status=self.status) + + if self.targetSize != self.currentSize: + # partedDisk has been restored to _origPartedDisk, so + # recalculate resize geometry because we may have new + # partitions on the disk, which could change constraints + partedDisk = self.disk.format.partedDisk + partition = partedDisk.getPartitionByPath(self.path) + (constraint, geometry) = self._computeResize(partition) + + partedDisk.setPartitionGeometry(partition=partition, + constraint=constraint, + start=geometry.start, + end=geometry.end) + + self.disk.format.commit() + self._currentSize = partition.getSize() + + def destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + if not self.sysfsPath: + return + + if not self.isleaf: + raise DeviceError("Cannot destroy non-leaf device", self.name) + + self.setupParents(orig=True) + # we should have already set self.partedPartition to point to the + # partition on the original disklabel + self.disk.originalFormat.removePartition(self.partedPartition) + try: + self.disk.originalFormat.commit() + except DiskLabelCommitError: + self.disk.originalFormat.addPartition(self.partedPartition) + self.partedPartition = self.disk.originalFormat.partedDisk.getPartitionByPath(self.path) + raise + + self.exists = False + + def teardown(self, recursive=None): + """ Close, or tear down, a device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists and not recursive: + raise DeviceError("device has not been created", self.name) + + if self.status: + if self.originalFormat.exists: + self.originalFormat.teardown() + if self.format.exists: + self.format.teardown() + if self.parents[0].type == 'dm-multipath': + devmap = block.getMap(major=self.major, minor=self.minor) + if devmap: + try: + block.removeDeviceMap(devmap) + except Exception as e: + raise DeviceTeardownError("failed to tear down device-mapper partition %s: %s" % (self.name, e)) + udev_settle() + + StorageDevice.teardown(self, recursive=recursive) + + def _getSize(self): + """ Get the device's size. """ + size = self._size + if self.partedPartition: + # this defaults to MB + size = self.partedPartition.getSize() + return size + + def _setSize(self, newsize): + """ Set the device's size (for resize, not creation). + + Arguments: + + newsize -- the new size (in MB) + + """ + log_method_call(self, self.name, + status=self.status, size=self._size, newsize=newsize) + if not self.exists: + raise DeviceError("device does not exist", self.name) + + if newsize > self.disk.size: + raise ValueError("partition size would exceed disk size") + + # this defaults to MB + maxAvailableSize = self.partedPartition.getMaxAvailableSize() + + if newsize > maxAvailableSize: + raise ValueError("new size is greater than available space") + + # now convert the size to sectors and update the geometry + geometry = self.partedPartition.geometry + physicalSectorSize = geometry.device.physicalSectorSize + + new_length = (newsize * (1024 * 1024)) / physicalSectorSize + geometry.length = new_length + + def _getDisk(self): + """ The disk that contains this partition.""" + try: + disk = self.parents[0] + except IndexError: + disk = None + return disk + + def _setDisk(self, disk): + """Change the parent. + + Setting up a disk is not trivial. It has the potential to change + the underlying object. If necessary we must also change this object. + """ + log_method_call(self, self.name, old=getattr(self.disk, "name", None), + new=getattr(disk, "name", None)) + if self.disk: + self.disk.removeChild() + + if disk: + self.parents = [disk] + disk.addChild() + else: + self.parents = [] + + disk = property(lambda p: p._getDisk(), lambda p,d: p._setDisk(d)) + + @property + def maxSize(self): + """ The maximum size this partition can be. """ + # XXX: this is MB by default + maxPartSize = self.partedPartition.getMaxAvailableSize() + + if self.format.maxSize > maxPartSize: + return maxPartSize + else: + return self.format.maxSize + + @property + def currentSize(self): + """ The device's actual size. """ + if self.exists: + return self._currentSize + else: + return 0 + + +class DMDevice(StorageDevice): + """ A device-mapper device """ + _type = "dm" + _devDir = "/dev/mapper" + + def __init__(self, name, format=None, size=None, dmUuid=None, + target=None, exists=None, parents=None, sysfsPath=''): + """ Create a DMDevice instance. + + Arguments: + + name -- the device name (generally a device node's basename) + + Keyword Arguments: + + target -- the device-mapper target type (string) + size -- the device's size (units/format TBD) + dmUuid -- the device's device-mapper UUID + sysfsPath -- sysfs device path + format -- a DeviceFormat instance + parents -- a list of required Device instances + exists -- indicates whether this is an existing device + """ + StorageDevice.__init__(self, name, format=format, size=size, + exists=exists, + parents=parents, sysfsPath=sysfsPath) + self.target = target + self.dmUuid = dmUuid + + def __str__(self): + s = StorageDevice.__str__(self) + s += (" target = %(target)s dmUuid = %(dmUuid)s" % + {"target": self.target, "dmUuid": self.dmUuid}) + return s + + @property + def dict(self): + d = super(DMDevice, self).dict + d.update({"target": self.target, "dmUuid": self.dmUuid}) + return d + + @property + def fstabSpec(self): + """ Return the device specifier for use in /etc/fstab. """ + return self.path + + @property + def mapName(self): + """ This device's device-mapper map name """ + return self.name + + @property + def status(self): + _status = False + for map in block.dm.maps(): + if map.name == self.mapName: + _status = map.live_table and not map.suspended + break + + return _status + + def updateSysfsPath(self): + """ Update this device's sysfs path. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + if self.status: + dm_node = self.getDMNode() + path = os.path.join("/sys", self.sysfsBlockDir, dm_node) + self.sysfsPath = os.path.realpath(path)[4:] + else: + self.sysfsPath = '' + + #def getTargetType(self): + # return dm.getDmTarget(name=self.name) + + def getDMNode(self): + """ Return the dm-X (eg: dm-0) device node for this device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + return dm.dm_node_from_name(self.name) + + def _setName(self, name): + """ Set the device's map name. """ + log_method_call(self, self.name, status=self.status) + if self.status: + raise DeviceError("cannot rename active device", self.name) + + self._name = name + #self.sysfsPath = "/dev/disk/by-id/dm-name-%s" % self.name + + name = property(lambda d: d._name, + lambda d,n: d._setName(n)) + + +class DMCryptDevice(DMDevice): + """ A dm-crypt device """ + _type = "dm-crypt" + + def __init__(self, name, format=None, size=None, uuid=None, + exists=None, sysfsPath='', parents=None): + """ Create a DMCryptDevice instance. + + Arguments: + + name -- the device name (generally a device node's basename) + + Keyword Arguments: + + size -- the device's size (units/format TBD) + sysfsPath -- sysfs device path + format -- a DeviceFormat instance + parents -- a list of required Device instances + exists -- indicates whether this is an existing device + """ + DMDevice.__init__(self, name, format=format, size=size, + parents=parents, sysfsPath=sysfsPath, + exists=exists, target="crypt") + +class LUKSDevice(DMCryptDevice): + """ A mapped LUKS device. """ + _type = "luks/dm-crypt" + + def __init__(self, name, format=None, size=None, uuid=None, + exists=None, sysfsPath='', parents=None): + """ Create a LUKSDevice instance. + + Arguments: + + name -- the device name + + Keyword Arguments: + + size -- the device's size in MB + uuid -- the device's UUID + sysfsPath -- sysfs device path + format -- a DeviceFormat instance + parents -- a list of required Device instances + exists -- indicates whether this is an existing device + """ + DMCryptDevice.__init__(self, name, format=format, size=size, + parents=parents, sysfsPath=sysfsPath, + uuid=None, exists=exists) + + def writeKS(self, f, preexisting=False, noformat=False, s=None): + self.slave.writeKS(f, preexisting=preexisting, noformat=noformat, s=s) + self.format.writeKS(f) + if s: + f.write(" %s" % s) + + @property + def size(self): + if not self.exists or not self.partedDevice: + # the LUKS header takes up 4040 512-byte sectors w/ a 512-bit key + size = float(self.slave.size) - ((4040 * 2.0) / 1024) + else: + size = self.partedDevice.getSize() + return size + + def create(self, intf=None): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + if self.exists: + raise DeviceError("device already exists", self.name) + + self.createParents() + self.setupParents() + + #if not self.slave.format.exists: + # self.slave.format.create() + self._name = self.slave.format.mapName + self.exists = True + self.setup() + + def setup(self, intf=None, orig=False): + """ Open, or set up, a device. """ + log_method_call(self, self.name, orig=orig, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + self.slave.setup(orig=orig) + if orig: + self.slave.originalFormat.setup() + else: + self.slave.format.setup() + udev_settle() + + # we always probe since the device may not be set up when we want + # information about it + self._size = self.currentSize + + def teardown(self, recursive=False): + """ Close, or tear down, a device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists and not recursive: + raise DeviceError("device has not been created", self.name) + + if self.status: + if self.originalFormat.exists: + self.originalFormat.teardown() + if self.format.exists: + self.format.teardown() + udev_settle() + + if self.slave.originalFormat.exists: + self.slave.originalFormat.teardown() + udev_settle() + + if self.slave.format.exists: + self.slave.format.teardown() + udev_settle() + + if recursive: + self.teardownParents(recursive=recursive) + + def destroy(self): + log_method_call(self, self.name, status=self.status) + self.format.teardown() + udev_settle() + self.teardown() + + @property + def slave(self): + """ This device's backing device. """ + return self.parents[0] + + def dracutSetupString(self): + return "rd_LUKS_UUID=luks-%s" % self.slave.format.uuid + + +class LVMVolumeGroupDevice(DMDevice): + """ An LVM Volume Group + + XXX Maybe this should inherit from StorageDevice instead of + DMDevice since there's no actual device. + """ + _type = "lvmvg" + + def __init__(self, name, parents, size=None, free=None, + peSize=None, peCount=None, peFree=None, pvCount=None, + lvNames=[], uuid=None, exists=None, sysfsPath=''): + """ Create a LVMVolumeGroupDevice instance. + + Arguments: + + name -- the device name (generally a device node's basename) + parents -- a list of physical volumes (StorageDevice) + + Keyword Arguments: + + peSize -- physical extent size (in MB) + exists -- indicates whether this is an existing device + sysfsPath -- sysfs device path + + For existing VG's only: + + size -- the VG's size (in MB) + free -- amount of free space in the VG + peFree -- number of free extents + peCount -- total number of extents + pvCount -- number of PVs in this VG + lvNames -- the names of this VG's LVs + uuid -- the VG's UUID + + """ + self.pvClass = get_device_format_class("lvmpv") + if not self.pvClass: + raise StorageError("cannot find 'lvmpv' class") + + if isinstance(parents, list): + for dev in parents: + if not isinstance(dev.format, self.pvClass): + raise ValueError("constructor requires a list of PVs") + elif not isinstance(parents.format, self.pvClass): + raise ValueError("constructor requires a list of PVs") + + DMDevice.__init__(self, name, parents=parents, + exists=exists, sysfsPath=sysfsPath) + + self.uuid = uuid + self.free = numeric_type(free) + self.peSize = numeric_type(peSize) + self.peCount = numeric_type(peCount) + self.peFree = numeric_type(peFree) + self.pvCount = numeric_type(pvCount) + self.lvNames = lvNames + + # circular references, here I come + self._lvs = [] + + # TODO: validate peSize if given + if not self.peSize: + self.peSize = 32.0 # MB + + #self.probe() + + def __str__(self): + s = DMDevice.__str__(self) + s += (" free = %(free)s PE Size = %(peSize)s PE Count = %(peCount)s\n" + " PE Free = %(peFree)s PV Count = %(pvCount)s\n" + " LV Names = %(lvNames)s modified = %(modified)s\n" + " extents = %(extents)s free space = %(freeSpace)s\n" + " free extents = %(freeExtents)s\n" + " PVs = %(pvs)s\n" + " LVs = %(lvs)s" % + {"free": self.free, "peSize": self.peSize, "peCount": self.peCount, + "peFree": self.peFree, "pvCount": self.pvCount, + "lvNames": self.lvNames, "modified": self.isModified, + "extents": self.extents, "freeSpace": self.freeSpace, + "freeExtents": self.freeExtents, "pvs": self.pvs, "lvs": self.lvs}) + return s + + @property + def dict(self): + d = super(LVMVolumeGroupDevice, self).dict + d.update({"free": self.free, "peSize": self.peSize, + "peCount": self.peCount, "peFree": self.peFree, + "pvCount": self.pvCount, "extents": self.extents, + "freeSpace": self.freeSpace, + "freeExtents": self.freeExtents, + "lvNames": [lv.name for lv in self.lvs]}) + return d + + def writeKS(self, f, preexisting=False, noformat=False, s=None): + args = ["--pesize=%s" % int(self.peSize * 1024)] + pvs = [] + + for pv in self.pvs: + pvs.append("pv.%s" % pv.format.uuid) + + if preexisting: + args.append("--useexisting") + if noformat: + args.append("--noformat") + + f.write("#volgroup %s %s %s" % (self.name, " ".join(args), " ".join(pvs))) + if s: + f.write(" %s" % s) + + def probe(self): + """ Probe for any information about this device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + @property + def mapName(self): + """ This device's device-mapper map name """ + # Thank you lvm for this lovely hack. + return self.name.replace("-","--") + + @property + def path(self): + """ Device node representing this device. """ + return "%s/%s" % (self._devDir, self.mapName) + + def updateSysfsPath(self): + """ Update this device's sysfs path. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + self.sysfsPath = '' + + @property + def status(self): + """ The device's status (True means active). """ + if not self.exists: + return False + + # certainly if any of this VG's LVs are active then so are we + for lv in self.lvs: + if lv.status: + return True + + # if any of our PVs are not active then we cannot be + for pv in self.pvs: + if not pv.status: + return False + + # if we are missing some of our PVs we cannot be active + if len(self.pvs) != self.pvCount: + return False + + return True + + def _addDevice(self, device): + """ Add a new physical volume device to the volume group. + + XXX This is for use by device probing routines and is not + intended for modification of the VG. + """ + log_method_call(self, + self.name, + device=device.name, + status=self.status) + if not self.exists: + raise DeviceError("device does not exist", self.name) + + if not isinstance(device.format, self.pvClass): + raise ValueError("addDevice requires a PV arg") + + if self.uuid and device.format.vgUuid != self.uuid: + raise ValueError("UUID mismatch") + + if device in self.pvs: + raise ValueError("device is already a member of this VG") + + self.parents.append(device) + device.addChild() + + # now see if the VG can be activated + if len(self.parents) == self.pvCount: + self.setup() + + def _removeDevice(self, device): + """ Remove a physical volume from the volume group. + + This is for cases like clearing of preexisting partitions. + """ + log_method_call(self, + self.name, + device=device.name, + status=self.status) + try: + self.parents.remove(device) + except ValueError, e: + raise ValueError("cannot remove non-member PV device from VG") + + device.removeChild() + + def setup(self, intf=None, orig=False): + """ Open, or set up, a device. + + XXX we don't do anything like "vgchange -ay" because we don't + want all of the LVs activated, just the VG itself. + """ + log_method_call(self, self.name, orig=orig, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + if self.status: + return + + if len(self.parents) < self.pvCount: + raise DeviceError("cannot activate VG with missing PV(s)", self.name) + + self.setupParents(orig=orig) + + def teardown(self, recursive=None): + """ Close, or tear down, a device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists and not recursive: + raise DeviceError("device has not been created", self.name) + + if self.status: + lvm.vgdeactivate(self.name) + + if recursive: + self.teardownParents(recursive=recursive) + + def create(self, intf=None): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + if self.exists: + raise DeviceError("device already exists", self.name) + + w = None + if intf: + w = intf.progressWindow(_("Creating"), + _("Creating device %s") + % (self.path,), + 100, pulse = True) + try: + self.createParents() + self.setupParents() + + pv_list = [pv.path for pv in self.parents] + lvm.vgcreate(self.name, pv_list, self.peSize, progress=w) + except Exception: + raise + else: + # FIXME set / update self.uuid here + self.exists = True + self.setup() + finally: + if w: + w.pop() + + def destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + # set up the pvs since lvm needs access to them to do the vgremove + self.setupParents(orig=True) + + # this sometimes fails for some reason. + try: + lvm.vgreduce(self.name, [], rm=True) + lvm.vgremove(self.name) + except lvm.LVMError: + raise DeviceError("Could not completely remove VG", self.name) + finally: + self.exists = False + + def reduce(self, pv_list): + """ Remove the listed PVs from the VG. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + lvm.vgreduce(self.name, pv_list) + # XXX do we need to notify the kernel? + + def _addLogVol(self, lv): + """ Add an LV to this VG. """ + if lv in self._lvs: + raise ValueError("lv is already part of this vg") + + # verify we have the space, then add it + # do not verify for growing vg (because of ks) + if not lv.exists and \ + not [pv for pv in self.pvs if getattr(pv, "req_grow", None)] and \ + lv.size > self.freeSpace: + raise DeviceError("new lv is too large to fit in free space", self.name) + + self._lvs.append(lv) + + def _removeLogVol(self, lv): + """ Remove an LV from this VG. """ + if lv not in self.lvs: + raise ValueError("specified lv is not part of this vg") + + self._lvs.remove(lv) + + def _addPV(self, pv): + """ Add a PV to this VG. """ + if pv in self.pvs: + raise ValueError("pv is already part of this vg") + + # for the time being we will not allow vgextend + if self.exists: + raise DeviceError("cannot add pv to existing vg", self.name) + + self.parents.append(pv) + pv.addChild() + + def _removePV(self, pv): + """ Remove an PV from this VG. """ + if not pv in self.pvs: + raise ValueError("specified pv is not part of this vg") + + # for the time being we will not allow vgreduce + if self.exists: + raise DeviceError("cannot remove pv from existing vg", self.name) + + self.parents.remove(pv) + pv.removeChild() + + # We can't rely on lvm to tell us about our size, free space, &c + # since we could have modifications queued, unless the VG and all of + # its PVs already exist. + # + # -- liblvm may contain support for in-memory devices + + @property + def isModified(self): + """ Return True if the VG has changes queued that LVM is unaware of. """ + modified = True + if self.exists and not filter(lambda d: not d.exists, self.pvs): + modified = False + + return modified + + @property + def size(self): + """ The size of this VG """ + # TODO: just ask lvm if isModified returns False + + # sum up the sizes of the PVs and align to pesize + size = 0 + for pv in self.pvs: + size += max(0, self.align(pv.size - pv.format.peStart)) + + return size + + @property + def extents(self): + """ Number of extents in this VG """ + # TODO: just ask lvm if isModified returns False + + return self.size / self.peSize + + @property + def freeSpace(self): + """ The amount of free space in this VG (in MB). """ + # TODO: just ask lvm if isModified returns False + + # total the sizes of any LVs + used = 0 + size = self.size + log.debug("%s size is %dMB" % (self.name, size)) + for lv in self.lvs: + log.debug("lv %s uses %dMB" % (lv.name, lv.vgSpaceUsed)) + used += self.align(lv.vgSpaceUsed, roundup=True) + + free = self.size - used + log.debug("vg %s has %dMB free" % (self.name, free)) + return free + + @property + def freeExtents(self): + """ The number of free extents in this VG. """ + # TODO: just ask lvm if isModified returns False + return self.freeSpace / self.peSize + + def align(self, size, roundup=None): + """ Align a size to a multiple of physical extent size. """ + size = numeric_type(size) + + if roundup: + round = math.ceil + else: + round = math.floor + + # we want Kbytes as a float for our math + size *= 1024.0 + pesize = self.peSize * 1024.0 + return long((round(size / pesize) * pesize) / 1024) + + @property + def pvs(self): + """ A list of this VG's PVs """ + return self.parents[:] # we don't want folks changing our list + + @property + def lvs(self): + """ A list of this VG's LVs """ + return self._lvs[:] # we don't want folks changing our list + + @property + def complete(self): + """Check if the vg has all its pvs in the system + Return True if complete. + """ + return len(self.pvs) == self.pvCount or not self.exists + + +class LVMLogicalVolumeDevice(DMDevice): + """ An LVM Logical Volume """ + _type = "lvmlv" + _resizable = True + + def __init__(self, name, vgdev, size=None, uuid=None, + stripes=1, logSize=0, snapshotSpace=0, + format=None, exists=None, sysfsPath='', + grow=None, maxsize=None, percent=None): + """ Create a LVMLogicalVolumeDevice instance. + + Arguments: + + name -- the device name (generally a device node's basename) + vgdev -- volume group (LVMVolumeGroupDevice instance) + + Keyword Arguments: + + size -- the device's size (in MB) + uuid -- the device's UUID + stripes -- number of copies in the vg (>1 for mirrored lvs) + logSize -- size of log volume (for mirrored lvs) + snapshotSpace -- sum of sizes of snapshots of this lv + sysfsPath -- sysfs device path + format -- a DeviceFormat instance + exists -- indicates whether this is an existing device + + For new (non-existent) LVs only: + + grow -- whether to grow this LV + maxsize -- maximum size for growable LV (in MB) + percent -- percent of VG space to take + + """ + if isinstance(vgdev, list): + if len(vgdev) != 1: + raise ValueError("constructor requires a single LVMVolumeGroupDevice instance") + elif not isinstance(vgdev[0], LVMVolumeGroupDevice): + raise ValueError("constructor requires a LVMVolumeGroupDevice instance") + elif not isinstance(vgdev, LVMVolumeGroupDevice): + raise ValueError("constructor requires a LVMVolumeGroupDevice instance") + DMDevice.__init__(self, name, size=size, format=format, + sysfsPath=sysfsPath, parents=vgdev, + exists=exists) + + self.uuid = uuid + self.snapshotSpace = snapshotSpace + self.stripes = stripes + self.logSize = logSize + + self.req_grow = None + self.req_max_size = 0 + self.req_size = 0 + self.req_percent = 0 + + if not self.exists: + self.req_grow = grow + self.req_max_size = numeric_type(maxsize) + # XXX should we enforce that req_size be pe-aligned? + self.req_size = self._size + self.req_percent = numeric_type(percent) + + # here we go with the circular references + self.vg._addLogVol(self) + + def __str__(self): + s = DMDevice.__str__(self) + s += (" VG device = %(vgdev)r percent = %(percent)s\n" + " mirrored = %(mirrored)s stripes = %(stripes)d" + " snapshot total = %(snapshots)dMB\n" + " VG space used = %(vgspace)dMB" % + {"vgdev": self.vg, "percent": self.req_percent, + "mirrored": self.mirrored, "stripes": self.stripes, + "snapshots": self.snapshotSpace, "vgspace": self.vgSpaceUsed }) + return s + + @property + def dict(self): + d = super(LVMLogicalVolumeDevice, self).dict + if self.exists: + d.update({"mirrored": self.mirrored, "stripes": self.stripes, + "snapshots": self.snapshotSpace, + "vgspace": self.vgSpaceUsed}) + else: + d.update({"percent": self.req_percent}) + + return d + + def writeKS(self, f, preexisting=False, noformat=False, s=None): + args = ["--name=%s" % self.lvname, + "--vgname=%s" % self.vg.name] + + if self.req_grow: + args.extend(["--grow", "--size=%s" % (self.req_size or 1)]) + + if self.req_max_size > 0: + args.append("--maxsize=%s" % self.req_max_size) + else: + if self.req_percent > 0: + args.append("--percent=%s" % self.req_percent) + elif self.req_size > 0: + args.append("--size=%s" % self.req_size) + + if preexisting: + args.append("--useexisting") + if noformat: + args.append("--noformat") + + f.write("#logvol ") + self.format.writeKS(f) + f.write(" %s" % " ".join(args)) + if s: + f.write(" %s" % s) + + @property + def mirrored(self): + return self.stripes > 1 + + def _setSize(self, size): + size = self.vg.align(numeric_type(size)) + log.debug("trying to set lv %s size to %dMB" % (self.name, size)) + if size <= (self.vg.freeSpace + self._size): + self._size = size + self.targetSize = size + else: + log.debug("failed to set size: %dMB short" % (size - (self.vg.freeSpace + self._size),)) + raise ValueError("not enough free space in volume group") + + size = property(StorageDevice._getSize, _setSize) + + @property + def vgSpaceUsed(self): + return self.size * self.stripes + self.logSize + self.snapshotSpace + + @property + def vg(self): + """ This Logical Volume's Volume Group. """ + return self.parents[0] + + @property + def mapName(self): + """ This device's device-mapper map name """ + # Thank you lvm for this lovely hack. + return "%s-%s" % (self.vg.mapName, self._name.replace("-","--")) + + @property + def path(self): + """ Device node representing this device. """ + return "%s/%s" % (self._devDir, self.mapName) + + def getDMNode(self): + """ Return the dm-X (eg: dm-0) device node for this device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + return dm.dm_node_from_name(self.mapName) + + @property + def name(self): + """ This device's name. """ + return "%s-%s" % (self.vg.name, self._name) + + @property + def lvname(self): + """ The LV's name (not including VG name). """ + return self._name + + @property + def complete(self): + """ Test if vg exits and if it has all pvs. """ + return self.vg.complete + + def setup(self, intf=None, orig=False): + """ Open, or set up, a device. """ + log_method_call(self, self.name, orig=orig, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + if self.status: + return + + self.vg.setup(orig=orig) + lvm.lvactivate(self.vg.name, self._name) + + # we always probe since the device may not be set up when we want + # information about it + self._size = self.currentSize + + def teardown(self, recursive=None): + """ Close, or tear down, a device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists and not recursive: + raise DeviceError("device has not been created", self.name) + + if self.status: + if self.originalFormat.exists: + self.originalFormat.teardown() + if self.format.exists: + self.format.teardown() + udev_settle() + + if self.status: + lvm.lvdeactivate(self.vg.name, self._name) + + if recursive: + # It's likely that teardown of a VG will fail due to other + # LVs being active (filesystems mounted, &c), so don't let + # it bring everything down. + try: + self.vg.teardown(recursive=recursive) + except Exception as e: + log.debug("vg %s teardown failed; continuing" % self.vg.name) + + def create(self, intf=None): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + if self.exists: + raise DeviceError("device already exists", self.name) + + w = None + if intf: + w = intf.progressWindow(_("Creating"), + _("Creating device %s") + % (self.path,), + 100, pulse = True) + try: + self.createParents() + self.setupParents() + + # should we use --zero for safety's sake? + lvm.lvcreate(self.vg.name, self._name, self.size, progress=w) + except Exception: + raise + else: + # FIXME set / update self.uuid here + self.exists = True + self.setup() + finally: + if w: + w.pop() + + def destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + self.teardown() + # set up the vg's pvs so lvm can remove the lv + self.vg.setupParents(orig=True) + lvm.lvremove(self.vg.name, self._name) + self.exists = False + + def resize(self, intf=None): + # XXX resize format probably, right? + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + # Setup VG parents (in case they are dmraid partitions for example) + self.vg.setupParents(orig=True) + + if self.originalFormat.exists: + self.originalFormat.teardown() + if self.format.exists: + self.format.teardown() + + udev_settle() + lvm.lvresize(self.vg.name, self._name, self.size) + + def dracutSetupString(self): + # Note no mapName usage here, this is a lvm cmdline name, which + # is different (ofcourse) + return "rd_LVM_LV=%s/%s" % (self.vg.name, self._name) + + +class MDRaidArrayDevice(StorageDevice): + """ An mdraid (Linux RAID) device. """ + _type = "mdarray" + + def __init__(self, name, level=None, major=None, minor=None, size=None, + memberDevices=None, totalDevices=None, bitmap=False, + uuid=None, format=None, exists=None, + parents=None, sysfsPath=''): + """ Create a MDRaidArrayDevice instance. + + Arguments: + + name -- the device name (generally a device node's basename) + + Keyword Arguments: + + level -- the device's RAID level (a string, eg: '1' or 'raid1') + parents -- list of member devices (StorageDevice instances) + size -- the device's size (units/format TBD) + uuid -- the device's UUID + minor -- the device minor + bitmap -- whether to use a bitmap (boolean) + sysfsPath -- sysfs device path + format -- a DeviceFormat instance + exists -- indicates whether this is an existing device + """ + StorageDevice.__init__(self, name, format=format, exists=exists, + major=major, minor=minor, size=size, + parents=parents, sysfsPath=sysfsPath) + + self.level = level + if level == "container": + self._type = "mdcontainer" + elif level is not None: + self.level = mdraid.raidLevel(level) + + # For new arrays check if we have enough members + if (not exists and parents and + len(parents) < mdraid.get_raid_min_members(self.level)): + raise ValueError, _("A RAID%d set requires atleast %d members") % ( + self.level, mdraid.get_raid_min_members(self.level)) + + self.uuid = uuid + self._totalDevices = numeric_type(totalDevices) + self._memberDevices = numeric_type(memberDevices) + self.sysfsPath = "/devices/virtual/block/%s" % name + self.chunkSize = 512.0 / 1024.0 # chunk size in MB + self.superBlockSize = 2.0 # superblock size in MB + + # For container members probe size now, as we cannot determine it + # when teared down. + if self.parents and self.parents[0].type == "mdcontainer": + self._size = self.currentSize + self._type = "mdbiosraidarray" + + # FIXME: Bitmap is more complicated than this. + # It can be internal or external. External requires a filename. + self.bitmap = bitmap + + self.formatClass = get_device_format_class("mdmember") + if not self.formatClass: + raise DeviceError("cannot find class for 'mdmember'", self.name) + + if self.exists and self.uuid: + # this is a hack to work around mdadm's insistence on giving + # really high minors to arrays it has no config entry for + md_f = open("/etc/mdadm.conf", "a") + md_f.write("ARRAY %s UUID=%s\n" % (self.path, self.uuid)) + md_f.close() + + @property + def smallestMember(self): + try: + smallest = sorted(self.devices, key=lambda d: d.size)[0] + except IndexError: + smallest = None + return smallest + + @property + def size(self): + if not self.devices: + return 0 + + # For container members return probed size, as we cannot determine it + # when teared down. + if self.type == "mdbiosraidarray": + return self._size + + size = 0 + smallestMemberSize = self.smallestMember.size - self.superBlockSize + if not self.exists or not self.partedDevice: + if self.level == mdraid.RAID0: + size = self.memberDevices * smallestMemberSize + size -= size % self.chunkSize + elif self.level == mdraid.RAID1: + size = smallestMemberSize + elif self.level == mdraid.RAID4: + size = (self.memberDevices - 1) * smallestMemberSize + size -= size % self.chunkSize + elif self.level == mdraid.RAID5: + size = (self.memberDevices - 1) * smallestMemberSize + size -= size % self.chunkSize + elif self.level == mdraid.RAID6: + size = (self.memberDevices - 2) * smallestMemberSize + size -= size % self.chunkSize + elif self.level == mdraid.RAID10: + size = (self.memberDevices / 2.0) * smallestMemberSize + size -= size % self.chunkSize + else: + size = self.partedDevice.getSize() + + return size + + @property + def description(self): + if self.level == mdraid.RAID0: + levelstr = "stripe" + elif self.level == mdraid.RAID1: + levelstr = "mirror" + else: + levelstr = "raid%s" % self.level + + if self.type == "mdcontainer": + return "BIOS RAID container" + elif self.type == "mdbiosraidarray": + return "BIOS RAID set (%s)" % levelstr + else: + return "MDRAID set (%s)" % levelstr + + def __str__(self): + s = StorageDevice.__str__(self) + s += (" level = %(level)s bitmap = %(bitmap)s spares = %(spares)s\n" + " members = %(memberDevices)s\n" + " total devices = %(totalDevices)s" % + {"level": self.level, "bitmap": self.bitmap, "spares": self.spares, + "memberDevices": self.memberDevices, "totalDevices": self.totalDevices}) + return s + + @property + def dict(self): + d = super(MDRaidArrayDevice, self).dict + d.update({"level": self.level, "bitmap": self.bitmap, + "spares": self.spares, "memberDevices": self.memberDevices, + "totalDevices": self.totalDevices}) + return d + + def writeKS(self, f, preexisting=False, noformat=False, s=None): + args = ["--level=%s" % self.level, + "--device=%s" % self.name] + mems = [] + + if self.spares > 0: + args.append("--spares=%s" % self.spares) + if preexisting: + args.append("--useexisting") + if noformat: + args.append("--noformat") + + for mem in self.parents: + mems.append("raid.%s" % mem.format.uuid) + + f.write("#raid ") + self.format.writeKS(f) + f.write(" %s" % " ".join(args)) + f.write(" %s" % " ".join(mems)) + if s: + f.write(" %s" % s) + + @property + def mdadmConfEntry(self): + """ This array's mdadm.conf entry. """ + if self.level is None or self.memberDevices is None or not self.uuid: + raise DeviceError("array is not fully defined", self.name) + + # containers and the sets within must only have a UUID= parameter + if self.type == "mdcontainer" or self.type == "mdbiosraidarray": + fmt = "ARRAY %s UUID=%s\n" + return fmt % (self.path, self.uuid) + + fmt = "ARRAY %s level=raid%d num-devices=%d UUID=%s\n" + return fmt % (self.path, self.level, self.memberDevices, self.uuid) + + @property + def totalDevices(self): + """ Total number of devices in the array, including spares. """ + count = len(self.parents) + if not self.exists: + count = self._totalDevices + return count + + def _getMemberDevices(self): + return self._memberDevices + + def _setMemberDevices(self, number): + if not isinstance(number, int): + raise ValueError("memberDevices is an integer") + + if number > self.totalDevices: + raise ValueError("memberDevices cannot be greater than totalDevices") + self._memberDevices = number + + memberDevices = property(_getMemberDevices, _setMemberDevices, + doc="number of member devices") + + def _getSpares(self): + spares = 0 + if self.memberDevices is not None: + if self.totalDevices is not None: + spares = self.totalDevices - self.memberDevices + else: + spares = self.memberDevices + self._totalDevices = self.memberDevices + return spares + + def _setSpares(self, spares): + # FIXME: this is too simple to be right + if self.totalDevices > spares: + self.memberDevices = self.totalDevices - spares + + spares = property(_getSpares, _setSpares) + + def probe(self): + """ Probe for any missing information about this device. + + I'd like to avoid paying any attention to "Preferred Minor" + as it seems problematic. + """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + try: + self.devices[0].setup() + except Exception: + return + + info = mdraid.mdexamine(self.devices[0].path) + if self.level is None: + self.level = mdraid.raidLevel(info['level']) + + def updateSysfsPath(self): + """ Update this device's sysfs path. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + if self.status: + self.sysfsPath = "/devices/virtual/block/%s" % self.name + else: + self.sysfsPath = '' + + def _addDevice(self, device): + """ Add a new member device to the array. + + XXX This is for use when probing devices, not for modification + of arrays. + """ + log_method_call(self, + self.name, + device=device.name, + status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + if not isinstance(device.format, self.formatClass): + raise ValueError("invalid device format for mdraid member") + + if self.uuid and device.format.mdUuid != self.uuid: + raise ValueError("cannot add member with non-matching UUID") + + if device in self.devices: + raise ValueError("device is already a member of this array") + + # we added it, so now set up the relations + self.devices.append(device) + device.addChild() + + device.setup() + udev_settle() + try: + mdraid.mdadd(device.path) + # mdadd causes udev events + udev_settle() + except MDRaidError as e: + log.warning("failed to add member %s to md array %s: %s" + % (device.path, self.path, e)) + + if self.status: + # we always probe since the device may not be set up when we want + # information about it + self._size = self.currentSize + + def _removeDevice(self, device): + """ Remove a component device from the array. + + XXX This is for use by clearpart, not for reconfiguration. + """ + log_method_call(self, + self.name, + device=device.name, + status=self.status) + + if device not in self.devices: + raise ValueError("cannot remove non-member device from array") + + self.devices.remove(device) + device.removeChild() + + @property + def status(self): + """ This device's status. + + For now, this should return a boolean: + True the device is open and ready for use + False the device is not open + """ + # check the status in sysfs + status = False + if not self.exists: + return status + + state_file = "/sys/%s/md/array_state" % self.sysfsPath + if os.access(state_file, os.R_OK): + state_f = open(state_file) + state = state_f.read().strip() + state_f.close() + log.debug("%s state is %s" % (self.name, state)) + if state in ("clean", "active", "active-idle", "readonly", "read-auto"): + status = True + # mdcontainers have state inactive when started (clear if stopped) + if self.type == "mdcontainer" and state == "inactive": + status = True + + return status + + @property + def degraded(self): + """ Return True if the array is running in degraded mode. """ + rc = False + degraded_file = "/sys/%s/md/degraded" % self.sysfsPath + if os.access(degraded_file, os.R_OK): + deg_f = open(degraded_file) + val = deg_f.read().strip() + deg_f.close() + log.debug("%s degraded is %s" % (self.name, val)) + if val == "1": + rc = True + + return rc + + @property + def devices(self): + """ Return a list of this array's member device instances. """ + return self.parents + + def setup(self, intf=None, orig=False): + """ Open, or set up, a device. """ + log_method_call(self, self.name, orig=orig, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + if self.status: + return + + disks = [] + for member in self.devices: + member.setup(orig=orig) + disks.append(member.path) + + update_super_minor = True + if self.type == "mdcontainer" or self.type == "mdbiosraidarray": + update_super_minor = False + + mdraid.mdactivate(self.path, + members=disks, + super_minor=self.minor, + update_super_minor=update_super_minor, + uuid=self.uuid) + + udev_settle() + + # we always probe since the device may not be set up when we want + # information about it + self._size = self.currentSize + + def teardown(self, recursive=None): + """ Close, or tear down, a device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists and not recursive: + raise DeviceError("device has not been created", self.name) + + if self.status: + if self.originalFormat.exists: + self.originalFormat.teardown() + if self.format.exists: + self.format.teardown() + udev_settle() + + # Since BIOS RAID sets (containers in mdraid terminology) never change + # there is no need to stop them and later restart them. Not stopping + # (and thus also not starting) them also works around bug 523334 + if self.type == "mdcontainer" or self.type == "mdbiosraidarray": + return + + # We don't really care what the array's state is. If the device + # file exists, we want to deactivate it. mdraid has too many + # states. + if self.exists and os.path.exists(self.path): + mdraid.mddeactivate(self.path) + + if recursive: + self.teardownParents(recursive=recursive) + + def create(self, intf=None): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + if self.exists: + raise DeviceError("device already exists", self.name) + + w = None + if intf: + w = intf.progressWindow(_("Creating"), + _("Creating device %s") + % (self.path,), + 100, pulse = True) + try: + self.createParents() + self.setupParents() + + disks = [disk.path for disk in self.devices] + spares = len(self.devices) - self.memberDevices + + # Figure out format specific options + metadata="1.1" + # bitmaps are not meaningful on raid0 according to mdadm-3.0.3 + bitmap = self.level != 0 + if getattr(self.format, "mountpoint", None) == "/boot": + metadata="1.0" + bitmap=False + elif self.format.type == "swap": + bitmap=False + + mdraid.mdcreate(self.path, + self.level, + disks, + spares, + metadataVer=metadata, + bitmap=bitmap, + progress=w) + except Exception: + raise + else: + self.exists = True + # the array is automatically activated upon creation, but... + self.setup() + udev_settle() + self.updateSysfsPath() + info = udev_get_block_device(self.sysfsPath) + self.uuid = udev_device_get_md_uuid(info) + for member in self.devices: + member.mdUuid = self.uuid + finally: + if w: + w.pop() + + @property + def formatArgs(self): + formatArgs = [] + if self.format.type == "ext2": + if self.level == mdraid.RAID5: + formatArgs = ['-R', + 'stride=%d' % ((self.memberDevices - 1) * 16)] + if self.level == mdraid.RAID4: + formatArgs = ['-R', + 'stride=%d' % ((self.memberDevices - 1) * 16)] + elif self.level == mdraid.RAID0: + formatArgs = ['-R', + 'stride=%d' % (self.memberDevices * 16)] + + def destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + self.teardown() + + # The destruction of the formatting on the member devices does the + # real work, but it isn't our place to do it from here. + self.exists = False + + @property + def mediaPresent(self): + # Containers should not get any format handling done + # (the device node does not allow read / write calls) + if self.type == "mdcontainer": + return False + # BIOS RAID sets should show as present even when teared down + elif self.type == "mdbiosraidarray": + return True + else: + return self.partedDevice is not None + + @property + def model(self): + return self.description + + @property + def partitionable(self): + return self.type == "mdbiosraidarray" + + @property + def isDisk(self): + return self.type == "mdbiosraidarray" + + def dracutSetupString(self): + return "rd_MD_UUID=%s" % self.uuid + + +class DMRaidArrayDevice(DMDevice): + """ A dmraid (device-mapper RAID) device """ + _type = "dm-raid array" + _packages = ["dmraid"] + _partitionable = True + _isDisk = True + + def __init__(self, name, raidSet=None, format=None, + size=None, parents=None, sysfsPath=''): + """ Create a DMRaidArrayDevice instance. + + Arguments: + + name -- the dmraid name also the device node's basename + + Keyword Arguments: + + raidSet -- the RaidSet object from block + parents -- a list of the member devices + sysfsPath -- sysfs device path + size -- the device's size + format -- a DeviceFormat instance + """ + if isinstance(parents, list): + for parent in parents: + if not parent.format or parent.format.type != "dmraidmember": + raise ValueError("parent devices must contain dmraidmember format") + DMDevice.__init__(self, name, format=format, size=size, + parents=parents, sysfsPath=sysfsPath, exists=True) + + self.formatClass = get_device_format_class("dmraidmember") + if not self.formatClass: + raise StorageError("cannot find class for 'dmraidmember'") + + self._raidSet = raidSet + + @property + def raidSet(self): + return self._raidSet + + def _addDevice(self, device): + """ Add a new member device to the array. + + XXX This is for use when probing devices, not for modification + of arrays. + """ + log_method_call(self, self.name, device=device.name, status=self.status) + + if not self.exists: + raise DeviceError("device has not been created", self.name) + + if not isinstance(device.format, self.formatClass): + raise ValueError("invalid device format for dmraid member") + + if device in self.members: + raise ValueError("device is already a member of this array") + + # we added it, so now set up the relations + self.devices.append(device) + device.addChild() + + @property + def members(self): + return self.parents + + @property + def devices(self): + """ Return a list of this array's member device instances. """ + return self.parents + + def deactivate(self): + """ Deactivate the raid set. """ + log_method_call(self, self.name, status=self.status) + # This call already checks if the set is not active. + self._raidSet.deactivate() + + def activate(self): + """ Activate the raid set. """ + log_method_call(self, self.name, status=self.status) + # This call already checks if the set is active. + self._raidSet.activate(mknod=True) + udev_settle() + + def setup(self, intf=None, orig=False): + """ Open, or set up, a device. """ + log_method_call(self, self.name, orig=orig, status=self.status) + StorageDevice.setup(self, intf=intf, orig=orig) + self.activate() + + def teardown(self, recursive=None): + """ Close, or tear down, a device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists and not recursive: + raise DeviceError("device has not been created", self.name) + + log.debug("not tearing down dmraid device %s" % self.name) + + @property + def description(self): + return "BIOS RAID set (%s)" % self._raidSet.rs.set_type + + @property + def model(self): + return self.description + + def dracutSetupString(self): + return "rd_DM_UUID=%s" % self.name + +class MultipathDevice(DMDevice): + """ A multipath device """ + _type = "dm-multipath" + _packages = ["device-mapper-multipath"] + _partitionable = True + _isDisk = True + + def __init__(self, name, info, format=None, size=None, + parents=None, sysfsPath=''): + """ Create a MultipathDevice instance. + + Arguments: + + name -- the device name (generally a device node's basename) + info -- the udev info for this device + + Keyword Arguments: + + sysfsPath -- sysfs device path + size -- the device's size + format -- a DeviceFormat instance + parents -- a list of the backing devices (Device instances) + """ + + self._info = info + self.setupIdentity() + DMDevice.__init__(self, name, format=format, size=size, + parents=parents, sysfsPath=sysfsPath, + exists=True) + + self.config = { + 'wwid' : self.identity, + 'alias' : self.name, + 'mode' : '0600', + 'uid' : '0', + 'gid' : '0', + } + + def setupIdentity(self): + """ Adds identifying remarks to MultipathDevice object. + + May be overridden by a sub-class for e.g. RDAC handling. + """ + self._identity_short = self._info['ID_SERIAL_SHORT'] + self._identity = self._info['ID_SERIAL'] + + @property + def identity(self): + """ Get identity set with setupIdentityFromInfo() + + May be overridden by a sub-class for e.g. RDAC handling. + """ + if not hasattr(self, "_identity"): + raise RuntimeError, "setupIdentityFromInfo() has not been called." + return self._identity + + @property + def wwid(self): + identity = self._identity_short + ret = [] + while identity: + ret.append(identity[:2]) + identity = identity[2:] + return ":".join(ret) + + @property + def model(self): + if not self.parents: + return "" + return self.parents[0].model + + @property + def vendor(self): + if not self.parents: + return "" + return self.parents[0].vendor + + @property + def description(self): + return "WWID %s" % (self.wwid,) + + def addParent(self, parent): + """ Add a parent device to the mpath. """ + log_method_call(self, self.name, status=self.status) + if self.status: + self.teardown() + self.parents.append(parent) + self.setup() + else: + self.parents.append(parent) + + def setupPartitions(self): + log_method_call(self, name=self.name, kids=self.kids) + rc = iutil.execWithRedirect("kpartx", + ["-a", "-p", "p", "/dev/mapper/%s" % self.name], + stdout = "/dev/tty5", + stderr = "/dev/tty5") + if rc: + raise MPathError("multipath partition activation failed for '%s'" % + self.name) + udev_settle() + + def teardown(self, recursive=None): + """ Tear down the mpath device. """ + log_method_call(self, self.name, status=self.status) + + if not self.exists and not recursive: + raise DeviceError("device has not been created", self.name) + + if self.exists and os.path.exists(self.path): + #self.teardownPartitions() + #rc = iutil.execWithRedirect("multipath", + # ['-f', self.name], + # stdout = "/dev/tty5", + # stderr = "/dev/tty5") + #if rc: + # raise MPathError("multipath deactivation failed for '%s'" % + # self.name) + bdev = block.getDevice(self.name) + devmap = block.getMap(major=bdev[0], minor=bdev[1]) + if devmap.open_count: + return + try: + block.removeDeviceMap(devmap) + except Exception as e: + raise MPathError("failed to tear down multipath device %s: %s" + % (self.name, e)) + + if recursive: + self.teardownParents(recursive=recursive) + + def setup(self, intf=None, orig=False): + """ Open, or set up, a device. """ + log_method_call(self, self.name, orig=orig, status=self.status) + + if self.status: + return + + StorageDevice.setup(self, intf=intf, orig=orig) + udev_settle() + rc = iutil.execWithRedirect("multipath", + [self.name], + stdout = "/dev/tty5", + stderr = "/dev/tty5") + if rc: + raise MPathError("multipath activation failed for '%s'" % + self.name) + udev_settle() + self.setupPartitions() + udev_settle() + +class NoDevice(StorageDevice): + """ A nodev device for nodev filesystems like tmpfs. """ + _type = "nodev" + + def __init__(self, format=None): + """ Create a NoDevice instance. + + Arguments: + + Keyword Arguments: + + format -- a DeviceFormat instance + """ + if format: + name = format.type + else: + name = "none" + + StorageDevice.__init__(self, name, format=format) + + @property + def path(self): + """ Device node representing this device. """ + return self.name + + def probe(self): + """ Probe for any missing information about this device. """ + log_method_call(self, self.name, status=self.status) + + def setup(self, intf=None, orig=False): + """ Open, or set up, a device. """ + log_method_call(self, self.name, orig=orig, status=self.status) + + def teardown(self, recursive=False): + """ Close, or tear down, a device. """ + log_method_call(self, self.name, status=self.status) + + def create(self, intf=None): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + self.setupParents() + + def destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + + +class FileDevice(StorageDevice): + """ A file on a filesystem. + + This exists because of swap files. + """ + _type = "file" + _devDir = "" + + def __init__(self, path, format=None, size=None, + exists=None, parents=None): + """ Create a FileDevice instance. + + Arguments: + + path -- full path to the file + + Keyword Arguments: + + format -- a DeviceFormat instance + size -- the file size (units TBD) + parents -- a list of required devices (Device instances) + exists -- indicates whether this is an existing device + """ + StorageDevice.__init__(self, path, format=format, size=size, + exists=exists, parents=parents) + + def probe(self): + """ Probe for any missing information about this device. """ + pass + + @property + def fstabSpec(self): + return self.name + + @property + def path(self): + path = self.name + root = "" + try: + status = self.parents[0].format.status + except (AttributeError, IndexError): + status = False + + if status: + # this is the actual active mountpoint + root = self.parents[0].format._mountpoint + # trim the mountpoint down to the chroot since we already have + # the otherwise fully-qualified path + mountpoint = self.parents[0].format.mountpoint + if mountpoint.endswith("/"): + mountpoint = mountpoint[:-1] + if mountpoint: + root = root[:-len(mountpoint)] + + return os.path.normpath("%s/%s" % (root, path)) + + def setup(self, intf=None, orig=False): + StorageDevice.setup(self, orig=orig) + if self.format and self.format.exists and not self.format.status: + self.format.device = self.path + + for parent in self.parents: + if orig: + parent.originalFormat.setup() + else: + parent.format.setup() + + def teardown(self, recursive=None): + StorageDevice.teardown(self) + if self.format and self.format.exists and not self.format.status: + self.format.device = self.path + + def create(self, intf=None): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + if self.exists: + raise DeviceError("device already exists", self.name) + + w = None + if intf: + w = intf.waitWindow(_("Creating"), + _("Creating file %s") % (self.path,)) + + try: + # this only checks that parents exist + self.createParents() + self.setupParents() + + fd = os.open(self.path, os.O_RDWR) + buf = '\0' * 1024 * 1024 * self.size + os.write(fd, buf) + except (OSError, TypeError) as e: + log.error("error writing out %s: %s" % (self.path, e)) + raise DeviceError(e, self.name) + else: + self.exists = True + finally: + os.close(fd) + if w: + w.pop() + + def destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + os.unlink(self.path) + self.exists = False + + +class DirectoryDevice(FileDevice): + """ A directory on a filesystem. + + This exists because of bind mounts. + """ + _type = "directory" + + def create(self): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + if self.exists: + raise DeviceError("device already exists", self.name) + + self.createParents() + self.setupParents() + try: + iutil.mkdirChain(self.path) + except Exception, e: + raise DeviceError(e, self.name) + + self.exists = True + + def destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + os.unlink(self.path) + self.exists = False + + +class iScsiDiskDevice(DiskDevice, NetworkStorageDevice): + """ An iSCSI disk. """ + _type = "iscsi" + _packages = ["iscsi-initiator-utils", "dracut-network"] + + def __init__(self, device, **kwargs): + self.node = kwargs.pop("node") + self.ibft = kwargs.pop("ibft") + self.initiator = kwargs.pop("initiator") + DiskDevice.__init__(self, device, **kwargs) + NetworkStorageDevice.__init__(self, host_address=self.node.address) + log.debug("created new iscsi disk %s %s:%d" % (self.node.name, self.node.address, self.node.port)) + + def dracutSetupString(self): + if self.ibft: + return "iscsi_firmware" + + netroot="netroot=iscsi:" + auth = self.node.getAuth() + if auth: + netroot += "%s:%s" % (auth.username, auth.password) + if len(auth.reverse_username) or len(auth.reverse_password): + netroot += ":%s:%s" % (auth.reverse_username, + auth.reverse_password) + + netroot += "@%s::%d::%s" % (self.node.address, self.node.port, + self.node.name) + + netroot += " iscsi_initiator=%s" % self.initiator + + return netroot + +class FcoeDiskDevice(DiskDevice, NetworkStorageDevice): + """ An FCoE disk. """ + _type = "fcoe" + _packages = ["fcoe-utils", "dracut-network"] + + def __init__(self, device, **kwargs): + self.nic = kwargs.pop("nic") + self.identifier = kwargs.pop("identifier") + DiskDevice.__init__(self, device, **kwargs) + NetworkStorageDevice.__init__(self, nic=self.nic) + log.debug("created new fcoe disk %s @ %s" % (device, self.nic)) + + def dracutSetupString(self): + dcb = True + + from .fcoe import fcoe + for nic, dcb in fcoe().nics: + if nic == self.nic: + break + + if dcb: + dcbOpt = "dcb" + else: + dcbOpt = "nodcb" + + return "netroot=fcoe:%s:%s" % (self.nic, dcbOpt) + + +class OpticalDevice(StorageDevice): + """ An optical drive, eg: cdrom, dvd+r, &c. + + XXX Is this useful? + """ + _type = "cdrom" + + def __init__(self, name, major=None, minor=None, exists=None, + format=None, parents=None, sysfsPath='', vendor="", + model=""): + StorageDevice.__init__(self, name, format=format, + major=major, minor=minor, exists=True, + parents=parents, sysfsPath=sysfsPath, + vendor=vendor, model=model) + + @property + def mediaPresent(self): + """ Return a boolean indicating whether or not the device contains + media. + """ + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + try: + fd = os.open(self.path, os.O_RDONLY) + except OSError as e: + # errno 123 = No medium found + if e.errno == 123: + return False + else: + return True + else: + os.close(fd) + return True + + def eject(self): + """ Eject the drawer. """ + import _isys + + log_method_call(self, self.name, status=self.status) + if not self.exists: + raise DeviceError("device has not been created", self.name) + + #try to umount and close device before ejecting + self.teardown() + + # Make a best effort attempt to do the eject. If it fails, it's not + # critical. + fd = os.open(self.path, os.O_RDONLY | os.O_NONBLOCK) + + try: + _isys.ejectcdrom(fd) + except SystemError as e: + log.warning("error ejecting cdrom %s: %s" % (self.name, e)) + + os.close(fd) + + +class ZFCPDiskDevice(DiskDevice): + """ A mainframe ZFCP disk. """ + _type = "zfcp" + + def __init__(self, device, **kwargs): + self.hba_id = kwargs.pop("hba_id") + self.wwpn = kwargs.pop("wwpn") + self.fcp_lun = kwargs.pop("fcp_lun") + DiskDevice.__init__(self, device, **kwargs) + + def __str__(self): + s = DiskDevice.__str__(self) + s += (" hba_id = %(hba_id)s wwpn = %(wwpn)s fcp_lun = %(fcp_lun)s" % + {"hba_id": self.hba_id, + "wwpn": self.wwpn, + "fcp_lun": self.fcp_lun}) + return s + + def dracutSetupString(self): + return "rd_ZFCP=%s,%s,%s" % (self.hba_id, self.wwpn, self.fcp_lun,) + + +class DASDDevice(DiskDevice): + """ A mainframe DASD. """ + _type = "dasd" + + def __init__(self, device, **kwargs): + self.busid = kwargs.pop('busid') + self.opts = kwargs.pop('opts') + self.dasd = kwargs.pop('dasd') + DiskDevice.__init__(self, device, **kwargs) + + if self.dasd: + self.dasd.addDASD(self) + + def getOpts(self): + return map(lambda (k, v): "%s=%s" % (k, v,), self.opts.items()) + + def dracutSetupString(self): + args = ["rd_DASD=%s" % (self.busid,)] + self.getOpts() + return ",".join(args) + + +class NFSDevice(StorageDevice, NetworkStorageDevice): + """ An NFS device """ + _type = "nfs" + _packages = ["dracut-network"] + + def __init__(self, device, format=None, parents=None): + # we could make host/ip, path, &c but will anything use it? + StorageDevice.__init__(self, device, format=format, parents=parents) + NetworkStorageDevice.__init__(self, device.split(":")[0]) + + @property + def path(self): + """ Device node representing this device. """ + return self.name + + def setup(self, intf=None, orig=False): + """ Open, or set up, a device. """ + log_method_call(self, self.name, orig=orig, status=self.status) + + def teardown(self, recursive=None): + """ Close, or tear down, a device. """ + log_method_call(self, self.name, status=self.status) + + def create(self, intf=None): + """ Create the device. """ + log_method_call(self, self.name, status=self.status) + self.createParents() + self.setupParents() + + def destroy(self): + """ Destroy the device. """ + log_method_call(self, self.name, status=self.status) diff --git a/storage/devicetree.py b/storage/devicetree.py new file mode 100644 index 0000000..a71b5b0 --- /dev/null +++ b/storage/devicetree.py @@ -0,0 +1,2259 @@ +# devicetree.py +# Device management for anaconda's storage configuration module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +import os +import stat +import block +import re + +from errors import * +from devices import * +from deviceaction import * +from partitioning import shouldClear +from pykickstart.constants import * +import formats +import devicelibs.mdraid +import devicelibs.dm +import devicelibs.lvm +import devicelibs.mpath +from udev import * +from .storage_log import log_method_call +import iutil + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + +def getLUKSPassphrase(intf, device, globalPassphrase): + """ Obtain a passphrase for a LUKS encrypted block device. + + The format's mapping name must already be set and the backing + device must already be set up before calling this function. + + If successful, this function leaves the device mapped. + + Return value is a two-tuple: (passphrase, isglobal) + + passphrase is the passphrase string, if obtained + isglobal is a boolean indicating whether the passphrase is global + + Either or both can be None, depending on the outcome. + """ + if device.format.type != "luks": + # this function only works on luks devices + raise ValueError("not a luks device") + + if not device.status: + # the device should have already been set up + raise RuntimeError("device is not set up") + + if device.format.status: + # the device is already mapped + raise RuntimeError("device is already mapped") + + if not device.format.configured and globalPassphrase: + # try the given passphrase first + device.format.passphrase = globalPassphrase + + try: + device.format.setup() + except CryptoError as e: + device.format.passphrase = None + else: + # we've opened the device so we're done. + return (globalPassphrase, False) + + if not intf: + return (None, None) + + buttons = [_("Back"), _("Continue")] + passphrase_incorrect = False + while True: + if passphrase_incorrect: + # TODO: add a flag to passphraseEntryWindow to say the last + # passphrase was incorrect so try again + passphrase_incorrect = False + (passphrase, isglobal) = intf.passphraseEntryWindow(device.name) + if not passphrase: + rc = intf.messageWindow(_("Confirm"), + _("Are you sure you want to skip " + "entering a passphrase for device " + "%s?\n\n" + "If you skip this step the " + "device's contents will not " + "be available during " + "installation.") % device.name, + type = "custom", + default = 0, + custom_buttons = buttons) + if rc == 0: + continue + else: + passphrase = None + isglobal = None + log.info("skipping passphrase for %s" % (device.name,)) + break + + device.format.passphrase = passphrase + + try: + device.format.setup() + except CryptoError as e: + device.format.passphrase = None + passphrase_incorrect = True + else: + # we've opened the device so we're done. + break + + return (passphrase, isglobal) + + +class DeviceTree(object): + """ A quasi-tree that represents the devices in the system. + + The tree contains a list of device instances, which does not + necessarily reflect the actual state of the system's devices. + DeviceActions are used to perform modifications to the tree, + except when initially populating the tree. + + DeviceAction instances are registered, possibly causing the + addition or removal of Device instances to/from the tree. The + DeviceActions are all reversible up to the time their execute + method has been called. + + Only one action of any given type/object pair should exist for + any given device at any given time. + + DeviceAction instances can only be registered for leaf devices, + except for resize actions. + """ + + def __init__(self, intf=None, ignored=[], exclusive=[], type=CLEARPART_TYPE_NONE, + clear=[], zeroMbr=None, reinitializeDisks=None, protected=[], + passphrase=None, luksDict=None, iscsi=None, dasd=None): + # internal data members + self._devices = [] + self._actions = [] + + # indicates whether or not the tree has been fully populated + self.populated = False + + self.intf = intf + self.exclusiveDisks = exclusive + self.clearPartType = type + self.clearPartDisks = clear + self.zeroMbr = zeroMbr + self.reinitializeDisks = reinitializeDisks + self.iscsi = iscsi + self.dasd = dasd + + # protected device specs as provided by the user + self.protectedDevSpecs = protected + + # names of protected devices at the time of tree population + self.protectedDevNames = [] + + self.unusedRaidMembers = [] + + self.__multipaths = {} + self.__multipathConfigWriter = devicelibs.mpath.MultipathConfigWriter() + + self.__passphrase = passphrase + self.__luksDevs = {} + if luksDict and isinstance(luksDict, dict): + self.__luksDevs = luksDict + self._ignoredDisks = [] + for disk in ignored: + self.addIgnoredDisk(disk) + self.immutableDevices = [] + lvm.lvm_cc_resetFilter() + + def addIgnoredDisk(self, disk): + self._ignoredDisks.append(disk) + lvm.lvm_cc_addFilterRejectRegexp(disk) + + def pruneActions(self): + """ Prune loops and redundant actions from the queue. """ + # handle device destroy actions + actions = self.findActions(type="destroy", object="device") + for a in actions: + if a not in self._actions: + # we may have removed some of the actions in a previous + # iteration of this loop + continue + + log.debug("action '%s' (%s)" % (a, id(a))) + destroys = self.findActions(devid=a.device.id, + type="destroy", + object="device") + + creates = self.findActions(devid=a.device.id, + type="create", + object="device") + + # If the device is not preexisting, we remove all actions up + # to and including the last destroy action. + # If the device is preexisting, we remove all actions from + # after the first destroy action up to and including the last + # destroy action. + loops = [] + first_destroy_idx = None + first_create_idx = None + stop_action = None + start = None + if len(destroys) > 1: + # there are multiple destroy actions for this device + loops = destroys + first_destroy_idx = self._actions.index(loops[0]) + start = self._actions.index(a) + 1 + stop_action = destroys[-1] + + if creates: + first_create_idx = self._actions.index(creates[0]) + if not loops or first_destroy_idx > first_create_idx: + # this device is not preexisting + start = first_create_idx + stop_action = destroys[-1] + + if start is None: + continue + + # now we remove all actions on this device between the start + # index (into self._actions) and stop_action. + dev_actions = self.findActions(path=a.device.path) + for rem in dev_actions: + end = self._actions.index(stop_action) + if start <= self._actions.index(rem) <= end: + log.debug(" removing action '%s' (%s)" % (rem, id(rem))) + self._actions.remove(rem) + + if rem == stop_action: + break + + # device create actions + actions = self.findActions(type="create", object="device") + for a in actions: + if a not in self._actions: + # we may have removed some of the actions in a previous + # iteration of this loop + continue + + log.debug("action '%s' (%s)" % (a, id(a))) + creates = self.findActions(devid=a.device.id, + type="create", + object="device") + + destroys = self.findActions(devid=a.device.id, + type="destroy", + object="device") + + # If the device is preexisting, we remove everything between + # the first destroy and the last create. + # If the device is not preexisting, we remove everything up to + # the last create. + loops = [] + first_destroy_idx = None + first_create_idx = None + stop_action = None + start = None + if len(creates) > 1: + # there are multiple create actions for this device + loops = creates + first_create_idx = self._actions.index(loops[0]) + start = 0 + stop_action = creates[-1] + + if destroys: + first_destroy_idx = self._actions.index(destroys[0]) + if not loops or first_create_idx > first_destroy_idx: + # this device is preexisting + start = first_destroy_idx + 1 + stop_action = creates[-1] + + if start is None: + continue + + # remove all actions on this from after the first destroy up + # to the last create + dev_actions = self.findActions(devid=a.device.id) + for rem in dev_actions: + if rem == stop_action: + break + + end = self._actions.index(stop_action) + if start <= self._actions.index(rem) < end: + log.debug(" removing action '%s' (%s)" % (rem, id(rem))) + self._actions.remove(rem) + + # device resize actions + actions = self.findActions(type="resize", object="device") + for a in actions: + if a not in self._actions: + # we may have removed some of the actions in a previous + # iteration of this loop + continue + + log.debug("action '%s' (%s)" % (a, id(a))) + loops = self.findActions(devid=a.device.id, + type="resize", + object="device") + + if len(loops) == 1: + continue + + # remove all but the last resize action on this device + for rem in loops[:-1]: + log.debug(" removing action '%s' (%s)" % (rem, id(rem))) + self._actions.remove(rem) + + # format destroy + # XXX I don't think there's a way for these loops to happen + actions = self.findActions(type="destroy", object="format") + for a in actions: + if a not in self._actions: + # we may have removed some of the actions in a previous + # iteration of this loop + continue + + log.debug("action '%s' (%s)" % (a, id(a))) + destroys = self.findActions(devid=a.device.id, + type="destroy", + object="format") + + creates = self.findActions(devid=a.device.id, + type="create", + object="format") + + # If the format is not preexisting, we remove all actions up + # to and including the last destroy action. + # If the format is preexisting, we remove all actions from + # after the first destroy action up to and including the last + # destroy action. + loops = [] + first_destroy_idx = None + first_create_idx = None + stop_action = None + start = None + if len(destroys) > 1: + # there are multiple destroy actions for this format + loops = destroys + first_destroy_idx = self._actions.index(loops[0]) + start = self._actions.index(a) + 1 + stop_action = destroys[-1] + + if creates: + first_create_idx = self._actions.index(creates[0]) + if not loops or first_destroy_idx > first_create_idx: + # this format is not preexisting + start = first_create_idx + stop_action = destroys[-1] + + if start is None: + continue + + # now we remove all actions on this device's format between + # the start index (into self._actions) and stop_action. + dev_actions = self.findActions(devid=a.device.id, + object="format") + for rem in dev_actions: + end = self._actions.index(stop_action) + if start <= self._actions.index(rem) <= end: + log.debug(" removing action '%s' (%s)" % (rem, id(rem))) + self._actions.remove(rem) + + if rem == stop_action: + break + + # format create + # XXX I don't think there's a way for these loops to happen + actions = self.findActions(type="create", object="format") + for a in actions: + if a not in self._actions: + # we may have removed some of the actions in a previous + # iteration of this loop + continue + + log.debug("action '%s' (%s)" % (a, id(a))) + creates = self.findActions(devid=a.device.id, + type="create", + object="format") + + destroys = self.findActions(devid=a.device.id, + type="destroy", + object="format") + + # If the format is preexisting, we remove everything between + # the first destroy and the last create. + # If the format is not preexisting, we remove everything up to + # the last create. + loops = [] + first_destroy_idx = None + first_create_idx = None + stop_action = None + start = None + if len(creates) > 1: + # there are multiple create actions for this format + loops = creates + first_create_idx = self._actions.index(loops[0]) + start = 0 + stop_action = creates[-1] + + if destroys: + first_destroy_idx = self._actions.index(destroys[0]) + if not loops or first_create_idx > first_destroy_idx: + # this format is preexisting + start = first_destroy_idx + 1 + stop_action = creates[-1] + + if start is None: + continue + + # remove all actions on this from after the first destroy up + # to the last create + dev_actions = self.findActions(devid=a.device.id, + object="format") + for rem in dev_actions: + if rem == stop_action: + break + + end = self._actions.index(stop_action) + if start <= self._actions.index(rem) < end: + log.debug(" removing action '%s' (%s)" % (rem, id(rem))) + self._actions.remove(rem) + + # format resize + actions = self.findActions(type="resize", object="format") + for a in actions: + if a not in self._actions: + # we may have removed some of the actions in a previous + # iteration of this loop + continue + + log.debug("action '%s' (%s)" % (a, id(a))) + loops = self.findActions(devid=a.device.id, + type="resize", + object="format") + + if len(loops) == 1: + continue + + # remove all but the last resize action on this format + for rem in loops[:-1]: + log.debug(" removing action '%s' (%s)" % (rem, id(rem))) + self._actions.remove(rem) + + # format migrate + # XXX I don't think there's away for these loops to occur + actions = self.findActions(type="migrate", object="format") + for a in actions: + if a not in self._actions: + # we may have removed some of the actions in a previous + # iteration of this loop + continue + + log.debug("action '%s' (%s)" % (a, id(a))) + loops = self.findActions(devid=a.device.id, + type="migrate", + object="format") + + if len(loops) == 1: + continue + + # remove all but the last migrate action on this format + for rem in loops[:-1]: + log.debug(" removing action '%s' (%s)" % (rem, id(rem))) + self._actions.remove(rem) + + def processActions(self, dryRun=None): + """ Execute all registered actions. """ + # in most cases the actions will already be sorted because of the + # rules for registration, but let's not rely on that + def cmpActions(a1, a2): + ret = 0 + if a1.isDestroy() and a2.isDestroy(): + if a1.device.path == a2.device.path: + # if it's the same device, destroy the format first + if a1.isFormat() and a2.isFormat(): + ret = 0 + elif a1.isFormat() and not a2.isFormat(): + ret = -1 + elif not a1.isFormat() and a2.isFormat(): + ret = 1 + elif a1.device.dependsOn(a2.device): + ret = -1 + elif a2.device.dependsOn(a1.device): + ret = 1 + # generally destroy partitions after lvs, vgs, &c + elif isinstance(a1.device, PartitionDevice) and \ + isinstance(a2.device, PartitionDevice): + if a1.device.disk == a2.device.disk: + ret = cmp(a2.device.partedPartition.number, + a1.device.partedPartition.number) + else: + ret = cmp(a2.device.name, a1.device.name) + elif isinstance(a1.device, PartitionDevice) and \ + a2.device.partitioned: + ret = 1 + elif isinstance(a2.device, PartitionDevice) and \ + a1.device.partitioned: + ret = -1 + else: + ret = 0 + elif a1.isDestroy(): + ret = -1 + elif a2.isDestroy(): + ret = 1 + elif a1.isResize() and a2.isResize(): + if a1.device.path == a2.device.path: + if a1.obj == a2.obj: + ret = 0 + elif a1.isFormat() and not a2.isFormat(): + # same path, one device, one format + if a1.isGrow(): + ret = 1 + else: + ret = -1 + elif not a1.isFormat() and a2.isFormat(): + # same path, one device, one format + if a1.isGrow(): + ret = -1 + else: + ret = 1 + else: + ret = cmp(a1.device.name, a2.device.name) + elif a1.device.dependsOn(a2.device): + if a1.isGrow(): + ret = 1 + else: + ret = -1 + elif a2.device.dependsOn(a1.device): + if a1.isGrow(): + ret = -1 + else: + ret = 1 + elif isinstance(a1.device, PartitionDevice) and \ + isinstance(a2.device, PartitionDevice): + ret = cmp(a1.device.name, a2.device.name) + elif isinstance(a1.device, PartitionDevice) and \ + a2.device.partitioned: + if a1.isGrow(): + ret = -1 + else: + ret = 1 + elif isinstance(a2.device, PartitionDevice) and \ + a1.device.partitioned: + if a2.isGrow(): + ret = 1 + else: + ret = -1 + else: + ret = 0 + elif a1.isResize(): + ret = -1 + elif a2.isResize(): + ret = 1 + elif a1.isCreate() and a2.isCreate(): + if a1.device.path == a2.device.path: + if a1.obj == a2.obj: + ret = 0 + if a1.isFormat(): + ret = 1 + elif a2.isFormat(): + ret = -1 + else: + ret = 0 + elif a1.device.dependsOn(a2.device): + ret = 1 + elif a2.device.dependsOn(a1.device): + ret = -1 + # generally create partitions before other device types + elif isinstance(a1.device, PartitionDevice) and \ + isinstance(a2.device, PartitionDevice): + if a1.device.disk == a2.device.disk: + ret = cmp(a1.device.partedPartition.number, + a2.device.partedPartition.number) + else: + ret = cmp(a1.device.name, a2.device.name) + elif isinstance(a1.device, PartitionDevice) and \ + a2.device.partitioned: + ret = 1 + elif isinstance(a2.device, PartitionDevice) and \ + a1.device.partitioned: + ret = -1 + elif isinstance(a1.device, PartitionDevice) and \ + not isinstance(a2.device, PartitionDevice): + ret = -1 + elif isinstance(a2.device, PartitionDevice) and \ + not isinstance(a1.device, PartitionDevice): + ret = 1 + else: + ret = 0 + elif a1.isCreate(): + ret = -1 + elif a2.isCreate(): + ret = 1 + elif a1.isMigrate() and a2.isMigrate(): + if a1.device.path == a2.device.path: + ret = 0 + elif a1.device.dependsOn(a2.device): + ret = 1 + elif a2.device.dependsOn(a1.device): + ret = -1 + elif isinstance(a1.device, PartitionDevice) and \ + isinstance(a2.device, PartitionDevice): + ret = cmp(a1.device.name, a2.device.name) + else: + ret = cmp(a1.device.name, a2.device.name) + else: + ret = 0 + + log.debug("cmp: %d -- %s | %s" % (ret, a1, a2)) + return ret + + log.debug("resetting parted disks...") + for device in self.devices: + if device.partitioned: + device.format.resetPartedDisk() + if device.originalFormat.type == "disklabel" and \ + device.originalFormat != device.format: + device.originalFormat.resetPartedDisk() + + # reget parted.Partition for remaining preexisting devices + for device in self.devices: + if isinstance(device, PartitionDevice) and device.exists: + device.resetPartedPartition() + + # reget parted.Partition for existing devices we're removing + for action in self._actions: + if isinstance(action.device, PartitionDevice) and \ + action.device.exists: + action.device.resetPartedPartition() + + # setup actions to create any extended partitions we added + # + # XXX At this point there can be duplicate partition paths in the + # tree (eg: non-existent sda6 and previous sda6 that will become + # sda5 in the course of partitioning), so we access the list + # directly here. + for device in self._devices: + if isinstance(device, PartitionDevice) and \ + device.isExtended and not device.exists: + # don't properly register the action since the device is + # already in the tree + self._actions.append(ActionCreateDevice(device)) + + for action in self._actions: + log.debug("action: %s" % action) + + log.debug("pruning action queue...") + self.pruneActions() + for action in self._actions: + log.debug("action: %s" % action) + + log.debug("sorting actions...") + self._actions.sort(cmp=cmpActions) + for action in self._actions: + log.debug("action: %s" % action) + + for action in self._actions: + log.info("executing action: %s" % action) + if not dryRun: + try: + action.execute(intf=self.intf) + except DiskLabelCommitError: + # it's likely that a previous format destroy action + # triggered setup of an lvm or md device. + self.teardownAll() + action.execute(intf=self.intf) + + udev_settle() + for device in self._devices: + # make sure we catch any renumbering parted does + if device.exists and isinstance(device, PartitionDevice): + device.updateName() + device.format.device = device.path + + def _addDevice(self, newdev): + """ Add a device to the tree. + + Raise ValueError if the device's identifier is already + in the list. + """ + if newdev.path in [d.path for d in self._devices] and \ + not isinstance(newdev, NoDevice): + raise ValueError("device is already in tree") + + # make sure this device's parent devices are in the tree already + for parent in newdev.parents: + if parent not in self._devices: + raise DeviceTreeError("parent device not in tree") + + self._devices.append(newdev) + log.debug("added %s %s (id %d) to device tree" % (newdev.type, + newdev.name, + newdev.id)) + + def _removeDevice(self, dev, force=None, moddisk=True): + """ Remove a device from the tree. + + Only leaves may be removed. + """ + if dev not in self._devices: + raise ValueError("Device '%s' not in tree" % dev.name) + + if not dev.isleaf and not force: + log.debug("%s has %d kids" % (dev.name, dev.kids)) + raise ValueError("Cannot remove non-leaf device '%s'" % dev.name) + + # if this is a partition we need to remove it from the parted.Disk + if moddisk and isinstance(dev, PartitionDevice) and \ + dev.disk is not None: + # if this partition hasn't been allocated it could not have + # a disk attribute + if dev.partedPartition.type == parted.PARTITION_EXTENDED and \ + len(dev.disk.format.logicalPartitions) > 0: + raise ValueError("Cannot remove extended partition %s. " + "Logical partitions present." % dev.name) + + dev.disk.format.removePartition(dev.partedPartition) + + # adjust all other PartitionDevice instances belonging to the + # same disk so the device name matches the potentially altered + # name of the parted.Partition + for device in self._devices: + if isinstance(device, PartitionDevice) and \ + device.disk == dev.disk: + device.updateName() + + self._devices.remove(dev) + log.debug("removed %s %s (id %d) from device tree" % (dev.type, + dev.name, + dev.id)) + + for parent in dev.parents: + # Will this cause issues with garbage collection? + # Do we care about garbage collection? At all? + parent.removeChild() + + def registerAction(self, action): + """ Register an action to be performed at a later time. + + Modifications to the Device instance are handled before we + get here. + """ + if (action.isDestroy() or action.isResize() or \ + (action.isCreate() and action.isFormat())) and \ + action.device not in self._devices: + raise DeviceTreeError("device is not in the tree") + elif (action.isCreate() and action.isDevice()): + # this allows multiple create actions w/o destroy in between; + # we will clean it up before processing actions + #raise DeviceTreeError("device is already in the tree") + if action.device in self._devices: + self._removeDevice(action.device) + for d in self._devices: + if d.path == action.device.path: + self._removeDevice(d) + + if action.isCreate() and action.isDevice(): + self._addDevice(action.device) + elif action.isDestroy() and action.isDevice(): + self._removeDevice(action.device) + elif action.isCreate() and action.isFormat(): + if isinstance(action.device.format, formats.fs.FS) and \ + action.device.format.mountpoint in self.filesystems: + raise DeviceTreeError("mountpoint already in use") + + log.debug("registered action: %s" % action) + self._actions.append(action) + + def cancelAction(self, action): + """ Cancel a registered action. + + This will unregister the action and do any required + modifications to the device list. + + Actions all operate on a Device, so we can use the devices + to determine dependencies. + """ + if action.isCreate() and action.isDevice(): + # remove the device from the tree + self._removeDevice(action.device) + elif action.isDestroy() and action.isDevice(): + # add the device back into the tree + self._addDevice(action.device) + elif action.isFormat() and \ + (action.isCreate() or action.isMigrate() or action.isResize()): + action.cancel() + + self._actions.remove(action) + + def findActions(self, device=None, type=None, object=None, path=None, + devid=None): + """ Find all actions that match all specified parameters. + + Keyword arguments: + + device -- device to match (Device, or None to match any) + type -- action type to match (string, or None to match any) + object -- operand type to match (string, or None to match any) + path -- device path to match (string, or None to match any) + + """ + if device is None and type is None and object is None and \ + path is None and devid is None: + return self._actions[:] + + # convert the string arguments to the types used in actions + _type = action_type_from_string(type) + _object = action_object_from_string(object) + + actions = [] + for action in self._actions: + if device is not None and action.device != device: + continue + + if _type is not None and action.type != _type: + continue + + if _object is not None and action.obj != _object: + continue + + if path is not None and action.device.path != path: + continue + + if devid is not None and action.device.id != devid: + continue + + actions.append(action) + + return actions + + def getDependentDevices(self, dep): + """ Return a list of devices that depend on dep. + + The list includes both direct and indirect dependents. + """ + dependents = [] + + # special handling for extended partitions since the logical + # partitions and their deps effectively depend on the extended + logicals = [] + if isinstance(dep, PartitionDevice) and dep.partType and \ + dep.isExtended: + # collect all of the logicals on the same disk + for part in self.getDevicesByInstance(PartitionDevice): + if part.partType and part.isLogical and part.disk == dep.disk: + logicals.append(part) + + for device in self.devices: + if device.dependsOn(dep): + dependents.append(device) + else: + for logical in logicals: + if device.dependsOn(logical): + dependents.append(device) + break + + return dependents + + def isIgnored(self, info): + """ Return True if info is a device we should ignore. + + Arguments: + + info -- a dict representing a udev db entry + + TODO: + + - filtering of SAN/FC devices + - filtering by driver? + + """ + sysfs_path = udev_device_get_sysfs_path(info) + name = udev_device_get_name(info) + if not sysfs_path: + return None + + if name in self._ignoredDisks: + return True + + # Special handling for mdraid external metadata sets (mdraid BIOSRAID): + # 1) The containers are intermediate devices which will never be + # in exclusiveDisks + # 2) Sets get added to exclusive disks with their dmraid set name by + # the filter ui. Note that making the ui use md names instead is not + # possible as the md names are simpy md# and we cannot predict the # + if udev_device_get_md_level(info) == "container": + return False + + if udev_device_get_md_container(info) and \ + udev_device_get_md_name(info): + md_name = udev_device_get_md_name(info) + for i in range(0, len(self.exclusiveDisks)): + if re.match("isw_[a-z]*_%s" % md_name, self.exclusiveDisks[i]): + self.exclusiveDisks[i] = name + return False + + if udev_device_is_disk(info) and \ + not udev_device_is_md(info) and \ + not udev_device_is_dm(info) and \ + not udev_device_is_biosraid(info) and \ + not udev_device_is_multipath_member(info): + if self.exclusiveDisks and name not in self.exclusiveDisks: + self.addIgnoredDisk(name) + return True + + # Ignore loop and ram devices, we normally already skip these in + # udev.py: enumerate_block_devices(), but we can still end up trying + # to add them to the tree when they are slaves of other devices, this + # happens for example with the livecd + if name.startswith("loop") or name.startswith("ram"): + return True + + # FIXME: check for virtual devices whose slaves are on the ignore list + + def addUdevDMDevice(self, info): + name = udev_device_get_name(info) + log_method_call(self, name=name) + uuid = udev_device_get_uuid(info) + sysfs_path = udev_device_get_sysfs_path(info) + device = None + + for dmdev in self.devices: + if not isinstance(dmdev, DMDevice): + continue + + try: + # there is a device in the tree already with the same + # major/minor as this one but with a different name + # XXX this is kind of racy + if dmdev.getDMNode() == os.path.basename(sysfs_path): + # XXX should we take the name already in use? + device = dmdev + break + except DMError: + # This is a little lame, but the VG device is a DMDevice + # and it won't have a dm node. At any rate, this is not + # important enough to crash the install. + log.debug("failed to find dm node for %s" % dmdev.name) + continue + + if device is None: + # we couldn't find it, so create it + # first, get a list of the slave devs and look them up + slaves = [] + dir = os.path.normpath("/sys/%s/slaves" % sysfs_path) + slave_names = os.listdir(dir) + for slave_name in slave_names: + # if it's a dm-X name, resolve it to a map name first + if slave_name.startswith("dm-"): + dev_name = dm.name_from_dm_node(slave_name) + else: + dev_name = slave_name + slave_dev = self.getDeviceByName(dev_name) + if slave_dev: + slaves.append(slave_dev) + else: + # we haven't scanned the slave yet, so do it now + path = os.path.normpath("%s/%s" % (dir, slave_name)) + new_info = udev_get_block_device(os.path.realpath(path)[4:]) + if new_info: + self.addUdevDevice(new_info) + if self.getDeviceByName(dev_name) is None: + # if the current slave is still not in + # the tree, something has gone wrong + log.error("failure scanning device %s: could not add slave %s" % (name, dev_name)) + return + + # try to get the device again now that we've got all the slaves + device = self.getDeviceByName(name) + + if device is None: + if udev_device_is_multipath_partition(info, self): + diskname = udev_device_get_multipath_partition_disk(info) + disk = self.getDeviceByName(diskname) + return self.addUdevPartitionDevice(info, disk=disk) + elif udev_device_is_dmraid_partition(info, self): + diskname = udev_device_get_dmraid_partition_disk(info) + disk = self.getDeviceByName(diskname) + return self.addUdevPartitionDevice(info, disk=disk) + + # if we get here, we found all of the slave devices and + # something must be wrong -- if all of the slaves are in + # the tree, this device should be as well + if device is None: + log.warning("ignoring dm device %s" % name) + + return device + + def addUdevMDDevice(self, info): + name = udev_device_get_name(info) + log_method_call(self, name=name) + uuid = udev_device_get_uuid(info) + sysfs_path = udev_device_get_sysfs_path(info) + device = None + + slaves = [] + dir = os.path.normpath("/sys/%s/slaves" % sysfs_path) + slave_names = os.listdir(dir) + for slave_name in slave_names: + # if it's a dm-X name, resolve it to a map name + if slave_name.startswith("dm-"): + dev_name = dm.name_from_dm_node(slave_name) + else: + dev_name = slave_name + slave_dev = self.getDeviceByName(dev_name) + if slave_dev: + slaves.append(slave_dev) + else: + # we haven't scanned the slave yet, so do it now + path = os.path.normpath("%s/%s" % (dir, slave_name)) + new_info = udev_get_block_device(os.path.realpath(path)[4:]) + if new_info: + self.addUdevDevice(new_info) + if self.getDeviceByName(dev_name) is None: + # if the current slave is still not in + # the tree, something has gone wrong + log.error("failure scanning device %s: could not add slave %s" % (name, dev_name)) + return + + # try to get the device again now that we've got all the slaves + device = self.getDeviceByName(name) + + # if we get here, we found all of the slave devices and + # something must be wrong -- if all of the slaves we in + # the tree, this device should be as well + if device is None: + log.warning("using MD RAID device for %s" % name) + try: + # level is reported as, eg: "raid1" + md_level = udev_device_get_md_level(info) + md_devices = int(udev_device_get_md_devices(info)) + md_uuid = udev_device_get_md_uuid(info) + except (KeyError, IndexError, ValueError) as e: + log.warning("invalid data for %s: %s" % (name, e)) + return + + device = MDRaidArrayDevice(name, + level=md_level, + memberDevices=md_devices, + uuid=md_uuid, + exists=True, + parents=slaves) + self._addDevice(device) + + return device + + def addUdevPartitionDevice(self, info, disk=None): + name = udev_device_get_name(info) + log_method_call(self, name=name) + uuid = udev_device_get_uuid(info) + sysfs_path = udev_device_get_sysfs_path(info) + device = None + + if disk is None: + disk_name = os.path.basename(os.path.dirname(sysfs_path)) + disk_name = disk_name.replace('!','/') + disk = self.getDeviceByName(disk_name) + + if disk is None: + # create a device instance for the disk + new_info = udev_get_block_device(os.path.dirname(sysfs_path)) + if new_info: + self.addUdevDevice(new_info) + disk = self.getDeviceByName(disk_name) + + if disk is None: + # if the current device is still not in + # the tree, something has gone wrong + log.error("failure scanning device %s" % disk_name) + lvm.lvm_cc_addFilterRejectRegexp(name) + return + + # Check that the disk has partitions. If it does not, we must have + # reinitialized the disklabel. + # + # Also ignore partitions on devices we do not support partitioning + # of, like logical volumes. + if not getattr(disk.format, "partitions", None) or \ + not disk.partitionable: + # When we got here because the disk does not have a disklabel + # format (ie a biosraid member), or because it is not + # partitionable we want LVM to ignore this partition too + if disk.format.type != "disklabel" or not disk.partitionable: + lvm.lvm_cc_addFilterRejectRegexp(name) + log.debug("ignoring partition %s" % name) + return + + try: + device = PartitionDevice(name, sysfsPath=sysfs_path, + major=udev_device_get_major(info), + minor=udev_device_get_minor(info), + exists=True, parents=[disk]) + except DeviceError: + # corner case sometime the kernel accepts a partition table + # which gets rejected by parted, in this case we will + # prompt to re-initialize the disk, so simply skip the + # faulty partitions. + return + + self._addDevice(device) + return device + + def addUdevDiskDevice(self, info): + name = udev_device_get_name(info) + log_method_call(self, name=name) + uuid = udev_device_get_uuid(info) + sysfs_path = udev_device_get_sysfs_path(info) + serial = udev_device_get_serial(info) + bus = udev_device_get_bus(info) + + # udev doesn't always provide a vendor. + vendor = udev_device_get_vendor(info) + if not vendor: + vendor = "" + + device = None + + kwargs = { "serial": serial, "vendor": vendor, "bus": bus } + if udev_device_is_iscsi(info): + diskType = iScsiDiskDevice + kwargs["node"] = self.iscsi.getNode( + udev_device_get_iscsi_name(info), + udev_device_get_iscsi_address(info), + udev_device_get_iscsi_port(info)) + kwargs["ibft"] = kwargs["node"] in self.iscsi.ibftNodes + kwargs["initiator"] = self.iscsi.initiator + log.debug("%s is an iscsi disk" % name) + elif udev_device_is_fcoe(info): + diskType = FcoeDiskDevice + kwargs["nic"] = udev_device_get_fcoe_nic(info) + kwargs["identifier"] = udev_device_get_fcoe_identifier(info) + log.debug("%s is an fcoe disk" % name) + elif udev_device_get_md_container(info): + diskType = MDRaidArrayDevice + parentName = devicePathToName(udev_device_get_md_container(info)) + kwargs["parents"] = [ self.getDeviceByName(parentName) ] + kwargs["level"] = udev_device_get_md_level(info) + kwargs["memberDevices"] = int(udev_device_get_md_devices(info)) + kwargs["uuid"] = udev_device_get_md_uuid(info) + kwargs["exists"] = True + del kwargs["serial"] + del kwargs["vendor"] + del kwargs["bus"] + elif udev_device_is_dasd(info): + diskType = DASDDevice + kwargs["dasd"] = self.dasd + kwargs["busid"] = udev_device_get_dasd_bus_id(info) + kwargs["opts"] = {} + + for attr in ['readonly', 'use_diag', 'erplog', 'failfast']: + kwargs["opts"][attr] = udev_device_get_dasd_flag(info, attr) + + log.debug("%s is a dasd device" % name) + elif udev_device_is_zfcp(info): + diskType = ZFCPDiskDevice + + for attr in ['hba_id', 'wwpn', 'fcp_lun']: + kwargs[attr] = udev_device_get_zfcp_attribute(info, attr=attr) + + log.debug("%s is a zfcp device" % name) + else: + diskType = DiskDevice + log.debug("%s is a disk" % name) + + device = diskType(name, + major=udev_device_get_major(info), + minor=udev_device_get_minor(info), + sysfsPath=sysfs_path, **kwargs) + self._addDevice(device) + return device + + def addUdevOpticalDevice(self, info): + log_method_call(self) + # XXX should this be RemovableDevice instead? + # + # Looks like if it has ID_INSTANCE=0:1 we can ignore it. + device = OpticalDevice(udev_device_get_name(info), + major=udev_device_get_major(info), + minor=udev_device_get_minor(info), + sysfsPath=udev_device_get_sysfs_path(info), + vendor=udev_device_get_vendor(info), + model=udev_device_get_model(info)) + self._addDevice(device) + return device + + def addUdevDevice(self, info): + name = udev_device_get_name(info) + log_method_call(self, name=name, info=info) + uuid = udev_device_get_uuid(info) + sysfs_path = udev_device_get_sysfs_path(info) + + if self.isIgnored(info): + log.debug("ignoring %s (%s)" % (name, sysfs_path)) + return + + log.debug("scanning %s (%s)..." % (name, sysfs_path)) + device = self.getDeviceByName(name) + + # + # The first step is to either look up or create the device + # + if udev_device_is_multipath_member(info): + device = DiskDevice(name, + major=udev_device_get_major(info), + minor=udev_device_get_minor(info), + sysfsPath=sysfs_path, exists=True, + serial=udev_device_get_serial(info), + vendor=udev_device_get_vendor(info), + model=udev_device_get_model(info)) + self._addDevice(device) + elif udev_device_is_dm(info) and \ + devicelibs.dm.dm_is_multipath(info): + log.debug("%s is a multipath device" % name) + self.addUdevDMDevice(info) + elif udev_device_is_dm(info): + log.debug("%s is a device-mapper device" % name) + # try to look up the device + if device is None and uuid: + # try to find the device by uuid + device = self.getDeviceByUuid(uuid) + + if device is None: + device = self.addUdevDMDevice(info) + elif udev_device_is_md(info): + log.debug("%s is an md device" % name) + if device is None and uuid: + # try to find the device by uuid + device = self.getDeviceByUuid(uuid) + + if device is None: + device = self.addUdevMDDevice(info) + elif udev_device_is_cdrom(info): + log.debug("%s is a cdrom" % name) + if device is None: + device = self.addUdevOpticalDevice(info) + elif udev_device_is_biosraid(info) and udev_device_is_disk(info): + log.debug("%s is part of a biosraid" % name) + if device is None: + device = DiskDevice(name, + major=udev_device_get_major(info), + minor=udev_device_get_minor(info), + sysfsPath=sysfs_path, exists=True) + self._addDevice(device) + elif udev_device_is_disk(info): + if device is None: + device = self.addUdevDiskDevice(info) + elif udev_device_is_partition(info): + log.debug("%s is a partition" % name) + if device is None: + device = self.addUdevPartitionDevice(info) + else: + log.error("Unknown block device type for: %s" % name) + return + + # If this device is protected, mark it as such now. Once the tree + # has been populated, devices' protected attribute is how we will + # identify protected devices. + if device and device.name in self.protectedDevNames: + device.protected = True + + # Don't try to do format handling on drives without media or + # if we didn't end up with a device somehow. + if not device or not device.mediaPresent: + return + + # now handle the device's formatting + self.handleUdevDeviceFormat(info, device) + log.debug("got device: %s" % device) + if device.format.type: + log.debug("got format: %s" % device.format) + device.originalFormat = device.format + + def handleUdevDiskLabelFormat(self, info, device): + log_method_call(self, device=device.name) + if device.partitioned: + # this device is already set up + log.debug("disklabel format on %s already set up" % device.name) + return + + try: + device.setup() + except Exception as e: + log.debug("setup of %s failed: %s" % (device.name, e)) + log.warning("aborting disklabel handler for %s" % device.name) + return + + # special handling for unsupported partitioned devices + if not device.partitionable: + try: + format = getFormat("disklabel", + device=device.path, + exists=True) + except InvalidDiskLabelError: + pass + else: + if format.partitions: + # parted's checks for disklabel presence are less than + # rigorous, so we will assume that detected disklabels + # with no partitions are spurious + device.format = format + return + + # if the disk contains protected partitions we will not wipe the + # disklabel even if clearpart --initlabel was specified + if not self.clearPartDisks or device.name in self.clearPartDisks: + initlabel = self.reinitializeDisks + sysfs_path = udev_device_get_sysfs_path(info) + for protected in self.protectedDevNames: + # check for protected partition + _p = "/sys/%s/%s" % (sysfs_path, protected) + if os.path.exists(os.path.normpath(_p)): + initlabel = False + break + + # check for protected partition on a device-mapper disk + disk_name = re.sub(r'p\d+$', '', protected) + if disk_name != protected and disk_name == device.name: + initlabel = False + break + else: + initlabel = False + + + if self.zeroMbr: + initcb = lambda: True + else: + path = device.path + description = device.description or device.model + bypath = os.path.basename(deviceNameToDiskByPath(path)) + if bypath: + details = "\n\nDevice details:\n%s" % (bypath,) + else: + details = "" + + initcb = lambda: self.intf.questionInitializeDisk(path, + description, + device.size, + details) + + try: + format = getFormat("disklabel", + device=device.path, + exists=not initlabel) + except InvalidDiskLabelError: + # if there is preexisting formatting on the device we will + # use it instead of ignoring the device + if not self.zeroMbr and \ + getFormat(udev_device_get_format(info)).type is not None: + return + # if we have a cb function use it. else we ignore the device. + if initcb is not None and initcb(): + format = getFormat("disklabel", + device=device.path, + exists=False) + else: + self._removeDevice(device) + self.addIgnoredDisk(device.name) + return + + if not format.exists: + # if we just initialized a disklabel we should schedule + # actions for destruction of the previous format and creation + # of the new one + self.registerAction(ActionDestroyFormat(device)) + self.registerAction(ActionCreateFormat(device, format)) + + # If this is a mac-formatted disk we just initialized, make + # sure the partition table partition gets added to the device + # tree. + if device.format.partedDisk.type == "mac" and \ + len(device.format.partitions) == 1: + name = device.format.partitions[0].getDeviceNodeName() + if not self.getDeviceByName(name): + partDevice = PartitionDevice(name, exists=True, + parents=[device]) + self._addDevice(partDevice) + + else: + device.format = format + + def handleUdevLUKSFormat(self, info, device): + log_method_call(self, name=device.name, type=device.format.type) + if not device.format.uuid: + log.info("luks device %s has no uuid" % device.path) + return + + # look up or create the mapped device + if not self.getDeviceByName(device.format.mapName): + passphrase = self.__luksDevs.get(device.format.uuid) + if passphrase: + device.format.passphrase = passphrase + else: + (passphrase, isglobal) = getLUKSPassphrase(self.intf, + device, + self.__passphrase) + if isglobal and device.format.status: + self.__passphrase = passphrase + + luks_device = LUKSDevice(device.format.mapName, + parents=[device], + exists=True) + try: + luks_device.setup() + except (LUKSError, CryptoError, DeviceError) as e: + log.info("setup of %s failed: %s" % (device.format.mapName, + e)) + device.removeChild() + else: + self._addDevice(luks_device) + else: + log.warning("luks device %s already in the tree" + % device.format.mapName) + + def handleUdevLVMPVFormat(self, info, device): + log_method_call(self, name=device.name, type=device.format.type) + # lookup/create the VG and LVs + try: + vg_name = udev_device_get_vg_name(info) + except KeyError: + # no vg name means no vg -- we're done with this pv + return + + vg_device = self.getDeviceByName(vg_name) + if vg_device: + vg_device._addDevice(device) + for lv in vg_device.lvs: + try: + lv.setup() + except DeviceError as (msg, name): + log.info("setup of %s failed: %s" % (lv.name, msg)) + else: + try: + vg_uuid = udev_device_get_vg_uuid(info) + vg_size = udev_device_get_vg_size(info) + vg_free = udev_device_get_vg_free(info) + pe_size = udev_device_get_vg_extent_size(info) + pe_count = udev_device_get_vg_extent_count(info) + pe_free = udev_device_get_vg_free_extents(info) + pv_count = udev_device_get_vg_pv_count(info) + except (KeyError, ValueError) as e: + log.warning("invalid data for %s: %s" % (device.name, e)) + return + + vg_device = LVMVolumeGroupDevice(vg_name, + device, + uuid=vg_uuid, + size=vg_size, + free=vg_free, + peSize=pe_size, + peCount=pe_count, + peFree=pe_free, + pvCount=pv_count, + exists=True) + self._addDevice(vg_device) + + try: + lv_names = udev_device_get_lv_names(info) + lv_uuids = udev_device_get_lv_uuids(info) + lv_sizes = udev_device_get_lv_sizes(info) + lv_attr = udev_device_get_lv_attr(info) + except KeyError as e: + log.warning("invalid data for %s: %s" % (device.name, e)) + return + + if not lv_names: + log.debug("no LVs listed for VG %s" % device.name) + return + + # make a list of indices with snapshots at the end + indices = range(len(lv_names)) + indices.sort(key=lambda i: lv_attr[i][0] in 'Ss') + for index in indices: + lv_name = lv_names[index] + name = "%s-%s" % (vg_name, lv_name) + if lv_attr[index][0] in 'Ss': + log.debug("found lvm snapshot volume '%s'" % name) + origin_name = devicelibs.lvm.lvorigin(vg_name, lv_name) + if not origin_name: + log.error("lvm snapshot '%s-%s' has unknown origin" + % (vg_name, lv_name)) + continue + + origin = self.getDeviceByName("%s-%s" % (vg_name, + origin_name)) + if not origin: + log.warning("snapshot lv '%s' origin lv '%s-%s' " + "not found" % (name, + vg_name, origin_name)) + continue + + log.debug("adding %dMB to %s snapshot total" + % (lv_sizes[index], origin.name)) + origin.snapshotSpace += lv_sizes[index] + continue + elif lv_attr[index][0] in 'Iil': + # skip mirror images and log volumes + continue + + log_size = 0 + if lv_attr[index][0] in 'Mm': + stripes = 0 + # identify mirror stripes/copies and mirror logs + for (j, _lvname) in enumerate(lv_names): + if lv_attr[j][0] not in 'Iil': + continue + + if _lvname == "[%s_mlog]" % lv_name: + log_size = lv_sizes[j] + elif _lvname.startswith("[%s_mimage_" % lv_name): + stripes += 1 + else: + stripes = 1 + + lv_dev = self.getDeviceByName(name) + if lv_dev is None: + lv_uuid = lv_uuids[index] + lv_size = lv_sizes[index] + lv_device = LVMLogicalVolumeDevice(lv_name, + vg_device, + uuid=lv_uuid, + size=lv_size, + stripes=stripes, + logSize=log_size, + exists=True) + self._addDevice(lv_device) + + try: + lv_device.setup() + except DeviceError as (msg, name): + log.info("setup of %s failed: %s" + % (lv_device.name, msg)) + + def handleUdevMDMemberFormat(self, info, device): + log_method_call(self, name=device.name, type=device.format.type) + # either look up or create the array device + name = udev_device_get_name(info) + sysfs_path = udev_device_get_sysfs_path(info) + + if udev_device_is_biosraid(info): + # this will prevent display of the member devices in the UI + device.format.biosraid = True + + md_array = self.getDeviceByUuid(device.format.mdUuid) + if device.format.mdUuid and md_array: + md_array._addDevice(device) + else: + # create the array with just this one member + # FIXME: why does this exact block appear twice? + try: + # level is reported as, eg: "raid1" + md_level = udev_device_get_md_level(info) + md_devices = int(udev_device_get_md_devices(info)) + md_uuid = udev_device_get_md_uuid(info) + except (KeyError, ValueError) as e: + log.warning("invalid data for %s: %s" % (name, e)) + return + + # try to name the array based on the preferred minor + md_info = devicelibs.mdraid.mdexamine(device.path) + md_path = md_info.get("device", "") + md_name = devicePathToName(md_info.get("device", "")) + if md_name: + try: + minor = int(md_name[2:]) # strip off leading "md" + except (IndexError, ValueError): + minor = None + md_name = None + else: + array = self.getDeviceByName(md_name) + if array and array.uuid != md_uuid: + md_name = None + + if not md_name: + # if we don't have a name yet, find the first unused minor + minor = 0 + while True: + if self.getDeviceByName("md%d" % minor): + minor += 1 + else: + break + + md_name = "md%d" % minor + + log.debug("using name %s for md array containing member %s" + % (md_name, device.name)) + md_array = MDRaidArrayDevice(md_name, + level=md_level, + minor=minor, + memberDevices=md_devices, + uuid=md_uuid, + sysfsPath=sysfs_path, + exists=True) + md_array._addDevice(device) + self._addDevice(md_array) + + def handleMultipathMemberFormat(self, info, device): + log_method_call(self, name=device.name, type=device.format.type) + + name = udev_device_get_multipath_name(info) + if self.__multipaths.has_key(name): + mp = self.__multipaths[name] + mp.addParent(device) + else: + mp = MultipathDevice(name, info, parents=[device]) + self.__multipaths[name] = mp + + def handleUdevDMRaidMemberFormat(self, info, device): + log_method_call(self, name=device.name, type=device.format.type) + name = udev_device_get_name(info) + sysfs_path = udev_device_get_sysfs_path(info) + uuid = udev_device_get_uuid(info) + major = udev_device_get_major(info) + minor = udev_device_get_minor(info) + + def _all_ignored(rss): + retval = True + for rs in rss: + if rs.name not in self._ignoredDisks: + retval = False + break + return retval + + # Have we already created the DMRaidArrayDevice? + rss = block.getRaidSetFromRelatedMem(uuid=uuid, name=name, + major=major, minor=minor) + if len(rss) == 0: + # we ignore the device in the hope that all the devices + # from this set will be ignored. + self.unusedRaidMembers.append(device.name) + self.addIgnoredDisk(device.name) + return + + # We ignore the device if all the rss are in self._ignoredDisks + if _all_ignored(rss): + self.addIgnoredDisk(device.name) + return + + for rs in rss: + dm_array = self.getDeviceByName(rs.name) + if dm_array is not None: + # We add the new device. + dm_array._addDevice(device) + else: + # Activate the Raid set. + rs.activate(mknod=True) + dm_array = DMRaidArrayDevice(rs.name, + raidSet=rs, + parents=[device]) + + self._addDevice(dm_array) + + # Wait for udev to scan the just created nodes, to avoid a race + # with the udev_get_block_device() call below. + udev_settle() + + # Get the DMRaidArrayDevice a DiskLabel format *now*, in case + # its partitions get scanned before it does. + dm_array.updateSysfsPath() + dm_array_info = udev_get_block_device(dm_array.sysfsPath) + self.handleUdevDiskLabelFormat(dm_array_info, dm_array) + + # Use the rs's object on the device. + # pyblock can return the memebers of a set and the + # device has the attribute to hold it. But ATM we + # are not really using it. Commenting this out until + # we really need it. + #device.format.raidmem = block.getMemFromRaidSet(dm_array, + # major=major, minor=minor, uuid=uuid, name=name) + + def handleUdevDeviceFormat(self, info, device): + log_method_call(self, name=getattr(device, "name", None)) + name = udev_device_get_name(info) + sysfs_path = udev_device_get_sysfs_path(info) + uuid = udev_device_get_uuid(info) + label = udev_device_get_label(info) + format_type = udev_device_get_format(info) + serial = udev_device_get_serial(info) + + # Now, if the device is a disk, see if there is a usable disklabel. + # If not, see if the user would like to create one. + # XXX ignore disklabels on multipath or biosraid member disks + if not udev_device_is_biosraid(info) and \ + not udev_device_is_multipath_member(info): + self.handleUdevDiskLabelFormat(info, device) + if device.partitioned or self.isIgnored(info) or \ + (not device.partitionable and + device.format.type == "disklabel"): + # If the device has a disklabel, or the user chose not to + # create one, we are finished with this device. Otherwise + # it must have some non-disklabel formatting, in which case + # we fall through to handle that. + return + + format = None + if (not device) or (not format_type) or device.format.type: + # this device has no formatting or it has already been set up + # FIXME: this probably needs something special for disklabels + log.debug("no type or existing type for %s, bailing" % (name,)) + return + + # set up the common arguments for the format constructor + args = [format_type] + kwargs = {"uuid": uuid, + "label": label, + "device": device.path, + "serial": serial, + "exists": True} + + # set up type-specific arguments for the format constructor + if format_type == "multipath_member": + kwargs["multipath_members"] = self.getDevicesBySerial(serial) + elif format_type == "crypto_LUKS": + # luks/dmcrypt + kwargs["name"] = "luks-%s" % uuid + elif format_type in formats.mdraid.MDRaidMember._udevTypes: + # mdraid + try: + kwargs["mdUuid"] = udev_device_get_md_uuid(info) + except KeyError: + log.debug("mdraid member %s has no md uuid" % name) + elif format_type == "LVM2_member": + # lvm + try: + kwargs["vgName"] = udev_device_get_vg_name(info) + except KeyError as e: + log.debug("PV %s has no vg_name" % name) + try: + kwargs["vgUuid"] = udev_device_get_vg_uuid(info) + except KeyError: + log.debug("PV %s has no vg_uuid" % name) + try: + kwargs["peStart"] = udev_device_get_pv_pe_start(info) + except KeyError: + log.debug("PV %s has no pe_start" % name) + elif format_type == "vfat": + # efi magic + if isinstance(device, PartitionDevice) and device.bootable: + efi = formats.getFormat("efi") + if efi.minSize <= device.size <= efi.maxSize: + args[0] = "efi" + elif format_type == "hfs": + # apple bootstrap magic + if isinstance(device, PartitionDevice) and device.bootable: + apple = formats.getFormat("appleboot") + if apple.minSize <= device.size <= apple.maxSize: + args[0] = "appleboot" + + try: + log.debug("type detected on '%s' is '%s'" % (name, format_type,)) + device.format = formats.getFormat(*args, **kwargs) + except FSError: + log.debug("type '%s' on '%s' invalid, assuming no format" % + (format_type, name,)) + device.format = formats.DeviceFormat() + return + + if shouldClear(device, self.clearPartType, + clearPartDisks=self.clearPartDisks): + # if this is a device that will be cleared by clearpart, + # don't bother with format-specific processing + return + + # + # now do any special handling required for the device's format + # + if device.format.type == "luks": + self.handleUdevLUKSFormat(info, device) + elif device.format.type == "mdmember": + self.handleUdevMDMemberFormat(info, device) + elif device.format.type == "dmraidmember": + self.handleUdevDMRaidMemberFormat(info, device) + elif device.format.type == "lvmpv": + self.handleUdevLVMPVFormat(info, device) + elif device.format.type == "multipath_member": + self.handleMultipathMemberFormat(info, device) + + def updateDeviceFormat(self, device): + log.debug("updating format of device: %s" % device) + iutil.notify_kernel("/sys%s" % device.sysfsPath) + udev_settle() + info = udev_get_device(device.sysfsPath) + self.handleUdevDeviceFormat(info, device) + if device.format.type: + log.debug("got format: %s" % device.format) + + def _handleInconsistencies(self): + def reinitializeVG(vg): + # First we remove VG data + try: + vg.destroy() + except DeviceError: + # the pvremoves will finish the job. + log.debug("There was an error destroying the VG %s." % vg.name) + + # remove VG device from list. + self._removeDevice(vg) + + for parent in vg.parents: + parent.format.destroy() + + # Give the vg the a default format + kwargs = {"device": parent.path, + "exists": parent.exists} + parent.format = formats.getFormat(*[""], **kwargs) + + def leafInconsistencies(device): + if device.type == "lvmvg": + if device.complete: + return + + paths = [] + for parent in device.parents: + paths.append(parent.path) + + # if zeroMbr is true don't ask. + if (self.zeroMbr or + self.intf.questionReinitInconsistentLVM(pv_names=paths, + vg_name=device.name)): + reinitializeVG(device) + else: + # The user chose not to reinitialize. + # hopefully this will ignore the vg components too. + self._removeDevice(device) + lvm.lvm_cc_addFilterRejectRegexp(device.name) + lvm.blacklistVG(device.name) + for parent in device.parents: + if parent.type == "partition": + self.immutableDevices.append([parent.name, + _("This partition is part of an inconsistent LVM Volume Group.")]) + else: + self._removeDevice(parent, moddisk=False) + self.addIgnoredDisk(parent.name) + lvm.lvm_cc_addFilterRejectRegexp(parent.name) + + elif device.type == "lvmlv": + # we might have already fixed this. + if device not in self._devices or \ + device.name in self._ignoredDisks: + return + if device.complete: + return + + paths = [] + for parent in device.vg.parents: + paths.append(parent.path) + + if (self.zeroMbr or + self.intf.questionReinitInconsistentLVM(pv_names=paths, + lv_name=device.name)): + + # destroy all lvs. + for lv in device.vg.lvs: + try: + # reinitializeVG should clean up if necessary + lv.destroy() + except StorageError as e: + log.info("error removing lv %s from " + "inconsistent/incomplete vg %s" + % (lv.lvname, device.vg.name)) + device.vg._removeLogVol(lv) + self._removeDevice(lv) + + reinitializeVG(device.vg) + else: + # ignore all the lvs. + for lv in device.vg.lvs: + self._removeDevice(lv) + lvm.lvm_cc_addFilterRejectRegexp(lv.name) + # ignore the vg + self._removeDevice(device.vg) + lvm.lvm_cc_addFilterRejectRegexp(device.vg.name) + lvm.blacklistVG(device.vg.name) + # ignore all the pvs + for parent in device.vg.parents: + if parent.type == "partition": + self.immutableDevices.append([parent.name, + _("This partition is part of an inconsistent LVM Volume Group.")]) + else: + self._removeDevice(parent, moddisk=False) + self.addIgnoredDisk(parent.name) + lvm.lvm_cc_addFilterRejectRegexp(parent.name) + + # Address the inconsistencies present in the tree leaves. + for leaf in self.leaves: + leafInconsistencies(leaf) + + # Check for unused BIOS raid members, unused dmraid members are added + # to self.unusedRaidMembers as they are processed, extend this list + # with unused mdraid BIOS raid members + for c in self.getDevicesByType("mdcontainer"): + if c.kids == 0: + self.unusedRaidMembers.extend(map(lambda m: m.name, c.devices)) + + self.intf.unusedRaidMembersWarning(self.unusedRaidMembers) + + def populate(self): + """ Locate all storage devices. """ + + # mark the tree as unpopulated so exception handlers can tell the + # exception originated while finding storage devices + self.populated = False + + # resolve the protected device specs to device names + for spec in self.protectedDevSpecs: + name = udev_resolve_devspec(spec) + if name: + self.protectedDevNames.append(name) + + # FIXME: the backing dev for the live image can't be used as an + # install target. note that this is a little bit of a hack + # since we're assuming that /dev/live will exist + if os.path.exists("/dev/live") and \ + stat.S_ISBLK(os.stat("/dev/live")[stat.ST_MODE]): + livetarget = devicePathToName(os.path.realpath("/dev/live")) + log.info("%s looks to be the live device; marking as protected" + % (livetarget,)) + self.protectedDevNames.append(livetarget) + + # First iteration - let's just look for disks. + old_devices = {} + + devices = udev_get_block_devices() + for dev in devices: + old_devices[dev['name']] = dev + + cfg = self.__multipathConfigWriter.write() + open("/etc/multipath.conf", "w+").write(cfg) + del cfg + + (singles, mpaths, partitions) = devicelibs.mpath.identifyMultipaths(devices) + devices = singles + reduce(list.__add__, mpaths, []) + partitions + log.info("devices to scan: %s" % [d['name'] for d in devices]) + for dev in devices: + self.addUdevDevice(dev) + + # Having found all the disks, we can now find all the multipaths built + # upon them. + whitelist = [] + mpaths = self.__multipaths.values() + mpaths.sort(key=lambda d: d.name) + for mp in mpaths: + log.info("adding mpath device %s" % mp.name) + mp.setup() + whitelist.append(mp.name) + for p in mp.parents: + whitelist.append(p.name) + self.__multipathConfigWriter.addMultipathDevice(mp) + self._addDevice(mp) + for d in self.devices: + if not d.name in whitelist: + self.__multipathConfigWriter.addBlacklistDevice(d) + cfg = self.__multipathConfigWriter.write() + open("/etc/multipath.conf", "w+").write(cfg) + del cfg + + # Now, loop and scan for devices that have appeared since the two above + # blocks or since previous iterations. + while True: + devices = [] + new_devices = udev_get_block_devices() + + for new_device in new_devices: + if not old_devices.has_key(new_device['name']): + old_devices[new_device['name']] = new_device + devices.append(new_device) + + if len(devices) == 0: + # nothing is changing -- we are finished building devices + break + + log.info("devices to scan: %s" % [d['name'] for d in devices]) + for dev in devices: + self.addUdevDevice(dev) + + self.populated = True + + # After having the complete tree we make sure that the system + # inconsistencies are ignored or resolved. + self._handleInconsistencies() + + self.teardownAll() + try: + os.unlink("/etc/mdadm.conf") + except OSError: + log.info("failed to unlink /etc/mdadm.conf") + + def teardownAll(self): + """ Run teardown methods on all devices. """ + for device in self.leaves: + try: + device.teardown(recursive=True) + except StorageError as e: + log.info("teardown of %s failed: %s" % (device.name, e)) + + def setupAll(self): + """ Run setup methods on all devices. """ + for device in self.leaves: + try: + device.setup() + except DeviceError as (msg, name): + log.debug("setup of %s failed: %s" % (device.name, msg)) + + def getDeviceBySysfsPath(self, path): + if not path: + return None + + found = None + for device in self._devices: + if device.sysfsPath == path: + found = device + break + + return found + + def getDeviceByUuid(self, uuid): + if not uuid: + return None + + found = None + for device in self._devices: + if device.uuid == uuid: + found = device + break + elif device.format.uuid == uuid: + found = device + break + + return found + + def getDevicesBySerial(self, serial): + devices = [] + for device in self._devices: + if not hasattr(device, "serial"): + log.warning("device %s has no serial attr" % device.name) + continue + if device.serial == serial: + devices.append(device) + return devices + + def getDeviceByLabel(self, label): + if not label: + return None + + found = None + for device in self._devices: + _label = getattr(device.format, "label", None) + if not _label: + continue + + if _label == label: + found = device + break + + return found + + def getDeviceByName(self, name): + log.debug("looking for device '%s'..." % name) + if not name: + return None + + found = None + for device in self._devices: + if device.name == name: + found = device + break + elif (device.type == "lvmlv" or device.type == "lvmvg") and \ + device.name == name.replace("--","-"): + found = device + break + + log.debug("found %s" % found) + return found + + def getDeviceByPath(self, path): + log.debug("looking for device '%s'..." % path) + if not path: + return None + + found = None + for device in self._devices: + if device.path == path: + found = device + break + elif (device.type == "lvmlv" or device.type == "lvmvg") and \ + device.path == path.replace("--","-"): + found = device + break + + log.debug("found %s" % found) + return found + + def getDevicesByType(self, device_type): + # TODO: expand this to catch device format types + return [d for d in self._devices if d.type == device_type] + + def getDevicesByInstance(self, device_class): + return [d for d in self._devices if isinstance(d, device_class)] + + @property + def devices(self): + """ List of device instances """ + devices = [] + for device in self._devices: + if device.path in [d.path for d in devices] and \ + not isinstance(device, NoDevice): + raise DeviceTreeError("duplicate paths in device tree") + + devices.append(device) + + return devices + + @property + def filesystems(self): + """ List of filesystems. """ + #""" Dict with mountpoint keys and filesystem values. """ + filesystems = [] + for dev in self.leaves: + if dev.format and getattr(dev.format, 'mountpoint', None): + filesystems.append(dev.format) + + return filesystems + + @property + def uuids(self): + """ Dict with uuid keys and Device values. """ + uuids = {} + for dev in self._devices: + try: + uuid = dev.uuid + except AttributeError: + uuid = None + + if uuid: + uuids[uuid] = dev + + try: + uuid = dev.format.uuid + except AttributeError: + uuid = None + + if uuid: + uuids[uuid] = dev + + return uuids + + @property + def labels(self): + """ Dict with label keys and Device values. + + FIXME: duplicate labels are a possibility + """ + labels = {} + for dev in self._devices: + if dev.format and getattr(dev.format, "label", None): + labels[dev.format.label] = dev + + return labels + + @property + def leaves(self): + """ List of all devices upon which no other devices exist. """ + leaves = [d for d in self._devices if d.isleaf] + return leaves + + def getChildren(self, device): + """ Return a list of a device's children. """ + return [c for c in self._devices if device in c.parents] + + def resolveDevice(self, devspec, blkidTab=None, cryptTab=None): + # find device in the tree + device = None + if devspec.startswith("UUID="): + # device-by-uuid + uuid = devspec.partition("=")[2] + device = self.uuids.get(uuid) + if device is None: + log.error("failed to resolve device %s" % devspec) + elif devspec.startswith("LABEL="): + # device-by-label + label = devspec.partition("=")[2] + device = self.labels.get(label) + if device is None: + log.error("failed to resolve device %s" % devspec) + elif devspec.startswith("/dev/"): + # device path + device = self.getDeviceByPath(devspec) + if device is None: + if blkidTab: + # try to use the blkid.tab to correlate the device + # path with a UUID + blkidTabEnt = blkidTab.get(devspec) + if blkidTabEnt: + log.debug("found blkid.tab entry for '%s'" % devspec) + uuid = blkidTabEnt.get("UUID") + if uuid: + device = self.getDeviceByUuid(uuid) + if device: + devstr = device.name + else: + devstr = "None" + log.debug("found device '%s' in tree" % devstr) + if device and device.format and \ + device.format.type == "luks": + map_name = device.format.mapName + log.debug("luks device; map name is '%s'" % map_name) + mapped_dev = self.getDeviceByName(map_name) + if mapped_dev: + device = mapped_dev + + if device is None and cryptTab and \ + devspec.startswith("/dev/mapper/"): + # try to use a dm-crypt mapping name to + # obtain the underlying device, possibly + # using blkid.tab + cryptTabEnt = cryptTab.get(devspec.split("/")[-1]) + if cryptTabEnt: + luks_dev = cryptTabEnt['device'] + try: + device = self.getChildren(luks_dev)[0] + except IndexError as e: + pass + elif device is None: + # dear lvm: can we please have a few more device nodes + # for each logical volume? + # three just doesn't seem like enough. + name = devspec[5:] # strip off leading "/dev/" + (vg_name, slash, lv_name) = name.partition("/") + if lv_name and not "/" in lv_name: + # looks like we may have one + lv = "%s-%s" % (vg_name, lv_name) + device = self.getDeviceByName(lv) + + if device: + log.debug("resolved '%s' to '%s' (%s)" % (devspec, device.name, device.type)) + else: + log.debug("failed to resolve '%s'" % devspec) + return device diff --git a/storage/errors.py b/storage/errors.py new file mode 100644 index 0000000..a0f5f60 --- /dev/null +++ b/storage/errors.py @@ -0,0 +1,150 @@ +# errors.py +# Exception classes for anaconda's storage configuration module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +class StorageError(Exception): + pass + +# Device +class DeviceError(StorageError): + pass + +class DeviceCreateError(DeviceError): + pass + +class DeviceDestroyError(DeviceError): + pass + +class DeviceResizeError(DeviceError): + pass + +class DeviceSetupError(DeviceError): + pass + +class DeviceTeardownError(DeviceError): + pass + +class DeviceUserDeniedFormatError(DeviceError): + pass + +# DeviceFormat +class DeviceFormatError(StorageError): + pass + +class FormatCreateError(DeviceFormatError): + pass + +class FormatDestroyError(DeviceFormatError): + pass + +class FormatSetupError(DeviceFormatError): + pass + +class FormatTeardownError(DeviceFormatError): + pass + +class DMRaidMemberError(DeviceFormatError): + pass + +class MultipathMemberError(DeviceFormatError): + pass + +class FSError(DeviceFormatError): + pass + +class FSResizeError(FSError): + pass + +class FSMigrateError(FSError): + pass + +class LUKSError(DeviceFormatError): + pass + +class MDMemberError(DeviceFormatError): + pass + +class PhysicalVolumeError(DeviceFormatError): + pass + +class SwapSpaceError(DeviceFormatError): + pass + +class DiskLabelError(DeviceFormatError): + pass + +class InvalidDiskLabelError(DiskLabelError): + pass + +class DiskLabelCommitError(DiskLabelError): + pass + +# devicelibs +class SwapError(StorageError): + pass + +class SuspendError(SwapError): + pass + +class OldSwapError(SwapError): + pass + +class UnknownSwapError(SwapError): + pass + +class MDRaidError(StorageError): + pass + +class DMError(StorageError): + pass + +class LVMError(StorageError): + pass + +class CryptoError(StorageError): + pass + +class MPathError(StorageError): + pass + +# DeviceTree +class DeviceTreeError(StorageError): + pass + +# DeviceAction +class DeviceActionError(StorageError): + pass + +# partitioning +class PartitioningError(StorageError): + pass + +class PartitioningWarning(StorageError): + pass + +# udev +class UdevError(StorageError): + pass + +# fstab +class UnrecognizedFSTabEntryError(StorageError): + pass + diff --git a/storage/fcoe.py b/storage/fcoe.py new file mode 100644 index 0000000..cd52bc8 --- /dev/null +++ b/storage/fcoe.py @@ -0,0 +1,172 @@ +# +# fcoe.py - fcoe class +# +# Copyright (C) 2009 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. +# + +import os +import iutil +import isys +import logging +import time +from flags import flags +log = logging.getLogger("anaconda") + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +_fcoe_module_loaded = False + +def has_fcoe(): + global _fcoe_module_loaded + if not _fcoe_module_loaded: + iutil.execWithRedirect("modprobe", [ "fcoe" ], + stdout = "/dev/tty5", stderr="/dev/tty5") + _fcoe_module_loaded = True + + return os.access("/sys/module/fcoe", os.X_OK) + +class fcoe(object): + """ FCoE utility class. + + This class will automatically discover and connect to EDD configured + FCoE SAN's when the startup() method gets called. It can also be + used to manually configure FCoE SAN's through the addSan() method. + + As this class needs to make sure certain things like starting fcoe + daemons and connecting to firmware discovered SAN's only happens once + and as it keeps a global list of all FCoE devices it is + implemented as a Singleton. + """ + + def __init__(self): + self.started = False + self.lldpadStarted = False + self.fcoemonStarted = False + self.nics = [] + + # So that users can write fcoe() to get the singleton instance + def __call__(self): + return self + + def _stabilize(self, intf = None): + if intf: + w = intf.waitWindow(_("Connecting to FCoE SAN"), + _("Connecting to FCoE SAN")) + + # I have no clue how long we need to wait, this ought to do the trick + time.sleep(10) + iutil.execWithRedirect("udevadm", [ "settle" ], + stdout = "/dev/tty5", stderr="/dev/tty5") + if intf: + w.pop() + + def _startEDD(self, intf = None): + rc = iutil.execWithCapture("/usr/libexec/fcoe/fcoe_edd.sh", [ "-i" ], + stderr="/dev/tty5") + if not rc.startswith("NIC="): + log.info("No FCoE EDD info found: %s" % rc) + return + + (key, val) = rc.split("=", 1) + if val not in isys.getDeviceProperties(): + log.error("Unknown FCoE NIC found in EDD: %s, ignoring" % val) + return + + log.info("FCoE NIC found in EDD: %s" % val) + self.addSan(val, dcb=True, intf=intf) + + def startup(self, intf = None): + if self.started: + return + + if not has_fcoe(): + return + + self._startEDD(intf) + self.started = True + + def _startLldpad(self): + if self.lldpadStarted: + return + + iutil.execWithRedirect("lldpad", [ "-d" ], + stdout = "/dev/tty5", stderr="/dev/tty5") + self.lldpadStarted = True + + def _startFcoemon(self): + if self.fcoemonStarted: + return + + iutil.execWithRedirect("fcoemon", [ ], + stdout = "/dev/tty5", stderr="/dev/tty5") + self.fcoemonStarted = True + + def addSan(self, nic, dcb=False, intf=None): + if not has_fcoe(): + raise IOError, _("FCoE not available") + + log.info("Activating FCoE SAN attached to %s, dcb: %s" % (nic, dcb)) + + iutil.execWithRedirect("ip", [ "link", "set", nic, "up" ], + stdout = "/dev/tty5", stderr="/dev/tty5") + + if dcb: + self._startLldpad() + iutil.execWithRedirect("dcbtool", [ "sc", nic, "dcb", "on" ], + stdout = "/dev/tty5", stderr="/dev/tty5") + iutil.execWithRedirect("dcbtool", [ "sc", nic, "app:fcoe", + "e:1", "a:1", "w:1" ], + stdout = "/dev/tty5", stderr="/dev/tty5") + self._startFcoemon() + else: + f = open("/sys/module/fcoe/parameters/create", "w") + f.write(nic) + f.close() + + self._stabilize(intf) + self.nics.append((nic, dcb)) + + def writeKS(self, f): + # fixme plenty (including add ks support for fcoe in general) + return + + def write(self, instPath, anaconda): + if not self.nics: + return + + if not os.path.isdir(instPath + "/etc/fcoe"): + os.makedirs(instPath + "/etc/fcoe", 0755) + + for nic, dcb in self.nics: + fd = os.open(instPath + "/etc/fcoe/cfg-" + nic, + os.O_RDWR | os.O_CREAT) + os.write(fd, '# Created by anaconda\n') + os.write(fd, '# Enable/Disable FCoE service at the Ethernet port\n') + os.write(fd, 'FCOE_ENABLE="yes"\n') + os.write(fd, '# Indicate if DCB service is required at the Ethernet port\n') + if dcb: + os.write(fd, 'DCB_REQUIRED="yes"\n') + else: + os.write(fd, 'DCB_REQUIRED="no"\n') + os.close(fd) + + return + +# Create FCoE singleton +fcoe = fcoe() + +# vim:tw=78:ts=4:et:sw=4 diff --git a/storage/formats/Makefile.am b/storage/formats/Makefile.am new file mode 100644 index 0000000..7ecaf07 --- /dev/null +++ b/storage/formats/Makefile.am @@ -0,0 +1,24 @@ +# storage/formats/Makefile.am for anaconda +# +# Copyright (C) 2009 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Author: David Cantrell <dcantrell@redhat.com> + +pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME) +storageformatsdir = $(pkgpyexecdir)/storage/formats +storageformats_PYTHON = *.py + +MAINTAINERCLEANFILES = Makefile.in diff --git a/storage/formats/__init__.py b/storage/formats/__init__.py new file mode 100644 index 0000000..8a8abd3 --- /dev/null +++ b/storage/formats/__init__.py @@ -0,0 +1,403 @@ +# __init__.py +# Entry point for anaconda storage formats subpackage. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +import os + +from iutil import notify_kernel, get_sysfs_path_by_name +from ..storage_log import log_method_call +from ..errors import * +from ..devicelibs.dm import dm_node_from_name + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + + +device_formats = {} +def register_device_format(fmt_class): + if not issubclass(fmt_class, DeviceFormat): + raise ValueError("arg1 must be a subclass of DeviceFormat") + + device_formats[fmt_class._type] = fmt_class + log.debug("registered device format class %s as %s" % (fmt_class.__name__, + fmt_class._type)) + +default_fstypes = ("ext4", "ext3", "ext2") +def get_default_filesystem_type(boot=None): + import pyanaconda.platform as platform + + if boot: + fstypes = [platform.getPlatform(None).defaultBootFSType] + else: + fstypes = default_fstypes + + for fstype in fstypes: + try: + supported = get_device_format_class(fstype).supported + except AttributeError: + supported = None + + if supported: + return fstype + + raise DeviceFormatError("None of %s is supported by your kernel" % ",".join(fstypes)) + +def getFormat(fmt_type, *args, **kwargs): + """ Return a DeviceFormat instance based on fmt_type and args. + + Given a device format type and a set of constructor arguments, + return a DeviceFormat instance. + + Return None if no suitable format class is found. + + Arguments: + + fmt_type -- the name of the format type (eg: 'ext3', 'swap') + + Keyword Arguments: + + The keyword arguments may vary according to the format type, + but here is the common set: + + device -- path to the device on which the format resides + uuid -- the UUID of the (preexisting) formatted device + exists -- whether or not the format exists on the device + + """ + fmt_class = get_device_format_class(fmt_type) + fmt = None + if fmt_class: + fmt = fmt_class(*args, **kwargs) + try: + className = fmt.__class__.__name__ + except AttributeError: + className = None + log.debug("getFormat('%s') returning %s instance" % (fmt_type, className)) + return fmt + +def collect_device_format_classes(): + """ Pick up all device format classes from this directory. + + Note: Modules must call register_device_format(FormatClass) in + order for the format class to be picked up. + """ + dir = os.path.dirname(__file__) + for module_file in os.listdir(dir): + # make sure we're not importing this module + if module_file.endswith(".py") and module_file != __file__: + mod_name = module_file[:-3] + # imputil is deprecated in python 2.6 + try: + globals()[mod_name] = __import__(mod_name, globals(), locals(), [], -1) + except ImportError, e: + log.debug("import of device format module '%s' failed" % mod_name) + +def get_device_format_class(fmt_type): + """ Return an appropriate format class based on fmt_type. """ + if not device_formats: + collect_device_format_classes() + + fmt = device_formats.get(fmt_type) + if not fmt: + for fmt_class in device_formats.values(): + if fmt_type and fmt_type == fmt_class._name: + fmt = fmt_class + break + elif fmt_type in fmt_class._udevTypes: + fmt = fmt_class + break + + # default to no formatting, AKA "Unknown" + if not fmt: + fmt = DeviceFormat + + return fmt + +class DeviceFormat(object): + """ Generic device format. """ + _type = None + _name = "Unknown" + _udevTypes = [] + partedFlag = None + partedSystem = None + _formattable = False # can be formatted + _supported = False # is supported + _linuxNative = False # for clearpart + _packages = [] # required packages + _resizable = False # can be resized + _bootable = False # can be used as boot + _migratable = False # can be migrated + _maxSize = 0 # maximum size in MB + _minSize = 0 # minimum size in MB + _dump = False + _check = False + _hidden = False # hide devices with this formatting? + + def __init__(self, *args, **kwargs): + """ Create a DeviceFormat instance. + + Keyword Arguments: + + device -- path to the underlying device + uuid -- this format's UUID + exists -- indicates whether this is an existing format + + """ + self.device = kwargs.get("device") + self.uuid = kwargs.get("uuid") + self.exists = kwargs.get("exists") + self.options = kwargs.get("options") + self._migrate = False + + # don't worry about existence if this is a DeviceFormat instance + #if self.__class__ is DeviceFormat: + # self.exists = True + + def __str__(self): + s = ("%(classname)s instance (%(id)s) --\n" + " type = %(type)s name = %(name)s status = %(status)s\n" + " device = %(device)s uuid = %(uuid)s exists = %(exists)s\n" + " options = %(options)s supported = %(supported)s" + " formattable = %(format)s resizable = %(resize)s\n" % + {"classname": self.__class__.__name__, "id": "%#x" % id(self), + "type": self.type, "name": self.name, "status": self.status, + "device": self.device, "uuid": self.uuid, "exists": self.exists, + "options": self.options, "supported": self.supported, + "format": self.formattable, "resize": self.resizable}) + return s + + @property + def dict(self): + d = {"type": self.type, "name": self.name, "device": self.device, + "uuid": self.uuid, "exists": self.exists, + "options": self.options, "supported": self.supported, + "resizable": self.resizable} + return d + + def _setOptions(self, options): + self._options = options + + def _getOptions(self): + return self._options + + options = property(_getOptions, _setOptions) + + def _setDevice(self, devspec): + if devspec and not devspec.startswith("/"): + raise ValueError("device must be a fully qualified path") + self._device = devspec + + def _getDevice(self): + return self._device + + device = property(lambda f: f._getDevice(), + lambda f,d: f._setDevice(d), + doc="Full path the device this format occupies") + + @property + def name(self): + if self._name: + name = self._name + else: + name = self.type + return name + + @property + def type(self): + return self._type + + def probe(self): + log_method_call(self, device=self.device, + type=self.type, status=self.status) + + def notifyKernel(self): + log_method_call(self, device=self.device, + type=self.type) + if not self.device: + return + + if self.device.startswith("/dev/mapper/"): + try: + name = dm_node_from_name(os.path.basename(self.device)) + except Exception, e: + log.warning("failed to get dm node for %s" % self.device) + return + elif self.device: + name = os.path.basename(self.device) + + path = get_sysfs_path_by_name(name) + try: + notify_kernel(path, action="change") + except Exception, e: + log.warning("failed to notify kernel of change: %s" % e) + + + def create(self, *args, **kwargs): + log_method_call(self, device=self.device, + type=self.type, status=self.status) + # allow late specification of device path + device = kwargs.get("device") + if device: + self.device = device + + if not os.path.exists(self.device): + raise FormatCreateError("invalid device specification", self.device) + + def destroy(self, *args, **kwargs): + log_method_call(self, device=self.device, + type=self.type, status=self.status) + # zero out the 1MB at the beginning and end of the device in the + # hope that it will wipe any metadata from filesystems that + # previously occupied this device + log.debug("zeroing out beginning and end of %s..." % self.device) + fd = None + + try: + fd = os.open(self.device, os.O_RDWR) + buf = '\0' * 1024 * 1024 + os.write(fd, buf) + os.lseek(fd, -1024 * 1024, 2) + os.write(fd, buf) + os.close(fd) + except OSError as e: + if getattr(e, "errno", None) == 28: # No space left in device + pass + else: + log.error("error zeroing out %s: %s" % (self.device, e)) + + if fd: + os.close(fd) + except Exception as e: + log.error("error zeroing out %s: %s" % (self.device, e)) + if fd: + os.close(fd) + + self.exists = False + + def setup(self, *args, **kwargs): + log_method_call(self, device=self.device, + type=self.type, status=self.status) + + if not self.exists: + raise FormatSetupError("format has not been created") + + if self.status: + return + + # allow late specification of device path + device = kwargs.get("device") + if device: + self.device = device + + if not self.device or not os.path.exists(self.device): + raise FormatSetupError("invalid device specification") + + def teardown(self, *args, **kwargs): + log_method_call(self, device=self.device, + type=self.type, status=self.status) + + @property + def status(self): + return (self.exists and + self.__class__ is not DeviceFormat and + isinstance(self.device, str) and + self.device and + os.path.exists(self.device)) + + @property + def formattable(self): + """ Can we create formats of this type? """ + return self._formattable + + @property + def supported(self): + """ Is this format a supported type? """ + return self._supported + + @property + def packages(self): + """ Packages required to manage formats of this type. """ + return self._packages + + @property + def resizable(self): + """ Can formats of this type be resized? """ + return self._resizable and self.exists + + @property + def bootable(self): + """ Is this format type suitable for a boot partition? """ + return self._bootable + + @property + def migratable(self): + """ Can formats of this type be migrated? """ + return self._migratable + + @property + def migrate(self): + return self._migrate + + @property + def linuxNative(self): + """ Is this format type native to linux? """ + return self._linuxNative + + @property + def mountable(self): + """ Is this something we can mount? """ + return False + + @property + def dump(self): + """ Whether or not this format will be dumped by dump(8). """ + return self._dump + + @property + def check(self): + """ Whether or not this format is checked on boot. """ + return self._check + + @property + def maxSize(self): + """ Maximum size (in MB) for this format type. """ + return self._maxSize + + @property + def minSize(self): + """ Minimum size (in MB) for this format type. """ + return self._minSize + + @property + def hidden(self): + """ Whether devices with this formatting should be hidden in UIs. """ + return self._hidden + + def writeKS(self, f): + return + + +collect_device_format_classes() + + diff --git a/storage/formats/disklabel.py b/storage/formats/disklabel.py new file mode 100644 index 0000000..3edb1df --- /dev/null +++ b/storage/formats/disklabel.py @@ -0,0 +1,359 @@ +# disklabel.py +# Device format classes for anaconda's storage configuration module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +import os +import copy + +from ..storage_log import log_method_call +import parted +import _ped +from ..errors import * +from ..udev import udev_settle +from . import DeviceFormat, register_device_format + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + + +class DiskLabel(DeviceFormat): + """ Disklabel """ + _type = "disklabel" + _name = "partition table" + _formattable = True # can be formatted + _supported = False # is supported + + def __init__(self, *args, **kwargs): + """ Create a DiskLabel instance. + + Keyword Arguments: + + device -- path to the underlying device + exists -- indicates whether this is an existing format + + """ + log_method_call(self, *args, **kwargs) + DeviceFormat.__init__(self, *args, **kwargs) + + self._size = None + + self._partedDevice = None + self._partedDisk = None + self._origPartedDisk = None + self._alignment = None + self._endAlignment = None + + if self.partedDevice: + # set up the parted objects and raise exception on failure + self._origPartedDisk = self.partedDisk.duplicate() + + def __deepcopy__(self, memo): + """ Create a deep copy of a Disklabel instance. + + We can't do copy.deepcopy on parted objects, which is okay. + For these parted objects, we just do a shallow copy. + """ + new = self.__class__.__new__(self.__class__) + memo[id(self)] = new + shallow_copy_attrs = ('_partedDevice', '_partedDisk', '_origPartedDisk') + for (attr, value) in self.__dict__.items(): + if attr in shallow_copy_attrs: + setattr(new, attr, copy.copy(value)) + else: + setattr(new, attr, copy.deepcopy(value, memo)) + + return new + + def __str__(self): + s = DeviceFormat.__str__(self) + s += (" type = %(type)s partition count = %(count)s" + " sectorSize = %(sectorSize)s\n" + " align_offset = %(offset)s align_grain = %(grain)s\n" + " partedDisk = %(disk)r\n" + " origPartedDisk = %(orig_disk)r\n" + " partedDevice = %(dev)r\n" % + {"type": self.labelType, "count": len(self.partitions), + "sectorSize": self.partedDevice.sectorSize, + "offset": self.alignment.offset, + "grain": self.alignment.grainSize, + "disk": self.partedDisk, "orig_disk": self._origPartedDisk, + "dev": self.partedDevice}) + return s + + @property + def dict(self): + d = super(DiskLabel, self).dict + d.update({"labelType": self.labelType, + "partitionCount": len(self.partitions), + "sectorSize": self.partedDevice.sectorSize, + "offset": self.alignment.offset, + "grainSize": self.alignment.grainSize}) + return d + + def resetPartedDisk(self): + """ Set this instance's partedDisk to reflect the disk's contents. """ + log_method_call(self, device=self.device) + self._partedDisk = self._origPartedDisk + + def freshPartedDisk(self): + """ Return a new, empty parted.Disk instance for this device. """ + log_method_call(self, device=self.device) + from pyanaconda.platform import getPlatform + platf = getPlatform(None) + labelType = platf.diskLabelType(self.partedDevice.type) + return parted.freshDisk(device=self.partedDevice, ty=labelType) + + @property + def partedDisk(self): + if not self._partedDisk: + if self.exists: + try: + self._partedDisk = parted.Disk(device=self.partedDevice) + except (_ped.DiskLabelException, _ped.IOException, + NotImplementedError) as e: + raise InvalidDiskLabelError() + + if self._partedDisk.type == "loop": + # When the device has no partition table but it has a FS, + # it will be created with label type loop. Treat the + # same as if the device had no label (cause it really + # doesn't). + raise InvalidDiskLabelError() + else: + self._partedDisk = self.freshPartedDisk() + + # turn off cylinder alignment + if self._partedDisk.isFlagAvailable(parted.DISK_CYLINDER_ALIGNMENT): + self._partedDisk.unsetFlag(parted.DISK_CYLINDER_ALIGNMENT) + + return self._partedDisk + + @property + def partedDevice(self): + if not self._partedDevice and self.device and \ + os.path.exists(self.device): + # We aren't guaranteed to be able to get a device. In + # particular, built-in USB flash readers show up as devices but + # do not always have any media present, so parted won't be able + # to find a device. + try: + self._partedDevice = parted.Device(path=self.device) + except (_ped.IOException, _ped.DeviceException): + pass + + return self._partedDevice + + @property + def labelType(self): + """ The disklabel type (eg: 'gpt', 'msdos') """ + return self.partedDisk.type + + @property + def name(self): + return "%s (%s)" % (self._name, self.labelType.upper()) + + @property + def size(self): + size = self._size + if not size: + try: + size = self.partedDevice.getSize(unit="MB") + except Exception: + size = 0 + + return size + + @property + def status(self): + """ Device status. """ + return False + + def setup(self, *args, **kwargs): + """ Open, or set up, a device. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise DeviceFormatError("format has not been created") + + if self.status: + return + + DeviceFormat.setup(self, *args, **kwargs) + + def teardown(self, *args, **kwargs): + """ Close, or tear down, a device. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise DeviceFormatError("format has not been created") + + def create(self, *args, **kwargs): + """ Create the device. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if self.exists: + raise DeviceFormatError("format already exists") + + if self.status: + raise DeviceFormatError("device exists and is active") + + DeviceFormat.create(self, *args, **kwargs) + + # We're relying on someone having called resetPartedDisk -- we + # could ensure a fresh disklabel by setting self._partedDisk to + # None right before calling self.commit(), but that might hide + # other problems. + self.commit() + self.exists = True + + def destroy(self, *args, **kwargs): + """ Wipe the disklabel from the device. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise DeviceFormatError("format does not exist") + + if not os.access(self.device, os.W_OK): + raise DeviceFormatError("device path does not exist") + + self.partedDevice.clobber() + self.exists = False + + def commit(self): + """ Commit the current partition table to disk and notify the OS. """ + log_method_call(self, device=self.device, + numparts=len(self.partitions)) + try: + self.partedDisk.commit() + except parted.DiskException as msg: + raise DiskLabelCommitError(msg) + else: + udev_settle() + + def commitToDisk(self): + """ Commit the current partition table to disk. """ + log_method_call(self, device=self.device, + numparts=len(self.partitions)) + try: + self.partedDisk.commitToDevice() + except parted.DiskException as msg: + raise DiskLabelCommitError(msg) + + def addPartition(self, *args, **kwargs): + partition = kwargs.get("partition", None) + if not partition: + partition = args[0] + geometry = partition.geometry + constraint = kwargs.get("constraint", None) + if not constraint and len(args) > 1: + constraint = args[1] + elif not constraint: + constraint = parted.Constraint(exactGeom=geometry) + + new_partition = parted.Partition(disk=self.partedDisk, + type=partition.type, + geometry=geometry) + self.partedDisk.addPartition(partition=new_partition, + constraint=constraint) + + def removePartition(self, partition): + try: + self.partedDisk.removePartition(partition) + except parted.PartitionException as err: + log.error("unable to remove partition %s: %s" %(partition, err)) + + @property + def extendedPartition(self): + try: + extended = self.partedDisk.getExtendedPartition() + except Exception: + extended = None + return extended + + @property + def logicalPartitions(self): + try: + logicals = self.partedDisk.getLogicalPartitions() + except Exception: + logicals = [] + return logicals + + @property + def firstPartition(self): + try: + part = self.partedDisk.getFirstPartition() + except Exception: + part = None + return part + + @property + def partitions(self): + try: + parts = self.partedDisk.partitions + except Exception: + parts = [] + return parts + + @property + def alignment(self): + """ Alignment requirements for this device. """ + if not self._alignment: + try: + disklabel_alignment = self.partedDisk.partitionAlignment + except _ped.CreateException: + disklabel_alignment = parted.Alignment(offset=0, grainSize=1) + + try: + optimum_device_alignment = self.partedDevice.optimumAlignment + except _ped.CreateException: + optimum_device_alignment = None + + try: + minimum_device_alignment = self.partedDevice.minimumAlignment + except _ped.CreateException: + minimum_device_alignment = None + + try: + a = optimum_device_alignment.intersect(disklabel_alignment) + except (ArithmeticError, AttributeError): + try: + a = minimum_device_alignment.intersect(disklabel_alignment) + except (ArithmeticError, AttributeError): + a = disklabel_alignment + + self._alignment = a + + return self._alignment + + @property + def endAlignment(self): + if not self._endAlignment: + self._endAlignment = parted.Alignment( + offset = self.alignment.offset - 1, + grainSize = self.alignment.grainSize) + + return self._endAlignment + +register_device_format(DiskLabel) + diff --git a/storage/formats/dmraid.py b/storage/formats/dmraid.py new file mode 100644 index 0000000..3d2ee86 --- /dev/null +++ b/storage/formats/dmraid.py @@ -0,0 +1,114 @@ +# dmraid.py +# dmraid device formats +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +from ..storage_log import log_method_call +from flags import flags +from ..errors import * +from . import DeviceFormat, register_device_format + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + + +class DMRaidMember(DeviceFormat): + """ A dmraid member disk. """ + _type = "dmraidmember" + _name = "dm-raid member device" + # XXX This looks like trouble. + # + # Maybe a better approach is a RaidMember format with subclass + # for MDRaidMember, letting all *_raid_member types fall through + # to the generic RaidMember format, which is basically read-only. + # + # One problem that presents is the possibility of someone passing + # a dmraid member to the MDRaidArrayDevice constructor. + _udevTypes = ["adaptec_raid_member", "ddf_raid_member", + "highpoint_raid_member", "isw_raid_member", + "jmicron_raid_member", "lsi_mega_raid_member", + "nvidia_raid_member", "promise_fasttrack_raid_member", + "silicon_medley_raid_member", "via_raid_member"] + _formattable = False # can be formatted + _supported = True # is supported + _linuxNative = False # for clearpart + _packages = ["dmraid"] # required packages + _resizable = False # can be resized + _bootable = False # can be used as boot + _maxSize = 0 # maximum size in MB + _minSize = 0 # minimum size in MB + _hidden = True # hide devices with this formatting? + + def __init__(self, *args, **kwargs): + """ Create a DeviceFormat instance. + + Keyword Arguments: + + device -- path to the underlying device + uuid -- this format's UUID + exists -- indicates whether this is an existing format + + On initialization this format is like DeviceFormat + + """ + log_method_call(self, *args, **kwargs) + DeviceFormat.__init__(self, *args, **kwargs) + + # Initialize the attribute that will hold the block object. + self._raidmem = None + + def __str__(self): + s = DeviceFormat.__str__(self) + s += (" raidmem = %(raidmem)r" % {"raidmem": self.raidmem}) + return s + + def _getRaidmem(self): + return self._raidmem + + def _setRaidmem(self, raidmem): + self._raidmem = raidmem + + raidmem = property(lambda d: d._getRaidmem(), + lambda d,r: d._setRaidmem(r)) + + def create(self, *args, **kwargs): + log_method_call(self, device=self.device, + type=self.type, status=self.status) + raise DMRaidMemberError("creation of dmraid members is non-sense") + + def destroy(self, *args, **kwargs): + log_method_call(self, device=self.device, + type=self.type, status=self.status) + raise DMRaidMemberError("destruction of dmraid members is non-sense") + + +if not flags.cmdline.has_key("noiswmd"): + DMRaidMember._udevTypes.remove("isw_raid_member") + +# The anaconda cmdline has not been parsed yet when we're first imported, +# so we can not use flags.dmraid here +if flags.cmdline.has_key("nodmraid"): + DMRaidMember._udevTypes = [] + +register_device_format(DMRaidMember) + diff --git a/storage/formats/fs.py b/storage/formats/fs.py new file mode 100644 index 0000000..ed2c02d --- /dev/null +++ b/storage/formats/fs.py @@ -0,0 +1,1476 @@ +# filesystems.py +# Filesystem classes for anaconda's storage configuration module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# David Cantrell <dcantrell@redhat.com> +# + +""" Filesystem classes for use by anaconda. + + TODO: + - migration + - bug 472127: allow creation of tmpfs filesystems (/tmp, /var/tmp, &c) +""" +import math +import os +import sys +import tempfile +import selinux +import isys + +from ..errors import * +from . import DeviceFormat, register_device_format +import iutil +from flags import flags +from parted import fileSystemType +from ..storage_log import log_method_call + +import logging +log = logging.getLogger("storage") + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +try: + lost_and_found_context = selinux.matchpathcon("/lost+found", 0)[1] +except OSError: + lost_and_found_context = None + +fs_configs = {} + +def get_kernel_filesystems(): + fs_list = [] + for line in open("/proc/filesystems").readlines(): + fs_list.append(line.split()[-1]) + return fs_list + +global kernel_filesystems +kernel_filesystems = get_kernel_filesystems() + +def fsConfigFromFile(config_file): + """ Generate a set of attribute name/value pairs with which a + filesystem type can be defined. + + The following config file would define a filesystem identical to + the static Ext3FS class definition: + + type = ext3 + mkfs = "mke2fs" + resizefs = "resize2fs" + labelfs = "e2label" + fsck = "e2fsck" + packages = ["e2fsprogs"] + formattable = True + supported = True + resizable = True + bootable = True + linuxNative = True + maxSize = 8 * 1024 * 1024 + minSize = 0 + defaultFormatOptions = "-t ext3" + defaultMountOptions = "defaults" + + """ + # XXX NOTUSED + lines = open(config_file).readlines() + fs_attrs = {} + for line in lines: + (key, value) = [t.strip() for t in line.split("=")] + if not hasattr(FS, "_" + key): + print "invalid key: %s" % key + continue + + fs_attrs[key] = value + + if not fs_attrs.has_key("type"): + raise ValueError, _("filesystem configuration missing a type") + + # XXX what's the policy about multiple configs for a given type? + fs_configs[fs_attrs['type']] = fs_attrs + +class FS(DeviceFormat): + """ Filesystem class. """ + _type = "Abstract Filesystem Class" # fs type name + _mountType = None # like _type but for passing to mount + _name = None + _mkfs = "" # mkfs utility + _modules = [] # kernel modules required for support + _resizefs = "" # resize utility + _labelfs = "" # labeling utility + _fsck = "" # fs check utility + _fsckErrors = {} # fs check command error codes & msgs + _migratefs = "" # fs migration utility + _infofs = "" # fs info utility + _defaultFormatOptions = [] # default options passed to mkfs + _defaultMountOptions = ["defaults"] # default options passed to mount + _defaultLabelOptions = [] + _defaultCheckOptions = [] + _defaultMigrateOptions = [] + _defaultInfoOptions = [] + _migrationTarget = None + _existingSizeFields = [] + _fsProfileSpecifier = None # mkfs option specifying fsprofile + + def __init__(self, *args, **kwargs): + """ Create a FS instance. + + Keyword Args: + + device -- path to the device containing the filesystem + mountpoint -- the filesystem's mountpoint + label -- the filesystem label + uuid -- the filesystem UUID + mountopts -- mount options for the filesystem + size -- the filesystem's size in MiB + exists -- indicates whether this is an existing filesystem + + """ + if self.__class__ is FS: + raise TypeError("FS is an abstract class.") + + DeviceFormat.__init__(self, *args, **kwargs) + self.mountpoint = kwargs.get("mountpoint") + self.mountopts = kwargs.get("mountopts") + self.label = kwargs.get("label") + self.fsprofile = kwargs.get("fsprofile") + + # filesystem size does not necessarily equal device size + self._size = kwargs.get("size", 0) + self._minInstanceSize = None # min size of this FS instance + self._mountpoint = None # the current mountpoint when mounted + if self.exists and self.supported: + self._size = self._getExistingSize() + foo = self.minSize # force calculation of minimum size + + self._targetSize = self._size + + if self.supported: + self.loadModule() + + def __str__(self): + s = DeviceFormat.__str__(self) + s += (" mountpoint = %(mountpoint)s mountopts = %(mountopts)s\n" + " label = %(label)s size = %(size)s" + " targetSize = %(targetSize)s\n" % + {"mountpoint": self.mountpoint, "mountopts": self.mountopts, + "label": self.label, "size": self._size, + "targetSize": self.targetSize}) + return s + + @property + def dict(self): + d = super(FS, self).dict + d.update({"mountpoint": self.mountpoint, "size": self._size, + "label": self.label, "targetSize": self.targetSize, + "mountable": self.mountable, + "migratable": self.migratable}) + return d + + def _setTargetSize(self, newsize): + """ Set a target size for this filesystem. """ + if not self.exists: + raise FSError("filesystem has not been created") + + if newsize is None: + # unset any outstanding resize request + self._targetSize = None + return + + if not self.minSize <= newsize < self.maxSize: + raise ValueError("invalid target size request") + + self._targetSize = newsize + + def _getTargetSize(self): + """ Get this filesystem's target size. """ + return self._targetSize + + targetSize = property(_getTargetSize, _setTargetSize, + doc="Target size for this filesystem") + + def _getSize(self): + """ Get this filesystem's size. """ + size = self._size + if self.resizable and self.targetSize != size: + size = self.targetSize + return size + + size = property(_getSize, doc="This filesystem's size, accounting " + "for pending changes") + + def _getExistingSize(self): + """ Determine the size of this filesystem. Filesystem must + exist. Each filesystem varies, but the general procedure + is to run the filesystem dump or info utility and read + the block size and number of blocks for the filesystem + and compute megabytes from that. + + The loop that reads the output from the infofsProg is meant + to be simple, but take in to account variations in output. + The general procedure: + 1) Capture output from infofsProg. + 2) Iterate over each line of the output: + a) Trim leading and trailing whitespace. + b) Break line into fields split on ' ' + c) If line begins with any of the strings in + _existingSizeFields, start at the end of + fields and take the first one that converts + to a long. Store this in the values list. + d) Repeat until the values list length equals + the _existingSizeFields length. + 3) If the length of the values list equals the length + of _existingSizeFields, compute the size of this + filesystem by multiplying all of the values together + to get bytes, then convert to megabytes. Return + this value. + 4) If we were unable to capture all fields, return 0. + + The caller should catch exceptions from this method. Any + exception raised indicates a need to change the fields we + are looking for, the command to run and arguments, or + something else. If you catch an exception from this method, + assume the filesystem cannot be resized. + """ + size = self._size + + if self.infofsProg and self.mountable and self.exists and not size: + try: + values = [] + argv = self._defaultInfoOptions + [ self.device ] + + buf = iutil.execWithCapture(self.infofsProg, argv, + stderr="/dev/tty5") + + for line in buf.splitlines(): + found = False + + line = line.strip() + tmp = line.split(' ') + tmp.reverse() + + for field in self._existingSizeFields: + if line.startswith(field): + for subfield in tmp: + try: + values.append(long(subfield)) + found = True + break + except ValueError: + continue + + if found: + break + + if len(values) == len(self._existingSizeFields): + break + + if len(values) != len(self._existingSizeFields): + return 0 + + size = 1 + for value in values: + size *= value + + # report current size as megabytes + size = math.floor(size / 1024.0 / 1024.0) + except Exception as e: + log.error("failed to obtain size of filesystem on %s: %s" + % (self.device, e)) + + return size + + @property + def currentSize(self): + """ The filesystem's current actual size. """ + size = 0 + if self.exists: + size = self._size + return float(size) + + def _getFormatOptions(self, options=None): + argv = [] + if options and isinstance(options, list): + argv.extend(options) + argv.extend(self.defaultFormatOptions) + if self._fsProfileSpecifier and self.fsprofile: + argv.extend([self._fsProfileSpecifier, self.fsprofile]) + argv.append(self.device) + return argv + + def doFormat(self, *args, **kwargs): + """ Create the filesystem. + + Arguments: + + None + + Keyword Arguments: + + intf -- InstallInterface instance + options -- list of options to pass to mkfs + + """ + log_method_call(self, type=self.mountType, device=self.device, + mountpoint=self.mountpoint) + + intf = kwargs.get("intf") + options = kwargs.get("options") + + if self.exists: + raise FormatCreateError("filesystem already exists", self.device) + + if not self.formattable: + return + + if not self.mkfsProg: + return + + if self.exists: + return + + if not os.path.exists(self.device): + raise FormatCreateError("device does not exist", self.device) + + argv = self._getFormatOptions(options=options) + + w = None + if intf: + w = intf.progressWindow(_("Formatting"), + _("Creating %s filesystem on %s") + % (self.type, self.device), + 100, pulse = True) + + try: + rc = iutil.execWithPulseProgress(self.mkfsProg, + argv, + stdout="/dev/tty5", + stderr="/dev/tty5", + progress=w) + except Exception as e: + raise FormatCreateError(e, self.device) + finally: + if w: + w.pop() + + if rc: + raise FormatCreateError("format failed: %s" % rc, self.device) + + self.exists = True + self.notifyKernel() + + if self.label: + self.writeLabel(self.label) + + def doMigrate(self, intf=None): + if not self.exists: + raise FSError("filesystem has not been created") + + if not self.migratable or not self.migrate: + return + + if not os.path.exists(self.device): + raise FSError("device does not exist") + + # if journal already exists skip + if isys.ext2HasJournal(self.device): + log.info("Skipping migration of %s, has a journal already." + % self.device) + return + + argv = self._defaultMigrateOptions[:] + argv.append(self.device) + try: + rc = iutil.execWithRedirect(self.migratefsProg, + argv, + stdout = "/dev/tty5", + stderr = "/dev/tty5") + except Exception as e: + raise FSMigrateError("filesystem migration failed: %s" % e, + self.device) + + if rc: + raise FSMigrateError("filesystem migration failed: %s" % rc, + self.device) + + # the other option is to actually replace this instance with an + # instance of the new filesystem type. + self._type = self.migrationTarget + + @property + def resizeArgs(self): + argv = [self.device, "%d" % (self.targetSize,)] + return argv + + def doResize(self, *args, **kwargs): + """ Resize this filesystem to new size @newsize. + + Arguments: + + None + + Keyword Arguments: + + intf -- InstallInterface instance + + """ + intf = kwargs.get("intf") + + if not self.exists: + raise FSResizeError("filesystem does not exist", self.device) + + if not self.resizable: + raise FSResizeError("filesystem not resizable", self.device) + + if self.targetSize == self.currentSize: + return + + if not self.resizefsProg: + return + + if not os.path.exists(self.device): + raise FSResizeError("device does not exist", self.device) + + self.doCheck(intf=intf) + + # The first minimum size can be incorrect if the fs was not + # properly unmounted. After doCheck the minimum size will be correct + # so run the check one last time and bump up the size if it was too + # small. + self._minInstanceSize = None + if self.targetSize < self.minSize: + self.targetSize = self.minSize + log.info("Minimum size changed, setting targetSize on %s to %s" \ + % (self.device, self.targetSize)) + + w = None + if intf: + w = intf.progressWindow(_("Resizing"), + _("Resizing filesystem on %s") + % (self.device,), + 100, pulse = True) + + try: + rc = iutil.execWithPulseProgress(self.resizefsProg, + self.resizeArgs, + stdout="/dev/tty5", + stderr="/dev/tty5", + progress=w) + except Exception as e: + raise FSResizeError(e, self.device) + finally: + if w: + w.pop() + + if rc: + raise FSResizeError("resize failed: %s" % rc, self.device) + + self.doCheck(intf=intf) + + # XXX must be a smarter way to do this + self._size = self.targetSize + self.notifyKernel() + + def _getCheckArgs(self): + argv = [] + argv.extend(self.defaultCheckOptions) + argv.append(self.device) + return argv + + def _fsckFailed(self, rc): + return False + + def _fsckErrorMessage(self, rc): + return _("Unknown return code: %d.") % (rc,) + + def doCheck(self, intf=None): + if not self.exists: + raise FSError("filesystem has not been created") + + if not self.fsckProg: + return + + if not os.path.exists(self.device): + raise FSError("device does not exist") + + w = None + if intf: + w = intf.progressWindow(_("Checking"), + _("Checking filesystem on %s") + % (self.device), + 100, pulse = True) + + try: + rc = iutil.execWithPulseProgress(self.fsckProg, + self._getCheckArgs(), + stdout="/dev/tty5", + stderr="/dev/tty5", + progress = w) + except Exception as e: + raise FSError("filesystem check failed: %s" % e) + finally: + if w: + w.pop() + + if self._fsckFailed(rc): + hdr = _("%(type)s filesystem check failure on %(device)s: ") % \ + {"type": self.type, "device": self.device} + + msg = self._fsckErrorMessage(rc) + + if intf: + help = _("Errors like this usually mean there is a problem " + "with the filesystem that will require user " + "interaction to repair. Before restarting " + "installation, reboot to rescue mode or another " + "system that allows you to repair the filesystem " + "interactively. Restart installation after you " + "have corrected the problems on the filesystem.") + + intf.messageWindow(_("Unrecoverable Error"), + hdr + "\n\n" + msg + "\n\n" + help, + custom_icon='error') + sys.exit(0) + else: + raise FSError(hdr + msg) + + def loadModule(self): + """Load whatever kernel module is required to support this filesystem.""" + global kernel_filesystems + + if not self._modules or self.mountType in kernel_filesystems: + return + + for module in self._modules: + try: + rc = iutil.execWithRedirect("modprobe", [module], + stdout="/dev/tty5", + stderr="/dev/tty5") + except Exception as e: + log.error("Could not load kernel module %s: %s" % (module, e)) + self._supported = False + return + + if rc: + log.error("Could not load kernel module %s" % module) + self._supported = False + return + + # If we successfully loaded a kernel module, for this filesystem, we + # also need to update the list of supported filesystems. + kernel_filesystems = get_kernel_filesystems() + + def mount(self, *args, **kwargs): + """ Mount this filesystem. + + Arguments: + + None + + Keyword Arguments: + + options -- mount options (overrides all other option strings) + chroot -- prefix to apply to mountpoint + mountpoint -- mountpoint (overrides self.mountpoint) + """ + options = kwargs.get("options", "") + chroot = kwargs.get("chroot", "/") + mountpoint = kwargs.get("mountpoint") + + if not self.exists: + raise FSError("filesystem has not been created") + + if not mountpoint: + mountpoint = self.mountpoint + + if not mountpoint: + raise FSError("no mountpoint given") + + if self.status: + return + + if not isinstance(self, NoDevFS) and not os.path.exists(self.device): + raise FSError("device %s does not exist" % self.device) + + # XXX os.path.join is FUBAR: + # + # os.path.join("/mnt/foo", "/") -> "/" + # + #mountpoint = os.path.join(chroot, mountpoint) + chrootedMountpoint = os.path.normpath("%s/%s" % (chroot, mountpoint)) + iutil.mkdirChain(chrootedMountpoint) + if flags.selinux: + ret = isys.resetFileContext(mountpoint, chroot) + log.info("set SELinux context for mountpoint %s to %s" \ + % (mountpoint, ret)) + + # passed in options override default options + if not options or not isinstance(options, str): + options = self.options + + try: + rc = isys.mount(self.device, chrootedMountpoint, + fstype=self.mountType, + options=options, + bindMount=isinstance(self, BindFS)) + except Exception as e: + raise FSError("mount failed: %s" % e) + + if rc: + raise FSError("mount failed: %s" % rc) + + if flags.selinux and "ro" not in options.split(","): + ret = isys.resetFileContext(mountpoint, chroot) + log.info("set SELinux context for newly mounted filesystem " + "root at %s to %s" %(mountpoint, ret)) + isys.setFileContext("%s/lost+found" % mountpoint, + lost_and_found_context, chroot) + + self._mountpoint = chrootedMountpoint + + def unmount(self): + """ Unmount this filesystem. """ + if not self.exists: + raise FSError("filesystem has not been created") + + if not self._mountpoint: + # not mounted + return + + if not os.path.exists(self._mountpoint): + raise FSError("mountpoint does not exist") + + rc = isys.umount(self._mountpoint, removeDir = False) + if rc: + raise FSError("umount failed") + + self._mountpoint = None + + def _getLabelArgs(self, label): + argv = [] + argv.extend(self.defaultLabelOptions) + argv.extend([self.device, label]) + return argv + + def writeLabel(self, label): + """ Create a label for this filesystem. """ + if not self.exists: + raise FSError("filesystem has not been created") + + if not self.labelfsProg: + return + + if not os.path.exists(self.device): + raise FSError("device does not exist") + + argv = self._getLabelArgs(label) + rc = iutil.execWithRedirect(self.labelfsProg, + argv, + stderr="/dev/tty5") + if rc: + raise FSError("label failed") + + self.label = label + self.notifyKernel() + + @property + def isDirty(self): + return False + + @property + def mkfsProg(self): + """ Program used to create filesystems of this type. """ + return self._mkfs + + @property + def fsckProg(self): + """ Program used to check filesystems of this type. """ + return self._fsck + + @property + def resizefsProg(self): + """ Program used to resize filesystems of this type. """ + return self._resizefs + + @property + def labelfsProg(self): + """ Program used to manage labels for this filesystem type. """ + return self._labelfs + + @property + def migratefsProg(self): + """ Program used to migrate filesystems of this type. """ + return self._migratefs + + @property + def infofsProg(self): + """ Program used to get information for this filesystem type. """ + return self._infofs + + @property + def migrationTarget(self): + return self._migrationTarget + + @property + def utilsAvailable(self): + # we aren't checking for fsck because we shouldn't need it + for prog in [self.mkfsProg, self.resizefsProg, self.labelfsProg, + self.infofsProg]: + if not prog: + continue + + if not filter(lambda d: os.access("%s/%s" % (d, prog), os.X_OK), + os.environ["PATH"].split(":")): + return False + + return True + + @property + def supported(self): + log_method_call(self, supported=self._supported) + return self._supported and self.utilsAvailable + + @property + def mountable(self): + return (self.mountType in kernel_filesystems) or \ + (os.access("/sbin/mount.%s" % (self.mountType,), os.X_OK)) + + @property + def defaultFormatOptions(self): + """ Default options passed to mkfs for this filesystem type. """ + # return a copy to prevent modification + return self._defaultFormatOptions[:] + + @property + def defaultMountOptions(self): + """ Default options passed to mount for this filesystem type. """ + # return a copy to prevent modification + return self._defaultMountOptions[:] + + @property + def defaultLabelOptions(self): + """ Default options passed to labeler for this filesystem type. """ + # return a copy to prevent modification + return self._defaultLabelOptions[:] + + @property + def defaultCheckOptions(self): + """ Default options passed to checker for this filesystem type. """ + # return a copy to prevent modification + return self._defaultCheckOptions[:] + + def _getOptions(self): + options = ",".join(self.defaultMountOptions) + if self.mountopts: + # XXX should we clobber or append? + options = self.mountopts + return options + + def _setOptions(self, options): + self.mountopts = options + + options = property(_getOptions, _setOptions) + + def _isMigratable(self): + """ Can filesystems of this type be migrated? """ + return bool(self._migratable and self.migratefsProg and + filter(lambda d: os.access("%s/%s" + % (d, self.migratefsProg,), + os.X_OK), + os.environ["PATH"].split(":")) and + self.migrationTarget) + + migratable = property(_isMigratable) + + def _setMigrate(self, migrate): + if not migrate: + self._migrate = migrate + return + + if self.migratable and self.exists: + self._migrate = migrate + else: + raise ValueError("cannot set migrate on non-migratable filesystem") + + migrate = property(lambda f: f._migrate, lambda f,m: f._setMigrate(m)) + + @property + def type(self): + _type = self._type + if self.migrate: + _type = self.migrationTarget + + return _type + + @property + def mountType(self): + if not self._mountType: + self._mountType = self._type + + return self._mountType + + # These methods just wrap filesystem-specific methods in more + # generically named methods so filesystems and formatted devices + # like swap and LVM physical volumes can have a common API. + def create(self, *args, **kwargs): + if self.exists: + raise FSError("filesystem already exists") + + DeviceFormat.create(self, *args, **kwargs) + + return self.doFormat(*args, **kwargs) + + def setup(self, *args, **kwargs): + """ Mount the filesystem. + + The filesystem will be mounted at the directory indicated by + self.mountpoint. + """ + return self.mount(**kwargs) + + def teardown(self, *args, **kwargs): + return self.unmount(*args, **kwargs) + + @property + def status(self): + # FIXME check /proc/mounts or similar + if not self.exists: + return False + return self._mountpoint is not None + + def writeKS(self, f): + f.write("%s --fstype=%s" % (self.mountpoint, self.type)) + + if self.label: + f.write(" --label=\"%s\"" % self.label) + + +class Ext2FS(FS): + """ ext2 filesystem. """ + _type = "ext2" + _mkfs = "mke2fs" + _modules = ["ext2"] + _resizefs = "resize2fs" + _labelfs = "e2label" + _fsck = "e2fsck" + _fsckErrors = {4: _("File system errors left uncorrected."), + 8: _("Operational error."), + 16: _("Usage or syntax error."), + 32: _("e2fsck cancelled by user request."), + 128: _("Shared library error.")} + _packages = ["e2fsprogs"] + _formattable = True + _supported = True + _resizable = True + _bootable = True + _linuxNative = True + _maxSize = 8 * 1024 * 1024 + _minSize = 0 + _defaultFormatOptions = [] + _defaultMountOptions = ["defaults"] + _defaultCheckOptions = ["-f", "-p", "-C", "0"] + _dump = True + _check = True + _migratable = True + _migrationTarget = "ext3" + _migratefs = "tune2fs" + _defaultMigrateOptions = ["-j"] + _infofs = "dumpe2fs" + _defaultInfoOptions = ["-h"] + _existingSizeFields = ["Block count:", "Block size:"] + _fsProfileSpecifier = "-T" + partedSystem = fileSystemType["ext2"] + + def _fsckFailed(self, rc): + for errorCode in self._fsckErrors.keys(): + if rc & errorCode: + return True + return False + + def _fsckErrorMessage(self, rc): + msg = '' + + for errorCode in self._fsckErrors.keys(): + if rc & errorCode: + msg += "\n" + self._fsckErrors[errorCode] + + return msg.strip() + + def doMigrate(self, intf=None): + FS.doMigrate(self, intf=intf) + self.tuneFS() + + def doFormat(self, *args, **kwargs): + FS.doFormat(self, *args, **kwargs) + self.tuneFS() + + def tuneFS(self): + if not isys.ext2HasJournal(self.device): + # only do this if there's a journal + return + + try: + rc = iutil.execWithRedirect("tune2fs", + ["-c0", "-i0", + "-ouser_xattr,acl", self.device], + stdout = "/dev/tty5", + stderr = "/dev/tty5") + except Exception as e: + log.error("failed to run tune2fs on %s: %s" % (self.device, e)) + + @property + def minSize(self): + """ Minimum size for this filesystem in MB. """ + if self._minInstanceSize is None: + # try once in the beginning to get the minimum size for an + # existing filesystem. + size = self._minSize + blockSize = None + + if self.exists and os.path.exists(self.device): + # get block size + buf = iutil.execWithCapture(self.infofsProg, + ["-h", self.device], + stderr="/dev/tty5") + for line in buf.splitlines(): + if line.startswith("Block size:"): + blockSize = int(line.split(" ")[-1]) + break + + if blockSize is None: + raise FSError("failed to get block size for %s filesystem " + "on %s" % (self.mountType, self.device)) + + # get minimum size according to resize2fs + buf = iutil.execWithCapture(self.resizefsProg, + ["-P", self.device], + stderr="/dev/tty5") + for line in buf.splitlines(): + if "minimum size of the filesystem:" not in line: + continue + + # line will look like: + # Estimated minimum size of the filesystem: 1148649 + # + # NOTE: The minimum size reported is in blocks. Convert + # to bytes, then megabytes, and finally round up. + (text, sep, minSize) = line.partition(": ") + size = long(minSize) * blockSize + size = math.ceil(size / 1024.0 / 1024.0) + break + + if size is None: + log.warning("failed to get minimum size for %s filesystem " + "on %s" % (self.mountType, self.device)) + + self._minInstanceSize = size + + return self._minInstanceSize + + @property + def isDirty(self): + return isys.ext2IsDirty(self.device) + + @property + def resizeArgs(self): + argv = ["-p", self.device, "%dM" % (self.targetSize,)] + return argv + +register_device_format(Ext2FS) + + +class Ext3FS(Ext2FS): + """ ext3 filesystem. """ + _type = "ext3" + _defaultFormatOptions = ["-t", "ext3"] + _migrationTarget = "ext4" + _modules = ["ext3"] + _defaultMigrateOptions = ["-O", "extents"] + partedSystem = fileSystemType["ext3"] + + def _isMigratable(self): + """ Can filesystems of this type be migrated? """ + return (flags.cmdline.has_key("ext4migrate") and + Ext2FS._isMigratable(self)) + + migratable = property(_isMigratable) + +register_device_format(Ext3FS) + + +class Ext4FS(Ext3FS): + """ ext4 filesystem. """ + _type = "ext4" + _defaultFormatOptions = ["-t", "ext4"] + _migratable = False + _modules = ["ext4"] + partedSystem = fileSystemType["ext4"] + +register_device_format(Ext4FS) + + +class FATFS(FS): + """ FAT filesystem. """ + _type = "vfat" + _mkfs = "mkdosfs" + _modules = ["vfat"] + _labelfs = "dosfslabel" + _fsck = "dosfsck" + _fsckErrors = {1: _("Recoverable errors have been detected or dosfsck has " + "discovered an internal inconsistency."), + 2: _("Usage error.")} + _supported = True + _formattable = True + _maxSize = 1024 * 1024 + _packages = [ "dosfstools" ] + _defaultMountOptions = ["umask=0077", "shortname=winnt"] + # FIXME this should be fat32 in some cases + partedSystem = fileSystemType["fat16"] + + def _fsckFailed(self, rc): + if rc >= 1: + return True + return False + + def _fsckErrorMessage(self, rc): + return self._fsckErrors[rc] + +register_device_format(FATFS) + + +class EFIFS(FATFS): + _type = "efi" + _mountType = "vfat" + _modules = ["vfat"] + _name = "EFI System Partition" + _minSize = 50 + _maxSize = 256 + _bootable = True + + @property + def supported(self): + import pyanaconda.platform as platform + p = platform.getPlatform(None) + return (isinstance(p, platform.EFI) and + p.isEfi and + self.utilsAvailable) + +register_device_format(EFIFS) + + +class BTRFS(FS): + """ btrfs filesystem """ + _type = "btrfs" + _mkfs = "mkfs.btrfs" + _modules = ["btrfs"] + _resizefs = "btrfsctl" + _formattable = True + _linuxNative = True + _bootable = False + _maxLabelChars = 256 + _supported = True + _dump = True + _check = True + _packages = ["btrfs-progs"] + _maxSize = 16 * 1024 * 1024 + # FIXME parted needs to be thaught about btrfs so that we can set the + # partition table type correctly for btrfs partitions + # partedSystem = fileSystemType["btrfs"] + + def _getFormatOptions(self, options=None): + argv = [] + if options and isinstance(options, list): + argv.extend(options) + argv.extend(self.defaultFormatOptions) + if self.label: + argv.extend(["-L", self.label]) + argv.append(self.device) + return argv + + @property + def resizeArgs(self): + argv = ["-r", "%dm" % (self.targetSize,), self.device] + return argv + + @property + def supported(self): + """ Is this filesystem a supported type? """ + supported = self._supported + if flags.cmdline.has_key("btrfs"): + supported = self.utilsAvailable + + return supported + +register_device_format(BTRFS) + + +class GFS2(FS): + """ gfs2 filesystem. """ + _type = "gfs2" + _mkfs = "mkfs.gfs2" + _modules = ["dlm", "gfs2"] + _formattable = True + _defaultFormatOptions = ["-j", "1", "-p", "lock_nolock", "-O"] + _linuxNative = True + _supported = False + _dump = True + _check = True + _packages = ["gfs2-utils"] + # FIXME parted needs to be thaught about btrfs so that we can set the + # partition table type correctly for btrfs partitions + # partedSystem = fileSystemType["gfs2"] + + @property + def supported(self): + """ Is this filesystem a supported type? """ + supported = self._supported + if flags.cmdline.has_key("gfs2"): + supported = self.utilsAvailable + + return supported + +register_device_format(GFS2) + + +class JFS(FS): + """ JFS filesystem """ + _type = "jfs" + _mkfs = "mkfs.jfs" + _modules = ["jfs"] + _labelfs = "jfs_tune" + _defaultFormatOptions = ["-q"] + _defaultLabelOptions = ["-L"] + _maxLabelChars = 16 + _maxSize = 8 * 1024 * 1024 + _formattable = True + _linuxNative = True + _supported = True + _bootable = True + _dump = True + _check = True + _infofs = "jfs_tune" + _defaultInfoOptions = ["-l"] + _existingSizeFields = ["Aggregate block size:", "Aggregate size:"] + partedSystem = fileSystemType["jfs"] + + @property + def supported(self): + """ Is this filesystem a supported type? """ + supported = self._supported + if flags.cmdline.has_key("jfs"): + supported = self.utilsAvailable + + return supported + +register_device_format(JFS) + + +class ReiserFS(FS): + """ reiserfs filesystem """ + _type = "reiserfs" + _mkfs = "mkreiserfs" + _resizefs = "resize_reiserfs" + _labelfs = "reiserfstune" + _modules = ["reiserfs"] + _defaultFormatOptions = ["-f", "-f"] + _defaultLabelOptions = ["-l"] + _maxLabelChars = 16 + _maxSize = 16 * 1024 * 1024 + _formattable = True + _linuxNative = True + _supported = True + _bootable = True + _dump = True + _check = True + _packages = ["reiserfs-utils"] + _infofs = "debugreiserfs" + _defaultInfoOptions = [] + _existingSizeFields = ["Count of blocks on the device:", "Blocksize:"] + partedSystem = fileSystemType["reiserfs"] + + @property + def supported(self): + """ Is this filesystem a supported type? """ + supported = self._supported + if flags.cmdline.has_key("reiserfs"): + supported = self.utilsAvailable + + return supported + + @property + def resizeArgs(self): + argv = ["-s", "%dM" % (self.targetSize,), self.device] + return argv + +register_device_format(ReiserFS) + + +class XFS(FS): + """ XFS filesystem """ + _type = "xfs" + _mkfs = "mkfs.xfs" + _modules = ["xfs"] + _labelfs = "xfs_admin" + _defaultFormatOptions = ["-f"] + _defaultLabelOptions = ["-L"] + _maxLabelChars = 16 + _maxSize = 16 * 1024 * 1024 + _formattable = True + _linuxNative = True + _supported = True + _bootable = True + _dump = True + _check = True + _packages = ["xfsprogs"] + _infofs = "xfs_db" + _defaultInfoOptions = ["-c", "\"sb 0\"", "-c", "\"p dblocks\"", + "-c", "\"p blocksize\""] + _existingSizeFields = ["dblocks =", "blocksize ="] + partedSystem = fileSystemType["xfs"] + + def _getLabelArgs(self, label): + argv = [] + argv.extend(self.defaultLabelOptions) + argv.extend([label, self.device]) + return argv + +register_device_format(XFS) + + +class HFS(FS): + _type = "hfs" + _mkfs = "hformat" + _modules = ["hfs"] + _formattable = True + partedSystem = fileSystemType["hfs"] + +register_device_format(HFS) + + +class AppleBootstrapFS(HFS): + _type = "appleboot" + _mountType = "hfs" + _name = "Apple Bootstrap" + _bootable = True + _minSize = 800.00 / 1024.00 + _maxSize = 1 + + @property + def supported(self): + import pyanaconda.platform as platform + return (isinstance(platform.getPlatform(None), platform.NewWorldPPC) + and self.utilsAvailable) + + def writeKS(self, f): + f.write("appleboot --fstype=%s" % self.type) + +register_device_format(AppleBootstrapFS) + + +# this doesn't need to be here +class HFSPlus(FS): + _type = "hfs+" + _modules = ["hfsplus"] + _udevTypes = ["hfsplus"] + partedSystem = fileSystemType["hfs+"] + +register_device_format(HFSPlus) + + +class NTFS(FS): + """ ntfs filesystem. """ + _type = "ntfs" + _resizefs = "ntfsresize" + _fsck = "ntfsresize" + _resizable = True + _minSize = 1 + _maxSize = 16 * 1024 * 1024 + _defaultMountOptions = ["defaults", "ro"] + _defaultCheckOptions = ["-c"] + _packages = ["ntfsprogs"] + _infofs = "ntfsinfo" + _defaultInfoOptions = ["-m"] + _existingSizeFields = ["Cluster Size:", "Volume Size in Clusters:"] + partedSystem = fileSystemType["ntfs"] + + def _fsckFailed(self, rc): + if rc != 0: + return True + return False + + @property + def minSize(self): + """ The minimum filesystem size in megabytes. """ + if self._minInstanceSize is None: + # we try one time to determine the minimum size. + size = self._minSize + if self.exists and os.path.exists(self.device): + minSize = None + buf = iutil.execWithCapture(self.resizefsProg, + ["-m", self.device], + stderr = "/dev/tty5") + for l in buf.split("\n"): + if not l.startswith("Minsize"): + continue + try: + min = l.split(":")[1].strip() + minSize = int(min) + 250 + except Exception, e: + minSize = None + log.warning("Unable to parse output for minimum size on %s: %s" %(self.device, e)) + + if minSize is None: + log.warning("Unable to discover minimum size of filesystem " + "on %s" %(self.device,)) + else: + size = minSize + + self._minInstanceSize = size + + return self._minInstanceSize + + @property + def resizeArgs(self): + # You must supply at least two '-f' options to ntfsresize or + # the proceed question will be presented to you. + argv = ["-ff", "-s", "%dM" % (self.targetSize,), self.device] + return argv + + +register_device_format(NTFS) + + +# if this isn't going to be mountable it might as well not be here +class NFS(FS): + """ NFS filesystem. """ + _type = "nfs" + _modules = ["nfs"] + + def _deviceCheck(self, devspec): + if devspec is not None and ":" not in devspec: + raise ValueError("device must be of the form <host>:<path>") + + @property + def mountable(self): + return False + + def _setDevice(self, devspec): + self._deviceCheck(devspec) + self._device = devspec + + def _getDevice(self): + return self._device + + device = property(lambda f: f._getDevice(), + lambda f,d: f._setDevice(d), + doc="Full path the device this format occupies") + +register_device_format(NFS) + + +class NFSv4(NFS): + """ NFSv4 filesystem. """ + _type = "nfs4" + _modules = ["nfs4"] + +register_device_format(NFSv4) + + +class Iso9660FS(FS): + """ ISO9660 filesystem. """ + _type = "iso9660" + _formattable = False + _supported = True + _resizable = False + _bootable = False + _linuxNative = False + _dump = False + _check = False + _migratable = False + _defaultMountOptions = ["ro"] + + def writeKS(self, f): + return + +register_device_format(Iso9660FS) + + +class NoDevFS(FS): + """ nodev filesystem base class """ + _type = "nodev" + + def __init__(self, *args, **kwargs): + FS.__init__(self, *args, **kwargs) + self.exists = True + self.device = self.type + + def _setDevice(self, devspec): + self._device = devspec + + def _getExistingSize(self): + pass + + def writeKS(self, f): + return + +register_device_format(NoDevFS) + + +class DevPtsFS(NoDevFS): + """ devpts filesystem. """ + _type = "devpts" + _defaultMountOptions = ["gid=5", "mode=620"] + +register_device_format(DevPtsFS) + + +# these don't really need to be here +class ProcFS(NoDevFS): + _type = "proc" + +register_device_format(ProcFS) + + +class SysFS(NoDevFS): + _type = "sysfs" + +register_device_format(SysFS) + + +class TmpFS(NoDevFS): + _type = "tmpfs" + +register_device_format(TmpFS) + + +class BindFS(FS): + _type = "bind" + + @property + def mountable(self): + return True + + def _getExistingSize(self): + pass + + def writeKS(self, f): + return + +register_device_format(BindFS) + + diff --git a/storage/formats/luks.py b/storage/formats/luks.py new file mode 100644 index 0000000..b164f14 --- /dev/null +++ b/storage/formats/luks.py @@ -0,0 +1,352 @@ +# luks.py +# Device format classes for anaconda's storage configuration module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + + + +import os + +try: + import volume_key +except ImportError: + volume_key = None + +from ..storage_log import log_method_call +from ..errors import * +from ..devicelibs import crypto +from . import DeviceFormat, register_device_format + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + + +class LUKS(DeviceFormat): + """ A LUKS device. """ + _type = "luks" + _name = "LUKS" + _lockedName = _("Encrypted") + _udevTypes = ["crypto_LUKS"] + _formattable = True # can be formatted + _supported = False # is supported + _linuxNative = True # for clearpart + _packages = ["cryptsetup-luks"] # required packages + + def __init__(self, *args, **kwargs): + """ Create a LUKS instance. + + Keyword Arguments: + + device -- the path to the underlying device + name -- the name of the mapped device + uuid -- this device's UUID + passphrase -- device passphrase (string) + key_file -- path to a file containing a key (string) + cipher -- cipher mode string + key_size -- key size in bits + exists -- indicates whether this is an existing format + escrow_cert -- certificate to use for key escrow + add_backup_passphrase -- generate a backup passphrase? + """ + log_method_call(self, *args, **kwargs) + DeviceFormat.__init__(self, *args, **kwargs) + self.cipher = kwargs.get("cipher") + self.key_size = kwargs.get("key_size") + self.mapName = kwargs.get("name") + + if not self.exists and not self.cipher: + self.cipher = "aes-xts-plain" + if not self.key_size: + # default to the max (512 bits) for aes-xts + self.key_size = 512 + + # FIXME: these should both be lists, but managing them will be a pain + self.__passphrase = kwargs.get("passphrase") + self._key_file = kwargs.get("key_file") + self.escrow_cert = kwargs.get("escrow_cert") + self.add_backup_passphrase = kwargs.get("add_backup_passphrase", False) + + if not self.mapName and self.exists and self.uuid: + self.mapName = "luks-%s" % self.uuid + elif not self.mapName and self.device: + self.mapName = "luks-%s" % os.path.basename(self.device) + + def __str__(self): + s = DeviceFormat.__str__(self) + if self.__passphrase: + passphrase = "(set)" + else: + passphrase = "(not set)" + s += (" cipher = %(cipher)s keySize = %(keySize)s" + " mapName = %(mapName)s\n" + " keyFile = %(keyFile)s passphrase = %(passphrase)s\n" + " escrowCert = %(escrowCert)s addBackup = %(backup)s" % + {"cipher": self.cipher, "keySize": self.key_size, + "mapName": self.mapName, "keyFile": self._key_file, + "passphrase": passphrase, "escrowCert": self.escrow_cert, + "backup": self.add_backup_passphrase}) + return s + + @property + def dict(self): + d = super(LUKS, self).dict + d.update({"cipher": self.cipher, "keySize": self.key_size, + "mapName": self.mapName, "hasKey": self.hasKey, + "escrowCert": self.escrow_cert, + "backup": self.add_backup_passphrase}) + return d + + @property + def name(self): + name = self._name + # for existing locked devices, show "Encrypted" instead of LUKS + if self.hasKey or not self.exists: + name = self._name + else: + name = "%s (%s)" % (self._lockedName, self._name) + return name + + def _setPassphrase(self, passphrase): + """ Set the passphrase used to access this device. """ + self.__passphrase = passphrase + + passphrase = property(fset=_setPassphrase) + + @property + def hasKey(self): + return ((self.__passphrase not in ["", None]) or + (self._key_file and os.access(self._key_file, os.R_OK))) + + @property + def configured(self): + """ To be ready we need a key or passphrase and a map name. """ + return self.hasKey and self.mapName + + @property + def status(self): + if not self.exists or not self.mapName: + return False + return os.path.exists("/dev/mapper/%s" % self.mapName) + + def probe(self): + """ Probe for any missing information about this format. + + cipher mode, key size + """ + raise NotImplementedError("probe method not defined for LUKS") + + def setup(self, *args, **kwargs): + """ Open, or set up, the format. """ + log_method_call(self, device=self.device, mapName=self.mapName, + type=self.type, status=self.status) + if not self.configured: + raise LUKSError("luks device not configured") + + if self.status: + return + + DeviceFormat.setup(self, *args, **kwargs) + crypto.luks_open(self.device, self.mapName, + passphrase=self.__passphrase, + key_file=self._key_file) + + def teardown(self, *args, **kwargs): + """ Close, or tear down, the format. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise LUKSError("format has not been created") + + if self.status: + log.debug("unmapping %s" % self.mapName) + crypto.luks_close(self.mapName) + + def create(self, *args, **kwargs): + """ Create the format. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.hasKey: + raise LUKSError("luks device has no key/passphrase") + + intf = kwargs.get("intf") + w = None + if intf: + w = intf.waitWindow(_("Formatting"), + _("Encrypting %s") % kwargs.get("device", + self.device)) + + try: + DeviceFormat.create(self, *args, **kwargs) + crypto.luks_format(self.device, + passphrase=self.__passphrase, + key_file=self._key_file, + cipher=self.cipher, + key_size=self.key_size) + except Exception: + raise + else: + self.uuid = crypto.luks_uuid(self.device) + self.exists = True + self.mapName = "luks-%s" % self.uuid + self.notifyKernel() + finally: + if w: + w.pop() + + def destroy(self, *args, **kwargs): + """ Create the format. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + self.teardown() + DeviceFormat.destroy(self, *args, **kwargs) + + @property + def keyFile(self): + """ Path to key file to be used in /etc/crypttab """ + return self._key_file + + def addKeyFromFile(self, keyfile): + """ Add a new key from a file. + + Add the contents of the specified key file to an available key + slot in the LUKS header. + """ + log_method_call(self, device=self.device, + type=self.type, status=self.status, file=keyfile) + if not self.exists: + raise LUKSError("format has not been created") + + crypto.luks_add_key(self.device, + passphrase=self.__passphrase, + key_file=self._key_file, + new_key_file=keyfile) + + def addPassphrase(self, passphrase): + """ Add a new passphrase. + + Add the specified passphrase to an available key slot in the + LUKS header. + """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise LUKSError("format has not been created") + + crypto.luks_add_key(self.device, + passphrase=self.__passphrase, + key_file=self._key_file, + new_passphrase=passphrase) + + def removeKeyFromFile(self, keyfile): + """ Remove a key contained in a file. + + Remove key contained in the specified key file from the LUKS + header. + """ + log_method_call(self, device=self.device, + type=self.type, status=self.status, file=keyfile) + if not self.exists: + raise LUKSError("format has not been created") + + crypto.luks_remove_key(self.device, + passphrase=self.__passphrase, + key_file=self._key_file, + del_key_file=keyfile) + + + def removePassphrase(self, passphrase): + """ Remove the specified passphrase from the LUKS header. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise LUKSError("format has not been created") + + crypto.luks_remove_key(self.device, + passphrase=self.__passphrase, + key_file=self._key_file, + del_passphrase=passphrase) + + def _escrowVolumeIdent(self, vol): + """ Return an escrow packet filename prefix for a volume_key.Volume. """ + label = vol.label + if label is not None: + label = label.replace("/", "_") + uuid = vol.uuid + if uuid is not None: + uuid = uuid.replace("/", "_") + # uuid is never None on LUKS volumes + if label is not None and uuid is not None: + volume_ident = "%s-%s" % (label, uuid) + elif uuid is not None: + volume_ident = uuid + elif label is not None: + volume_ident = label + else: + volume_ident = "_unknown" + return volume_ident + + def escrow(self, directory, backupPassphrase): + log.debug("escrow: escrowVolume start for %s" % self.device) + if volume_key is None: + raise LUKSError("Missing key escrow support libraries") + + vol = volume_key.Volume.open(self.device) + volume_ident = self._escrowVolumeIdent(vol) + + ui = volume_key.UI() + # This callback is not expected to be used, let it always fail + ui.generic_cb = lambda unused_prompt, unused_echo: None + def known_passphrase_cb(unused_prompt, failed_attempts): + if failed_attempts == 0: + return self.__passphrase + return None + ui.passphrase_cb = known_passphrase_cb + + log.debug("escrow: getting secret") + vol.get_secret(volume_key.SECRET_DEFAULT, ui) + log.debug("escrow: creating packet") + default_packet = vol.create_packet_assymetric_from_cert_data \ + (volume_key.SECRET_DEFAULT, self.escrow_cert, ui) + log.debug("escrow: packet created") + with open("%s/%s-escrow" % (directory, volume_ident), "wb") as f: + f.write(default_packet) + log.debug("escrow: packet written") + + if self.add_backup_passphrase: + log.debug("escrow: adding backup passphrase") + vol.add_secret(volume_key.SECRET_PASSPHRASE, backupPassphrase) + log.debug("escrow: creating backup packet") + backup_passphrase_packet = \ + vol.create_packet_assymetric_from_cert_data \ + (volume_key.SECRET_PASSPHRASE, self.escrow_cert, ui) + log.debug("escrow: backup packet created") + with open("%s/%s-escrow-backup-passphrase" % + (directory, volume_ident), "wb") as f: + f.write(backup_passphrase_packet) + log.debug("escrow: backup packet written") + + log.debug("escrow: escrowVolume done for %s" % repr(self.device)) + + +register_device_format(LUKS) + diff --git a/storage/formats/lvmpv.py b/storage/formats/lvmpv.py new file mode 100644 index 0000000..9fe9ba3 --- /dev/null +++ b/storage/formats/lvmpv.py @@ -0,0 +1,156 @@ +# lvmpv.py +# Device format classes for anaconda's storage configuration module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +import os + +from ..storage_log import log_method_call +from parted import PARTITION_LVM +from ..errors import * +from ..devicelibs import lvm +from . import DeviceFormat, register_device_format + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + + +class LVMPhysicalVolume(DeviceFormat): + """ An LVM physical volume. """ + _type = "lvmpv" + _name = "physical volume (LVM)" + _udevTypes = ["LVM2_member"] + partedFlag = PARTITION_LVM + _formattable = True # can be formatted + _supported = True # is supported + _linuxNative = True # for clearpart + _packages = ["lvm2"] # required packages + + def __init__(self, *args, **kwargs): + """ Create an LVMPhysicalVolume instance. + + Keyword Arguments: + + device -- path to the underlying device + uuid -- this PV's uuid (not the VG uuid) + vgName -- the name of the VG this PV belongs to + vgUuid -- the UUID of the VG this PV belongs to + peStart -- offset of first physical extent + exists -- indicates whether this is an existing format + + """ + log_method_call(self, *args, **kwargs) + DeviceFormat.__init__(self, *args, **kwargs) + self.vgName = kwargs.get("vgName") + self.vgUuid = kwargs.get("vgUuid") + # liblvm may be able to tell us this at some point, even + # for not-yet-created devices + self.peStart = kwargs.get("peStart", 0.1875) # in MB + + def __str__(self): + s = DeviceFormat.__str__(self) + s += (" vgName = %(vgName)s vgUUID = %(vgUUID)s" + " peStart = %(peStart)s" % + {"vgName": self.vgName, "vgUUID": self.vgUuid, + "peStart": self.peStart}) + return s + + @property + def dict(self): + d = super(LVMPhysicalVolume, self).dict + d.update({"vgName": self.vgName, "vgUUID": self.vgUuid, + "peStart": self.peStart}) + return d + + def probe(self): + """ Probe for any missing information about this device. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise PhysicalVolumeError("format has not been created") + + #info = lvm.pvinfo(self.device) + #self.vgName = info['vg_name'] + #self.vgUuid = info['vg_uuid'] + + def create(self, *args, **kwargs): + """ Create the format. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + intf = kwargs.get("intf") + w = None + if intf: + w = intf.progressWindow(_("Formatting"), + _("Creating %s on %s") + % (self.name, self.device), + 100, pulse = True) + + try: + DeviceFormat.create(self, *args, **kwargs) + # Consider use of -Z|--zero + # -f|--force or -y|--yes may be required + + # lvm has issues with persistence of metadata, so here comes the + # hammer... + DeviceFormat.destroy(self, *args, **kwargs) + + lvm.pvcreate(self.device, progress=w) + except Exception: + raise + else: + self.exists = True + self.notifyKernel() + finally: + if w: + w.pop() + + def destroy(self, *args, **kwargs): + """ Destroy the format. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise PhysicalVolumeError("format has not been created") + + if self.status: + raise PhysicalVolumeError("device is active") + + # FIXME: verify path exists? + try: + lvm.pvremove(self.device) + except LVMError: + DeviceFormat.destroy(self, *args, **kwargs) + + self.exists = False + self.notifyKernel() + + @property + def status(self): + # XXX hack + return (self.exists and self.vgName and + os.path.isdir("/dev/mapper/%s" % self.vgName)) + + def writeKS(self, f): + f.write("pv.%s" % self.uuid) + +register_device_format(LVMPhysicalVolume) + diff --git a/storage/formats/mdraid.py b/storage/formats/mdraid.py new file mode 100644 index 0000000..d153807 --- /dev/null +++ b/storage/formats/mdraid.py @@ -0,0 +1,124 @@ +# mdraid.py +# Device format classes for anaconda's storage configuration module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +import os + +from ..storage_log import log_method_call +from flags import flags +from parted import PARTITION_RAID +from ..errors import * +from ..devicelibs import mdraid +from . import DeviceFormat, register_device_format + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + + +class MDRaidMember(DeviceFormat): + """ An mdraid member disk. """ + _type = "mdmember" + _name = "software RAID" + _udevTypes = ["linux_raid_member"] + partedFlag = PARTITION_RAID + _formattable = True # can be formatted + _supported = True # is supported + _linuxNative = True # for clearpart + _packages = ["mdadm"] # required packages + + def __init__(self, *args, **kwargs): + """ Create a MDRaidMember instance. + + Keyword Arguments: + + device -- path to underlying device + uuid -- this member device's uuid + mdUuid -- the uuid of the array this device belongs to + exists -- indicates whether this is an existing format + + """ + log_method_call(self, *args, **kwargs) + DeviceFormat.__init__(self, *args, **kwargs) + self.mdUuid = kwargs.get("mdUuid") + self.raidMinor = None + + #self.probe() + self.biosraid = False + + def __str__(self): + s = DeviceFormat.__str__(self) + s += (" mdUUID = %(mdUUID)s biosraid = %(biosraid)s" % + {"mdUUID": self.mdUuid, "biosraid": self.biosraid}) + return s + + @property + def dict(self): + d = super(MDRaidMember, self).dict + d.update({"mdUUID": self.mdUuid, "biosraid": self.biosraid}) + return d + + def probe(self): + """ Probe for any missing information about this format. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise MDMemberError("format does not exist") + + info = mdraid.mdexamine(self.device) + if self.uuid is None: + self.uuid = info['uuid'] + if self.raidMinor is None: + self.raidMinor = info['mdMinor'] + + def destroy(self, *args, **kwargs): + if not self.exists: + raise MDMemberError("format does not exist") + + if not os.access(self.device, os.W_OK): + raise MDMemberError("device path does not exist") + + mdraid.mddestroy(self.device) + self.exists = False + + @property + def status(self): + # XXX hack -- we don't have a nice way to see if the array is active + return False + + @property + def hidden(self): + return (self._hidden or self.biosraid) + + def writeKS(self, f): + f.write("raid.%s" % self.mdUuid) + +# nodmraid -> Wether to use BIOS RAID or not +# Note the anaconda cmdline has not been parsed yet when we're first imported, +# so we can not use flags.dmraid here +if not flags.cmdline.has_key("noiswmd") and \ + not flags.cmdline.has_key("nodmraid"): + MDRaidMember._udevTypes.append("isw_raid_member") + +register_device_format(MDRaidMember) + diff --git a/storage/formats/multipath.py b/storage/formats/multipath.py new file mode 100644 index 0000000..86c05d6 --- /dev/null +++ b/storage/formats/multipath.py @@ -0,0 +1,95 @@ +# multipath.py +# multipath device formats +# +# Copyright (C) 2009 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. +# +# 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): Peter Jones <pjones@redhat.com> +# + +from ..storage_log import log_method_call +from ..errors import * +from . import DeviceFormat, register_device_format + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + +class MultipathMember(DeviceFormat): + """ A multipath member disk. """ + _type = "multipath_member" + _name = "multipath member device" + _udev_types = ["multipath_member"] + _formattable = False # can be formatted + _supported = True # is supported + _linuxNative = False # for clearpart + _packages = ["device-mapper-multipath"] # required packages + _resizable = False # can be resized + _bootable = False # can be used as boot + _maxSize = 0 # maximum size in MB + _minSize = 0 # minimum size in MB + _hidden = True # hide devices with this formatting? + + def __init__(self, *args, **kwargs): + """ Create a DeviceFormat instance. + + Keyword Arguments: + + device -- path to the underlying device + uuid -- this format's UUID + exists -- indicates whether this is an existing format + + On initialization this format is like DeviceFormat + + """ + log_method_call(self, *args, **kwargs) + DeviceFormat.__init__(self, *args, **kwargs) + + # Initialize the attribute that will hold the block object. + self._member = None + + def __str__(self): + s = DeviceFormat.__str__(self) + s += (" member = %(member)r" % {"member": self.member}) + return s + + def _getMember(self): + return self._member + + def _setMember(self, member): + self._member = member + + member = property(lambda s: s._getMember(), + lambda s,m: s._setMember(m)) + + def create(self, *args, **kwargs): + log_method_call(self, device=self.device, + type=self.type, status=self.status) + raise MultipathMemberError("creation of multipath members is non-sense") + + def destroy(self, *args, **kwargs): + log_method_call(self, device=self.device, + type=self.type, status=self.status) + raise MultipathMemberError("destruction of multipath members is non-sense") + +register_device_format(MultipathMember) + diff --git a/storage/formats/prepboot.py b/storage/formats/prepboot.py new file mode 100644 index 0000000..b1a868b --- /dev/null +++ b/storage/formats/prepboot.py @@ -0,0 +1,64 @@ +# prepboot.py +# Format class for PPC PReP Boot. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +from ..errors import * +from . import DeviceFormat, register_device_format +from parted import PARTITION_PREP + +class PPCPRePBoot(DeviceFormat): + """ Generic device format. """ + _type = "prepboot" + _name = "PPC PReP Boot" + _udevTypes = [] + partedFlag = PARTITION_PREP + _formattable = True # can be formatted + _linuxNative = True # for clearpart + _bootable = True # can be used as boot + _maxSize = 4 # maximum size in MB + _minSize = 10 # minimum size in MB + + def __init__(self, *args, **kwargs): + """ Create a PRePBoot instance. + + Keyword Arguments: + + device -- path to the underlying device + exists -- indicates whether this is an existing format + + """ + DeviceFormat.__init__(self, *args, **kwargs) + + @property + def status(self): + return False + + @property + def supported(self): + import pyanaconda.platform as platform + return isinstance(platform.getPlatform(None), platform.IPSeriesPPC) + + def writeKS(self, f): + f.write("prepboot --fstype=%s" % self.type) + + +register_device_format(PPCPRePBoot) + diff --git a/storage/formats/swap.py b/storage/formats/swap.py new file mode 100644 index 0000000..362f6d5 --- /dev/null +++ b/storage/formats/swap.py @@ -0,0 +1,186 @@ +# swap.py +# Device format classes for anaconda's storage configuration module. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +from iutil import numeric_type +from parted import PARTITION_SWAP, fileSystemType +from ..storage_log import log_method_call +from ..errors import * +from ..devicelibs import swap +from . import DeviceFormat, register_device_format + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + + +class SwapSpace(DeviceFormat): + """ Swap space """ + _type = "swap" + _name = None + _udevTypes = ["swap"] + partedFlag = PARTITION_SWAP + partedSystem = fileSystemType["linux-swap(v1)"] + _formattable = True # can be formatted + _supported = True # is supported + _linuxNative = True # for clearpart + + def __init__(self, *args, **kwargs): + """ Create a SwapSpace instance. + + Keyword Arguments: + + device -- path to the underlying device + uuid -- this swap space's uuid + label -- this swap space's label + priority -- this swap space's priority + exists -- indicates whether this is an existing format + + """ + log_method_call(self, *args, **kwargs) + DeviceFormat.__init__(self, *args, **kwargs) + + self.priority = kwargs.get("priority") + self.label = kwargs.get("label") + + def __str__(self): + s = DeviceFormat.__str__(self) + s += (" priority = %(priority)s label = %(label)s" % + {"priority": self.priority, "label": self.label}) + return s + + @property + def dict(self): + d = super(SwapSpace, self).dict + d.update({"priority": self.priority, "label": self.label}) + return d + + def _setPriority(self, priority): + if priority is None: + self._priority = None + return + + if not isinstance(priority, int) or not 0 <= priority <= 32767: + raise ValueError("swap priority must be an integer between 0 and 32767") + + self._priority = priority + + def _getPriority(self): + return self._priority + + priority = property(_getPriority, _setPriority, + doc="The priority of the swap device") + + def _getOptions(self): + opts = "" + if self.priority is not None: + opts += "pri=%d" % self.priority + + return opts + + def _setOptions(self, opts): + if not opts: + self.priority = None + return + + for option in opts.split(","): + (opt, equals, arg) = option.partition("=") + if equals and opt == "pri": + try: + self.priority = int(arg) + except ValueError: + log.info("invalid value for swap priority: %s" % arg) + + options = property(_getOptions, _setOptions, + doc="The swap device's fstab options string") + + @property + def status(self): + """ Device status. """ + return self.exists and swap.swapstatus(self.device) + + def setup(self, *args, **kwargs): + """ Open, or set up, a device. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise SwapSpaceError("format has not been created") + + if self.status: + return + + DeviceFormat.setup(self, *args, **kwargs) + swap.swapon(self.device, priority=self.priority) + + def teardown(self, *args, **kwargs): + """ Close, or tear down, a device. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + if not self.exists: + raise SwapSpaceError("format has not been created") + + if self.status: + swap.swapoff(self.device) + + def create(self, *args, **kwargs): + """ Create the device. """ + log_method_call(self, device=self.device, + type=self.type, status=self.status) + intf = kwargs.get("intf") + force = kwargs.get("force") + if not force and self.exists: + raise SwapSpaceError("format already exists") + + if force: + self.teardown() + elif self.status: + raise SwapSpaceError("device exists and is active") + + w = None + if intf: + w = intf.progressWindow(_("Formatting"), + _("Creating %s on %s") + % (self.type, + kwargs.get("device", self.device)), + 100, pulse = True) + + try: + DeviceFormat.create(self, *args, **kwargs) + swap.mkswap(self.device, label=self.label, progress=w) + except Exception: + raise + else: + self.exists = True + finally: + if w: + w.pop() + + def writeKS(self, f): + f.write("swap") + + if self.label: + f.write(" --label=\"%s\"" % self.label) + + +register_device_format(SwapSpace) + diff --git a/storage/iscsi.py b/storage/iscsi.py new file mode 100644 index 0000000..55caa17 --- /dev/null +++ b/storage/iscsi.py @@ -0,0 +1,333 @@ +# +# iscsi.py - iscsi class +# +# Copyright (C) 2005, 2006 IBM, Inc. All rights reserved. +# Copyright (C) 2006 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. +# + +from constants import * +from udev import * +import os +import iutil +from flags import flags +import logging +import shutil +import time +import hashlib +import random +log = logging.getLogger("anaconda") + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +has_libiscsi = True +try: + import libiscsi +except ImportError: + has_libiscsi = False + +# Note that stage2 copies all files under /sbin to /usr/sbin +ISCSID="" +INITIATOR_FILE="/etc/iscsi/initiatorname.iscsi" + +def find_iscsi_files(): + global ISCSID + if ISCSID == "": + for dir in ("/usr/sbin", "/tmp/updates", "/mnt/source/RHupdates"): + path="%s/iscsid" % (dir,) + if os.access(path, os.X_OK): + ISCSID=path + +def has_iscsi(): + find_iscsi_files() + if ISCSID == "" or not has_libiscsi: + return False + + log.info("ISCSID is %s" % (ISCSID,)) + + # make sure the module is loaded + if not os.access("/sys/module/iscsi_tcp", os.X_OK): + return False + return True + +def randomIname(): + """Generate a random initiator name the same way as iscsi-iname""" + + s = "iqn.1994-05.com.fedora:01." + m = hashlib.md5() + u = os.uname() + for i in u: + m.update(i) + dig = m.hexdigest() + + for i in range(0, 6): + s += dig[random.randrange(0, 32)] + return s + +def stabilize(intf = None): + # Wait for udev to create the devices for the just added disks + if intf: + w = intf.waitWindow(_("Scanning iSCSI nodes"), + _("Scanning iSCSI nodes")) + # It is possible when we get here the events for the new devices + # are not send yet, so sleep to make sure the events are fired + time.sleep(2) + udev_settle() + if intf: + w.pop() + +class iscsi(object): + """ iSCSI utility class. + + This class will automatically discover and login to iBFT (or + other firmware) configured iscsi devices when the startup() method + gets called. It can also be used to manually configure iscsi devices + through the addTarget() method. + + As this class needs to make sure certain things like starting iscsid + and logging in to firmware discovered disks only happens once + and as it keeps a global list of all iSCSI devices it is implemented as + a Singleton. + """ + + def __init__(self): + # This list contains all nodes + self.nodes = [] + # This list contains nodes discovered through iBFT (or other firmware) + self.ibftNodes = [] + self._initiator = "" + self.initiatorSet = False + self.started = False + + if flags.ibft: + try: + initiatorname = libiscsi.get_firmware_initiator_name() + self._initiator = initiatorname + self.initiatorSet = True + except: + pass + + # So that users can write iscsi() to get the singleton instance + def __call__(self): + return self + + def _getInitiator(self): + if self._initiator != "": + return self._initiator + + return randomIname() + + def _setInitiator(self, val): + if self.initiatorSet and val != self._initiator: + raise ValueError, "Unable to change iSCSI initiator name once set" + if len(val) == 0: + raise ValueError, "Must provide a non-zero length string" + self._initiator = val + + initiator = property(_getInitiator, _setInitiator) + + def _startIBFT(self, intf = None): + if not flags.ibft: + return + + try: + found_nodes = libiscsi.discover_firmware() + except: + # an exception here means there is no ibft firmware, just return + return + + for node in found_nodes: + try: + node.login() + log.info("iscsi._startIBFT logged in to %s %s %s" % (node.name, node.address, node.port)) + self.nodes.append(node) + self.ibftNodes.append(node) + except IOError, e: + log.error("Could not log into ibft iscsi target %s: %s" % + (node.name, str(e))) + pass + + stabilize(intf) + + def startup(self, intf = None): + if self.started: + return + + if not has_iscsi(): + return + + if self._initiator == "": + log.info("no initiator set") + return + + if intf: + w = intf.waitWindow(_("Initializing iSCSI initiator"), + _("Initializing iSCSI initiator")) + + log.debug("Setting up %s" % (INITIATOR_FILE, )) + log.info("iSCSI initiator name %s", self.initiator) + if os.path.exists(INITIATOR_FILE): + os.unlink(INITIATOR_FILE) + if not os.path.isdir("/etc/iscsi"): + os.makedirs("/etc/iscsi", 0755) + fd = os.open(INITIATOR_FILE, os.O_RDWR | os.O_CREAT) + os.write(fd, "InitiatorName=%s\n" %(self.initiator)) + os.close(fd) + self.initiatorSet = True + + for dir in ['ifaces','isns','nodes','send_targets','slp','static']: + fulldir = "/var/lib/iscsi/%s" % (dir,) + if not os.path.isdir(fulldir): + os.makedirs(fulldir, 0755) + + log.info("iSCSI startup") + iutil.execWithRedirect(ISCSID, [], + stdout="/dev/tty5", stderr="/dev/tty5") + time.sleep(1) + + if intf: + w.pop() + + self._startIBFT(intf) + self.started = True + + def addTarget(self, ipaddr, port="3260", user=None, pw=None, + user_in=None, pw_in=None, intf=None): + authinfo = None + found = 0 + logged_in = 0 + + if not has_iscsi(): + raise IOError, _("iSCSI not available") + if self._initiator == "": + raise ValueError, _("No initiator name set") + + if user or pw or user_in or pw_in: + # Note may raise a ValueError + authinfo = libiscsi.chapAuthInfo(username=user, password=pw, + reverse_username=user_in, + reverse_password=pw_in) + self.startup(intf) + + # Note may raise an IOError + found_nodes = libiscsi.discover_sendtargets(address=ipaddr, + port=int(port), + authinfo=authinfo) + if found_nodes == None: + raise IOError, _("No iSCSI nodes discovered") + + if intf: + w = intf.waitWindow(_("Logging in to iSCSI nodes"), + _("Logging in to iSCSI nodes")) + + for node in found_nodes: + # skip nodes we already have + if node in self.nodes: + continue + + found = found + 1 + try: + if (authinfo): + node.setAuth(authinfo) + node.login() + log.info("iscsi.addTarget logged in to %s %s %s" % (node.name, node.address, node.port)) + self.nodes.append(node) + logged_in = logged_in + 1 + except IOError, e: + log.warning( + "Could not log into discovered iscsi target %s: %s" % + (node.name, str(e))) + # some nodes may require different credentials + pass + + if intf: + w.pop() + + if found == 0: + raise IOError, _("No new iSCSI nodes discovered") + + if logged_in == 0: + raise IOError, _("Could not log in to any of the discovered nodes") + + stabilize(intf) + + def writeKS(self, f): + if not self.initiatorSet: + return + f.write("iscsiname %s\n" %(self.initiator,)) + for n in self.nodes: + f.write("iscsi --ipaddr %s --port %s" %(n.address, n.port)) + auth = n.getAuth() + if auth: + f.write(" --user %s" % auth.username) + f.write(" --password %s" % auth.password) + if len(auth.reverse_username): + f.write(" --reverse-user %s" % auth.reverse_username) + if len(auth.reverse_password): + f.write(" --reverse-password %s" % auth.reverse_password) + f.write("\n") + + def write(self, instPath, anaconda): + if not self.initiatorSet: + return + + # set iscsi nodes to autostart + root = anaconda.storage.rootDevice + for node in self.nodes: + autostart = True + disks = self.getNodeDisks(node, anaconda.storage) + for disk in disks: + # nodes used for root get started by the initrd + if root.dependsOn(disk): + autostart = False + + if autostart: + node.setParameter("node.startup", "automatic") + + if not os.path.isdir(instPath + "/etc/iscsi"): + os.makedirs(instPath + "/etc/iscsi", 0755) + fd = os.open(instPath + INITIATOR_FILE, os.O_RDWR | os.O_CREAT) + os.write(fd, "InitiatorName=%s\n" %(self.initiator)) + os.close(fd) + + # copy "db" files. *sigh* + if os.path.isdir(instPath + "/var/lib/iscsi"): + shutil.rmtree(instPath + "/var/lib/iscsi") + if os.path.isdir("/var/lib/iscsi"): + shutil.copytree("/var/lib/iscsi", instPath + "/var/lib/iscsi", + symlinks=True) + + def getNode(self, name, address, port): + for node in self.nodes: + if node.name == name and node.address == address and \ + node.port == int(port): + return node + + return None + + def getNodeDisks(self, node, storage): + nodeDisks = [] + iscsiDisks = storage.devicetree.getDevicesByType("iscsi") + for disk in iscsiDisks: + if disk.node == node: + nodeDisks.append(disk) + + return nodeDisks + +# Create iscsi singleton +iscsi = iscsi() + +# vim:tw=78:ts=4:et:sw=4 diff --git a/storage/miscutils.py b/storage/miscutils.py new file mode 100644 index 0000000..e577497 --- /dev/null +++ b/storage/miscutils.py @@ -0,0 +1,57 @@ +# iutil.py stubs +import os + +import logging +log = logging.getLogger("storage") + +def notify_kernel(path, action="change"): + """ Signal the kernel that the specified device has changed. """ + log.debug("notifying kernel of '%s' event on device %s" % (action, path)) + path = os.path.join(path, "uevent") + if not path.startswith("/sys/") or not os.access(path, os.W_OK): + log.debug("sysfs path '%s' invalid" % path) + raise ValueError("invalid sysfs path") + + f = open(path, "a") + f.write("%s\n" % action) + f.close() + +def get_sysfs_path_by_name(dev_name, class_name="block"): + dev_name = os.path.basename(dev_name) + sysfs_class_dir = "/sys/class/%s" % class_name + dev_path = os.path.join(sysfs_class_dir, dev_name) + if os.path.exists(dev_path): + return dev_path + +import inspect +def log_method_call(d, *args, **kwargs): + classname = d.__class__.__name__ + methodname = inspect.stack()[1][3] + fmt = "%s.%s:" + fmt_args = [classname, methodname] + for arg in args: + fmt += " %s ;" + fmt_args.append(arg) + + for k, v in kwargs.items(): + fmt += " %s: %s ;" + fmt_args.extend([k, v]) + + log.debug(fmt % tuple(fmt_args)) + +def numeric_type(num): + """ Verify that a value is given as a numeric data type. + + Return the number if the type is sensible or raise ValueError + if not. + """ + if num is None: + num = 0 + elif not (isinstance(num, int) or \ + isinstance(num, long) or \ + isinstance(num, float)): + raise ValueError("value (%s) must be either a number or None" % num) + + return num + + diff --git a/storage/partitioning.py b/storage/partitioning.py new file mode 100644 index 0000000..719e21f --- /dev/null +++ b/storage/partitioning.py @@ -0,0 +1,1647 @@ +# partitioning.py +# Disk partitioning functions. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +import sys +import os +from operator import add, sub, gt, lt + +import parted +from pykickstart.constants import * + +from constants import * + +from errors import * +from deviceaction import * +from devices import PartitionDevice, LUKSDevice, devicePathToName +from formats import getFormat + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("storage") + +def _createFreeSpacePartitions(anaconda): + # get a list of disks that have at least one free space region of at + # least 100MB + disks = [] + for disk in anaconda.storage.partitioned: + if anaconda.storage.clearPartDisks and \ + (disk.name not in anaconda.storage.clearPartDisks): + continue + + part = disk.format.firstPartition + while part: + if not part.type & parted.PARTITION_FREESPACE: + part = part.nextPartition() + continue + + if part.getSize(unit="MB") > 100: + disks.append(disk) + break + + part = part.nextPartition() + + # create a separate pv partition for each disk with free space + devs = [] + for disk in disks: + if anaconda.storage.encryptedAutoPart: + fmt_type = "luks" + fmt_args = {"escrow_cert": anaconda.storage.autoPartEscrowCert, + "add_backup_passphrase": anaconda.storage.autoPartAddBackupPassphrase} + else: + fmt_type = "lvmpv" + fmt_args = {} + part = anaconda.storage.newPartition(fmt_type=fmt_type, + fmt_args=fmt_args, + grow=True, + disks=[disk]) + anaconda.storage.createDevice(part) + devs.append(part) + + return (disks, devs) + +def _schedulePartitions(anaconda, disks): + # + # Convert storage.autoPartitionRequests into Device instances and + # schedule them for creation + # + # First pass is for partitions only. We'll do LVs later. + # + for request in anaconda.storage.autoPartitionRequests: + if request.asVol: + continue + + if request.fstype is None: + request.fstype = anaconda.storage.defaultFSType + # This is a little unfortunate but let the backend dictate the rootfstype + # so that things like live installs can do the right thing + if request.mountpoint == "/" and anaconda.backend.rootFsType != None: + request.fstype = anaconda.backend.rootFsType + + dev = anaconda.storage.newPartition(fmt_type=request.fstype, + size=request.size, + grow=request.grow, + maxsize=request.maxSize, + mountpoint=request.mountpoint, + disks=disks, + weight=request.weight) + + # schedule the device for creation + anaconda.storage.createDevice(dev) + + # make sure preexisting broken lvm/raid configs get out of the way + return + +def _scheduleLVs(anaconda, devs): + if anaconda.storage.encryptedAutoPart: + pvs = [] + for dev in devs: + pv = LUKSDevice("luks-%s" % dev.name, + format=getFormat("lvmpv", device=dev.path), + size=dev.size, + parents=dev) + pvs.append(pv) + anaconda.storage.createDevice(pv) + else: + pvs = devs + + # create a vg containing all of the autopart pvs + vg = anaconda.storage.newVG(pvs=pvs) + anaconda.storage.createDevice(vg) + + initialVGSize = vg.size + + # + # Convert storage.autoPartitionRequests into Device instances and + # schedule them for creation. + # + # Second pass, for LVs only. + for request in anaconda.storage.autoPartitionRequests: + if not request.asVol: + continue + + if request.requiredSpace and request.requiredSpace > initialVGSize: + continue + + if request.fstype is None: + request.fstype = anaconda.storage.defaultFSType + + # This is a little unfortunate but let the backend dictate the rootfstype + # so that things like live installs can do the right thing + if request.mountpoint == "/" and anaconda.backend.rootFsType != None: + request.fstype = anaconda.backend.rootFsType + + # FIXME: move this to a function and handle exceptions + dev = anaconda.storage.newLV(vg=vg, + fmt_type=request.fstype, + mountpoint=request.mountpoint, + grow=request.grow, + maxsize=request.maxSize, + size=request.size) + + # schedule the device for creation + anaconda.storage.createDevice(dev) + + +def doAutoPartition(anaconda): + log.debug("doAutoPartition(%s)" % anaconda) + log.debug("doAutoPart: %s" % anaconda.storage.doAutoPart) + log.debug("clearPartType: %s" % anaconda.storage.clearPartType) + log.debug("clearPartDisks: %s" % anaconda.storage.clearPartDisks) + log.debug("autoPartitionRequests: %s" % anaconda.storage.autoPartitionRequests) + log.debug("storage.disks: %s" % [d.name for d in anaconda.storage.disks]) + log.debug("storage.partitioned: %s" % [d.name for d in anaconda.storage.partitioned]) + log.debug("all names: %s" % [d.name for d in anaconda.storage.devices]) + if anaconda.dir == DISPATCH_BACK: + anaconda.storage.reset() + return + + disks = [] + devs = [] + + if anaconda.storage.doAutoPart: + clearPartitions(anaconda.storage) + + if anaconda.storage.doAutoPart: + (disks, devs) = _createFreeSpacePartitions(anaconda) + + if disks == []: + if anaconda.ksdata: + msg = _("Could not find enough free space for automatic " + "partitioning. Press 'OK' to exit the installer.") + else: + msg = _("Could not find enough free space for automatic " + "partitioning, please use another partitioning method.") + + anaconda.intf.messageWindow(_("Error Partitioning"), msg, + custom_icon='error') + + if anaconda.ksdata: + sys.exit(0) + + anaconda.storage.reset() + return DISPATCH_BACK + + _schedulePartitions(anaconda, disks) + + # sanity check the individual devices + log.warning("not sanity checking devices because I don't know how yet") + + # run the autopart function to allocate and grow partitions + try: + doPartitioning(anaconda.storage, + exclusiveDisks=anaconda.storage.clearPartDisks) + + if anaconda.storage.doAutoPart: + _scheduleLVs(anaconda, devs) + + # grow LVs + growLVM(anaconda.storage) + except PartitioningWarning as msg: + if not anaconda.ksdata: + anaconda.intf.messageWindow(_("Warnings During Automatic " + "Partitioning"), + _("Following warnings occurred during automatic " + "partitioning:\n\n%s") % (msg,), + custom_icon='warning') + else: + log.warning(msg) + except PartitioningError as msg: + # restore drives to original state + anaconda.storage.reset() + if not anaconda.ksdata: + extra = "" + + if anaconda.displayMode != "t": + anaconda.dispatch.skipStep("partition", skip = 0) + else: + extra = _("\n\nPress 'OK' to exit the installer.") + anaconda.intf.messageWindow(_("Error Partitioning"), + _("Could not allocate requested partitions: \n\n" + "%(msg)s.%(extra)s") % {'msg': msg, 'extra': extra}, + custom_icon='error') + + if anaconda.ksdata: + sys.exit(0) + else: + return + + # sanity check the collection of devices + log.warning("not sanity checking storage config because I don't know how yet") + # now do a full check of the requests + (errors, warnings) = anaconda.storage.sanityCheck() + if warnings: + for warning in warnings: + log.warning(warning) + if errors: + errortxt = "\n".join(errors) + if anaconda.ksdata: + extra = _("\n\nPress 'OK' to exit the installer.") + else: + extra = _("\n\nPress 'OK' to choose a different partitioning option.") + + anaconda.intf.messageWindow(_("Automatic Partitioning Errors"), + _("The following errors occurred with your " + "partitioning:\n\n%(errortxt)s\n\n" + "This can happen if there is not enough " + "space on your hard drive(s) for the " + "installation. %(extra)s") + % {'errortxt': errortxt, 'extra': extra}, + custom_icon='error') + # + # XXX if in kickstart we reboot + # + if anaconda.ksdata: + anaconda.intf.messageWindow(_("Unrecoverable Error"), + _("The system will now reboot.")) + sys.exit(0) + anaconda.storage.reset() + return DISPATCH_BACK + +def shouldClear(device, clearPartType, clearPartDisks=None): + if clearPartType not in [CLEARPART_TYPE_LINUX, CLEARPART_TYPE_ALL]: + return False + + if isinstance(device, PartitionDevice): + # Never clear the special first partition on a Mac disk label, as that + # holds the partition table itself. + if device.disk.format.partedDisk.type == "mac" and \ + device.partedPartition.number == 1 and \ + device.partedPartition.name == "Apple": + return False + + # If we got a list of disks to clear, make sure this one's on it + if clearPartDisks and device.disk.name not in clearPartDisks: + return False + + # We don't want to fool with extended partitions, freespace, &c + if device.partType not in [parted.PARTITION_NORMAL, + parted.PARTITION_LOGICAL]: + return False + + if clearPartType == CLEARPART_TYPE_LINUX and \ + not device.format.linuxNative and \ + not device.getFlag(parted.PARTITION_LVM) and \ + not device.getFlag(parted.PARTITION_RAID) and \ + not device.getFlag(parted.PARTITION_SWAP): + return False + elif device.isDisk and not device.partitioned: + # If we got a list of disks to clear, make sure this one's on it + if clearPartDisks and device.name not in clearPartDisks: + return False + + if clearPartType == CLEARPART_TYPE_LINUX and \ + not device.format.linuxNative: + return False + + # Don't clear devices holding install media. + if device.protected: + return False + + # TODO: do platform-specific checks on ia64, pSeries, iSeries, mac + + return True + +def clearPartitions(storage): + """ Clear partitions and dependent devices from disks. + + Arguments: + + storage -- a storage.Storage instance + + Keyword arguments: + + None + + NOTES: + + - Needs some error handling, especially for the parted bits. + + """ + if storage.clearPartType is None or storage.clearPartType == CLEARPART_TYPE_NONE: + # not much to do + return + + # we are only interested in partitions that physically exist + partitions = [p for p in storage.partitions if p.exists] + # Sort partitions by descending partition number to minimize confusing + # things like multiple "destroy sda5" actions due to parted renumbering + # partitions. This can still happen through the UI but it makes sense to + # avoid it where possible. + partitions.sort(key=lambda p: p.partedPartition.number, reverse=True) + for part in partitions: + log.debug("clearpart: looking at %s" % part.name) + if not shouldClear(part, storage.clearPartType, storage.clearPartDisks): + continue + + log.debug("clearing %s" % part.name) + + # XXX is there any argument for not removing incomplete devices? + # -- maybe some RAID devices + devices = storage.deviceDeps(part) + while devices: + log.debug("devices to remove: %s" % ([d.name for d in devices],)) + leaves = [d for d in devices if d.isleaf] + log.debug("leaves to remove: %s" % ([d.name for d in leaves],)) + for leaf in leaves: + storage.destroyDevice(leaf) + devices.remove(leaf) + + log.debug("partitions: %s" % [p.getDeviceNodeName() for p in part.partedPartition.disk.partitions]) + storage.destroyDevice(part) + + # now remove any empty extended partitions + removeEmptyExtendedPartitions(storage) + +def removeEmptyExtendedPartitions(storage): + for disk in storage.partitioned: + log.debug("checking whether disk %s has an empty extended" % disk.name) + extended = disk.format.extendedPartition + logical_parts = disk.format.logicalPartitions + log.debug("extended is %s ; logicals is %s" % (extended, [p.getDeviceNodeName() for p in logical_parts])) + if extended and not logical_parts: + log.debug("removing empty extended partition from %s" % disk.name) + extended_name = devicePathToName(extended.getDeviceNodeName()) + extended = storage.devicetree.getDeviceByName(extended_name) + storage.destroyDevice(extended) + #disk.partedDisk.removePartition(extended.partedPartition) + + for disk in [d for d in storage.disks if d not in storage.partitioned]: + # clear any whole-disk formats that need clearing + if shouldClear(disk, storage.clearPartType, storage.clearPartDisks): + log.debug("clearing %s" % disk.name) + devices = storage.deviceDeps(disk) + while devices: + log.debug("devices to remove: %s" % ([d.name for d in devices],)) + leaves = [d for d in devices if d.isleaf] + log.debug("leaves to remove: %s" % ([d.name for d in leaves],)) + for leaf in leaves: + storage.destroyDevice(leaf) + devices.remove(leaf) + + destroy_action = ActionDestroyFormat(disk) + newLabel = getFormat("disklabel", device=disk.path) + create_action = ActionCreateFormat(disk, format=newLabel) + storage.devicetree.registerAction(destroy_action) + storage.devicetree.registerAction(create_action) + +def partitionCompare(part1, part2): + """ More specifically defined partitions come first. + + < 1 => x < y + 0 => x == y + > 1 => x > y + """ + ret = 0 + + if part1.req_base_weight: + ret -= part1.req_base_weight + + if part2.req_base_weight: + ret += part2.req_base_weight + + # bootable partitions to the front + ret -= cmp(part1.req_bootable, part2.req_bootable) * 1000 + + # more specific disk specs to the front of the list + # req_disks being empty is equivalent to it being an infinitely long list + if part1.req_disks and not part2.req_disks: + ret -= 500 + elif not part1.req_disks and part2.req_disks: + ret += 500 + else: + ret += cmp(len(part1.req_disks), len(part2.req_disks)) * 500 + + # primary-only to the front of the list + ret -= cmp(part1.req_primary, part2.req_primary) * 200 + + # fixed size requests to the front + ret += cmp(part1.req_grow, part2.req_grow) * 100 + + # larger requests go to the front of the list + ret -= cmp(part1.req_base_size, part2.req_base_size) * 50 + + # potentially larger growable requests go to the front + if part1.req_grow and part2.req_grow: + if not part1.req_max_size and part2.req_max_size: + ret -= 25 + elif part1.req_max_size and not part2.req_max_size: + ret += 25 + else: + ret -= cmp(part1.req_max_size, part2.req_max_size) * 25 + + # give a little bump based on mountpoint + if hasattr(part1.format, "mountpoint") and \ + hasattr(part2.format, "mountpoint"): + ret += cmp(part1.format.mountpoint, part2.format.mountpoint) * 10 + + if ret > 0: + ret = 1 + elif ret < 0: + ret = -1 + + return ret + +def getNextPartitionType(disk, no_primary=None): + """ Find the type of partition to create next on a disk. + + Return a parted partition type value representing the type of the + next partition we will create on this disk. + + If there is only one free primary partition and we can create an + extended partition, we do that. + + If there are free primary slots and an extended partition we will + recommend creating a primary partition. This can be overridden + with the keyword argument no_primary. + + Arguments: + + disk -- a parted.Disk instance representing the disk + + Keyword arguments: + + no_primary -- given a choice between primary and logical + partitions, prefer logical + + """ + part_type = None + extended = disk.getExtendedPartition() + supports_extended = disk.supportsFeature(parted.DISK_TYPE_EXTENDED) + logical_count = len(disk.getLogicalPartitions()) + max_logicals = disk.getMaxLogicalPartitions() + primary_count = disk.primaryPartitionCount + + if primary_count < disk.maxPrimaryPartitionCount: + if primary_count == disk.maxPrimaryPartitionCount - 1: + # can we make an extended partition? now's our chance. + if not extended and supports_extended: + part_type = parted.PARTITION_EXTENDED + elif not extended: + # extended partitions not supported. primary or nothing. + if not no_primary: + part_type = parted.PARTITION_NORMAL + else: + # there is an extended and a free primary + if not no_primary: + part_type = parted.PARTITION_NORMAL + elif logical_count < max_logicals: + # we have an extended with logical slots, so use one. + part_type = parted.PARTITION_LOGICAL + else: + # there are two or more primary slots left. use one unless we're + # not supposed to make primaries. + if not no_primary: + part_type = parted.PARTITION_NORMAL + elif extended and logical_count < max_logicals: + part_type = parted.PARTITION_LOGICAL + elif extended and logical_count < max_logicals: + part_type = parted.PARTITION_LOGICAL + + return part_type + +def getBestFreeSpaceRegion(disk, part_type, req_size, + boot=None, best_free=None, grow=None): + """ Return the "best" free region on the specified disk. + + For non-boot partitions, we return the largest free region on the + disk. For boot partitions, we return the first region that is + large enough to hold the partition. + + Partition type (parted's PARTITION_NORMAL, PARTITION_LOGICAL) is + taken into account when locating a suitable free region. + + For locating the best region from among several disks, the keyword + argument best_free allows the specification of a current "best" + free region with which to compare the best from this disk. The + overall best region is returned. + + Arguments: + + disk -- the disk (a parted.Disk instance) + part_type -- the type of partition we want to allocate + (one of parted's partition type constants) + req_size -- the requested size of the partition (in MB) + + Keyword arguments: + + boot -- indicates whether this will be a bootable partition + (boolean) + best_free -- current best free region for this partition + grow -- indicates whether this is a growable request + + """ + log.debug("getBestFreeSpaceRegion: disk=%s part_type=%d req_size=%dMB " + "boot=%s best=%s grow=%s" % + (disk.device.path, part_type, req_size, boot, best_free, grow)) + extended = disk.getExtendedPartition() + + for _range in disk.getFreeSpaceRegions(): + if extended: + # find out if there is any overlap between this region and the + # extended partition + log.debug("looking for intersection between extended (%d-%d) and free (%d-%d)" % + (extended.geometry.start, extended.geometry.end, _range.start, _range.end)) + + # parted.Geometry.overlapsWith can handle this + try: + free_geom = extended.geometry.intersect(_range) + except ArithmeticError, e: + # this freespace region does not lie within the extended + # partition's geometry + free_geom = None + + if (free_geom and part_type == parted.PARTITION_NORMAL) or \ + (not free_geom and part_type == parted.PARTITION_LOGICAL): + log.debug("free region not suitable for request") + continue + + if part_type == parted.PARTITION_NORMAL: + # we're allocating a primary and the region is not within + # the extended, so we use the original region + free_geom = _range + else: + free_geom = _range + + log.debug("current free range is %d-%d (%dMB)" % (free_geom.start, + free_geom.end, + free_geom.getSize())) + free_size = free_geom.getSize() + + # For boot partitions, we want the first suitable region we find. + # For growable or extended partitions, we want the largest possible + # free region. + # For all others, we want the smallest suitable free region. + if grow or part_type == parted.PARTITION_EXTENDED: + op = gt + else: + op = lt + if req_size <= free_size: + if not best_free or op(free_geom.length, best_free.length): + best_free = free_geom + + if boot: + # if this is a bootable partition we want to + # use the first freespace region large enough + # to satisfy the request + break + + return best_free + +def sectorsToSize(sectors, sectorSize): + """ Convert length in sectors to size in MB. + + Arguments: + + sectors - sector count + sectorSize - sector size for the device, in bytes + """ + return (sectors * sectorSize) / (1024.0 * 1024.0) + +def sizeToSectors(size, sectorSize): + """ Convert size in MB to length in sectors. + + Arguments: + + size - size in MB + sectorSize - sector size for the device, in bytes + """ + return (size * 1024.0 * 1024.0) / sectorSize + +def removeNewPartitions(disks, partitions): + """ Remove newly added input partitions from input disks. + + Arguments: + + disks -- list of StorageDevice instances with DiskLabel format + partitions -- list of PartitionDevice instances + + """ + log.debug("removing all non-preexisting partitions %s from disk(s) %s" + % (["%s(id %d)" % (p.name, p.id) for p in partitions + if not p.exists], + [d.name for d in disks])) + for part in partitions: + if part.partedPartition and part.disk in disks: + if part.exists: + # we're only removing partitions that don't physically exist + continue + + if part.isExtended: + # these get removed last + continue + + part.disk.format.partedDisk.removePartition(part.partedPartition) + part.partedPartition = None + part.disk = None + + for disk in disks: + # remove empty extended so it doesn't interfere + extended = disk.format.extendedPartition + if extended and not disk.format.logicalPartitions: + log.debug("removing empty extended partition from %s" % disk.name) + disk.format.partedDisk.removePartition(extended) + +def addPartition(disklabel, free, part_type, size): + """ Return new partition after adding it to the specified disk. + + Arguments: + + disklabel -- disklabel instance to add partition to + free -- where to add the partition (parted.Geometry instance) + part_type -- partition type (parted.PARTITION_* constant) + size -- size (in MB) of the new partition + + The new partition will be aligned. + + Return value is a parted.Partition instance. + + """ + start = free.start + if not disklabel.alignment.isAligned(free, start): + start = disklabel.alignment.alignNearest(free, start) + + if part_type == parted.PARTITION_LOGICAL: + # make room for logical partition's metadata + start += disklabel.alignment.grainSize + + if start != free.start: + log.debug("adjusted start sector from %d to %d" % (free.start, start)) + + if part_type == parted.PARTITION_EXTENDED: + end = free.end + length = end - start + 1 + else: + # size is in MB + length = sizeToSectors(size, disklabel.partedDevice.sectorSize) + end = start + length - 1 + + if not disklabel.endAlignment.isAligned(free, end): + end = disklabel.endAlignment.alignNearest(free, end) + log.debug("adjusted length from %d to %d" % (length, end - start + 1)) + + new_geom = parted.Geometry(device=disklabel.partedDevice, + start=start, + end=end) + + max_length = disklabel.partedDisk.maxPartitionLength + if max_length and new_geom.length > max_length: + raise PartitioningError("requested size exceeds maximum allowed") + + # create the partition and add it to the disk + partition = parted.Partition(disk=disklabel.partedDisk, + type=part_type, + geometry=new_geom) + constraint = parted.Constraint(exactGeom=new_geom) + disklabel.partedDisk.addPartition(partition=partition, + constraint=constraint) + return partition + +def getFreeRegions(disks): + """ Return a list of free regions on the specified disks. + + Arguments: + + disks -- list of parted.Disk instances + + Return value is a list of unaligned parted.Geometry instances. + + """ + free = [] + for disk in disks: + for f in disk.format.partedDisk.getFreeSpaceRegions(): + if f.length > 0: + free.append(f) + + return free + +def doPartitioning(storage, exclusiveDisks=None): + """ Allocate and grow partitions. + + When this function returns without error, all PartitionDevice + instances must have their parents set to the disk they are + allocated on, and their partedPartition attribute set to the + appropriate parted.Partition instance from their containing + disk. All req_xxxx attributes must be unchanged. + + Arguments: + + storage - Main anaconda Storage instance + + Keyword arguments: + + exclusiveDisks -- list of names of disks to use + + """ + anaconda = storage.anaconda + disks = storage.partitioned + if exclusiveDisks: + disks = [d for d in disks if d.name in exclusiveDisks] + + for disk in disks: + disk.setup() + + partitions = storage.partitions[:] + for part in storage.partitions: + part.req_bootable = False + + if part.exists or \ + (storage.deviceImmutable(part) and part.partedPartition): + # if the partition is preexisting or part of a complex device + # then we shouldn't modify it + partitions.remove(part) + continue + + if not part.exists: + # start over with flexible-size requests + part.req_size = part.req_base_size + + # FIXME: isn't there a better place for this to happen? + try: + bootDev = anaconda.platform.bootDevice() + except DeviceError: + bootDev = None + + if bootDev: + bootDev.req_bootable = True + + removeNewPartitions(disks, partitions) + free = getFreeRegions(disks) + allocatePartitions(storage, disks, partitions, free) + growPartitions(disks, partitions, free) + + # The number and thus the name of partitions may have changed now, + # allocatePartitions() takes care of this for new partitions, but not + # for pre-existing ones, so we update the name of all partitions here + for part in storage.partitions: + # needed because of XXX hack below + if part.isExtended: + continue + part.updateName() + + # XXX hack -- if we created any extended partitions we need to add + # them to the tree now + for disk in disks: + extended = disk.format.extendedPartition + if not extended: + # remove any obsolete extended partitions + for part in storage.partitions: + if part.disk == disk and part.isExtended: + storage.devicetree._removeDevice(part, moddisk=False) + continue + + extendedName = devicePathToName(extended.getDeviceNodeName()) + # remove any obsolete extended partitions + for part in storage.partitions: + if part.disk == disk and part.isExtended and \ + part.name != extendedName: + storage.devicetree._removeDevice(part, moddisk=False) + + device = storage.devicetree.getDeviceByName(extendedName) + if device: + if not device.exists: + # created by us, update partedPartition + device.partedPartition = extended + continue + + # This is a little odd because normally instantiating a partition + # that does not exist means leaving self.parents empty and instead + # populating self.req_disks. In this case, we need to skip past + # that since this partition is already defined. + device = PartitionDevice(extendedName, parents=disk) + device.parents = [disk] + device.partedPartition = extended + # just add the device for now -- we'll handle actions at the last + # moment to simplify things + storage.devicetree._addDevice(device) + +def allocatePartitions(storage, disks, partitions, freespace): + """ Allocate partitions based on requested features. + + Non-existing partitions are sorted according to their requested + attributes, and then allocated. + + The basic approach to sorting is that the more specifically- + defined a request is, the earlier it will be allocated. See + the function partitionCompare for details on the sorting + criteria. + + The PartitionDevice instances will have their name and parents + attributes set once they have been allocated. + """ + log.debug("allocatePartitions: disks=%s ; partitions=%s" % + ([d.name for d in disks], + ["%s(id %d)" % (p.name, p.id) for p in partitions])) + + new_partitions = [p for p in partitions if not p.exists] + new_partitions.sort(cmp=partitionCompare) + + # the following dicts all use device path strings as keys + disklabels = {} # DiskLabel instances for each disk + all_disks = {} # StorageDevice for each disk + for disk in disks: + if disk.path not in disklabels.keys(): + disklabels[disk.path] = disk.format + all_disks[disk.path] = disk + + removeNewPartitions(disks, new_partitions) + + for _part in new_partitions: + if _part.partedPartition and _part.isExtended: + # ignore new extendeds as they are implicit requests + continue + + # obtain the set of candidate disks + req_disks = [] + if _part.disk: + # we have a already selected a disk for this request + req_disks = [_part.disk] + elif _part.req_disks: + # use the requested disk set + req_disks = _part.req_disks + else: + # no disks specified means any disk will do + req_disks = disks + + req_disks.sort(key=lambda d: d.name, cmp=storage.compareDisks) + log.debug("allocating partition: %s ; id: %d ; disks: %s ;\n" + "boot: %s ; primary: %s ; size: %dMB ; grow: %s ; " + "max_size: %s" % (_part.name, _part.id, req_disks, + _part.req_bootable, _part.req_primary, + _part.req_size, _part.req_grow, + _part.req_max_size)) + free = None + use_disk = None + part_type = None + growth = 0 + # loop through disks + for _disk in req_disks: + disklabel = disklabels[_disk.path] + sectorSize = disklabel.partedDevice.sectorSize + best = None + current_free = free + + # for growable requests, we don't want to pass the current free + # geometry to getBestFreeRegion -- this allows us to try the + # best region from each disk and choose one based on the total + # growth it allows + if _part.req_grow: + current_free = None + + log.debug("checking freespace on %s" % _disk.name) + + new_part_type = getNextPartitionType(disklabel.partedDisk) + if new_part_type is None: + # can't allocate any more partitions on this disk + log.debug("no free partition slots on %s" % _disk.name) + continue + + if _part.req_primary and new_part_type != parted.PARTITION_NORMAL: + if (disklabel.partedDisk.primaryPartitionCount < + disklabel.partedDisk.maxPrimaryPartitionCount): + # don't fail to create a primary if there are only three + # primary partitions on the disk (#505269) + new_part_type = parted.PARTITION_NORMAL + else: + # we need a primary slot and none are free on this disk + log.debug("no primary slots available on %s" % _disk.name) + continue + + best = getBestFreeSpaceRegion(disklabel.partedDisk, + new_part_type, + _part.req_size, + best_free=current_free, + boot=_part.req_bootable, + grow=_part.req_grow) + + if best == free and not _part.req_primary and \ + new_part_type == parted.PARTITION_NORMAL: + # see if we can do better with a logical partition + log.debug("not enough free space for primary -- trying logical") + new_part_type = getNextPartitionType(disklabel.partedDisk, + no_primary=True) + if new_part_type: + best = getBestFreeSpaceRegion(disklabel.partedDisk, + new_part_type, + _part.req_size, + best_free=current_free, + boot=_part.req_bootable, + grow=_part.req_grow) + + if best and free != best: + update = True + if _part.req_grow: + log.debug("evaluating growth potential for new layout") + new_growth = 0 + for disk_path in disklabels.keys(): + log.debug("calculating growth for disk %s" % disk_path) + # Now we check, for growable requests, which of the two + # free regions will allow for more growth. + + # set up chunks representing the disks' layouts + temp_parts = [] + for _p in new_partitions[:new_partitions.index(_part)]: + if _p.disk.path == disk_path: + temp_parts.append(_p) + + # add the current request to the temp disk to set up + # its partedPartition attribute with a base geometry + if disk_path == _disk.path: + temp_part = addPartition(disklabel, + best, + new_part_type, + _part.req_size) + _part.partedPartition = temp_part + _part.disk = _disk + temp_parts.append(_part) + + chunks = getDiskChunks(all_disks[disk_path], + temp_parts, freespace) + + # grow all growable requests + disk_growth = 0 + disk_sector_size = disklabels[disk_path].partedDevice.sectorSize + for chunk in chunks: + chunk.growRequests() + # record the growth for this layout + new_growth += chunk.growth + disk_growth += chunk.growth + for req in chunk.requests: + log.debug("request %d (%s) growth: %d (%dMB) " + "size: %dMB" % + (req.partition.id, + req.partition.name, + req.growth, + sectorsToSize(req.growth, + disk_sector_size), + sectorsToSize(req.growth + req.base, + disk_sector_size))) + log.debug("disk %s growth: %d (%dMB)" % + (disk_path, disk_growth, + sectorsToSize(disk_growth, + disk_sector_size))) + + disklabel.partedDisk.removePartition(temp_part) + _part.partedPartition = None + _part.disk = None + + log.debug("total growth: %d sectors" % new_growth) + + # update the chosen free region unless the previous + # choice yielded greater total growth + if new_growth < growth: + log.debug("keeping old free: %d < %d" % (new_growth, + growth)) + update = False + else: + growth = new_growth + + if update: + # now we know we are choosing a new free space, + # so update the disk and part type + log.debug("updating use_disk to %s (%s), type: %s" + % (_disk, _disk.name, new_part_type)) + part_type = new_part_type + use_disk = _disk + log.debug("new free: %s (%d-%d / %dMB)" % (best, + best.start, + best.end, + best.getSize())) + log.debug("new free allows for %d sectors of growth" % + growth) + free = best + + # For platforms with a fake boot partition (like Apple Bootstrap or + # PReP) and multiple disks, we need to ensure the /boot partition + # ends up on the same disk as the fake one. + mountpoint = getattr(_part.format, "mountpoint", "") + if not mountpoint: + mountpoint = "" + + if free and (_part.req_bootable or mountpoint.startswith("/boot")): + # if this is a bootable partition we want to + # use the first freespace region large enough + # to satisfy the request + log.debug("found free space for bootable request") + break + + if free is None: + raise PartitioningError("not enough free space on disks") + + _disk = use_disk + disklabel = _disk.format + + # create the extended partition if needed + if part_type == parted.PARTITION_EXTENDED: + log.debug("creating extended partition") + addPartition(disklabel, free, part_type, None) + + # now the extended partition exists, so set type to logical + part_type = parted.PARTITION_LOGICAL + + # recalculate freespace + log.debug("recalculating free space") + free = getBestFreeSpaceRegion(disklabel.partedDisk, + part_type, + _part.req_size, + boot=_part.req_bootable, + grow=_part.req_grow) + if not free: + raise PartitioningError("not enough free space after " + "creating extended partition") + + partition = addPartition(disklabel, free, part_type, _part.req_size) + log.debug("created partition %s of %dMB and added it to %s" % + (partition.getDeviceNodeName(), partition.getSize(), + disklabel.device)) + + # this one sets the name + _part.partedPartition = partition + _part.disk = _disk + + # parted modifies the partition in the process of adding it to + # the disk, so we need to grab the latest version... + _part.partedPartition = disklabel.partedDisk.getPartitionByPath(_part.path) + + +class Request(object): + """ A partition request. + + Request instances are used for calculating how much to grow + partitions. + """ + def __init__(self, partition): + """ Create a Request instance. + + Arguments: + + partition -- a PartitionDevice instance + + """ + self.partition = partition # storage.devices.PartitionDevice + self.growth = 0 # growth in sectors + self.max_growth = 0 # max growth in sectors + self.done = not partition.req_grow # can we grow this request more? + self.base = partition.partedPartition.geometry.length # base sectors + + sector_size = partition.partedPartition.disk.device.sectorSize + + if partition.req_grow: + limits = filter(lambda l: l > 0, + [sizeToSectors(partition.req_max_size, sector_size), + sizeToSectors(partition.format.maxSize, sector_size), + partition.partedPartition.disk.maxPartitionLength]) + + if limits: + max_sectors = min(limits) + self.max_growth = max_sectors - self.base + + @property + def growable(self): + """ True if this request is growable. """ + return self.partition.req_grow + + @property + def id(self): + """ The id of the PartitionDevice this request corresponds to. """ + return self.partition.id + + def __str__(self): + s = ("%(type)s instance --\n" + "id = %(id)s name = %(name)s growable = %(growable)\n" + "base = %(base)d growth = %(grow)d max_grow = %(max_grow)d\n" + "done = %(done)s" % + {"type": self.__class__.__name__, "id": self.id, + "name": self.partition.name, "growable": self.growable, + "base": self.base, "growth": self.growth, + "max_grow": self.max_growth, "done": self.done}) + return s + + +class Chunk(object): + """ A free region on disk from which partitions will be allocated """ + def __init__(self, geometry, requests=None): + """ Create a Chunk instance. + + Arguments: + + geometry -- parted.Geometry instance describing the free space + + + Keyword Arguments: + + requests -- list of Request instances allocated from this chunk + + """ + self.geometry = geometry # parted.Geometry + self.pool = self.geometry.length # free sector count + self.sectorSize = self.geometry.device.sectorSize + self.base = 0 # sum of growable requests' base + # sizes, in sectors + self.requests = [] # list of Request instances + if isinstance(requests, list): + for req in requests: + self.addRequest(req) + + def __str__(self): + s = ("%(type)s instance --\n" + "device = %(device)s start = %(start)d end = %(end)d\n" + "length = %(length)d size = %(size)d pool = %(pool)d\n" + "remaining = %(rem)d sectorSize = %(sectorSize)d" % + {"type": self.__class__.__name__, + "device": self.geometry.device.path, + "start": self.geometry.start, "end": self.geometry.end, + "length": self.geometry.length, "size": self.geometry.getSize(), + "pool": self.pool, "rem": self.remaining, + "sectorSize": self.sectorSize}) + + return s + + def addRequest(self, req): + """ Add a Request to this chunk. """ + log.debug("adding request %d to chunk %s" % (req.partition.id, self)) + self.requests.append(req) + self.pool -= req.base + + if not req.done: + self.base += req.base + + def getRequestByID(self, id): + """ Retrieve a request from this chunk based on its id. """ + for request in self.requests: + if request.id == id: + return request + + @property + def growth(self): + """ Sum of growth in sectors for all requests in this chunk. """ + return sum(r.growth for r in self.requests) + + @property + def hasGrowable(self): + """ True if this chunk contains at least one growable request. """ + for req in self.requests: + if req.growable: + return True + return False + + @property + def remaining(self): + """ Number of requests still being grown in this chunk. """ + return len([d for d in self.requests if not d.done]) + + @property + def done(self): + """ True if we are finished growing all requests in this chunk. """ + return self.remaining == 0 + + def trimOverGrownRequest(self, req, base=None): + """ Enforce max growth and return extra sectors to the pool. """ + if req.max_growth and req.growth >= req.max_growth: + if req.growth > req.max_growth: + # we've grown beyond the maximum. put some back. + extra = req.growth - req.max_growth + log.debug("taking back %d (%dMB) from %d (%s)" % + (extra, + sectorsToSize(extra, self.sectorSize), + req.partition.id, req.partition.name)) + self.pool += extra + req.growth = req.max_growth + + # We're done growing this partition, so it no longer + # factors into the growable base used to determine + # what fraction of the pool each request gets. + if base is not None: + base -= req.base + req.done = True + + return base + + def growRequests(self): + """ Calculate growth amounts for requests in this chunk. """ + log.debug("Chunk.growRequests: %s" % self) + + # sort the partitions by start sector + self.requests.sort(key=lambda r: r.partition.partedPartition.geometry.start) + + # we use this to hold the base for the next loop through the + # chunk's requests since we want the base to be the same for + # all requests in any given growth iteration + new_base = self.base + last_pool = 0 # used to track changes to the pool across iterations + while not self.done and self.pool and last_pool != self.pool: + last_pool = self.pool # to keep from getting stuck + self.base = new_base + log.debug("%d partitions and %d (%dMB) left in chunk" % + (self.remaining, self.pool, + sectorsToSize(self.pool, self.sectorSize))) + for p in self.requests: + if p.done: + continue + + # Each partition is allocated free sectors from the pool + # based on the relative _base_ sizes of the remaining + # growable partitions. + share = p.base / float(self.base) + growth = int(share * last_pool) # truncate, don't round + p.growth += growth + self.pool -= growth + log.debug("adding %d (%dMB) to %d (%s)" % + (growth, + sectorsToSize(growth, self.sectorSize), + p.partition.id, p.partition.name)) + + new_base = self.trimOverGrownRequest(p, base=new_base) + log.debug("new grow amount for partition %d (%s) is %d " + "sectors, or %dMB" % + (p.partition.id, p.partition.name, p.growth, + sectorsToSize(p.growth, self.sectorSize))) + + if self.pool: + # allocate any leftovers in pool to the first partition + # that can still grow + for p in self.requests: + if p.done: + continue + + p.growth += self.pool + self.pool = 0 + + self.trimOverGrownRequest(p) + if self.pool == 0: + break + + +def getDiskChunks(disk, partitions, free): + """ Return a list of Chunk instances representing a disk. + + Arguments: + + disk -- a StorageDevice with a DiskLabel format + partitions -- list of PartitionDevice instances + free -- list of parted.Geometry instances representing free space + + Partitions and free regions not on the specified disk are ignored. + + """ + # list of all new partitions on this disk + disk_parts = [p for p in partitions if p.disk == disk and not p.exists] + disk_free = [f for f in free if f.device.path == disk.path] + + + chunks = [Chunk(f) for f in disk_free] + + for p in disk_parts: + if p.isExtended: + # handle extended partitions specially since they are + # indeed very special + continue + + for i, f in enumerate(disk_free): + if f.contains(p.partedPartition.geometry): + chunks[i].addRequest(Request(p)) + break + + return chunks + +def growPartitions(disks, partitions, free): + """ Grow all growable partition requests. + + Partitions have already been allocated from chunks of free space on + the disks. This function does not modify the ordering of partitions + or the free chunks from which they are allocated. + + Free space within a given chunk is allocated to each growable + partition allocated from that chunk in an amount corresponding to + the ratio of that partition's base size to the sum of the base sizes + of all growable partitions allocated from the chunk. + + Arguments: + + disks -- a list of all usable disks (DiskDevice instances) + partitions -- a list of all partitions (PartitionDevice instances) + free -- a list of all free regions (parted.Geometry instances) + """ + log.debug("growPartitions: disks=%s, partitions=%s" % + ([d.name for d in disks], + ["%s(id %d)" % (p.name, p.id) for p in partitions])) + all_growable = [p for p in partitions if p.req_grow] + if not all_growable: + log.debug("no growable partitions") + return + + log.debug("growable partitions are %s" % [p.name for p in all_growable]) + + for disk in disks: + log.debug("growing partitions on %s" % disk.name) + sector_size = disk.format.partedDevice.sectorSize + + # find any extended partition on this disk + extended_geometry = getattr(disk.format.extendedPartition, + "geometry", + None) # parted.Geometry + + # list of free space regions on this disk prior to partition allocation + disk_free = [f for f in free if f.device.path == disk.path] + if not disk_free: + log.debug("no free space on %s" % disk.name) + continue + + chunks = getDiskChunks(disk, partitions, disk_free) + log.debug("disk %s has %d chunks" % (disk.name, len(chunks))) + # grow the partitions in each chunk as a group + for chunk in chunks: + if not chunk.hasGrowable: + # no growable partitions in this chunk + continue + + chunk.growRequests() + + # recalculate partition geometries + disklabel = disk.format + start = chunk.geometry.start + # align start sector as needed + if not disklabel.alignment.isAligned(chunk.geometry, start): + start = disklabel.alignment.alignUp(chunk.geometry, start) + new_partitions = [] + for p in chunk.requests: + ptype = p.partition.partedPartition.type + log.debug("partition %s (%d): %s" % (p.partition.name, + p.partition.id, ptype)) + if ptype == parted.PARTITION_EXTENDED: + continue + + # XXX since we need one metadata sector before each + # logical partition we burn one logical block to + # safely align the start of each logical partition + if ptype == parted.PARTITION_LOGICAL: + start += disklabel.alignment.grainSize + + old_geometry = p.partition.partedPartition.geometry + new_length = p.base + p.growth + end = start + new_length - 1 + # align end sector as needed + if not disklabel.endAlignment.isAligned(chunk.geometry, end): + end = disklabel.endAlignment.alignDown(chunk.geometry, end) + new_geometry = parted.Geometry(device=disklabel.partedDevice, + start=start, + end=end) + log.debug("new geometry for %s: %s" % (p.partition.name, + new_geometry)) + start = end + 1 + new_partition = parted.Partition(disk=disklabel.partedDisk, + type=ptype, + geometry=new_geometry) + new_partitions.append((new_partition, p.partition)) + + # remove all new partitions from this chunk + removeNewPartitions([disk], [r.partition for r in chunk.requests]) + log.debug("back from removeNewPartitions") + + # adjust the extended partition as needed + # we will ony resize an extended partition that we created + log.debug("extended: %s" % extended_geometry) + if extended_geometry and \ + chunk.geometry.contains(extended_geometry): + log.debug("setting up new geometry for extended on %s" % disk.name) + ext_start = 0 + ext_end = 0 + for (partition, device) in new_partitions: + if partition.type != parted.PARTITION_LOGICAL: + continue + + if not ext_start or partition.geometry.start < ext_start: + # account for the logical block difference in start + # sector for the extended -v- first logical + # (partition.geometry.start is already aligned) + ext_start = partition.geometry.start - disklabel.alignment.grainSize + + if not ext_end or partition.geometry.end > ext_end: + ext_end = partition.geometry.end + + new_geometry = parted.Geometry(device=disklabel.partedDevice, + start=ext_start, + end=ext_end) + log.debug("new geometry for extended: %s" % new_geometry) + new_extended = parted.Partition(disk=disklabel.partedDisk, + type=parted.PARTITION_EXTENDED, + geometry=new_geometry) + ptypes = [p.type for (p, d) in new_partitions] + for pt_idx, ptype in enumerate(ptypes): + if ptype == parted.PARTITION_LOGICAL: + new_partitions.insert(pt_idx, (new_extended, None)) + break + + # add the partitions with their new geometries to the disk + for (partition, device) in new_partitions: + if device: + name = device.name + else: + # If there was no extended partition on this disk when + # doPartitioning was called we won't have a + # PartitionDevice instance for it. + name = partition.getDeviceNodeName() + + log.debug("setting %s new geometry: %s" % (name, + partition.geometry)) + constraint = parted.Constraint(exactGeom=partition.geometry) + disklabel.partedDisk.addPartition(partition=partition, + constraint=constraint) + path = partition.path + if device: + # set the device's name + device.partedPartition = partition + # without this, the path attr will be a basename. eek. + device.disk = disk + + # make sure we store the disk's version of the partition + newpart = disklabel.partedDisk.getPartitionByPath(path) + device.partedPartition = newpart + + +def hasFreeDiskSpace(storage, exclusiveDisks=None): + """Returns True if there is at least 100Mb of free usable space in any of + the disks. False otherwise. + + """ + # FIXME: This function needs to be implemented. It is used, at least, by + # iw/partition_gui.py. It should be implemented after the new + # doPartitioning code is commited for fedora 13. Since it returns True + # the user will always be able to access the create partition screen. If + # no partition can be created, the user will go back to the previous + # storage state after seeing a warning message. + return True + + +def lvCompare(lv1, lv2): + """ More specifically defined lvs come first. + + < 1 => x < y + 0 => x == y + > 1 => x > y + """ + ret = 0 + + # larger requests go to the front of the list + ret -= cmp(lv1.size, lv2.size) * 100 + + # fixed size requests to the front + ret += cmp(lv1.req_grow, lv2.req_grow) * 50 + + # potentially larger growable requests go to the front + if lv1.req_grow and lv2.req_grow: + if not lv1.req_max_size and lv2.req_max_size: + ret -= 25 + elif lv1.req_max_size and not lv2.req_max_size: + ret += 25 + else: + ret -= cmp(lv1.req_max_size, lv2.req_max_size) * 25 + + if ret > 0: + ret = 1 + elif ret < 0: + ret = -1 + + return ret + +def growLVM(storage): + """ Grow LVs according to the sizes of the PVs. """ + for vg in storage.vgs: + total_free = vg.freeSpace + if total_free < 0: + # by now we have allocated the PVs so if there isn't enough + # space in the VG we have a real problem + raise PartitioningError("not enough space for LVM requests") + elif not total_free: + log.debug("vg %s has no free space" % vg.name) + continue + + log.debug("vg %s: %dMB free ; lvs: %s" % (vg.name, vg.freeSpace, + [l.lvname for l in vg.lvs])) + + # figure out how much to grow each LV + grow_amounts = {} + lv_total = vg.size - total_free + log.debug("used: %dMB ; vg.size: %dMB" % (lv_total, vg.size)) + + # This first loop is to calculate percentage-based growth + # amounts. These are based on total free space. + lvs = vg.lvs + lvs.sort(cmp=lvCompare) + for lv in lvs: + if not lv.req_grow or not lv.req_percent: + continue + + portion = (lv.req_percent * 0.01) + grow = portion * vg.vgFree + new_size = lv.req_size + grow + if lv.req_max_size and new_size > lv.req_max_size: + grow -= (new_size - lv.req_max_size) + + if lv.format.maxSize and lv.format.maxSize < new_size: + grow -= (new_size - lv.format.maxSize) + + # clamp growth amount to a multiple of vg extent size + grow_amounts[lv.name] = vg.align(grow) + total_free -= grow + lv_total += grow + + # This second loop is to calculate non-percentage-based growth + # amounts. These are based on free space remaining after + # calculating percentage-based growth amounts. + + # keep a tab on space not allocated due to format or requested + # maximums -- we'll dole it out to subsequent requests + leftover = 0 + for lv in lvs: + log.debug("checking lv %s: req_grow: %s ; req_percent: %s" + % (lv.name, lv.req_grow, lv.req_percent)) + if not lv.req_grow or lv.req_percent: + continue + + portion = float(lv.req_size) / float(lv_total) + grow = portion * total_free + log.debug("grow is %dMB" % grow) + + todo = lvs[lvs.index(lv):] + unallocated = reduce(lambda x,y: x+y, + [l.req_size for l in todo + if l.req_grow and not l.req_percent]) + extra_portion = float(lv.req_size) / float(unallocated) + extra = extra_portion * leftover + log.debug("%s getting %dMB (%d%%) of %dMB leftover space" + % (lv.name, extra, extra_portion * 100, leftover)) + leftover -= extra + grow += extra + log.debug("grow is now %dMB" % grow) + max_size = lv.req_size + grow + if lv.req_max_size and max_size > lv.req_max_size: + max_size = lv.req_max_size + + if lv.format.maxSize and max_size > lv.format.maxSize: + max_size = lv.format.maxSize + + log.debug("max size is %dMB" % max_size) + max_size = max_size + leftover += (lv.req_size + grow) - max_size + grow = max_size - lv.req_size + log.debug("lv %s gets %dMB" % (lv.name, vg.align(grow))) + grow_amounts[lv.name] = vg.align(grow) + + if not grow_amounts: + log.debug("no growable lvs in vg %s" % vg.name) + continue + + # now grow the lvs by the amounts we've calculated above + for lv in lvs: + if lv.name not in grow_amounts.keys(): + continue + lv.size += grow_amounts[lv.name] + + # now there shouldn't be any free space left, but if there is we + # should allocate it to one of the LVs + vg_free = vg.freeSpace + log.debug("vg %s has %dMB free" % (vg.name, vg_free)) + if vg_free: + for lv in lvs: + if not lv.req_grow: + continue + + if lv.req_max_size and lv.size == lv.req_max_size: + continue + + if lv.format.maxSize and lv.size == lv.format.maxSize: + continue + + # first come, first served + projected = lv.size + vg.freeSpace + if lv.req_max_size and projected > lv.req_max_size: + projected = lv.req_max_size + + if lv.format.maxSize and projected > lv.format.maxSize: + projected = lv.format.maxSize + + log.debug("giving leftover %dMB to %s" % (projected - lv.size, + lv.name)) + lv.size = projected + diff --git a/storage/partspec.py b/storage/partspec.py new file mode 100644 index 0000000..8ad81ca --- /dev/null +++ b/storage/partspec.py @@ -0,0 +1,66 @@ +# partspec.py +# +# Copyright (C) 2009 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> +# + +class PartSpec(object): + def __init__(self, mountpoint=None, fstype=None, size=None, maxSize=None, + grow=False, asVol=False, weight=0, requiredSpace=0): + """ Create a new storage specification. These are used to specify + the default partitioning layout as an object before we have the + storage system up and running. The attributes are obvious + except for the following: + + asVol -- Should this be allocated as a logical volume? If not, + it will be allocated as a partition. + weight -- An integer that modifies the sort algorithm for partition + requests. A larger value means the partition will end up + closer to the front of the disk. This is mainly used to + make sure /boot ends up in front, and any special (PReP, + appleboot, etc.) partitions end up in front of /boot. + This value means nothing if asVol=False. + requiredSpace -- This value is only taken into account if + asVol=True, and specifies the size in MB that the + containing VG must be for this PartSpec to even + get used. The VG's size is calculated before any + other LVs are created inside it. If not enough + space exists, this PartSpec will never get turned + into an LV. + """ + + self.mountpoint = mountpoint + self.fstype = fstype + self.size = size + self.maxSize = maxSize + self.grow = grow + self.asVol = asVol + self.weight = weight + self.requiredSpace = requiredSpace + + def __str__(self): + s = ("%(type)s instance (%(id)s) -- \n" + " mountpoint = %(mountpoint)s asVol = %(asVol)s\n" + " weight = %(weight)s fstype = %(fstype)s\n" + " size = %(size)s maxSize = %(maxSize)s grow = %(grow)s\n" % + {"type": self.__class__.__name__, "id": "%#x" % id(self), + "mountpoint": self.mountpoint, "asVol": self.asVol, + "weight": self.weight, "fstype": self.fstype, "size": self.size, + "maxSize": self.maxSize, "grow": self.grow}) + + return s diff --git a/storage/storage_log.py b/storage/storage_log.py new file mode 100644 index 0000000..a52513d --- /dev/null +++ b/storage/storage_log.py @@ -0,0 +1,32 @@ +import logging +import anaconda_log +import inspect + +def log_method_call(d, *args, **kwargs): + classname = d.__class__.__name__ + stack = inspect.stack() + methodname = stack[1][3] + + spaces = len(stack) * ' ' + fmt = "%s%s.%s:" + fmt_args = [spaces, classname, methodname] + + for arg in args: + fmt += " %s ;" + fmt_args.append(arg) + + for k, v in kwargs.items(): + fmt += " %s: %s ;" + fmt_args.extend([k, v]) + + logger.debug(fmt % tuple(fmt_args)) + + +logger = logging.getLogger("storage") +logger.setLevel(logging.DEBUG) +anaconda_log.logger.addFileHandler("/tmp/storage.log", logger, logging.DEBUG) +anaconda_log.logger.addFileHandler("/dev/tty3", logger, + anaconda_log.DEFAULT_TTY_LEVEL, + anaconda_log.TTY_FORMAT, + autoLevel=True) +anaconda_log.logger.forwardToSyslog(logger) diff --git a/storage/udev.py b/storage/udev.py new file mode 100644 index 0000000..625bfca --- /dev/null +++ b/storage/udev.py @@ -0,0 +1,515 @@ +# udev.py +# Python module for querying the udev database for device information. +# +# Copyright (C) 2009 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): Dave Lehman <dlehman@redhat.com> +# + +import os + +import iutil +from errors import * +from baseudev import * + +import logging +log = logging.getLogger("storage") + +def udev_resolve_devspec(devspec): + if not devspec: + return None + + import devices as _devices + ret = None + for dev in udev_get_block_devices(): + if devspec.startswith("LABEL="): + if udev_device_get_label(dev) == devspec[6:]: + ret = dev + break + elif devspec.startswith("UUID="): + if udev_device_get_uuid(dev) == devspec[5:]: + ret = dev + break + elif udev_device_get_name(dev) == _devices.devicePathToName(devspec): + ret = dev + break + else: + for link in dev["symlinks"]: + if devspec == link: + ret = dev + break + + del _devices + if ret: + return udev_device_get_name(ret) + +def udev_resolve_glob(glob): + import fnmatch + ret = [] + + if not glob: + return ret + + for dev in udev_get_block_devices(): + name = udev_device_get_name(dev) + + if fnmatch.fnmatch(name, glob): + ret.append(name) + else: + for link in dev["symlinks"]: + if fnmatch.fnmatch(link, glob): + ret.append(name) + + return ret + +def udev_get_block_devices(): + udev_settle() + entries = [] + for path in udev_enumerate_block_devices(): + entry = udev_get_block_device(path) + if entry: + if entry["name"].startswith("md"): + # mdraid is really braindead, when a device is stopped + # it is no longer usefull in anyway (and we should not + # probe it) yet it still sticks around, see bug rh523387 + state = None + state_file = "/sys/%s/md/array_state" % entry["sysfs_path"] + if os.access(state_file, os.R_OK): + with open(state_file) as state_f: + state = state_f.read().strip() + if state == "clear": + continue + entries.append(entry) + return entries + +def __is_blacklisted_blockdev(dev_name): + """Is this a blockdev we never want for an install?""" + if dev_name.startswith("loop") or dev_name.startswith("ram") or dev_name.startswith("fd"): + return True + + dev_path = "/sys/class/block/%s/device/model" %(dev_name,) + if os.path.exists(dev_path): + with open(dev_path) as dev_f: + model = dev_f.read() + for bad in ("IBM *STMF KERNEL", "SCEI Flash-5", "DGC LUNZ"): + if model.find(bad) != -1: + log.info("ignoring %s with model %s" %(dev_name, model)) + return True + + return False + +def udev_enumerate_block_devices(): + import os.path + + return filter(lambda d: not __is_blacklisted_blockdev(os.path.basename(d)), + udev_enumerate_devices(deviceClass="block")) + +def udev_get_block_device(sysfs_path): + dev = udev_get_device(sysfs_path) + if not dev or not dev.has_key("name"): + return None + else: + return dev + + +# These are functions for retrieving specific pieces of information from +# udev database entries. +def udev_device_get_name(udev_info): + """ Return the best name for a device based on the udev db data. """ + return udev_info.get("DM_NAME", udev_info["name"]) + +def udev_device_get_format(udev_info): + """ Return a device's format type as reported by udev. """ + return udev_info.get("ID_FS_TYPE") + +def udev_device_get_uuid(udev_info): + """ Get the UUID from the device's format as reported by udev. """ + md_uuid = udev_info.get("MD_UUID") + uuid = udev_info.get("ID_FS_UUID") + # we don't want to return the array's uuid as a member's uuid + if uuid and not md_uuid == uuid: + return udev_info.get("ID_FS_UUID") + +def udev_device_get_label(udev_info): + """ Get the label from the device's format as reported by udev. """ + return udev_info.get("ID_FS_LABEL") + +def udev_device_is_dm(info): + """ Return True if the device is a device-mapper device. """ + return info.has_key("DM_NAME") + +def udev_device_is_md(info): + """ Return True if the device is a mdraid array device. """ + # Don't identify partitions on mdraid arrays as raid arrays + if udev_device_is_partition(info): + return False + # isw raid set *members* have MD_METADATA set, but are not arrays! + return info.has_key("MD_METADATA") and \ + info.get("ID_FS_TYPE") != "isw_raid_member" + +def udev_device_is_cciss(info): + """ Return True if the device is a CCISS device. """ + return udev_device_get_name(info).startswith("cciss") + +def udev_device_is_dasd(info): + """ Return True if the device is a dasd device. """ + devname = info.get("DEVNAME") + if devname: + return devname.startswith("dasd") + else: + return False + +def udev_device_is_zfcp(info): + """ Return True if the device is a zfcp device. """ + if info.get("DEVTYPE") != "disk": + return False + + subsystem = "/sys" + info.get("sysfs_path") + + while True: + topdir = os.path.realpath(os.path.dirname(subsystem)) + driver = "%s/driver" % (topdir,) + + if os.path.islink(driver): + subsystemname = os.path.basename(os.readlink(subsystem)) + drivername = os.path.basename(os.readlink(driver)) + + if subsystemname == 'ccw' and drivername == 'zfcp': + return True + + newsubsystem = os.path.dirname(topdir) + + if newsubsystem == topdir: + break + + subsystem = newsubsystem + "/subsystem" + + return False + +def udev_device_get_zfcp_attribute(info, attr=None): + """ Return the value of the specified attribute of the zfcp device. """ + if not attr: + log.debug("udev_device_get_zfcp_attribute() called with attr=None") + return None + + attribute = "/sys%s/device/%s" % (info.get("sysfs_path"), attr,) + attribute = os.path.realpath(attribute) + + if not os.path.isfile(attribute): + log.warning("%s is not a valid zfcp attribute" % (attribute,)) + return None + + with open(attribute, "r") as f: + return f.read().strip() + +def udev_device_get_dasd_bus_id(info): + """ Return the CCW bus ID of the dasd device. """ + return info.get("sysfs_path").split("/")[-3] + +def udev_device_get_dasd_flag(info, flag=None): + """ Return the specified flag for the dasd device. """ + if flag is None: + return None + + path = "/sys" + info.get("sysfs_path") + "/device/" + flag + if not os.path.isfile(path): + return None + + with open(path, "r") as f: + return f.read().strip() + +def udev_device_is_cdrom(info): + """ Return True if the device is an optical drive. """ + # FIXME: how can we differentiate USB drives from CD-ROM drives? + # -- USB drives also generate a sdX device. + return info.get("ID_CDROM") == "1" + +def udev_device_is_disk(info): + """ Return True is the device is a disk. """ + if udev_device_is_cdrom(info): + return False + has_range = os.path.exists("/sys/%s/range" % info['sysfs_path']) + return info.get("DEVTYPE") == "disk" or has_range + +def udev_device_is_partition(info): + has_start = os.path.exists("/sys/%s/start" % info['sysfs_path']) + return info.get("DEVTYPE") == "partition" or has_start + +def udev_device_get_serial(udev_info): + """ Get the serial number/UUID from the device as reported by udev. """ + return udev_info.get("ID_SERIAL_SHORT", udev_info.get("ID_SERIAL")) + +def udev_device_get_wwid(udev_info): + """ The WWID of a device is typically just its serial number, but with + colons in the name to make it more readable. """ + serial = udev_device_get_serial(udev_info) + + if serial and len(serial) == 32: + retval = "" + for i in range(0, 16): + retval += serial[i*2:i*2+2] + ":" + + return retval[0:-1] + + return "" + +def udev_device_get_vendor(udev_info): + """ Get the vendor of the device as reported by udev. """ + return udev_info.get("ID_VENDOR_FROM_DATABASE", udev_info.get("ID_VENDOR")) + +def udev_device_get_model(udev_info): + """ Get the model of the device as reported by udev. """ + return udev_info.get("ID_MODEL_FROM_DATABASE", udev_info.get("ID_MODEL")) + +def udev_device_get_bus(udev_info): + """ Get the bus a device is connected to the system by. """ + return udev_info.get("ID_BUS", "").upper() + +def udev_device_get_path(info): + return info["ID_PATH"] + +def udev_device_get_sysfs_path(info): + return info['sysfs_path'] + +def udev_device_get_major(info): + return int(info["MAJOR"]) + +def udev_device_get_minor(info): + return int(info["MINOR"]) + +def udev_device_get_md_level(info): + return info.get("MD_LEVEL") + +def udev_device_get_md_devices(info): + return int(info["MD_DEVICES"]) + +def udev_device_get_md_uuid(info): + return info["MD_UUID"] + +def udev_device_get_md_container(info): + return info.get("MD_CONTAINER") + +def udev_device_get_md_name(info): + return info.get("MD_DEVNAME") + +def udev_device_get_vg_name(info): + return info['LVM2_VG_NAME'] + +def udev_device_get_vg_uuid(info): + return info['LVM2_VG_UUID'] + +def udev_device_get_vg_size(info): + # lvm's decmial precision is not configurable, so we tell it to use + # KB and convert to MB here + return float(info['LVM2_VG_SIZE']) / 1024 + +def udev_device_get_vg_free(info): + # lvm's decmial precision is not configurable, so we tell it to use + # KB and convert to MB here + return float(info['LVM2_VG_FREE']) / 1024 + +def udev_device_get_vg_extent_size(info): + # lvm's decmial precision is not configurable, so we tell it to use + # KB and convert to MB here + return float(info['LVM2_VG_EXTENT_SIZE']) / 1024 + +def udev_device_get_vg_extent_count(info): + return int(info['LVM2_VG_EXTENT_COUNT']) + +def udev_device_get_vg_free_extents(info): + return int(info['LVM2_VG_FREE_COUNT']) + +def udev_device_get_vg_pv_count(info): + return int(info['LVM2_PV_COUNT']) + +def udev_device_get_pv_pe_start(info): + # lvm's decmial precision is not configurable, so we tell it to use + # KB and convert to MB here + return float(info['LVM2_PE_START']) / 1024 + +def udev_device_get_lv_names(info): + names = info['LVM2_LV_NAME'] + if not names: + names = [] + elif not isinstance(names, list): + names = [names] + return names + +def udev_device_get_lv_uuids(info): + uuids = info['LVM2_LV_UUID'] + if not uuids: + uuids = [] + elif not isinstance(uuids, list): + uuids = [uuids] + return uuids + +def udev_device_get_lv_sizes(info): + # lvm's decmial precision is not configurable, so we tell it to use + # KB and convert to MB here + sizes = info['LVM2_LV_SIZE'] + if not sizes: + sizes = [] + elif not isinstance(sizes, list): + sizes = [sizes] + + return [float(s) / 1024 for s in sizes] + +def udev_device_get_lv_attr(info): + attr = info['LVM2_LV_ATTR'] + if not attr: + attr = [] + elif not isinstance(attr, list): + attr = [attr] + return attr + +def udev_device_is_biosraid(info): + # Note that this function does *not* identify raid sets. + # Tests to see if device is parto of a dmraid set. + # dmraid and mdraid have the same ID_FS_USAGE string, ID_FS_TYPE has a + # string that describes the type of dmraid (isw_raid_member...), I don't + # want to maintain a list and mdraid's ID_FS_TYPE='linux_raid_member', so + # dmraid will be everything that is raid and not linux_raid_member + from formats.dmraid import DMRaidMember + from formats.mdraid import MDRaidMember + if info.has_key("ID_FS_TYPE") and \ + (info["ID_FS_TYPE"] in DMRaidMember._udevTypes or \ + info["ID_FS_TYPE"] in MDRaidMember._udevTypes) and \ + info["ID_FS_TYPE"] != "linux_raid_member": + return True + + return False + +def udev_device_get_dmraid_partition_disk(info): + try: + p_index = info["DM_NAME"].rindex("p") + except (KeyError, AttributeError, ValueError): + return None + + if not info["DM_NAME"][p_index+1:].isdigit(): + return None + + return info["DM_NAME"][:p_index] + +def udev_device_is_dmraid_partition(info, devicetree): + diskname = udev_device_get_dmraid_partition_disk(info) + dmraid_devices = devicetree.getDevicesByType("dm-raid array") + + for device in dmraid_devices: + if diskname == device.name: + return True + + return False + +def udev_device_is_multipath_partition(info, devicetree): + """ Return True if the device is a partition of a multipath device. """ + if not udev_device_is_dm(info): + return False + if not info["DM_NAME"].startswith("mpath"): + return False + diskname = udev_device_get_dmraid_partition_disk(info) + if diskname is None: + return False + + # this is sort of a lame check, but basically, if diskname gave us "mpath0" + # and we start with "mpath" but we're not "mpath0", then we must be + # "mpath0" plus some non-numeric crap. + if diskname != info["DM_NAME"]: + return True + + return False + +def udev_device_get_multipath_partition_disk(info): + """ Return True if the device is a partition of a multipath device. """ + # XXX PJFIX This whole function is crap. + if not udev_device_is_dm(info): + return False + if not info["DM_NAME"].startswith("mpath"): + return False + diskname = udev_device_get_dmraid_partition_disk(info) + return diskname + +def udev_device_is_multipath_member(info): + """ Return True if the device is part of a multipath. """ + return info.get("ID_FS_TYPE") == "multipath_member" + +def udev_device_get_multipath_name(info): + """ Return the name of the multipath that the device is a member of. """ + if udev_device_is_multipath_member(info): + return info['ID_MPATH_NAME'] + return None + +# iscsi disks have ID_PATH in the form of: +# ip-${iscsi_address}:${iscsi_port}-iscsi-${iscsi_tgtname}-lun-${lun} +# Note that in the case of IPV6 iscsi_address itself can contain : +# too, but iscsi_port never contains : +def udev_device_is_iscsi(info): + try: + path_components = udev_device_get_path(info).split("-") + + if info["ID_BUS"] == "scsi" and len(path_components) >= 6 and \ + path_components[0] == "ip" and path_components[2] == "iscsi": + return True + except KeyError: + pass + + return False + +def udev_device_get_iscsi_name(info): + path_components = udev_device_get_path(info).split("-") + + # Tricky, the name itself contains atleast 1 - char + return "-".join(path_components[3:len(path_components)-2]) + +def udev_device_get_iscsi_address(info): + path_components = udev_device_get_path(info).split("-") + + # IPV6 addresses contain : within the address, so take everything + # before the last : as address + return ":".join(path_components[1].split(":")[:-1]) + +def udev_device_get_iscsi_port(info): + path_components = udev_device_get_path(info).split("-") + + # IPV6 contains : within the address, the part after the last : is the port + return path_components[1].split(":")[-1] + +# fcoe disks have ID_PATH in the form of: +# pci-eth#-fc-${id} +# fcoe parts look like this: +# pci-eth#-fc-${id}-part# +def udev_device_is_fcoe(info): + try: + path_components = udev_device_get_path(info).split("-") + + if info["ID_BUS"] == "scsi" and len(path_components) >= 4 and \ + path_components[0] == "pci" and path_components[2] == "fc" and \ + path_components[1][0:3] == "eth": + return True + except LookupError: + pass + + return False + +def udev_device_get_fcoe_nic(info): + path_components = udev_device_get_path(info).split("-") + + return path_components[1] + +def udev_device_get_fcoe_identifier(info): + path_components = udev_device_get_path(info).split("-") + + return path_components[3] diff --git a/storage/zfcp.py b/storage/zfcp.py new file mode 100644 index 0000000..7692cad --- /dev/null +++ b/storage/zfcp.py @@ -0,0 +1,441 @@ +# +# zfcp.py - mainframe zfcp configuration install data +# +# Copyright (C) 2001, 2002, 2003, 2004 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty 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, see <http://www.gnu.org/licenses/>. +# +# Author(s): Karsten Hopp <karsten@redhat.com> +# + +import string +import os +from constants import * +from udev import udev_settle + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) + +import logging +log = logging.getLogger("anaconda") +import warnings + +def loggedWriteLineToFile(fn, value): + f = open(fn, "w") + log.debug("echo %s > %s" % (value, fn)) + f.write("%s\n" % (value)) + f.close() + +zfcpsysfs = "/sys/bus/ccw/drivers/zfcp" +scsidevsysfs = "/sys/bus/scsi/devices" + +class ZFCPDevice: + def __init__(self, devnum, wwpn, fcplun): + self.devnum = self.sanitizeDeviceInput(devnum) + self.wwpn = self.sanitizeWWPNInput(wwpn) + self.fcplun = self.sanitizeFCPLInput(fcplun) + + if not self.checkValidDevice(self.devnum): + raise ValueError, _("You have not specified a device number or the number is invalid") + if not self.checkValidWWPN(self.wwpn): + raise ValueError, _("You have not specified a worldwide port name or the name is invalid.") + if not self.checkValidFCPLun(self.fcplun): + raise ValueError, _("You have not specified a FCP LUN or the number is invalid.") + + def __str__(self): + return "%s %s %s" %(self.devnum, self.wwpn, self.fcplun) + + def sanitizeDeviceInput(self, dev): + if dev is None or dev == "": + return None + dev = dev.lower() + bus = dev[:string.rfind(dev, ".") + 1] + dev = dev[string.rfind(dev, ".") + 1:] + dev = "0" * (4 - len(dev)) + dev + if not len(bus): + return "0.0." + dev + else: + return bus + dev + + def sanitizeWWPNInput(self, id): + if id is None or id == "": + return None + id = id.lower() + if id[:2] != "0x": + return "0x" + id + return id + + # ZFCP LUNs are usually entered as 16 bit, sysfs accepts only 64 bit + # (#125632), expand with zeroes if necessary + def sanitizeFCPLInput(self, lun): + if lun is None or lun == "": + return None + lun = lun.lower() + if lun[:2] == "0x": + lun = lun[2:] + lun = "0x" + "0" * (4 - len(lun)) + lun + lun = lun + "0" * (16 - len(lun) + 2) + return lun + + def _hextest(self, hex): + try: + int(hex, 16) + return True + except TypeError: + return False + + def checkValidDevice(self, id): + if id is None or id == "": + return False + if len(id) != 8: # p.e. 0.0.0600 + return False + if id[0] not in string.digits or id[2] not in string.digits: + return False + if id[1] != "." or id[3] != ".": + return False + return self._hextest(id[4:]) + + def checkValid64BitHex(self, hex): + if hex is None or hex == "": + return False + if len(hex) != 18: + return False + return self._hextest(hex) + checkValidWWPN = checkValidFCPLun = checkValid64BitHex + + def onlineDevice(self): + online = "%s/%s/online" %(zfcpsysfs, self.devnum) + portadd = "%s/%s/port_add" %(zfcpsysfs, self.devnum) + portdir = "%s/%s/%s" %(zfcpsysfs, self.devnum, self.wwpn) + unitadd = "%s/unit_add" %(portdir) + unitdir = "%s/%s" %(portdir, self.fcplun) + failed = "%s/failed" %(unitdir) + + try: + if not os.path.exists(online): + loggedWriteLineToFile("/proc/cio_ignore", + "free %s" %(self.devnum,)) + udev_settle() + except IOError as e: + raise ValueError, _("Could not free zFCP device %(devnum)s from " + "device ignore list (%(e)s).") \ + % {'devnum': self.devnum, 'e': e} + + if not os.path.exists(online): + raise ValueError, _( + "zFCP device %s not found, not even in device ignore list." + %(self.devnum,)) + + try: + f = open(online, "r") + devonline = f.readline().strip() + f.close() + if devonline != "1": + loggedWriteLineToFile(online, "1") + else: + log.info("zFCP device %s already online." %(self.devnum,)) + except IOError as e: + raise ValueError, _("Could not set zFCP device %(devnum)s " + "online (%(e)s).") \ + % {'devnum': self.devnum, 'e': e} + + if not os.path.exists(portdir): + if os.path.exists(portadd): + # older zfcp sysfs interface + try: + loggedWriteLineToFile(portadd, self.wwpn) + udev_settle() + except IOError as e: + raise ValueError, _("Could not add WWPN %(wwpn)s to zFCP " + "device %(devnum)s (%(e)s).") \ + % {'wwpn': self.wwpn, + 'devnum': self.devnum, + 'e': e} + else: + # newer zfcp sysfs interface with auto port scan + raise ValueError, _("WWPN %(wwpn)s not found at zFCP device " + "%(devnum)s.") % {'wwpn': self.wwpn, + 'devnum': self.devnum} + else: + if os.path.exists(portadd): + # older zfcp sysfs interface + log.info("WWPN %(wwpn)s at zFCP device %(devnum)s already " + "there.") % {'wwpn': self.wwpn, + 'devnum': self.devnum} + + if not os.path.exists(unitdir): + try: + loggedWriteLineToFile(unitadd, self.fcplun) + udev_settle() + except IOError as e: + raise ValueError, _("Could not add LUN %(fcplun)s to WWPN " + "%(wwpn)s on zFCP device %(devnum)s " + "(%(e)s).") \ + % {'fcplun': self.fcplun, 'wwpn': self.wwpn, + 'devnum': self.devnum, 'e': e} + else: + raise ValueError, _("LUN %(fcplun)s at WWPN %(wwpn)s on zFCP " + "device %(devnum)s already configured.") \ + % {'fcplun': self.fcplun, + 'wwpn': self.wwpn, + 'devnum': self.devnum} + + fail = "0" + try: + f = open(failed, "r") + fail = f.readline().strip() + f.close() + except IOError as e: + raise ValueError, _("Could not read failed attribute of LUN " + "%(fcplun)s at WWPN %(wwpn)s on zFCP device " + "%(devnum)s (%(e)s).") \ + % {'fcplun': self.fcplun, + 'wwpn': self.wwpn, + 'devnum': self.devnum, + 'e': e} + if fail != "0": + self.offlineDevice() + raise ValueError, _("Failed LUN %(fcplun)s at WWPN %(wwpn)s on " + "zFCP device %(devnum)s removed again.") \ + % {'fcplun': self.fcplun, + 'wwpn': self.wwpn, + 'devnum': self.devnum} + + return True + + def offlineSCSIDevice(self): + f = open("/proc/scsi/scsi", "r") + lines = f.readlines() + f.close() + # alternatively iterate over /sys/bus/scsi/devices/*:0:*:*/ + + for line in lines: + if not line.startswith("Host"): + continue + scsihost = string.split(line) + host = scsihost[1] + channel = "0" + id = scsihost[5] + lun = scsihost[7] + scsidev = "%s:%s:%s:%s" % (host[4:], channel, id, lun) + fcpsysfs = "%s/%s" % (scsidevsysfs, scsidev) + scsidel = "%s/%s/delete" % (scsidevsysfs, scsidev) + + f = open("%s/hba_id" %(fcpsysfs), "r") + fcphbasysfs = f.readline().strip() + f.close() + f = open("%s/wwpn" %(fcpsysfs), "r") + fcpwwpnsysfs = f.readline().strip() + f.close() + f = open("%s/fcp_lun" %(fcpsysfs), "r") + fcplunsysfs = f.readline().strip() + f.close() + + if fcphbasysfs == self.devnum \ + and fcpwwpnsysfs == self.wwpn \ + and fcplunsysfs == self.fcplun: + loggedWriteLineToFile(scsidel, "1") + udev_settle() + return + + log.warn("no scsi device found to delete for zfcp %s %s %s" + %(self.devnum, self.wwpn, self.fcplun)) + + def offlineDevice(self): + offline = "%s/%s/online" %(zfcpsysfs, self.devnum) + portadd = "%s/%s/port_add" %(zfcpsysfs, self.devnum) + portremove = "%s/%s/port_remove" %(zfcpsysfs, self.devnum) + unitremove = "%s/%s/%s/unit_remove" %(zfcpsysfs, self.devnum, self.wwpn) + portdir = "%s/%s/%s" %(zfcpsysfs, self.devnum, self.wwpn) + devdir = "%s/%s" %(zfcpsysfs, self.devnum) + + try: + self.offlineSCSIDevice() + except IOError as e: + raise ValueError, _("Could not correctly delete SCSI device of " + "zFCP %(devnum)s %(wwpn)s %(fcplun)s " + "(%(e)s).") \ + % {'devnum': self.devnum, 'wwpn': self.wwpn, + 'fcplun': self.fcplun, 'e': e} + + try: + loggedWriteLineToFile(unitremove, self.fcplun) + except IOError as e: + raise ValueError, _("Could not remove LUN %(fcplun)s at WWPN " + "%(wwpn)s on zFCP device %(devnum)s " + "(%(e)s).") \ + % {'fcplun': self.fcplun, 'wwpn': self.wwpn, + 'devnum': self.devnum, 'e': e} + + if os.path.exists(portadd): + # only try to remove ports with older zfcp sysfs interface + for lun in os.listdir(portdir): + if lun.startswith("0x") and \ + os.path.isdir(os.path.join(portdir, lun)): + log.info("Not removing WWPN %s at zFCP device %s since port still has other LUNs, e.g. %s." + %(self.wwpn, self.devnum, lun)) + return True + + try: + loggedWriteLineToFile(portremove, self.wwpn) + except IOError as e: + raise ValueError, _("Could not remove WWPN %(wwpn)s on zFCP " + "device %(devnum)s (%(e)s).") \ + % {'wwpn': self.wwpn, + 'devnum': self.devnum, 'e': e} + + if os.path.exists(portadd): + # older zfcp sysfs interface + for port in os.listdir(devdir): + if port.startswith("0x") and \ + os.path.isdir(os.path.join(devdir, port)): + log.info("Not setting zFCP device %s offline since it still has other ports, e.g. %s." + %(self.devnum, port)) + return True + else: + # newer zfcp sysfs interface with auto port scan + import glob + luns = glob.glob("%s/0x????????????????/0x????????????????" + %(devdir,)) + if len(luns) != 0: + log.info("Not setting zFCP device %s offline since it still has other LUNs, e.g. %s." + %(self.devnum, luns[0])) + return True + + try: + loggedWriteLineToFile(offline, "0") + except IOError as e: + raise ValueError, _("Could not set zFCP device %(devnum)s " + "offline (%(e)s).") \ + % {'devnum': self.devnum, 'e': e} + + return True + +class ZFCP: + """ ZFCP utility class. + + This class will automatically online to ZFCP drives configured in + /tmp/fcpconfig when the startup() method gets called. It can also be + used to manually configure ZFCP devices through the addFCP() method. + + As this class needs to make sure that /tmp/fcpconfig configured + drives are only onlined once and as it keeps a global list of all ZFCP + devices it is implemented as a Singleton. + """ + + def __init__(self): + self.fcpdevs = [] + self.hasReadConfig = False + self.down = True + + # So that users can write zfcp() to get the singleton instance + def __call__(self): + return self + + def readConfig(self): + try: + f = open("/tmp/fcpconfig", "r") + except IOError: + log.info("no /tmp/fcpconfig; not configuring zfcp") + return + + lines = f.readlines() + f.close() + for line in lines: + # each line is a string separated list of values to describe a dev + # there are two valid formats for the line: + # devnum scsiid wwpn scsilun fcplun (scsiid + scsilun ignored) + # devnum wwpn fcplun + line = string.strip(line).lower() + if line.startswith("#"): + continue + fcpconf = string.split(line) + if len(fcpconf) == 3: + devnum = fcpconf[0] + wwpn = fcpconf[1] + fcplun = fcpconf[2] + elif len(fcpconf) == 5: + warnings.warn("SCSI ID and SCSI LUN values for ZFCP devices are ignored and deprecated.", DeprecationWarning) + devnum = fcpconf[0] + wwpn = fcpconf[2] + fcplun = fcpconf[4] + else: + log.warn("Invalid line found in /tmp/fcpconfig!") + continue + + try: + self.addFCP(devnum, wwpn, fcplun) + except ValueError, e: + log.warn(str(e)) + continue + + def addFCP(self, devnum, wwpn, fcplun): + d = ZFCPDevice(devnum, wwpn, fcplun) + if d.onlineDevice(): + self.fcpdevs.append(d) + + def shutdown(self): + if self.down: + return + self.down = True + if len(self.fcpdevs) == 0: + return + for d in self.fcpdevs: + try: + d.offlineDevice() + except ValueError, e: + log.warn(str(e)) + + def startup(self): + if not self.down: + return + self.down = False + if not self.hasReadConfig: + self.readConfig() + self.hasReadConfig = True + # readConfig calls addFCP which calls onlineDevice already + return + + if len(self.fcpdevs) == 0: + return + for d in self.fcpdevs: + try: + d.onlineDevice() + except ValueError, e: + log.warn(str(e)) + + def writeKS(self, f): + if len(self.fcpdevs) == 0: + return + for d in self.fcpdevs: + f.write("zfcp --devnum %s --wwpn %s --fcplun %s\n" %(d.devnum, + d.wwpn, + d.fcplun)) + + def write(self, instPath): + if len(self.fcpdevs) == 0: + return + f = open(instPath + "/etc/zfcp.conf", "w") + for d in self.fcpdevs: + f.write("%s\n" %(d,)) + f.close() + + f = open(instPath + "/etc/modprobe.conf", "a") + f.write("alias scsi_hostadapter zfcp\n") + f.close() + +# Create ZFCP singleton +ZFCP = ZFCP() + +# vim:tw=78:ts=4:et:sw=4 |