[Ovirt-devel] [PATCH] edit-livecd in python

David Huff dhuff at redhat.com
Tue Sep 22 19:48:34 UTC 2009


This is the second generation of the edit-livecd tool that replaces our
original bash script with a python version and aims to reuse as much as
the livecd-creator libraries as possible.  The main reason for the new
creator class is to avoid the need for a kisckstart file in order to
edit an existing livecd image.

edit-livecd is useful for people who don't have the development and yum
infrastructure/repos set up to build livecd's from scratch. This is
especially useful for custom livecd images.

Changes to packaging and binaries should always be done by
modifying kickstart and rebuilding the image via livecd-creator.  But if
all you want to do is add/edit a config file, public key for SSH, or
change the root password this can be done with the edit-livecd script.
---
 edit-livecd |  528 ++++++++++++++++++++++++++++++++++++-----------------------
 1 files changed, 322 insertions(+), 206 deletions(-)

diff --git a/edit-livecd b/edit-livecd
index d69ca9d..279b225 100755
--- a/edit-livecd
+++ b/edit-livecd
@@ -1,8 +1,10 @@
-#!/bin/bash
+#!/usr/bin/python -tt
 #
-# Edit a livecd to insert files
-# Copyright 2008 Red Hat, Inc.
-# Written by Perry Myers <pmyers at redhat.com>
+# edit livecd: Edit a livecd to insert files
+#
+# Copyright 2009, Red Hat  Inc.
+# Written by Perry Myers <pmyers at redhat.com> & David Huff <dhuff at redhat.com>
+# 
 #
 # 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
@@ -16,205 +18,319 @@
 # 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-#!/bin/bash
