470 lines
17 KiB
Python
470 lines
17 KiB
Python
#
|
|
# livecd.py: An anaconda backend to do an install from a live CD image
|
|
#
|
|
# The basic idea is that with a live CD, we already have an install
|
|
# and should be able to just copy those bits over to the disk. So we dd
|
|
# the image, move things to the "right" filesystem as needed, and then
|
|
# resize the rootfs to the size of its container.
|
|
#
|
|
# Copyright (C) 2007 Red Hat, Inc. All rights reserved.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# Author(s): Jeremy Katz <katzj@redhat.com>
|
|
#
|
|
|
|
import os, sys
|
|
import stat
|
|
import shutil
|
|
import time
|
|
import subprocess
|
|
import storage
|
|
|
|
import selinux
|
|
|
|
from flags import flags
|
|
from constants import *
|
|
|
|
import gettext
|
|
_ = lambda x: gettext.ldgettext("anaconda", x)
|
|
|
|
import backend
|
|
import isys
|
|
import iutil
|
|
|
|
import packages
|
|
|
|
import logging
|
|
log = logging.getLogger("anaconda")
|
|
|
|
class Error(EnvironmentError):
|
|
pass
|
|
def copytree(src, dst, symlinks=False, preserveOwner=False,
|
|
preserveSelinux=False):
|
|
def tryChown(src, dest):
|
|
try:
|
|
os.chown(dest, os.stat(src)[stat.ST_UID], os.stat(src)[stat.ST_GID])
|
|
except OverflowError:
|
|
log.error("Could not set owner and group on file %s" % dest)
|
|
|
|
def trySetfilecon(src, dest):
|
|
try:
|
|
selinux.lsetfilecon(dest, selinux.lgetfilecon(src)[1])
|
|
except:
|
|
log.error("Could not set selinux context on file %s" % dest)
|
|
|
|
# copy of shutil.copytree which doesn't require dst to not exist
|
|
# and which also has options to preserve the owner and selinux contexts
|
|
names = os.listdir(src)
|
|
if not os.path.isdir(dst):
|
|
os.makedirs(dst)
|
|
errors = []
|
|
for name in names:
|
|
srcname = os.path.join(src, name)
|
|
dstname = os.path.join(dst, name)
|
|
try:
|
|
if symlinks and os.path.islink(srcname):
|
|
linkto = os.readlink(srcname)
|
|
os.symlink(linkto, dstname)
|
|
if preserveSelinux:
|
|
trySetfilecon(srcname, dstname)
|
|
elif os.path.isdir(srcname):
|
|
copytree(srcname, dstname, symlinks, preserveOwner, preserveSelinux)
|
|
else:
|
|
shutil.copyfile(srcname, dstname)
|
|
if preserveOwner:
|
|
tryChown(srcname, dstname)
|
|
|
|
if preserveSelinux:
|
|
trySetfilecon(srcname, dstname)
|
|
|
|
shutil.copystat(srcname, dstname)
|
|
except (IOError, os.error), why:
|
|
errors.append((srcname, dstname, str(why)))
|
|
# catch the Error from the recursive copytree so that we can
|
|
# continue with other files
|
|
except Error, err:
|
|
errors.extend(err.args[0])
|
|
try:
|
|
if preserveOwner:
|
|
tryChown(src, dst)
|
|
if preserveSelinux:
|
|
trySetfilecon(src, dst)
|
|
|
|
shutil.copystat(src, dst)
|
|
except OSError as e:
|
|
errors.extend((src, dst, e.strerror))
|
|
if errors:
|
|
raise Error, errors
|
|
|
|
class LiveCDCopyBackend(backend.AnacondaBackend):
|
|
def __init__(self, anaconda):
|
|
backend.AnacondaBackend.__init__(self, anaconda)
|
|
flags.livecdInstall = True
|
|
self.supportsUpgrades = False
|
|
self.supportsPackageSelection = False
|
|
self.skipFormatRoot = True
|
|
|
|
self.osimg = anaconda.methodstr[8:]
|
|
if not stat.S_ISBLK(os.stat(self.osimg)[stat.ST_MODE]):
|
|
anaconda.intf.messageWindow(_("Unable to find image"),
|
|
_("The given location isn't a valid %s "
|
|
"live CD to use as an installation source.")
|
|
%(productName,), type = "custom",
|
|
custom_icon="error",
|
|
custom_buttons=[_("Exit installer")])
|
|
sys.exit(0)
|
|
self.rootFsType = isys.readFSType(self.osimg)
|
|
|
|
def _getLiveBlockDevice(self):
|
|
return os.path.normpath(self.osimg)
|
|
|
|
def _getLiveSize(self):
|
|
def parseField(output, field):
|
|
for line in output.split("\n"):
|
|
if line.startswith(field + ":"):
|
|
return line[len(field) + 1:].strip()
|
|
raise KeyError("Failed to find field '%s' in output" % field)
|
|
|
|
output = subprocess.Popen(['/sbin/dumpe2fs', '-h', self.osimg],
|
|
stdout=subprocess.PIPE,
|
|
stderr=open('/dev/null', 'w')
|
|
).communicate()[0]
|
|
blkcnt = int(parseField(output, "Block count"))
|
|
blksize = int(parseField(output, "Block size"))
|
|
return blkcnt * blksize
|
|
|
|
def _getLiveSizeMB(self):
|
|
return self._getLiveSize() / 1048576
|
|
|
|
def _unmountNonFstabDirs(self, anaconda):
|
|
# unmount things that aren't listed in /etc/fstab. *sigh*
|
|
dirs = []
|
|
if flags.selinux:
|
|
dirs.append("/selinux")
|
|
for dir in dirs:
|
|
try:
|
|
isys.umount("%s/%s" %(anaconda.rootPath,dir), removeDir = False)
|
|
except Exception, e:
|
|
log.error("unable to unmount %s: %s" %(dir, e))
|
|
|
|
def postAction(self, anaconda):
|
|
self._unmountNonFstabDirs(anaconda)
|
|
try:
|
|
anaconda.storage.umountFilesystems(swapoff = False)
|
|
os.rmdir(anaconda.rootPath)
|
|
except Exception, e:
|
|
log.error("Unable to unmount filesystems: %s" % e)
|
|
|
|
def doPreInstall(self, anaconda):
|
|
if anaconda.dir == DISPATCH_BACK:
|
|
self._unmountNonFstabDirs(anaconda)
|
|
return
|
|
anaconda.storage.umountFilesystems(swapoff = False)
|
|
|
|
def doInstall(self, anaconda):
|
|
log.info("Preparing to install packages")
|
|
|
|
progress = anaconda.intf.instProgress
|
|
progress.set_label(_("Copying live image to hard drive."))
|
|
progress.processEvents()
|
|
|
|
osimg = self._getLiveBlockDevice() # the real image
|
|
osfd = os.open(osimg, os.O_RDONLY)
|
|
|
|
rootDevice = anaconda.storage.rootDevice
|
|
rootDevice.setup()
|
|
rootfd = os.open(rootDevice.path, os.O_WRONLY)
|
|
|
|
readamt = 1024 * 1024 * 8 # 8 megs at a time
|
|
size = self._getLiveSize()
|
|
copied = 0
|
|
while copied < size:
|
|
try:
|
|
buf = os.read(osfd, readamt)
|
|
written = os.write(rootfd, buf)
|
|
except:
|
|
rc = anaconda.intf.messageWindow(_("Error"),
|
|
_("There was an error installing the live image to "
|
|
"your hard drive. This could be due to bad media. "
|
|
"Please verify your installation media.\n\nIf you "
|
|
"exit, your system will be left in an inconsistent "
|
|
"state that will require reinstallation."),
|
|
type="custom", custom_icon="error",
|
|
custom_buttons=[_("_Exit installer"), _("_Retry")])
|
|
|
|
if rc == 0:
|
|
sys.exit(0)
|
|
else:
|
|
os.lseek(osfd, 0, 0)
|
|
os.lseek(rootfd, 0, 0)
|
|
copied = 0
|
|
continue
|
|
|
|
if (written < readamt) and (written < len(buf)):
|
|
raise RuntimeError, "error copying filesystem!"
|
|
copied += written
|
|
progress.set_fraction(pct = copied / float(size))
|
|
progress.processEvents()
|
|
|
|
os.close(osfd)
|
|
os.close(rootfd)
|
|
|
|
anaconda.intf.setInstallProgressClass(None)
|
|
|
|
def _doFilesystemMangling(self, anaconda):
|
|
log.info("doing post-install fs mangling")
|
|
wait = anaconda.intf.waitWindow(_("Post-Installation"),
|
|
_("Performing post-installation filesystem changes. This may take several minutes."))
|
|
|
|
# resize rootfs first, since it is 100% full due to genMinInstDelta
|
|
self._resizeRootfs(anaconda, wait)
|
|
|
|
# remount filesystems
|
|
anaconda.storage.mountFilesystems()
|
|
|
|
# restore the label of / to what we think it is
|
|
rootDevice = anaconda.storage.rootDevice
|
|
rootDevice.setup()
|
|
# ensure we have a random UUID on the rootfs
|
|
# FIXME: this should be abstracted per filesystem type
|
|
iutil.execWithRedirect("tune2fs",
|
|
["-U",
|
|
"random",
|
|
rootDevice.path],
|
|
stdout="/dev/tty5",
|
|
stderr="/dev/tty5")
|
|
# and now set the uuid in the storage layer
|
|
rootDevice.updateSysfsPath()
|
|
iutil.notify_kernel("/sys%s" %rootDevice.sysfsPath)
|
|
storage.udev.udev_settle()
|
|
rootDevice.updateSysfsPath()
|
|
info = storage.udev.udev_get_block_device(rootDevice.sysfsPath)
|
|
rootDevice.format.uuid = storage.udev.udev_device_get_uuid(info)
|
|
log.info("reset the rootdev (%s) to have a uuid of %s" %(rootDevice.sysfsPath, rootDevice.format.uuid))
|
|
|
|
# for any filesystem that's _not_ on the root, we need to handle
|
|
# moving the bits from the livecd -> the real filesystems.
|
|
# this is pretty distasteful, but should work with things like
|
|
# having a separate /usr/local
|
|
|
|
def _setupFilesystems(mounts, chroot="", teardown=False):
|
|
""" Setup or teardown all filesystems except for "/" """
|
|
mountpoints = sorted(mounts.keys(),
|
|
reverse=teardown is True)
|
|
if teardown:
|
|
method = "teardown"
|
|
kwargs = {}
|
|
else:
|
|
method = "setup"
|
|
kwargs = {"chroot": chroot}
|
|
|
|
mountpoints.remove("/")
|
|
for mountpoint in mountpoints:
|
|
device = mounts[mountpoint]
|
|
getattr(device.format, method)(**kwargs)
|
|
|
|
# Start by sorting the mountpoints in decreasing-depth order.
|
|
mountpoints = sorted(anaconda.storage.mountpoints.keys(),
|
|
reverse=True)
|
|
# We don't want to copy the root filesystem.
|
|
mountpoints.remove("/")
|
|
stats = {} # mountpoint: posix.stat_result
|
|
|
|
# unmount the filesystems, except for /
|
|
_setupFilesystems(anaconda.storage.mountpoints, teardown=True)
|
|
|
|
# mount all of the filesystems under /mnt so we can copy in content
|
|
_setupFilesystems(anaconda.storage.mountpoints,
|
|
chroot=anaconda.rootPath + "/mnt")
|
|
|
|
# And now let's do the real copies
|
|
for tocopy in mountpoints:
|
|
device = anaconda.storage.mountpoints[tocopy]
|
|
|
|
# FIXME: all calls to wait.refresh() are kind of a hack... we
|
|
# should do better about not doing blocking things in the
|
|
# main thread. but threading anaconda is a job for another
|
|
# time.
|
|
wait.refresh()
|
|
|
|
if not os.path.exists("%s/%s" % (anaconda.rootPath, tocopy)):
|
|
# the directory does not exist in the live image, so there's
|
|
# nothing to move
|
|
continue
|
|
|
|
copytree("%s/%s" % (anaconda.rootPath, tocopy),
|
|
"%s/mnt/%s" % (anaconda.rootPath, tocopy),
|
|
True, True, flags.selinux)
|
|
wait.refresh()
|
|
shutil.rmtree("%s/%s" % (anaconda.rootPath, tocopy))
|
|
wait.refresh()
|
|
|
|
# now unmount each fs, collect stat info for the mountpoint, then
|
|
# remove the entire tree containing the mountpoint
|
|
for tocopy in mountpoints:
|
|
device = anaconda.storage.mountpoints[tocopy]
|
|
device.format.teardown()
|
|
if not os.path.exists("%s/%s" % (anaconda.rootPath, tocopy)):
|
|
continue
|
|
|
|
try:
|
|
stats[tocopy]= os.stat("%s/mnt/%s" % (anaconda.rootPath,
|
|
tocopy))
|
|
except Exception as e:
|
|
log.info("failed to get stat info for mountpoint %s: %s"
|
|
% (tocopy, e))
|
|
|
|
shutil.rmtree("%s/mnt/%s" % (anaconda.rootPath,
|
|
tocopy.split("/")[1]))
|
|
wait.refresh()
|
|
|
|
# now mount all of the filesystems so that post-install writes end
|
|
# up where they're supposed to end up
|
|
_setupFilesystems(anaconda.storage.mountpoints,
|
|
chroot=anaconda.rootPath)
|
|
|
|
# restore stat info for each mountpoint
|
|
for mountpoint in reversed(mountpoints):
|
|
if mountpoint not in stats:
|
|
# there's no info to restore since the mountpoint did not
|
|
# exist in the live image
|
|
continue
|
|
|
|
dest = "%s/%s" % (anaconda.rootPath, mountpoint)
|
|
st = stats[mountpoint]
|
|
|
|
# restore the correct stat info for this mountpoint
|
|
os.utime(dest, (st.st_atime, st.st_mtime))
|
|
os.chown(dest, st.st_uid, st.st_gid)
|
|
os.chmod(dest, stat.S_IMODE(st.st_mode))
|
|
|
|
# ensure that non-fstab filesystems are mounted in the chroot
|
|
if flags.selinux:
|
|
try:
|
|
isys.mount("/selinux", anaconda.rootPath + "/selinux", "selinuxfs")
|
|
except Exception, e:
|
|
log.error("error mounting selinuxfs: %s" %(e,))
|
|
|
|
wait.pop()
|
|
|
|
def _resizeRootfs(self, anaconda, win = None):
|
|
log.info("going to do resize")
|
|
rootDevice = anaconda.storage.rootDevice
|
|
|
|
# FIXME: we'd like to have progress here to give an idea of
|
|
# how long it will take. or at least, to give an indefinite
|
|
# progress window. but, not for this time
|
|
cmd = ["resize2fs", rootDevice.path, "-p"]
|
|
out = open("/dev/tty5", "w")
|
|
proc = subprocess.Popen(cmd, stdout=out, stderr=out)
|
|
rc = proc.poll()
|
|
while rc is None:
|
|
win and win.refresh()
|
|
time.sleep(0.5)
|
|
rc = proc.poll()
|
|
|
|
if rc:
|
|
log.error("error running resize2fs; leaving filesystem as is")
|
|
return
|
|
|
|
# we should also do a fsck afterwards
|
|
cmd = ["e2fsck", "-f", "-y", rootDevice.path]
|
|
out = open("/dev/tty5", "w")
|
|
proc = subprocess.Popen(cmd, stdout=out, stderr=out)
|
|
rc = proc.poll()
|
|
while rc is None:
|
|
win and win.refresh()
|
|
time.sleep(0.5)
|
|
rc = proc.poll()
|
|
|
|
def doPostInstall(self, anaconda):
|
|
import rpm
|
|
|
|
self._doFilesystemMangling(anaconda)
|
|
|
|
# setup /etc/rpm/ for the post-install environment
|
|
iutil.writeRpmPlatform(anaconda.rootPath)
|
|
|
|
storage.writeEscrowPackets(anaconda)
|
|
|
|
packages.rpmSetupGraphicalSystem(anaconda)
|
|
|
|
# now write out the "real" fstab and mtab
|
|
anaconda.storage.write(anaconda.rootPath)
|
|
f = open(anaconda.rootPath + "/etc/mtab", "w+")
|
|
f.write(anaconda.storage.mtab)
|
|
f.close()
|
|
|
|
# copy over the modprobe.conf
|
|
if os.path.exists("/etc/modprobe.conf"):
|
|
shutil.copyfile("/etc/modprobe.conf",
|
|
anaconda.rootPath + "/etc/modprobe.conf")
|
|
# set the same keyboard the user selected in the keyboard dialog:
|
|
anaconda.keyboard.write(anaconda.rootPath)
|
|
|
|
# rebuild the initrd(s)
|
|
vers = self.kernelVersionList(anaconda.rootPath)
|
|
for (n, arch, tag) in vers:
|
|
packages.recreateInitrd(n, anaconda.rootPath)
|
|
|
|
def writeConfiguration(self):
|
|
pass
|
|
|
|
def kernelVersionList(self, rootPath = "/"):
|
|
return packages.rpmKernelVersionList(rootPath)
|
|
|
|
def getMinimumSizeMB(self, part):
|
|
if part == "/":
|
|
return self._getLiveSizeMB()
|
|
return 0
|
|
|
|
def doBackendSetup(self, anaconda):
|
|
# ensure there's enough space on the rootfs
|
|
# FIXME: really, this should be in the general sanity checking, but
|
|
# trying to weave that in is a little tricky at present.
|
|
ossize = self._getLiveSizeMB()
|
|
slash = anaconda.storage.rootDevice
|
|
if slash.size < ossize:
|
|
rc = anaconda.intf.messageWindow(_("Error"),
|
|
_("The root filesystem you created is "
|
|
"not large enough for this live "
|
|
"image (%.2f MB required).") % ossize,
|
|
type = "custom",
|
|
custom_icon = "error",
|
|
custom_buttons=[_("_Back"),
|
|
_("_Exit installer")])
|
|
if rc == 0:
|
|
return DISPATCH_BACK
|
|
else:
|
|
sys.exit(1)
|
|
|
|
# package/group selection doesn't apply for this backend
|
|
def groupExists(self, group):
|
|
pass
|
|
def selectGroup(self, group, *args):
|
|
pass
|
|
def deselectGroup(self, group, *args):
|
|
pass
|
|
def selectPackage(self, pkg, *args):
|
|
pass
|
|
def deselectPackage(self, pkg, *args):
|
|
pass
|
|
def packageExists(self, pkg):
|
|
return True
|
|
def getDefaultGroups(self, anaconda):
|
|
return []
|
|
def writePackagesKS(self, f, anaconda):
|
|
pass
|