# # 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 . # # Author(s): Jeremy Katz # 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 OSError: 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, OSError) as why: errors.append((srcname, dstname, str(why))) # catch the Error from the recursive copytree so that we can # continue with other files except Error as 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 osimg_path = anaconda.methodstr[9:] if not stat.S_ISBLK(os.stat(osimg_path)[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) def postAction(self, anaconda): try: anaconda.storage.umountFilesystems(swapoff = False) os.rmdir(ROOT_PATH) except Exception as e: log.error("Unable to unmount filesystems: %s" % e) def doPreInstall(self, anaconda): 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() osfd = os.open(self.anaconda.storage.liveImage.path, 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.anaconda.storage.liveImage.format.currentSize * 1024 * 1024 copied = 0 done = False while not done: try: buf = os.read(osfd, readamt) written = os.write(rootfd, buf) except (IOError, OSError): 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): # Either something went wrong with the write if (written < len(buf)): raise RuntimeError, "error copying filesystem!" else: # Or we're done done = True 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 rootDevice = anaconda.storage.rootDevice rootDevice.setup() # This is a workaround to make sure the m_time it set to current time so that # the e2fsck before resizing doesn't get confused by times in the future rootDevice.format.testMount() rootDevice.format.targetSize = rootDevice.size rootDevice.format.doResize(intf=anaconda.intf) # ensure we have a random UUID on the rootfs rootDevice.format.writeRandomUUID() # remount filesystems anaconda.storage.mountFilesystems() # 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. # Only include ones that exist on the original livecd filesystem mountpoints = filter(os.path.exists, 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="/mnt") # And now let's do the real copies for tocopy in mountpoints: device = anaconda.storage.mountpoints[tocopy] source = "%s/%s" % (ROOT_PATH, tocopy) dest = "/mnt/%s" % (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() try: log.info("Gathering stats on %s" % (source,)) stats[tocopy]= os.stat(source) except Exception as e: log.info("failed to get stat info for mountpoint %s: %s" % (source, e)) log.info("Copying %s to %s" % (source, dest)) copytree(source, dest, True, True, flags.selinux) wait.refresh() log.info("Removing %s" % (source,)) shutil.rmtree(source) wait.refresh() # unmount the target filesystems and remount in their final locations # so that post-install writes end up where they're supposed to end up _setupFilesystems(anaconda.storage.mountpoints, teardown=True) _setupFilesystems(anaconda.storage.mountpoints, chroot=ROOT_PATH) # restore stat info for each mountpoint for mountpoint in reversed(mountpoints): dest = "%s/%s" % (ROOT_PATH, mountpoint) log.info("Restoring stats on %s" % (dest,)) 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)) wait.pop() def doPostInstall(self, anaconda): import rpm self._doFilesystemMangling(anaconda) storage.writeEscrowPackets(anaconda) packages.rpmSetupGraphicalSystem(anaconda) # now write out the "real" fstab and mtab anaconda.storage.write() # copy over the modprobe.conf if os.path.exists("/etc/modprobe.conf"): shutil.copyfile("/etc/modprobe.conf", ROOT_PATH + "/etc/modprobe.conf") # rebuild the initrd(s) vers = self.kernelVersionList() for (n, arch, tag) in vers: packages.recreateInitrd(n, ROOT_PATH) def kernelVersionList(self): return packages.rpmKernelVersionList() def getMinimumSizeMB(self, part): if part == "/": return self.anaconda.storage.liveImage.format.size return 0