-
-PATH=$PATH:/sbin:/usr/sbin
-
-ME=$(basename "$0")
-warn() { printf '%s: %s\n' "$ME" "$*" >&2; }
-try_h() { printf "Try \`$ME -h' for more information.\n" >&2; }
-die() { warn "$@"; try_h; exit 1; }
-
-NODEIMG_DEFAULT=/usr/share/ovirt-node-image/ovirt-node-image.iso
-CD=$NODEIMG_DEFAULT
-
-usage() {
-    case $# in 1) warn "$1"; try_h; exit 1;; esac
-    cat <<EOF
-Usage: $ME -i LiveCD.iso [-b bootparams] [-p program]
-  -b BOOTPARAMS  optional parameters appended to the kernel command line
-  -i LIVECD.iso  LiveCD ISO to edit (default: $NODEIMG_DEFAULT)
-  -o OUTPUT.iso  specify the output file (required)
-  -p CODE        Arbitrary CODE that is eval'd while 'cd'd into the root of
-                   the livecd root filesystem.  Note; the code is not run in
-                   a chroot environment, so it can access the host filesystem.
-                   If this option is omitted, this program pauses and allows
-                   the user (in another terminal) to modify the filesystem
-                   manually.  Type <enter> when done, and the script
-                   re-packages the ISO.
-  -h             display this help and exit
-
-EXAMPLES
-
-  Example Script:
-    #!/bin/sh
-    touch etc/sysconfig/foo
-  Save as foo and make executable:
-    chmod a+x foo
-  Run this to create a file /etc/sysconfig/foo in the livecd filesystem
-  (note the use of "\$PWD/foo", not "./foo", since it will be run from a
-   different directory):
-
-    $ME -i input.iso -o /tmp/result.iso -p "\$PWD/foo"
-
-  or, equivalently, but without a separate script:
-
-    $ME -i input.iso -o /tmp/result.iso -p 'touch etc/sysconfig/foo'
-
-EOF
-}
-
-# exit after any error:
-set -e
-
-CODE=
-OUTPUT_FILE=
-
-err=0 help=0
-while getopts :b:hi:o:p: c; do
-    case $c in
-        i) CD=$OPTARG;;
-        b) PARAMS=$OPTARG;;
-        o) OUTPUT_FILE=$OPTARG;;
-        p) CODE=$OPTARG;;
-        h) help=1;;
-        '?') err=1; warn "invalid option: \`-$OPTARG'";;
-        :) err=1; warn "missing argument to \`-$OPTARG' option";;
-        *) err=1; warn "internal error: \`-$OPTARG' not handled";;
-    esac
-done
-test $err = 1 && { try_h; exit 1; }
-test $help = 1 && { usage; exit 0; }
-
-# Require "-o OUTPUT_FILE"
-test -z "$OUTPUT_FILE" \
-  && { warn "no output file specified; use -o FILE.iso"; try_h; exit 1; }
-
-# Fail if there are any extra command-line arguments.
-if test $OPTIND -le $#; then
-  bad_arg=$(eval "echo \$$OPTIND")
-  warn "extra argument '$bad_arg'"; try_h; exit 1
-fi
-
-# first, check to see we are root
-if [ $( id -u ) -ne 0 ]; then
-    die "Must run as root"
-fi
-
-# Check for some prerequisites.
-# "type" prints "PROG not found" if it's not in $PATH.
-type mkisofs
-type mksquashfs
-type sed
-type implantisomd5
-
-sane_name()
-{
-  case $1 in
-    *[^a-zA-Z0-9._,+:/@%=-]*) false;;
-    *) true;;
-  esac
-}
-
-# Fail if names we'll use contain white space or shell meta-characters
-sane_name "$PWD" || die "invalid working directory name: $PWD"
-sane_name "$CD" || die "invalid ISO name: $CD"
-
-WDIR=`mktemp -d $PWD/livecd.XXXXXXXXXX`
-
-addExit() {
-    EXIT="$@ ; $EXIT"
-    trap "$EXIT" EXIT HUP TERM INT QUIT
-}
-
-mnt() {
-    local margs="$1" ; shift
-    local mp="$WDIR/$1"
-    for D in "$@" ; do
-        mkdir -v -p "$WDIR/$D"
-    done
-    eval mount -v $margs "$mp"
-    addExit "df | grep $mp > /dev/null 2>&1 && umount -v $mp"
-}
-
-addExit "rm -rf $WDIR"
-
-ID_FS_LABEL= # initialize, in case vol_id fails
-eval "$(/lib/udev/vol_id $CD)"
-LABEL=$ID_FS_LABEL
-
-# mount the CD image
-mnt "-t iso9660 $CD -o loop,ro" cd
-
-# mount compressed filesystem
-mnt "-t squashfs $WDIR/cd/LiveOS/squashfs.img -o ro,loop" sq
-
-# create writable copy of the new filesystem for the CD
-cp -pr $WDIR/cd $WDIR/cd-w
-
-# create writable copy of the filesystem for the new compressed
-# squashfs filesystem
-cp -pr $WDIR/sq $WDIR/sq-w
-
-# mount root filesystem
-mnt "-t ext2 $WDIR/sq-w/LiveOS/ext3fs.img -o rw,loop" ex
-
-echo ">>> Updating CD content"
-if [ -n "$CODE" ]; then
-    (
-      cd $WDIR/ex
-      set +e
-      eval "$CODE"
-      set -e
-    )
-else
-    echo "***"
-    echo "*** Pausing to allow manual changes.  Press any key to continue."
-    echo "***"
-    read
-fi
-
-# Try to unmount.  But this is likely to fail, so let the user retry,
-# e.g., if he forgot to "cd" out of $WDIR/ex.
-while :; do
-  echo ">>> Unmounting ext3fs"
-  umount $WDIR/ex && break
-  echo ">>> Unmounting the working file system copy failed"
-  echo "***"
-  echo "*** Did you forget to 'cd' out of $WDIR/ex?"
-  echo "***"
-  echo "*** Press any key to repeat the attempt."
-  echo "***"
-  read
-done
-
-echo ">>> Compressing filesystem"
-mksquashfs $WDIR/sq-w/ $WDIR/cd-w/LiveOS/squashfs.img -noappend
-
-echo ">>> Recomputing MD5 sums"
-( cd $WDIR/cd-w && find . -type f -not -name md5sum.txt \
-    -not -path '*/isolinux/*' -print0 | xargs -0 -- md5sum > md5sum.txt )
-
-if [ -n "$PARAMS" ]; then
-    case $PARAMS in
-      *@*) warn "PARAMS contains the @ sed delimiter, be sure it's escaped";;
-    esac
-    echo ">>> Appending boot parameters"
-    sed -i 's@^  append .*$@& '"$PARAMS@" "$WDIR/cd-w/isolinux/isolinux.cfg"
-fi
-
-echo ">>> Creating ISO image $ISO"
-mkisofs \
-    -V "$LABEL" \
-    -r -cache-inodes -J -l \
-    -b isolinux/isolinux.bin \
-    -c isolinux/boot.cat \
-    -no-emul-boot -boot-load-size 4 -boot-info-table \
-    -o "$OUTPUT_FILE" \
-    $WDIR/cd-w
-
-echo ">>> Implanting ISO MD5 Sum"
-implantisomd5 --force "$OUTPUT_FILE"
-
-# The trap ... callbacks will unmount everything.
-set +e
+
+import os
+import sys
+import tempfile
+import shutil
+import subprocess
+import optparse
+import logging
+
+from imgcreate.debug import *
+from imgcreate.fs import *
+from imgcreate.live import *
+
+class ExistingSparseLoopbackDisk(SparseLoopbackDisk):
+    """don't want to expand the disk"""
+    def __init__(self, lofile, size):
+        SparseLoopbackDisk.__init__(self, lofile, size)
+    
+    def create(self):
+        #self.expand(create = True)
+        LoopbackDisk.create(self)
+
+class LiveImageEditor(LiveImageCreator):
+    """class for editing LiveCD images.
+    
+    We need an instance of LiveImageCreator however we do not have a kickstart 
+    file nor do we need to create a new image. We just want to reuse some of 
+    LiveImageCreators methods on an existing livecd image.   
+
+    """
+    
+    def __init__(self, name):
+        """Initialize a LiveImageEditor instance.
+
+        creates a dummy instance of LiveImageCreator
+        We do not initialize any sub classes b/c we have no ks file. 
+
+        """
+        self.name = name
+        
+        self.tmpdir = "/var/tmp"
+        """The directory in which all temporary files will be created.""" 
+           
+        self.skip_compression = False
+        """Controls whether to use squashfs to compress the image."""
+
+        self.skip_minimize = False
+        """Controls whether an image minimizing snapshot should be created."""  
+        
+        self._isofstype = "iso9660" 
+        self.__isodir = None
+        
+        self._ImageCreator__builddir = None
+        """working directory"""
+                
+        self._ImageCreator_instroot = None
+        """where the extfs.img is mounted for modification"""
+        
+        self._ImageCreator_outdir = None
+        """where final iso gets written"""
+        
+        self._ImageCreator__bindmounts = []
+        
+        self._LoopImageCreator__imagedir = None
+        """dir for the extfs.img"""
+        
+        self._LoopImageCreator__blocksize = 4096
+        self._LoopImageCreator__fslabel = None
+        self._LoopImageCreator__instloop = None
+        self._LoopImageCreator__fstype = None
+        self._LoopImageCreator__image_size = None
+        
+        self._LiveImageCreatorBase__isodir = None
+        """directory where the iso is staged"""  
+        
+    # properties
+    def __get_image(self):
+        if self._LoopImageCreator__imagedir is None:
+            self.__ensure_builddir()
+            self._LoopImageCreator__imagedir = tempfile.mkdtemp(dir = os.path.abspath(self.tmpdir), prefix = self.name + "-")
+        return self._LoopImageCreator__imagedir + "/ext3fs.img"
+    _image = property(__get_image)
+    """The location of the image file"""
+
+
+    def _get_fstype(self):
+        dev_null = os.open("/dev/null", os.O_WRONLY)
+        try:
+            out = subprocess.Popen(["/sbin/blkid", self._image],
+                                   stdout = subprocess.PIPE,
+                                   stderr = dev_null).communicate()[0]
+            for word in out.split():
+                if word.startswith("TYPE"):
+                    self._LoopImageCreator__fstype = word.split("=")[1].strip("\"")
+        
+        except IOError, e:
+            raise CreatorError("Failed to determine fsimage TYPE: %s" % e )
+        
+        
+    def _get_fslable(self):
+        dev_null = os.open("/dev/null", os.O_WRONLY)
+        try:
+            out = subprocess.Popen(["/sbin/e2label", self._image],
+                                   stdout = subprocess.PIPE,
+                                   stderr = dev_null).communicate()[0]
+
+            self._LoopImageCreator__fslable = out.strip()
+        
+        except IOError, e:
+            raise CreatorError("Failed to determine fsimage TYPE: %s" % e )
+    
+    
+    def __ensure_builddir(self):
+        if not self._ImageCreator__builddir is None:
+            return
+
+        try:
+            self._ImageCreator__builddir = tempfile.mkdtemp(dir =  os.path.abspath(self.tmpdir),
+                                               prefix = "edit-livecd-")
+        except OSError, (err, msg):
+            raise CreatorError("Failed create build directory in %s: %s" %
+                               (self.tmpdir, msg))
+        
+        
+    def _run_script(self, script):
+        
+        (fd, path) = tempfile.mkstemp(prefix = "script-",
+                                          dir = self._instroot + "/tmp")
+        
+        logging.debug("copying script to install root: %s" % path)
+        shutil.copy(os.path.abspath(script), path)
+        os.close(fd)
+        os.chmod(path, 0700)
+        
+        script = "/tmp/" + os.path.basename(path)
+        
+             
+        try:
+            subprocess.call([script], preexec_fn = self._chroot)
+        except OSError, e:
+            raise CreatorError("Failed to execute script %s, %s " % (script, e))
+        finally:
+            os.unlink(path)       
+
+        
+    def mount(self, base_on, cachedir = None):
+        """mount existing file system.  
+    
+        we have to override mount b/c we are not creating an new install root 
+        nor do we need to setup the file system, ie makedirs(/etc/, /boot, ...),
+        nor do we want to overwrite fstab, or create selinuxfs
+               
+        We also need to get some info about the image before we
+        can mount it.
+    
+        """
+        
+        if not base_on:
+            raise CreatorError("No base livecd image specified")
+        
+        self.__ensure_builddir()
+        
+        self._ImageCreator_instroot = self._ImageCreator__builddir + "/install_root"
+        self._LoopImageCreator__imagedir = self._ImageCreator__builddir + "/ex"
+        self._ImageCreator_outdir = self._ImageCreator__builddir + "/out"
+                       
+        makedirs(self._ImageCreator_instroot)
+        makedirs(self._LoopImageCreator__imagedir)
+        makedirs(self._ImageCreator_outdir)
+        
+        LiveImageCreator._base_on(self, base_on)
+
+        self._LoopImageCreator__image_size = os.stat(self._image)[stat.ST_SIZE]
+        self._get_fstype()
+        self._get_fslable()
+                
+        self._LoopImageCreator__instloop = ExtDiskMount(ExistingSparseLoopbackDisk(self._image,
+                                                                                   self._LoopImageCreator__image_size),
+                                                        self._ImageCreator_instroot,
+                                                        self._fstype,
+                                                        self._LoopImageCreator__blocksize,
+                                                        self.fslabel)
+        try:
+            self._LoopImageCreator__instloop.mount()
+        except MountError, e:
+            raise CreatorError("Failed to loopback mount '%s' : %s" %
+                               (self._image, e))
+
+        cachesrc = cachedir or (self._ImageCreator__builddir + "/yum-cache")
+        makedirs(cachesrc)
+
+        for (f, dest) in [("/sys", None), ("/proc", None),
+                          ("/dev/pts", None), ("/dev/shm", None),
+                          (cachesrc, "/var/cache/yum")]:
+            self._ImageCreator__bindmounts.append(BindChrootMount(f, self._instroot, dest))
+
+        self._do_bindmounts()
+        
+        os.symlink("../proc/mounts", self._instroot + "/etc/mtab")
+        
+        self.__copy_cd_root(base_on)
+        
+    
+    def __copy_cd_root(self, base_on):
+        """helper function to root content of the base liveCD to ISOdir"""
+        
+        isoloop = DiskMount(LoopbackDisk(base_on, 0), self._mkdtemp())
+        self._LiveImageCreatorBase__isodir = self._ImageCreator__builddir + "/iso"
+
+        try:
+            isoloop.mount()
+            # legacy LiveOS filesystem layout support, remove for F9 or F10
+            if os.path.exists(isoloop.mountdir + "/squashfs.img"):
+                squashimg = isoloop.mountdir + "/squashfs.img"
+            else:
+                squashimg = isoloop.mountdir + "/LiveOS/squashfs.img"
+                
+            #copy over everything but squashimg
+            shutil.copytree(isoloop.mountdir, 
+                            self._LiveImageCreatorBase__isodir, 
+                            ignore=shutil.ignore_patterns("squashfs.img", "osmin.img"))
+        except MountError, e:
+            raise CreatorError("Failed to loopback mount '%s' : %s" %
+                               (base_on, e))
+            
+        finally:
+            isoloop.cleanup()
+    
+    
+def parse_options(args):
+    parser = optparse.OptionParser(usage = "%prog [-s=<script.sh>] <LIVECD.iso>")
+    
+    parser.add_option("-n", "--name", type="string", dest="name",
+                      help="name of new livecd (don't include .iso will be added)")
+
+    parser.add_option("-o", "--output", type="string", dest="output",
+                      help="specify the output dir")
+    
+    parser.add_option("-s", "--script", type="string", dest="script",
+                      help="specify script to run chrooted in the livecd fsimage")
+    
+    parser.add_option("-t", "--tmpdir", type="string",
+                      dest="tmpdir", default="/var/tmp",
+                      help="Temporary directory to use (default: /var/tmp)")
+    
+    parser.add_option("", "--skip-compression", action="store_true", dest="skip_compression")
+    
+    parser.add_option("", "--skip-minimize", action="store_true", dest="skip_minimize")
+    
+    setup_logging(parser)
+
+    (options, args) = parser.parse_args()
+
+    if len(args) != 1:
+        parser.print_usage()
+        sys.exit(1)
+
+    return (args[0], options)
+
+
+def main():
+    (livecd, options) = parse_options(sys.argv[1:])
+
+    if os.geteuid () != 0:
+        print >> sys.stderr, "You must run edit-livecd as root"
+        return 1
+
+    if options.name:
+        name = options.name
+    else:
+        name = os.path.basename(livecd) + ".edited"
+        
+    if options.output:
+        output = options.output
+    else:
+        output = os.path.dirname(livecd)
+        
+        
+    editor = LiveImageEditor(name)
+    editor.tmpdir = os.path.abspath(options.tmpdir)
+    editor.skip_compression = options.skip_compression
+    editor.skip_minimize = options.skip_minimize
+    
+    try:
+        editor.mount(livecd, cachedir = None)
+        if options.script:
+            print "Running edit script '%s'" % options.script
+            editor._run_script(options.script)
+        else:
+            print "Launching shell. Exit to continue."
+            print "----------------------------------"
+            editor.launch_shell()
+        editor.unmount()
+        editor.package(output)
+    except CreatorError, e:
+        logging.error(u"Error editing Live CD : %s" % e)
+        return 1
+    finally:
+        editor.cleanup()
+
+    return 0
+    
+
+if __name__ == "__main__":
+    sys.exit(main())
+    
+
+arch = rpmUtils.arch.getBaseArch()
+if arch in ("i386", "x86_64"):
+    LiveImageCreator = x86LiveImageCreator
+elif arch in ("ppc",):
+    LiveImageCreator = ppcLiveImageCreator
+elif arch in ("ppc64",):
+    LiveImageCreator = ppc64LiveImageCreator
+else:
+    raise CreatorError("Architecture not supported!")
-- 
1.6.2.5




More information about the ovirt-devel mailing list