3529 lines
130 KiB
Python
3529 lines
130 KiB
Python
|
# __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)
|