qubes-installer-qubes-os/anaconda/pyanaconda/storage/__init__.py

3529 lines
130 KiB
Python
Raw Normal View History

# __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 copy
import nss.nss
import parted
from pyanaconda import isys
from pyanaconda import iutil
from pyanaconda.constants import *
from pykickstart.constants import *
from pyanaconda.flags import flags
from pyanaconda import tsort
from pyanaconda.errors import *
from pyanaconda.bootloader import BootLoaderError
from pyanaconda.anaconda_log import log_method_call
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.dm import name_from_dm_node
from devicelibs.crypto import generateBackupPassphrase
from devicelibs.mpath import MultipathConfigWriter
from devicelibs.edd import get_edd_dict
from devicelibs.mdraid import get_member_space
from devicelibs.mdraid import raidLevelString
from devicelibs.lvm import get_pv_space
from .partitioning import SameSizeSet
from .partitioning import TotalSizeSet
from .partitioning import doPartitioning
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")
DEVICE_TYPE_LVM = 0
DEVICE_TYPE_MD = 1
DEVICE_TYPE_PARTITION = 2
DEVICE_TYPE_BTRFS = 3
DEVICE_TYPE_DISK = 4
def getDeviceType(device):
device_types = {"partition": DEVICE_TYPE_PARTITION,
"lvmlv": DEVICE_TYPE_LVM,
"btrfs subvolume": DEVICE_TYPE_BTRFS,
"btrfs volume": DEVICE_TYPE_BTRFS,
"mdarray": DEVICE_TYPE_MD}
use_dev = device
if isinstance(device, LUKSDevice):
use_dev = device.slave
if use_dev.isDisk:
device_type = DEVICE_TYPE_DISK
else:
device_type = device_types.get(use_dev.type)
return device_type
def getRAIDLevel(device):
# TODO: move this into StorageDevice
use_dev = device
if isinstance(device, LUKSDevice):
use_dev = device.slave
# TODO: lvm and perhaps pulling raid level from md pvs
raid_level = None
if hasattr(use_dev, "level"):
raid_level = raidLevelString(use_dev.level)
elif hasattr(use_dev, "dataLevel"):
raid_level = use_dev.dataLevel or "single"
elif hasattr(use_dev, "volume"):
raid_level = use_dev.volume.dataLevel or "single"
return raid_level
def storageInitialize(storage, ksdata, protected):
storage.shutdown()
# 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
iutil.execWithRedirect("udevadm", ["control", "--property=ANACONDA=1"],
stdout="/dev/tty5", stderr="/dev/tty5")
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.
storage.config.update(ksdata)
lvm.lvm_vg_blacklist = []
# Set up the protected partitions list now.
if protected:
storage.config.protectedDevSpecs.extend(protected)
storage.reset()
if not flags.livecdInstall and not storage.protectedDevices:
if anaconda.upgrade:
return
else:
raise UnknownSourceDeviceError(protected)
else:
storage.reset()
# kickstart uses all the disks
if flags.automatedInstall:
if not ksdata.ignoredisk.onlyuse:
ksdata.ignoredisk.onlyuse = [d.name for d in storage.disks \
if d.name not in ksdata.ignoredisk.ignoredisk]
log.debug("onlyuse is now: %s" % (",".join(ksdata.ignoredisk.onlyuse)))
def turnOnFilesystems(storage):
upgrade = "preupgrade" in flags.cmdline
if not upgrade:
if (flags.livecdInstall and not flags.imageInstall and not storage.fsset.active):
# turn off any swaps that we didn't turn on
# needed for live installs
iutil.execWithRedirect("swapoff", ["-a"],
stdout = "/dev/tty5", stderr="/dev/tty5")
storage.devicetree.teardownAll()
upgrade_migrate = False
if upgrade:
for d in storage.migratableDevices:
if d.format.migrate:
upgrade_migrate = True
try:
storage.doIt()
except FSResizeError as e:
if os.path.exists("/tmp/resize.out"):
details = open("/tmp/resize.out", "r").read()
else:
details = e.args[1]
if errorHandler.cb(e, e.args[0], details=details) == ERROR_RAISE:
raise
except FSMigrateError as e:
if errorHandler.cb(e, e.args[0], e.args[1]) == ERROR_RAISE:
raise
except Exception as e:
raise
storage.turnOnSwap()
# FIXME: For livecd, skipRoot needs to be True.
storage.mountFilesystems(raiseErrors=False,
readOnly=False,
skipRoot=False)
writeEscrowPackets(storage)
def writeEscrowPackets(storage):
escrowDevices = filter(lambda d: d.format.type == "luks" and \
d.format.escrow_cert,
storage.devices)
if not escrowDevices:
return
log.debug("escrow: writeEscrowPackets start")
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(ROOT_PATH + "/root",
backupPassphrase)
except (IOError, RuntimeError) as e:
# TODO: real error handling
log.error("failed to store encryption key: %s" % e)
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 StorageDiscoveryConfig(object):
def __init__(self):
# storage configuration variables
self.ignoreDiskInteractive = False
self.ignoredDisks = []
self.exclusiveDisks = []
self.clearPartType = None
self.clearPartDisks = []
self.clearPartDevices = []
self.initializeDisks = False
self.protectedDevSpecs = []
self.diskImages = {}
self.mpathFriendlyNames = True
# Whether clearPartitions removes scheduled/non-existent devices and
# disklabels depends on this flag.
self.clearNonExistent = False
def update(self, ksdata):
self.ignoredDisks = ksdata.ignoredisk.ignoredisk[:]
self.exclusiveDisks = ksdata.ignoredisk.onlyuse[:]
self.clearPartType = ksdata.clearpart.type
self.clearPartDisks = ksdata.clearpart.drives[:]
self.clearPartDevices = ksdata.clearpart.devices[:]
self.initializeDisks = ksdata.clearpart.initAll
self.zeroMbr = ksdata.zerombr.zerombr
class Storage(object):
def __init__(self, data=None, platform=None):
""" Create a Storage instance.
Keyword Arguments:
data - a pykickstart Handler instance
platform - a Platform instance
"""
self.data = data
self.platform = platform
self._bootloader = None
self.config = StorageDiscoveryConfig()
# storage configuration variables
self.doAutoPart = False
self.clearPartChoice = None
self.encryptedAutoPart = False
self.autoPartType = AUTOPART_TYPE_LVM
self.encryptionPassphrase = None
self.encryptionCipher = None
self.escrowCertificates = {}
self.autoPartEscrowCert = None
self.autoPartAddBackupPassphrase = False
self.encryptionRetrofit = False
self.autoPartitionRequests = []
self.eddDict = {}
self.__luksDevs = {}
self.size_sets = []
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._dumpFile = "/tmp/storage.state"
# these will both be empty until our reset method gets called
self.devicetree = DeviceTree(conf=self.config,
passphrase=self.encryptionPassphrase,
luksDict=self.__luksDevs,
iscsi=self.iscsi,
dasd=self.dasd)
self.fsset = FSSet(self.devicetree)
self.roots = []
self.services = set()
def doIt(self):
self.devicetree.processActions()
self.doEncryptionPassphraseRetrofits()
# now set the boot partition's flag
if self.bootloader:
if self.bootloader.stage2_bootable:
boot = self.bootDevice
else:
boot = self.bootLoaderDevice
if boot.type == "mdarray":
bootDevs = boot.parents
else:
bootDevs = [boot]
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
# GPT labeled disks should only have bootable set on the
# EFI system partition (parted sets the EFI System GUID on
# GPT partitions with the boot flag)
if dev.disk.format.labelType == "gpt" and \
dev.format.type != "efi":
skip = True
if skip:
log.info("not setting boot flag on %s" % dev.name)
continue
# hfs+ partitions on gpt can't be marked bootable via
# parted
if dev.disk.format.partedDisk.type == "gpt" and \
dev.format.type == "hfs+":
log.info("not setting boot flag on hfs+ partition"
" %s" % dev.name)
continue
log.info("setting boot flag on %s" % dev.name)
dev.bootable = True
# Set the boot partition's name on disk labels that support it
if dev.partedPartition.disk.supportsFeature(parted.DISK_TYPE_PARTITION_NAME):
ped_partition = dev.partedPartition.getPedPartition()
ped_partition.set_name(dev.format.name)
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)
def reset(self, cleanupOnly=False):
""" 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
if self.data:
self.config.update(self.data)
if not flags.imageInstall:
self.iscsi.startup()
self.fcoe.startup()
self.zfcp.startup()
self.dasd.startup(None,
self.config.exclusiveDisks,
self.config.initializeDisks)
clearPartType = self.config.clearPartType # save this before overriding it
if self.data and self.data.upgrade.upgrade:
self.config.clearPartType = CLEARPART_TYPE_NONE
if self.dasd:
# Reset the internal dasd list (823534)
self.dasd.clear_device_list()
self.devicetree.reset(conf=self.config,
passphrase=self.encryptionPassphrase,
luksDict=self.__luksDevs,
iscsi=self.iscsi,
dasd=self.dasd)
self.devicetree.populate(cleanupOnly=cleanupOnly)
self.config.clearPartType = clearPartType # set it back
self.fsset = FSSet(self.devicetree)
self.eddDict = get_edd_dict(self.partitioned)
if self.bootloader:
# clear out bootloader attributes that refer to devices that are
# no longer in the tree
self.bootloader.stage1_disk = None
self.bootloader.stage1_device = None
self.bootloader.stage2_device = None
self.roots = findExistingInstallations(self.devicetree)
self.dumpState("initial")
self.updateBootLoaderDiskList()
@property
def unusedDevices(self):
used_devices = []
for root in self.roots:
for device in root.mounts.values() + root.swaps:
if device not in self.devices:
continue
used_devices.extend(device.ancestors)
for new in [d for d in self.devicetree.leaves if not d.format.exists]:
if new in self.swaps or getattr(new.format, "mountpoint", None):
used_devices.extend(new.ancestors)
for device in self.partitions:
if getattr(device, "isLogical", False):
extended = device.disk.format.extendedPartition.path
used_devices.append(self.devicetree.getDeviceByPath(extended))
used = set(used_devices)
_all = set(self.devices)
return list(_all.difference(used))
@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 btrfsVolumes(self):
return sorted(self.devicetree.getDevicesByType("btrfs volume"),
key=lambda d: d.name)
@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
@property
def liveImage(self):
""" The OS image used by live installs. """
return None
def shouldClear(self, device, **kwargs):
clearPartType = kwargs.get("clearPartType", self.config.clearPartType)
clearPartDisks = kwargs.get("clearPartDisks",
self.config.clearPartDisks)
clearPartDevices = kwargs.get("clearPartDevices",
self.config.clearPartDevices)
def empty_disk(device):
empty = True
if device.partitioned:
partitions = self.devicetree.getChildren(device)
empty = all([p.isMagic for p in partitions])
else:
empty = (device.format.type is None)
return empty
for disk in device.disks:
# this will not include disks with hidden formats like multipath
# and firmware raid member disks
if clearPartDisks and disk.name not in clearPartDisks:
return False
if not self.config.clearNonExistent:
if (device.isDisk and not device.format.exists) or \
(not device.isDisk and not device.exists):
return False
# the only devices we want to clear when clearPartType is
# CLEARPART_TYPE_NONE are uninitialized disks, or disks with no
# partitions, in clearPartDisks, and then only when we have been asked
# to initialize disks as needed
if clearPartType in [CLEARPART_TYPE_NONE, None]:
if not self.config.initializeDisks or not device.isDisk:
return False
if not empty_disk(device):
return False
if isinstance(device, PartitionDevice):
# Never clear the special first partition on a Mac disk label, as
# that holds the partition table itself.
# Something similar for the third partition on a Sun disklabel.
if device.isMagic:
return False
# We don't want to fool with extended partitions, freespace, &c
if not device.isPrimary and not device.isLogical:
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:
if device.partitioned and clearPartType != CLEARPART_TYPE_ALL:
# if clearPartType is not CLEARPART_TYPE_ALL but we'll still be
# removing every partition from the disk, return True since we
# will want to be able to create a new disklabel on this disk
if not empty_disk(device):
return False
# Never clear disks with hidden formats
if device.format.hidden:
return False
# When clearPartType is CLEARPART_TYPE_LINUX and a disk has non-
# linux whole-disk formatting, do not clear it. The exception is
# the case of an uninitialized disk when we've been asked to
# initialize disks as needed
if clearPartType == CLEARPART_TYPE_LINUX and \
not (self.config.initializeDisks and
device.format.type is None) and \
not device.partitioned and not device.format.linuxNative:
return False
# Don't clear devices holding install media.
if device.protected:
return False
if clearPartType == CLEARPART_TYPE_LIST and \
device.name not in clearPartDevices:
return False
return True
def recursiveRemove(self, device):
log.debug("removing %s" % device.name)
# XXX is there any argument for not removing incomplete devices?
# -- maybe some RAID devices
devices = self.deviceDeps(device)
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:
self.destroyDevice(leaf)
devices.remove(leaf)
if device.isDisk:
self.devicetree.registerAction(ActionDestroyFormat(device))
else:
self.destroyDevice(device)
def clearPartitions(self):
""" Clear partitions and dependent devices from disks.
Arguments:
None
NOTES:
- Needs some error handling
"""
if not hasattr(self.platform, "diskLabelTypes"):
raise StorageError("can't clear partitions without platform data")
# 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 = sorted(self.partitions,
key=lambda p: p.partedPartition.number,
reverse=True)
for part in partitions:
log.debug("clearpart: looking at %s" % part.name)
if not self.shouldClear(part):
continue
self.recursiveRemove(part)
log.debug("partitions: %s" % [p.getDeviceNodeName() for p in part.partedPartition.disk.partitions])
# now remove any empty extended partitions
self.removeEmptyExtendedPartitions()
# ensure all disks have appropriate disklabels
for disk in self.disks:
if not self.shouldClear(disk):
continue
log.debug("clearpart: initializing %s" % disk.name)
self.initializeDisk(disk)
self.updateBootLoaderDiskList()
def initializeDisk(self, disk):
""" (Re)initialize a disk by creating a disklabel on it.
The disk should not contain any partitions except perhaps for a
magic partitions on mac and sun disklabels.
"""
# first, remove magic mac/sun partitions from the parted Disk
if disk.partitioned:
magic_partitions = {"mac": 1, "sun": 3}
if disk.format.labelType in magic_partitions:
number = magic_partitions[disk.format.labelType]
# remove the magic partition
for part in disk.format.partitions:
if part.disk == disk and part.partedPartition.number == number:
log.debug("removing %s" % part.name)
# We can't schedule the magic partition for removal
# because parted will not allow us to remove it from the
# disk. Still, we need it out of the devicetree.
self.devicetree._removeDevice(part, moddisk=False)
if disk.partitioned and disk.format.partitions:
raise ValueError("cannot initialize a disk that has partitions")
# remove existing formatting from the disk
destroy_action = ActionDestroyFormat(disk)
self.devicetree.registerAction(destroy_action)
if self.platform:
labelType = self.platform.bestDiskLabelType(disk)
else:
labelType = None
# create a new disklabel on the disk
newLabel = getFormat("disklabel", device=disk.path,
labelType=labelType)
create_action = ActionCreateFormat(disk, format=newLabel)
self.devicetree.registerAction(create_action)
def removeEmptyExtendedPartitions(self):
for disk in self.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 = self.devicetree.getDeviceByName(extended_name)
self.destroyDevice(extended)
def getFreeSpace(self, disks=None, clearPartType=None):
""" Return a dict with free space info for each disk.
The dict values are 2-tuples: (disk_free, fs_free). fs_free is
space available by shrinking filesystems. disk_free is space not
allocated to any partition.
disks and clearPartType allow specifying a set of disks other than
self.disks and a clearPartType value other than
self.config.clearPartType.
"""
from size import Size
if disks is None:
disks = self.disks
if clearPartType is None:
clearPartType = self.config.clearPartType
free = {}
for disk in disks:
should_clear = self.shouldClear(disk, clearPartType=clearPartType,
clearPartDisks=[disk.name])
if should_clear:
free[disk.name] = (Size(spec="%f mb" % disk.size), 0)
continue
disk_free = 0
fs_free = 0
if disk.partitioned:
disk_free = disk.format.free
for partition in [p for p in self.partitions if p.disk == disk]:
# only check actual filesystems since lvm &c require a bunch of
# operations to translate free filesystem space into free disk
# space
should_clear = self.shouldClear(partition,
clearPartType=clearPartType,
clearPartDisks=[disk.name])
if should_clear:
disk_free += partition.size
elif hasattr(partition.format, "free"):
fs_free += partition.format.free
elif hasattr(disk.format, "free"):
fs_free = disk.format.free
elif disk.format.type is None:
disk_free = disk.size
free[disk.name] = (Size(spec="%f mb" % disk_free),
Size(spec="%f mb" % fs_free))
return free
@property
def names(self):
return self.devicetree.names
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 and \
not getattr(device.format, "inconsistentVG", False):
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":
if device.format.inconsistentVG:
return _("This device is part of an inconsistent LVM "
"Volume Group.")
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
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("name"):
name = kwargs.pop("name")
else:
name = "req%d" % self.nextID
if "weight" not in kwargs:
fmt = kwargs.get("format")
if fmt:
mountpoint = getattr(fmt, "mountpoint", None)
kwargs["weight"] = self.platform.weight(mountpoint=mountpoint,
fstype=fmt.type)
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),
**kwargs.pop("fmt_args", {}))
if kwargs.has_key("name"):
name = kwargs.pop("name")
else:
swap = getattr(kwargs.get("format"), "type", None) == "swap"
mountpoint = getattr(kwargs.get("format"), "mountpoint", None)
name = self.suggestDeviceName(prefix=shortProductName,
swap=swap,
mountpoint=mountpoint)
return MDRaidArrayDevice(name, *args, **kwargs)
def newVG(self, *args, **kwargs):
""" Return a new LVMVolumeGroupDevice instance. """
pvs = kwargs.pop("parents", [])
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:
hostname = ""
if self.data and self.data.network.hostname is not None:
hostname = self.data.network.hostname
name = self.suggestContainerName(hostname=hostname)
if name in self.names:
raise ValueError("name already in use")
return LVMVolumeGroupDevice(name, pvs, *args, **kwargs)
def newLV(self, *args, **kwargs):
""" Return a new LVMLogicalVolumeDevice instance. """
vg = kwargs.get("parents", [None])[0]
mountpoint = kwargs.pop("mountpoint", None)
if kwargs.has_key("fmt_type"):
kwargs["format"] = getFormat(kwargs.pop("fmt_type"),
mountpoint=mountpoint,
**kwargs.pop("fmt_args", {}))
if kwargs.has_key("name"):
name = kwargs.pop("name")
# make sure the specified name is sensible
safe_vg_name = self.safeDeviceName(vg.name)
full_name = "%s-%s" % (safe_vg_name, name)
safe_name = self.safeDeviceName(full_name)
if safe_name != full_name:
new_name = safe_name[len(safe_vg_name)+1:]
log.warning("using '%s' instead of specified name '%s'"
% (new_name, name))
name = new_name
else:
if kwargs.get("format") and kwargs["format"].type == "swap":
swap = True
else:
swap = False
name = self.suggestDeviceName(parent=vg,
swap=swap,
mountpoint=mountpoint)
if "%s-%s" % (vg.name, name) in self.names:
raise ValueError("name already in use")
return LVMLogicalVolumeDevice(name, *args, **kwargs)
def newBTRFS(self, *args, **kwargs):
""" Return a new BTRFSVolumeDevice or BRFSSubVolumeDevice. """
log.debug("newBTRFS: args = %s ; kwargs = %s" % (args, kwargs))
name = kwargs.pop("name", None)
if args:
name = args[0]
mountpoint = kwargs.pop("mountpoint", None)
fmt_args = kwargs.pop("fmt_args", {})
fmt_args.update({"mountpoint": mountpoint})
if kwargs.pop("subvol", False):
dev_class = BTRFSSubVolumeDevice
# make sure there's a valid parent device
parents = kwargs.get("parents", [])
if not parents or len(parents) != 1 or \
not isinstance(parents[0], BTRFSVolumeDevice):
raise ValueError("new btrfs subvols require a parent volume")
# set up the subvol name, using mountpoint if necessary
if not name:
# for btrfs this only needs to ensure the subvol name is not
# already in use within the parent volume
name = self.suggestDeviceName(mountpoint=mountpoint)
fmt_args["mountopts"] = "subvol=%s" % name
kwargs.pop("metaDataLevel", None)
kwargs.pop("dataLevel", None)
else:
dev_class = BTRFSVolumeDevice
# set up the volume label, using hostname if necessary
if not name:
hostname = ""
if self.data and self.data.network.hostname is not None:
hostname = self.data.network.hostname
name = self.suggestContainerName(hostname=hostname)
if "label" not in fmt_args:
fmt_args["label"] = name
# discard fmt_type since it's btrfs always
kwargs.pop("fmt_type", None)
# this is to avoid auto-scheduled format create actions
device = dev_class(name, **kwargs)
device.format = getFormat("btrfs", **fmt_args)
return device
def newBTRFSSubVolume(self, *args, **kwargs):
kwargs["subvol"] = True
return self.newBTRFS(*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 resetDevice(self, device):
""" Cancel all scheduled actions and reset formatting. """
actions = self.devicetree.findActions(device=device)
for action in reversed(actions):
self.devicetree.cancelAction(action)
# make sure any random overridden attributes are reset
device.format = copy.copy(device.originalFormat)
def resizeDevice(self, device, new_size):
classes = []
if device.resizable:
classes.append(ActionResizeDevice)
if device.format.resizable:
classes.append(ActionResizeFormat)
if not classes:
raise ValueError("device cannot be resized")
# if this is a shrink, schedule the format resize first
if new_size < device.size:
classes.reverse()
for action_class in classes:
self.devicetree.registerAction(action_class(device, new_size))
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 mustFormat(self, device):
""" Return a string explaining why the device must be reformatted.
Return None if the device need not be reformatted.
"""
if device.format.mountable and device.format.mountpoint == "/":
return _("You must create a new filesystem on the root device.")
return None
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 safeDeviceName(self, name):
""" Convert a device name to something safe and return that.
LVM limits lv names to 128 characters. I don't know the limits for
the other various device types, so I'm going to pick a number so
that we don't have to have an entire fucking library to determine
device name limits.
"""
max_len = 96 # No, you don't need longer names than this. Really.
tmp = name.strip()
tmp = tmp.replace("/", "_")
tmp = re.sub("[^0-9a-zA-Z._-]", "", tmp)
tmp = tmp.lstrip("_")
if len(tmp) > max_len:
tmp = tmp[:max_len]
return tmp
def suggestContainerName(self, hostname=None, prefix=""):
""" Return a reasonable, unused device name. """
if not prefix:
prefix = shortProductName
# try to create a device name incorporating the hostname
if hostname not in (None, "", 'localhost', 'localhost.localdomain'):
template = "%s_%s" % (prefix, hostname.split('.')[0].lower())
template = self.safeDeviceName(template)
else:
template = prefix
if flags.imageInstall:
template = "%s_image" % template
names = self.names
name = template
if name in names:
name = None
for i in range(100):
tmpname = "%s%02d" % (template, i,)
if tmpname not in names:
name = tmpname
break
if not name:
log.error("failed to create device name based on prefix "
"'%s' and hostname '%s'" % (prefix, hostname))
raise RuntimeError("unable to find suitable device name")
return name
def suggestDeviceName(self, parent=None, swap=None,
mountpoint=None, prefix=""):
""" Return a suitable, unused name for a new logical volume. """
body = ""
if mountpoint:
if mountpoint == "/":
body = "root"
else:
body = mountpoint[1:].replace("/", "_")
elif swap:
body = "swap"
if prefix:
body = "_" + body
template = self.safeDeviceName(prefix + body)
names = self.names
name = template
def full_name(name, parent):
full = ""
if parent:
full = "%s-" % parent.name
full += name
return full
# also include names of any lvs in the parent for the case of the
# temporary vg in the lvm dialogs, which can contain lvs that are
# not yet in the devicetree and therefore not in self.names
if full_name(name, parent) in names or not body:
for i in range(100):
name = "%s%02d" % (template, i)
if full_name(name, parent) not in names:
break
else:
name = ""
if not name:
log.error("failed to create device name based on parent '%s', "
"prefix '%s', mountpoint '%s', swap '%s'"
% (parent.name, prefix, mountpoint, swap))
raise RuntimeError("unable to find suitable device name")
return name
def savePassphrase(self, device):
""" Save a device's LUKS passphrase in case of reset. """
passphrase = device.format._LUKS__passphrase
self.__luksDevs[device.format.uuid] = passphrase
self.devicetree._DeviceTree__luksDevs[device.format.uuid] = passphrase
self.devicetree._DeviceTree__passphrases.append(passphrase)
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 setupDiskImages(self):
self.devicetree.setDiskImages(self.config.diskImages)
self.devicetree.setupDiskImages()
@property
def fileSystemFreeSpace(self):
mountpoints = ["/", "/usr"]
free = 0
btrfs_volumes = []
for mountpoint in mountpoints:
device = self.mountpoints.get(mountpoint)
if not device:
continue
# don't count the size of btrfs volumes repeatedly when multiple
# subvolumes are present
if isinstance(device, BTRFSSubVolumeDevice):
if device.volume in btrfs_volumes:
continue
else:
btrfs_volumes.append(device.volume)
if device.format.exists:
free += device.format.free
else:
free += device.size
return free
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.bootDevice
except (DeviceError, AttributeError):
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,))
# Prevent users from installing on s390x with (a) no /boot volume, (b) the
# root volume on LVM, and (c) the root volume not restricted to a single
# PV
# NOTE: There is not really a way for users to create a / volume
# restricted to a single PV. The backend support is there, but there are
# no UI hook-ups to drive that functionality, but I do not personally
# care. --dcantrell
if iutil.isS390() and \
not self.mountpoints.has_key('/boot') and \
root.type == 'lvmlv' and not root.singlePV:
errors.append(_("This platform requires /boot on a dedicated "
"partition or logical volume. If you do not "
"want a /boot volume, you must place / on a "
"dedicated non-LVM partition."))
# FIXME: put a check here for enough space on the filesystems. maybe?
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})
for (mount, device) in filesystems.items():
problem = filesystems[mount].checkSize()
if problem < 0:
errors.append(_("Your %(mount)s partition is too small for %(format)s formatting "
"(allowable size is %(minSize)d MB to %(maxSize)d MB)")
% {"mount": mount, "format": device.format.name,
"minSize": device.minSize, "maxSize": device.maxSize})
elif problem > 0:
errors.append(_("Your %(mount)s partition is too large for %(format)s formatting "
"(allowable size is %(minSize)d MB to %(maxSize)d MB)")
% {"mount":mount, "format": device.format.name,
"minSize": device.minSize, "maxSize": device.maxSize})
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."))
if self.bootloader and not self.bootloader.skip_bootloader:
stage1 = self.bootloader.stage1_device
if not stage1:
errors.append(_("you have not created a bootloader stage1 "
"target device"))
else:
self.bootloader.is_valid_stage1_device(stage1)
errors.extend(self.bootloader.errors)
warnings.extend(self.bootloader.warnings)
stage2 = self.bootloader.stage2_device
if not stage2:
errors.append(_("You have not created a bootable partition."))
else:
self.bootloader.is_valid_stage2_device(stage2)
errors.extend(self.bootloader.errors)
warnings.extend(self.bootloader.warnings)
if not self.bootloader.check():
errors.extend(self.bootloader.errors)
#
# check that GPT boot disk on BIOS system has a BIOS boot partition
#
if self.platform.weight(fstype="biosboot") and \
stage1 and stage1.isDisk and \
getattr(stage1.format, "labelType", None) == "gpt":
missing = True
for part in [p for p in self.partitions if p.disk == stage1]:
if part.format.type == "biosboot":
missing = False
break
if missing:
errors.append(_("Your BIOS-based system needs a special "
"partition to boot with %s's new "
"disk label format (GPT). To continue, "
"please create a 1MB 'BIOS Boot' type "
"partition.") % productName)
if not swaps:
from pyanaconda.storage.size import Size
installed = Size(spec="%s kb" % iutil.memInstalled())
required = Size(spec="%s kb" % isys.EARLY_SWAP_RAM)
if installed < required:
errors.append(_("You have not specified a swap partition. "
"%(requiredMem)s MB of memory is required to continue installation "
"without a swap partition, but you only have %(installedMem)s MB.")
% {"requiredMem": int(required.convertTo(spec="MB")),
"installedMem": int(installed.convertTo(spec="MB"))})
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."))
no_uuid = [s for s in swaps if s.format.exists and not s.format.uuid]
if no_uuid:
warnings.append(_("At least one of your swap devices does not have "
"a UUID, which is common in swap space created "
"using older versions of mkswap. These devices "
"will be referred to by device path in "
"/etc/fstab, which is not ideal since device "
"paths can change under a variety of "
"circumstances. "))
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)
if self.rootDevice and self.rootDevice.format.exists:
e = self.mustFormat(self.rootDevice)
if e:
errors.append(e)
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:
raise NoDisksError()
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]
@property
def packages(self):
pkgs = set()
if self.platform:
pkgs.update(self.platform.packages)
if self.bootloader:
pkgs.update(self.bootloader.packages)
for device in self.fsset.devices:
# this takes care of device and filesystem packages
pkgs.update(device.packages)
return list(pkgs)
def write(self):
if not os.path.isdir("%s/etc" % ROOT_PATH):
os.mkdir("%s/etc" % ROOT_PATH)
self.fsset.write()
self.makeMtab()
self.iscsi.write(self)
self.fcoe.write()
self.zfcp.write()
self.dasd.write()
def turnOnSwap(self, upgrading=None):
self.fsset.turnOnSwap(rootPath=ROOT_PATH,
upgrading=upgrading)
def mountFilesystems(self, raiseErrors=None, readOnly=None, skipRoot=False):
self.fsset.mountFilesystems(rootPath=ROOT_PATH,
raiseErrors=raiseErrors,
readOnly=readOnly, skipRoot=skipRoot)
def umountFilesystems(self, ignoreErrors=True, swapoff=True):
self.fsset.umountFilesystems(ignoreErrors=ignoreErrors, swapoff=swapoff)
def parseFSTab(self, chroot=None):
self.fsset.parseFSTab(chroot=chroot)
def mkDevRoot(self):
self.fsset.mkDevRoot()
def createSwapFile(self, device, size):
self.fsset.createSwapFile(device, size)
@property
def bootloader(self):
if self._bootloader is None and self.platform is not None:
self._bootloader = self.platform.bootloaderClass(self.platform)
return self._bootloader
def updateBootLoaderDiskList(self):
if not self.bootloader:
return
boot_disks = [d for d in self.disks if d.partitioned]
boot_disks.sort(cmp=self.compareDisks, key=lambda d: d.name)
self.bootloader.set_disk_list(boot_disks)
def setUpBootLoader(self):
""" Propagate ksdata into BootLoader. """
if not self.bootloader or not self.data:
log.warning("either ksdata or bootloader data missing")
return
if self.bootloader.skip_bootloader:
log.info("user specified that bootloader install be skipped")
return
self.bootloader.stage1_disk = self.devicetree.resolveDevice(self.data.bootloader.bootDrive)
self.bootloader.stage2_device = self.bootDevice
try:
self.bootloader.set_stage1_device(self.devices)
except BootLoaderError as e:
log.debug("failed to set bootloader stage1 device: %s" % e)
@property
def bootDisk(self):
disk = None
if self.data:
spec = self.data.bootloader.bootDrive
disk = self.devicetree.resolveDevice(spec)
return disk
@property
def bootDevice(self):
dev = None
if self.fsset:
dev = self.mountpoints.get("/boot", self.rootDevice)
return dev
@property
def bootLoaderDevice(self):
return getattr(self.bootloader, "stage1_device", None)
@property
def bootFSTypes(self):
"""A list of all valid filesystem types for the boot partition."""
fstypes = []
if self.bootloader:
fstypes = self.bootloader.stage2_format_types
return fstypes
@property
def defaultBootFSType(self):
"""The default filesystem type for the boot partition."""
fstype = None
if self.bootloader:
fstype = self.bootFSTypes[0]
return fstype
@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 makeMtab(self):
path = "/etc/mtab"
target = "/proc/self/mounts"
path = os.path.normpath("%s/%s" % (ROOT_PATH, path))
if os.path.islink(path):
# return early if the mtab symlink is already how we like it
current_target = os.path.normpath(os.path.dirname(path) +
"/" + os.readlink(path))
if current_target == target:
return
if os.path.exists(path):
os.unlink(path)
os.symlink(target, path)
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 getFSType(self, mountpoint=None):
""" Return the default filesystem type based on mountpoint. """
fstype = self.defaultFSType
if not mountpoint:
# just return the default
pass
elif mountpoint.lower() in ("swap", "biosboot", "prepboot"):
fstype = mountpoint.lower()
elif mountpoint == "/boot":
fstype = self.defaultBootFSType
elif mountpoint == "/boot/efi":
if iutil.isMactel():
fstype = "hfs+"
else:
fstype = "efi"
return fstype
def setContainerMembers(self, container, factory, members=None,
device=None):
""" Set up and return the container's member partitions. """
log_members = []
if members:
log_members = [str(m) for m in members]
log_method_call(self, container=container, factory=factory,
members=log_members, device=device)
if factory.member_list is not None:
# short-circuit the logic below for partitions
return factory.member_list
if container and container.exists:
# don't try to modify an existing container
return container.parents
if factory.container_size_func is None:
return []
# set up member devices
container_size = factory.device_size
add_disks = []
remove_disks = []
if members is None:
members = []
if container:
members = container.parents[:]
elif members:
# mdarray
container = device
# The basis for whether we are modifying a member set versus creating
# one must be the member list, as container will be None when modifying
# the member set of an md array.
# XXX how can we detect/handle failure to use one or more of the disks?
if members and device:
# See if we need to add/remove any disks, but only if we are
# adjusting a device. When adding a new device to a container we do
# not want to modify the container's disk set.
_disks = list(set([d for m in members for d in m.disks]))
add_disks = [d for d in factory.disks if d not in _disks]
remove_disks = [d for d in _disks if d not in factory.disks]
elif not members:
# new container, so use the factory's disk set
add_disks = factory.disks
# drop any new disks that don't have free space
min_free = min(500, factory.size)
add_disks = [d for d in add_disks if d.partitioned and
d.format.free >= min_free]
base_size = max(1, getFormat(factory.member_format).minSize)
# XXX TODO: multiple member devices per disk
# prepare already-defined member partitions for reallocation
for member in members[:]:
if any([d in remove_disks for d in member.disks]):
if isinstance(member, LUKSDevice):
if container:
container.removeMember(member)
self.destroyDevice(member)
members.remove(member)
member = member.slave
else:
if container:
container.removeMember(member)
members.remove(member)
self.destroyDevice(member)
continue
if isinstance(member, LUKSDevice):
if not factory.encrypted:
# encryption was toggled for the member devices
if container:
container.removeMember(member)
self.destroyDevice(member)
members.remove(member)
self.formatDevice(member.slave,
getFormat(factory.member_format))
members.append(member.slave)
if container:
container.addMember(member.slave)
member = member.slave
elif factory.encrypted:
# encryption was toggled for the member devices
if container:
container.removeMember(member)
members.remove(member)
self.formatDevice(member, getFormat("luks"))
luks_member = LUKSDevice("luks-%s" % member.name,
parents=[member],
format=getFormat(factory.member_format))
self.createDevice(luks_member)
members.append(luks_member)
if container:
container.addMember(luks_member)
member.req_base_size = base_size
member.req_size = member.req_base_size
member.req_grow = True
# set up new members as needed to accommodate the device
new_members = []
for disk in add_disks:
if factory.encrypted and factory.encrypt_members:
luks_format = factory.member_format
member_format = "luks"
else:
member_format = factory.member_format
try:
member = self.newPartition(parents=[disk], grow=True,
size=base_size,
fmt_type=member_format)
except StorageError as e:
log.error("failed to create new member partition: %s" % e)
continue
self.createDevice(member)
if factory.encrypted and factory.encrypt_members:
fmt = getFormat(luks_format)
member = LUKSDevice("luks-%s" % member.name,
parents=[member], format=fmt)
self.createDevice(member)
members.append(member)
new_members.append(member)
if container:
container.addMember(member)
if container:
log.debug("using container %s with %d devices" % (container.name,
len(self.devicetree.getChildren(container))))
container_size = factory.container_size_func(container, device)
log.debug("raw container size reported as %d" % container_size)
log.debug("adding a %s with size %d" % (factory.set_class.__name__,
container_size))
size_set = factory.set_class(members, container_size)
self.size_sets.append(size_set)
for member in members[:]:
if isinstance(member, LUKSDevice):
member = member.slave
member.req_max_size = size_set.size
try:
self.allocatePartitions()
except PartitioningError as e:
# try to clean up by destroying all newly added members before re-
# raising the exception
self.__cleanUpMemberDevices(new_members, container=container)
raise
return members
def allocatePartitions(self):
""" Allocate all requested partitions. """
try:
doPartitioning(self)
except StorageError as e:
log.error("failed to allocate partitions: %s" % e)
raise
def getDeviceFactory(self, device_type, size, **kwargs):
""" Return a suitable DeviceFactory instance for device_type. """
disks = kwargs.get("disks", [])
raid_level = kwargs.get("raid_level")
encrypted = kwargs.get("encrypted", False)
class_table = {DEVICE_TYPE_LVM: LVMFactory,
DEVICE_TYPE_BTRFS: BTRFSFactory,
DEVICE_TYPE_PARTITION: PartitionFactory,
DEVICE_TYPE_MD: MDFactory,
DEVICE_TYPE_DISK: DiskFactory}
factory_class = class_table[device_type]
log.debug("instantiating %s: %r, %s, %s, %s" % (factory_class,
self, size, [d.name for d in disks], raid_level))
return factory_class(self, size, disks, raid_level, encrypted)
def getContainer(self, factory, device=None, name=None, existing=False):
# XXX would it be useful to implement this as a series of fallbacks
# instead of mutually exclusive branches?
container = None
if name:
container = self.devicetree.getDeviceByName(name)
if container and container not in factory.container_list:
log.debug("specified container name %s is wrong type (%s)"
% (name, container.type))
container = None
elif device:
if hasattr(device, "vg"):
container = device.vg
elif hasattr(device, "volume"):
container = device.volume
else:
containers = [c for c in factory.container_list if not c.exists]
if containers:
container = containers[0]
if container is None and existing:
containers = [c for c in factory.container_list if c.exists]
if containers:
containers.sort(key=lambda c: getattr(c, "freeSpace", c.size),
reverse=True)
container = containers[0]
return container
def __cleanUpMemberDevices(self, members, container=None):
for member in members:
if container:
container.removeMember(member)
if isinstance(member, LUKSDevice):
self.destroyDevice(member)
member = member.slave
if not member.isDisk:
self.destroyDevice(member)
def newDevice(self, device_type, size, **kwargs):
""" Schedule creation of a device based on a top-down specification.
Arguments:
device_type an AUTOPART_TYPE constant (lvm|btrfs|plain)
size device's requested size
Keyword arguments:
mountpoint the device's mountpoint
fstype the device's filesystem type, or swap
label filesystem label
disks the set of disks we can allocate from
encrypted boolean
raid_level (btrfs/md/lvm only) RAID level (string)
name name for new device
container_name name of requested container
device an already-defined but non-existent device
to adjust instead of creating a new device
Error handling:
If device is None, meaning we're creating a device, the error
handling aims to remove all evidence of the attempt to create a
new device by removing unused container devices, reverting the
size of container devices that contain other devices, &c.
If the device is not None, meaning we're adjusting the size of
a defined device, the error handling aims to revert the device
and any container to it previous size.
In either case, we re-raise the exception so the caller knows
there was a failure. If we failed to clean up as described above
we raise ErrorRecoveryFailure to alert the caller that things
will likely be in an inconsistent state.
"""
log_method_call(self, device_type, size, **kwargs)
mountpoint = kwargs.get("mountpoint")
fstype = kwargs.get("fstype")
label = kwargs.get("label")
disks = kwargs.get("disks")
encrypted = kwargs.get("encrypted", self.data.autopart.encrypted)
name = kwargs.get("name")
container_name = kwargs.get("container_name")
device = kwargs.get("device")
# md, btrfs
raid_level = kwargs.get("raid_level")
# we can't do anything with existing devices
if device and device.exists:
log.info("newDevice refusing to change device %s" % device)
return
if not fstype:
fstype = self.getFSType(mountpoint=mountpoint)
if fstype == "swap":
mountpoint = None
if fstype == "swap" and device_type == DEVICE_TYPE_BTRFS:
device_type = DEVICE_TYPE_PARTITION
fmt_args = {}
if label:
fmt_args["label"] = label
factory = self.getDeviceFactory(device_type, size, **kwargs)
if not factory.disks:
raise StorageError("no disks specified for new device")
self.size_sets = [] # clear this since there are no growable reqs now
container = self.getContainer(factory, device=device,
name=container_name)
# TODO: striping, mirroring, &c
# TODO: non-partition members (pv-on-md)
# setContainerMembers can modify these, so save them now
old_size = None
old_disks = []
if device:
old_size = device.size
old_disks = device.disks[:]
members = []
if device and device.type == "mdarray":
members = device.parents[:]
try:
parents = self.setContainerMembers(container, factory,
members=members, device=device)
except PartitioningError as e:
# If this is a new device, just clean up and get out.
if device:
# If this is a defined device, try to clean up by reallocating
# members as before and then get out.
factory.disks = device.disks
factory.size = device.size # this should work
if members:
# If this is an md array we have to reset its member set
# here.
# If there is a container device, its member set was reset
# in the exception handler in setContainerMembers.
device.parents = members
try:
self.setContainerMembers(container, factory,
members=members,
device=device)
except StorageError as e:
log.error("failed to revert device size: %s" % e)
raise ErrorRecoveryFailure("failed to revert container")
raise
# set up container
if not container and factory.new_container_attr:
if not parents:
raise StorageError("not enough free space on disks")
log.debug("creating new container")
if container_name:
kwa = {"name": container_name}
else:
kwa = {}
try:
container = factory.new_container(parents=parents, **kwa)
except StorageError as e:
log.error("failed to create new device: %s" % e)
# Clean up by destroying the newly created member devices.
self.__cleanUpMemberDevices(parents)
raise
self.createDevice(container)
elif container and not container.exists and \
hasattr(container, "dataLevel"):
container.dataLevel = factory.raid_level
if container:
parents = [container]
log.debug("%r" % container)
# this will set the device's size if a device is passed in
size = factory.set_device_size(container, device=device)
if device:
# We are adjusting a defined device: size, disk set, encryption,
# raid level, fstype. The StorageDevice instance exists, but the
# underlying device does not.
# TODO: handle toggling of encryption for leaf device
e = None
try:
factory.post_create()
except StorageError as e:
log.error("device post-create method failed: %s" % e)
else:
if device.size <= device.format.minSize:
e = StorageError("failed to adjust device -- not enough free space in specified disks?")
if e:
# Clean up by reverting the device to its previous size.
factory.size = old_size
factory.disks = old_disks
try:
self.setContainerMembers(container, factory,
members=members, device=device)
except StorageError as e:
# yes, we're replacing e here.
log.error("failed to revert device size: %s" % e)
raise ErrorRecoveryFailure("failed to revert device size")
factory.set_device_size(container, device=device)
try:
factory.post_create()
except StorageError as e:
# yes, we're replacing e here.
log.error("failed to revert device size: %s" % e)
raise ErrorRecoveryFailure("failed to revert device size")
raise(e)
elif factory.new_device_attr:
log.debug("creating new device")
if factory.encrypted and factory.encrypt_leaves:
luks_fmt_type = fstype
luks_fmt_args = fmt_args
luks_mountpoint = mountpoint
fstype = "luks"
mountpoint = None
fmt_args = {}
def _container_post_error():
# Clean up. If there is a container and it has other devices,
# try to revert it. If there is a container and it has no other
# devices, remove it. If there is not a container, remove all of
# the parents.
if container:
if container.kids:
factory.size = 0
factory.disks = container.disks
try:
self.setContainerMembers(container, factory)
except StorageError as e:
log.error("failed to revert container: %s" % e)
raise ErrorRecoveryFailure("failed to revert container")
else:
self.destroyDevice(container)
self.__cleanUpMemberDevices(container.parents)
else:
self.__cleanUpMemberDevices(parents)
if name:
kwa = {"name": name}
else:
kwa = {}
try:
device = factory.new_device(parents=parents,
size=size,
fmt_type=fstype,
mountpoint=mountpoint,
fmt_args=fmt_args,
**kwa)
except (StorageError, ValueError) as e:
log.error("device instance creation failed: %s" % e)
_container_post_error()
raise
self.createDevice(device)
e = None
try:
factory.post_create()
except StorageError as e:
log.error("device post-create method failed: %s" % e)
else:
if not device.size:
e = StorageError("failed to create device")
if e:
self.destroyDevice(device)
_container_post_error()
raise StorageError(e)
if factory.encrypted and factory.encrypt_leaves:
fmt = getFormat(luks_fmt_type,
mountpoint=luks_mountpoint,
**luks_fmt_args)
luks_device = LUKSDevice("luks-" + device.name,
parents=[device], format=fmt)
self.createDevice(luks_device)
def copy(self):
new = copy.deepcopy(self)
# go through and re-get partedPartitions from the disks since they
# don't get deep-copied
for partition in new.partitions:
if not partition._partedPartition:
continue
# don't ask me why, but we have to update the refs in req_disks
req_disks = []
for disk in partition.req_disks:
req_disks.append(new.devicetree.getDeviceByID(disk.id))
partition.req_disks = req_disks
p = partition.disk.format.partedDisk.getPartitionByPath(partition.path)
partition.partedPartition = p
return new
def mountExistingSystem(fsset, rootDevice,
allowDirty=None, dirtyCB=None,
readOnly=None):
""" Mount filesystems specified in rootDevice's /etc/fstab file. """
rootPath = ROOT_PATH
if dirtyCB is None:
dirtyCB = lambda l: False
if readOnly:
readOnly = "ro"
else:
readOnly = ""
if rootDevice.protected and os.path.ismount("/mnt/install/isodir"):
isys.mount("/mnt/install/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.mountpoints.values():
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)
if dirtyDevs and (not allowDirty or dirtyCB(dirtyDevs)):
raise DirtyFSError("\n".join(dirtyDevs))
fsset.mountFilesystems(rootPath=ROOT_PATH, 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):
self.devicetree = devicetree
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._usb = None
self._selinux = 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 usb(self):
if not self._usb:
self._usb = NoDevice(format=getFormat("usbfs",
device="usbfs",
mountpoint="/proc/bus/usb"))
return self._usb
@property
def selinux(self):
if not self._selinux:
self._selinux = NoDevice(format=getFormat("selinuxfs",
device="selinuxfs",
mountpoint="/sys/fs/selinux"))
return self._selinux
@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)):
# no sense in doing any legwork for a noauto entry
if "noauto" in options.split(","):
log.info("ignoring noauto entry")
raise UnrecognizedFSTabEntryError()
# 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,
exists=True,
format=getFormat(fstype,
exists=True,
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",
"/sys/fs/selinux", "/proc/bus/usb"):
# 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)
if device is None:
log.error("failed to resolve %s (%s) from fstab" % (devspec,
fstype))
raise UnrecognizedFSTabEntryError()
device.setup()
fmt = getFormat(fstype, device=device.path, exists=True)
if fstype != "auto" and None in (device.format.type, fmt.type):
log.info("Unrecognized filesystem type for %s (%s)"
% (device.name, fstype))
device.teardown()
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
ftype = getattr(fmt, "mountType", fmt.type)
dtype = getattr(device.format, "mountType", device.format.type)
if fstype != "auto" and ftype != dtype:
log.info("fstab says %s at %s is %s" % (dtype, mountpoint, ftype))
if fmt.testMount():
# XXX we should probably disallow migration for this fs
device.format = fmt
else:
device.teardown()
raise FSTabTypeMismatchError("%s: detected as %s, fstab says %s"
% (mountpoint, dtype, ftype))
del ftype
del dtype
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 = ROOT_PATH
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
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 turnOnSwap(self, rootPath="", upgrading=None):
""" Activate the system's swap space. """
for device in self.swapDevices:
if isinstance(device, FileDevice):
# set up FileDevices' parents now that they are accessible
targetDir = "%s/%s" % (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 StorageError as e:
if errorHandler.cb(e, device) == ERROR_RAISE:
raise
else:
break
def mountFilesystems(self, rootPath="", readOnly=None,
skipRoot=False, raiseErrors=None):
""" Mount the system's filesystems. """
devices = self.mountpoints.values() + self.swapDevices
devices.extend([self.dev, self.devshm, self.devpts, self.sysfs,
self.proc, self.selinux, self.usb])
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" % (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 e:
if errorHandler.cb(e, device) == ERROR_RAISE:
raise
else:
continue
if readOnly:
options = "%s,%s" % (options, readOnly)
try:
device.format.setup(options=options,
chroot=rootPath)
except Exception as e:
log.error("error mounting %s on %s: %s"
% (device.path, device.format.mountpoint, e))
if errorHandler.cb(e, device) == ERROR_RAISE:
raise
self.active = True
def umountFilesystems(self, ignoreErrors=True, swapoff=True):
""" unmount filesystems, except swap if swapoff == False """
devices = self.mountpoints.values() + self.swapDevices
devices.extend([self.dev, self.devshm, self.devpts, self.sysfs,
self.proc, self.usb, self.selinux])
devices.sort(key=lambda d: getattr(d.format, "mountpoint", None))
devices.reverse()
for device in devices:
if (not device.format.mountable) or \
(device.format.type == "swap" and not swapoff):
continue
device.format.teardown()
device.teardown()
self.active = False
def createSwapFile(self, device, size):
""" Create and activate a swap file under ROOT_PATH. """
filename = "/SWAP"
count = 0
basedir = os.path.normpath("%s/%s" % (ROOT_PATH,
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):
root = self.rootDevice
dev = "%s/%s" % (ROOT_PATH, root.path)
if not os.path.exists("%s/dev/root" %(ROOT_PATH,)) and os.path.exists(dev):
rdev = os.stat(dev).st_rdev
os.mknod("%s/dev/root" % (ROOT_PATH,), 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 ["/", ROOT_PATH]:
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):
""" write out all config files based on the set of filesystems """
# /etc/fstab
fstab_path = os.path.normpath("%s/etc/fstab" % ROOT_PATH)
fstab = self.fstab()
open(fstab_path, "w").write(fstab)
# /etc/crypttab
crypttab_path = os.path.normpath("%s/etc/crypttab" % ROOT_PATH)
crypttab = self.crypttab()
origmask = os.umask(0077)
open(crypttab_path, "w").write(crypttab)
os.umask(origmask)
# /etc/mdadm.conf
mdadm_path = os.path.normpath("%s/etc/mdadm.conf" % ROOT_PATH)
mdadm_conf = self.mdadmConf()
if mdadm_conf:
open(mdadm_path, "w").write(mdadm_conf)
# /etc/multipath.conf
multipath_conf = self.multipathConf()
if multipath_conf:
multipath_path = os.path.normpath("%s/etc/multipath.conf" %
ROOT_PATH)
conf_contents = multipath_conf.write(self.devicetree.mpathFriendlyNames)
f = open(multipath_path, "w")
f.write(conf_contents)
f.close()
else:
log.info("not writing out mpath configuration")
iutil.copy_to_sysimage("/etc/multipath/wwids")
if self.devicetree.mpathFriendlyNames:
iutil.copy_to_sysimage("/etc/multipath/bindings")
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)
if not arrays:
return ""
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
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
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
if device.encrypted:
options += ",x-systemd.device-timeout=0"
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
def getReleaseString():
relName = None
relVer = None
try:
relArch = iutil.execWithCapture("arch", [], root=ROOT_PATH).strip()
except:
relArch = None
filename = "%s/etc/redhat-release" % ROOT_PATH
if os.access(filename, os.R_OK):
with open(filename) as f:
try:
relstr = f.readline().strip()
except (IOError, AttributeError):
relstr = ""
# get the release name and version
# assumes that form is something
# like "Red Hat Linux release 6.2 (Zoot)"
(product, sep, version) = relstr.partition(" release ")
if sep:
relName = product
relVer = version.split()[0]
return (relArch, relName, relVer)
def findExistingInstallations(devicetree):
if not os.path.exists(ROOT_PATH):
iutil.mkdirChain(ROOT_PATH)
roots = []
for device in devicetree.leaves:
if not device.format.linuxNative or not device.format.mountable or \
not device.controllable:
continue
try:
device.setup()
except Exception as e:
log.warning("setup of %s failed: %s" % (device.name, e))
continue
options = device.format.options + ",ro"
try:
device.format.mount(options=options, mountpoint=ROOT_PATH)
except Exception as e:
log.warning("mount of %s as %s failed: %s" % (device.name,
device.format.type,
e))
device.teardown()
continue
if not os.access(ROOT_PATH + "/etc/fstab", os.R_OK):
device.teardown(recursive=True)
continue
try:
(arch, product, version) = getReleaseString()
except ValueError:
name = _("Linux on %s") % device.name
else:
# I'd like to make this finer grained, but it'd be very difficult
# to translate.
if not product or not version or not arch:
name = _("Unknown Linux")
else:
name = _("%(product)s Linux %(version)s for %(arch)s") % \
{"product": product, "version": version, "arch": arch}
(mounts, swaps) = parseFSTab(devicetree, chroot=ROOT_PATH)
device.teardown()
if not mounts and not swaps:
# empty /etc/fstab. weird, but I've seen it happen.
continue
roots.append(Root(mounts=mounts, swaps=swaps, name=name))
return roots
class Root(object):
def __init__(self, mounts=None, swaps=None, name=None):
# mountpoint key, StorageDevice value
if not mounts:
self.mounts = {}
else:
self.mounts = mounts
# StorageDevice
if not swaps:
self.swaps = []
else:
self.swaps = swaps
self.name = name # eg: "Fedora Linux 16 for x86_64", "Linux on sda2"
if not self.name and "/" in self.mounts:
self.name = self.mounts["/"].format.uuid
@property
def device(self):
return self.mounts.get("/")
def parseFSTab(devicetree, chroot=None):
""" parse /etc/fstab and return a tuple of a mount dict and swap list """
if not chroot or not os.path.isdir(chroot):
chroot = ROOT_PATH
mounts = {}
swaps = []
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 (mounts, swaps)
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(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
with open(path) as f:
log.debug("parsing %s" % path)
for line in f.readlines():
# strip off comments
(line, pound, comment) = line.partition("#")
fields = line.split(None, 4)
if len(fields) < 5:
continue
(devspec, mountpoint, fstype, options, rest) = fields
# find device in the tree
device = devicetree.resolveDevice(devspec,
cryptTab=cryptTab,
blkidTab=blkidTab,
options=options)
if device is None:
continue
if fstype != "swap":
mounts[mountpoint] = device
else:
swaps.append(device)
return (mounts, swaps)
class DeviceFactory(object):
type_desc = None
member_format = None # format type for member devices
new_container_attr = None # name of Storage method to create a container
new_device_attr = None # name of Storage method to create a device
container_list_attr = None # name of Storage attribute to list containers
encrypt_members = False
encrypt_leaves = True
def __init__(self, storage, size, disks, raid_level, encrypted):
self.storage = storage # the Storage instance
self.size = size # the requested size for this device
self.disks = disks # the set of disks to allocate from
self.raid_level = raid_level
self.encrypted = encrypted
# this is a list of member devices, used to short-circuit the logic in
# setContainerMembers for case of a partition
self.member_list = None
# choose a size set class for member partition allocation
if raid_level is not None and raid_level.startswith("raid"):
self.set_class = SameSizeSet
else:
self.set_class = TotalSizeSet
@property
def container_list(self):
""" A list of containers of the type used by this device. """
if not self.container_list_attr:
return []
return getattr(self.storage, self.container_list_attr)
def new_container(self, *args, **kwargs):
""" Return the newly created container for this device. """
return getattr(self.storage, self.new_container_attr)(*args, **kwargs)
def new_device(self, *args, **kwargs):
""" Return the newly created device. """
return getattr(self.storage, self.new_device_attr)(*args, **kwargs)
def post_create(self):
""" Perform actions required after device creation. """
pass
def container_size_func(self, container, device=None):
""" Return the total space needed for the specified container. """
size = container.size
size += self.device_size
if device:
size -= device.size
return size
@property
def device_size(self):
""" The total disk space required for this device. """
return self.size
def set_device_size(self, container, device=None):
return self.size
class DiskFactory(DeviceFactory):
type_desc = "disk"
# this is to protect against changes to these settings in the base class
encrypt_members = False
encrypt_leaves = True
class PartitionFactory(DeviceFactory):
type_desc = "partition"
new_device_attr = "newPartition"
default_size = 1
def __init__(self, storage, size, disks, raid_level, encrypted):
super(PartitionFactory, self).__init__(storage, size, disks, raid_level,
encrypted)
self.member_list = self.disks
def new_device(self, *args, **kwargs):
grow = True
max_size = kwargs.pop("size")
kwargs["size"] = 1
device = self.storage.newPartition(*args,
grow=grow, maxsize=max_size,
**kwargs)
return device
def post_create(self):
self.storage.allocatePartitions()
def set_device_size(self, container, device=None):
size = self.size
if device:
if size != device.size:
log.info("adjusting device size from %.2f to %.2f"
% (device.size, size))
base_size = max(PartitionFactory.default_size,
device.format.minSize)
size = max(base_size, size)
device.req_base_size = base_size
device.req_size = base_size
device.req_max_size = size
device.req_grow = size > base_size
# this probably belongs somewhere else but this is our chance to
# update the disk set
device.req_disks = self.disks[:]
return size
class BTRFSFactory(DeviceFactory):
type_desc = "btrfs"
member_format = "btrfs"
new_container_attr = "newBTRFS"
new_device_attr = "newBTRFSSubVolume"
container_list_attr = "btrfsVolumes"
encrypt_members = True
encrypt_leaves = False
def __init__(self, storage, size, disks, raid_level, encrypted):
super(BTRFSFactory, self).__init__(storage, size, disks, raid_level,
encrypted)
self.raid_level = raid_level or "single"
def new_container(self, *args, **kwargs):
""" Return the newly created container for this device. """
kwargs["dataLevel"] = self.raid_level
return getattr(self.storage, self.new_container_attr)(*args, **kwargs)
def container_size_func(self, container, device=None):
""" Return the total space needed for the specified container. """
if container.exists:
container_size = container.size
else:
container_size = sum([s.req_size for s in container.subvolumes])
if device:
size = self.device_size
else:
size = container_size + self.device_size
return size
@property
def device_size(self):
# until we get/need something better
if self.raid_level in ("single", "raid0"):
return self.size
elif self.raid_level in ("raid1", "raid10"):
return self.size * len(self.disks)
def new_device(self, *args, **kwargs):
kwargs["dataLevel"] = self.raid_level
kwargs["metaDataLevel"] = self.raid_level
return super(BTRFSFactory, self).new_device(*args, **kwargs)
class LVMFactory(DeviceFactory):
type_desc = "lvm"
member_format = "lvmpv"
new_container_attr = "newVG"
new_device_attr = "newLV"
container_list_attr = "vgs"
encrypt_members = True
encrypt_leaves = False
@property
def device_size(self):
size_func_kwargs = {}
if self.raid_level in ("raid1", "raid10"):
size_func_kwargs["mirrored"] = True
if self.raid_level in ("raid0", "raid10"):
size_func_kwargs["striped"] = True
return get_pv_space(self.size, len(self.disks), **size_func_kwargs)
def container_size_func(self, container, device=None):
size = sum([p.size for p in container.parents])
size -= container.freeSpace
size += self.device_size
if device:
size -= get_pv_space(device.size, len(container.parents))
return size
def set_device_size(self, container, device=None):
size = self.size
free = container.freeSpace
if device:
free += device.size
if free < size:
log.info("adjusting device size from %.2f to %.2f so it fits "
"in container" % (size, free))
size = free
if device:
if size != device.size:
log.info("adjusting device size from %.2f to %.2f"
% (device.size, size))
device.size = size
return size
class MDFactory(DeviceFactory):
type_desc = "md"
member_format = "mdmember"
new_container_attr = None
new_device_attr = "newMDArray"
@property
def container_list(self):
return []
@property
def device_size(self):
return get_member_space(self.size, len(self.disks),
level=self.raid_level)
def container_size_func(self, container, device=None):
return get_member_space(self.size, len(container.parents),
level=self.raid_level)
def new_device(self, *args, **kwargs):
kwargs["level"] = self.raid_level
kwargs["totalDevices"] = len(kwargs.get("parents"))
kwargs["memberDevices"] = len(kwargs.get("parents"))
return super(MDFactory, self).new_device(*args, **kwargs)