From fa1758b950e229bd61cb08705a74ee0906cd0069 Mon Sep 17 00:00:00 2001
From: Bob Lantz <rlantz@cs.stanford.edu>
Date: Fri, 23 Aug 2013 17:26:46 -0700
Subject: [PATCH] First draft of new world order (create build image from iso)

---
 util/vm/build.py | 422 +++++++++++++++++++++--------------------------
 1 file changed, 190 insertions(+), 232 deletions(-)

diff --git a/util/vm/build.py b/util/vm/build.py
index 9b02da09..26bf7462 100755
--- a/util/vm/build.py
+++ b/util/vm/build.py
@@ -6,11 +6,12 @@
 Basic idea:
 
     prepare
-    - download cloud image if it's missing
-    - write-protect it
-
+    -> create base install image if it's missing
+        - download iso if it's missing
+        - install from iso onto image
+    
     build
-    -> create cow disk for vm
+    -> create cow disk for new VM, based on base image
     -> boot it in qemu/kvm with text /serial console
     -> install Mininet
 
@@ -23,72 +24,40 @@
     -> shrink-wrap VM
     -> upload to storage
 
-
-Notes du jour:
-
-- our infrastructure is currently based on 12.04 LTS, so we
-  can't rely on cloud-localds which is only in 12.10+
-
-- as a result, we should download the tar image, extract it,
-  and boot with tty0 as the console (I think)
-
-- and we'll manually add the mininet user to it
- 
-- and use pexpect to interact with it on the serial console
-
-More notes:
-    
-- We use Ubuntu's cloud images, which means that we need to 
-  adapt them for our own (evil) purposes. This isn't ideal
-  but is the easiest way to get official Ubuntu images until
-  they start building official non-cloud images.
-
-- We could install grub into a raw ext4 partition rather than
-  partitioning everything. This would save time and it might
-  also confuse people who might be expecting a "normal" volume
-  and who might want to expand it and add more partitions.
-  On the other hand it makes the file system a lot easier to mount
-  and modify!! But vmware might not be able to boot it.
-
-- grub-install fails miserably unless you load part_msdos !!
-
-- Installing TexLive is just painful - I would like to avoid it
-  if we could... wireshark plugin build is also slow and painful...
-
-- Maybe we want to install our own packages for these things...
-  that would make the whole installation process a lot easier,
-  but it would mean that we don't automatically get upstream
-  updates
-
 """
 
 import os
-from os import stat
+from os import stat, path
 from stat import ST_MODE
-from os.path import exists, splitext, abspath
+from os.path import abspath
 from sys import exit, argv
 from glob import glob
 from urllib import urlretrieve
-from subprocess import check_output, call, Popen, PIPE
+from subprocess import check_output, call, Popen
 from tempfile import mkdtemp
 from time import time, strftime, localtime
 import argparse
 
 pexpect = None  # For code check - imported dynamically
 
-
 # boot can be slooooow!!!! need to debug/optimize somehow
 TIMEOUT=600
 
 VMImageDir = os.environ[ 'HOME' ] + '/vm-images'
 
-ImageURLBase = {
+isoURLs = {
+    'quetzal32server':
+    'http://mirrors.kernel.org/ubuntu-releases/12.10/'
+    'ubuntu-12.10-server-i386.iso',
+    'quetzal64server':
+    'http://mirrors.kernel.org/ubuntu-releases/13.04/'
+    'ubuntu-12.04-server-amd64.iso',
     'raring32server':
-    'http://cloud-images.ubuntu.com/raring/current/'
-    'raring-server-cloudimg-i386',
+    'http://mirrors.kernel.org/ubuntu-releases/13.04/'
+    'ubuntu-13.04-server-i386.iso',
     'raring64server':
-    'http://cloud-images.ubuntu.com/raring/current/'
-    'raring-server-cloudimg-amd64'
+    'http://mirrors.kernel.org/ubuntu-releases/13.04/'
+    'ubuntu-13.04-server-amd64.iso',
 }
 
 logStartTime = time()
@@ -120,7 +89,7 @@ def srun( cmd, **kwargs ):
 
 
 def depend():
-    "Install packagedependencies"
+    "Install package dependencies"
     log( '* Installing package dependencies' )
     run( 'sudo apt-get -y update' )
     run( 'sudo apt-get install -y'
@@ -143,111 +112,30 @@ def remove( fname ):
     return run( 'rm -f %s' % fname )
 
 
-
-def imageURL( image ):
-    "Return base URL for VM image"
-    return ImageURLBase[ image ]
-
-
-def imagePath( image ):
-    "Return base pathname for VM image files"
-    url = imageURL( image )
-    fname = url.split( '/' )[ -1 ]
-    path = os.path.join( VMImageDir, fname )
-    return path
-
-
-def fetchImage( image, path=None ):
-    "Fetch base VM image if it's not there already"
-    if not path:
-        path = imagePath( image )
-    tgz = path + '.disk1.img'
-    disk = path + '.img'
-    kernel = path + '-vmlinuz-generic'
-    if exists( disk ):
-        log( '* Found', disk )
+def findiso( flavor ):
+    "Find iso, fetching it if it's not there already"
+    url = isoURLs[ flavor ]
+    name = path.basename( url )
+    iso = path.join( VMImageDir, name )
+    if path.exists( iso ):
         # Detect race condition with multiple builds
-        perms = stat( disk )[ ST_MODE ] & 0777
+        perms = stat( iso )[ ST_MODE ] & 0777
         if perms != 0444:
-            raise Exception( 'Error - %s is writable ' % disk +
+            raise Exception( 'Error - %s is writable ' % iso +
                              '; are multiple builds running?' )
     else:
-        dir = os.path.dirname( path )
-        run( 'mkdir -p %s' % dir )
-        if not os.path.exists( tgz ):
-            url = imageURL( image ) + '.tar.gz'
-            log( '* Retrieving', url )
-            urlretrieve( url, tgz )
-        log( '* Extracting', tgz )
-        run( 'tar -C %s -xzf %s' % ( dir, tgz ) )
-        # Write-protect disk image so it remains pristine;
-        # We will not use it directly but will use a COW disk
-        log( '* Write-protecting disk image', disk )
-        os.chmod( disk, 0444 )
-    return disk, kernel
-
-
-def addTo( file, line ):
-    "Add line to file if it's not there already"
-    if call( [ 'sudo', 'grep', line, file ] ) != 0:
-        call( 'echo "%s" | sudo tee -a %s > /dev/null' % ( line, file ),
-              shell=True )
-
-
-def disableCloud( bind ):
-    "Disable cloud junk for disk mounted at bind"
-    log( '* Disabling cloud startup scripts' )
-    modules = glob( '%s/etc/init/cloud*.conf' % bind )
-    for module in modules:
-        path, ext = splitext( module )
-        call( 'echo manual | sudo tee %s.override > /dev/null' % path,
-              shell=True )
-
-
-def addMininetUser( nbd ):
-    "Add mininet user/group to filesystem"
-    log( '* Adding mininet user to filesystem on device', nbd )
-    # 1. We bind-mount / into a temporary directory, and
-    # then mount the volume's /etc and /home on top of it!
-    mnt = mkdtemp()
-    bind = mkdtemp()
-    srun( 'mount %s %s' % ( nbd, mnt ) )
-    srun( 'mount -B / ' + bind )
-    srun( 'mount -B %s/etc %s/etc' % ( mnt, bind ) )
-    srun( 'mount -B %s/home %s/home' % ( mnt, bind ) )
-    def chroot( cmd ):
-        "Chroot into bind mount and run command"
-        call( 'sudo chroot %s ' % bind + cmd, shell=True )
-    # 1a. Add hostname entry in /etc/hosts
-    addTo( bind + '/etc/hosts', '127.0.1.1 mininet-vm' )
-    # 2. Next, we delete any old mininet user and add a new one
-    chroot( 'deluser mininet' )
-    chroot( 'useradd --create-home --shell /bin/bash mininet' )
-    log( '* Setting password' )
-    call( 'echo mininet:mininet | sudo chroot %s chpasswd -c SHA512'
-          % bind, shell=True )
-    # 2a. Add mininet to sudoers
-    addTo( bind + '/etc/sudoers', 'mininet ALL=NOPASSWD: ALL' )
-    # 2b. Disable cloud junk
-    disableCloud( bind )
-    chroot( 'sudo update-rc.d landscape-client disable' )
-    # 2c. Add serial getty
-    log( '* Adding getty on ttyS0' )
-    chroot( 'cp /etc/init/tty1.conf /etc/init/ttyS0.conf' )
-    chroot( 'sed -i "s/tty1/ttyS0/g" /etc/init/ttyS0.conf' )
-    # 3. Lastly, we umount and clean up everything
-    run( 'sync' )
-    srun( 'umount %s/home ' % bind )
-    srun( 'umount %s/etc ' % bind )
-    srun( 'umount %s' % bind )
-    srun( 'umount ' + mnt )
-    run( 'rmdir ' + bind )
-    run( 'rmdir ' + mnt )
+        log( '* Retrieving', url )
+        urlretrieve( url, iso )
+        # Write-protect iso, signaling it is complete
+        log( '* Write-protecting iso', iso)
+        os.chmod( iso, 0444 )
+    log( '* Using iso', iso )
+    return iso
 
 
 def attachNBD( cow, flags='' ):
     """Attempt to attach a COW disk image and return its nbd device
-       flags: additional flags for qemu-nbd (e.g. -r for readonly)"""
+        flags: additional flags for qemu-nbd (e.g. -r for readonly)"""
     # qemu-nbd requires an absolute path
     cow = abspath( cow )
     log( '* Checking for unused /dev/nbdX device ' )
@@ -268,94 +156,165 @@ def detachNBD( nbd ):
     srun( 'qemu-nbd -d ' + nbd )
 
 
-def makeCOWDisk( image, dir='.' ):
-    "Create new COW disk for image"
-    disk, kernel = fetchImage( image )
-    cow = '%s/%s.qcow2' % ( dir, image )
-    log( '* Creating COW disk', cow )
-    run( 'qemu-img create -f qcow2 -b %s %s' % ( disk, cow ) )
-    log( '* Resizing COW disk and file system' )
-    run( 'qemu-img resize %s +8G' % cow )
-    nbd = attachNBD( cow )
-    srun( 'e2fsck -y ' + nbd )
-    srun( 'resize2fs ' + nbd )
-    addMininetUser( nbd )
-    detachNBD( nbd )
-    return cow, kernel
-
-
-def makeVolume( volume, size='8G'  ):
-    """Create volume as a qcow2 and add a single boot partition"""
-    log( '* Creating volume of size', size )
-    run( 'qemu-img create -f qcow2 %s %s' % ( volume, size ) )
-    log( '* Partitioning volume' )
-    # We need to mount it using qemu-nbd!!
-    nbd = attachNBD( volume )
-    parted = Popen( [ 'sudo', 'parted', nbd ], stdin=PIPE )
-    cmds = [ 'mklabel msdos',
-             'mkpart primary ext4 1 %s' % size,
-             'set 1 boot on',
-             'quit' ]
-    parted.stdin.write( '\n'.join( cmds ) + '\n' )
-    parted.wait()
-    log( '* Volume partition table:' )
-    log( srun( 'fdisk -l ' + nbd ) )
-    detachNBD( nbd )
-
-
-def installGrub( voldev, partnum=1 ):
-    "Install grub2 on voldev to boot from partition partnum"
+def kernelpath( flavor ):
+    "Return kernel path for flavor"
+    return path.join( VMImageDir, flavor + '-vmlinuz' )
+
+
+def extractKernel( image, kernel ):
+    "Extract kernel from base image"
+    nbd = attachNBD( image )
+    print srun( 'partx ' + nbd )
+    # Assume kernel is in partition 1/boot/vmlinuz*generic for now
+    part = nbd + 'p1'
     mnt = mkdtemp()
-    # Find partitions and make sure we have partition 1
-    assert ( '# %d:' % partnum ) in srun( 'partx ' + voldev )
-    partdev = voldev + 'p%d' % partnum
-    srun( 'mount %s %s' % ( partdev, mnt ) )
-    # Make sure we have a boot directory
-    bootdir = mnt + '/boot'
-    run( 'ls ' + bootdir )
-    # Install grub - make sure we preload part_msdos !!
-    srun( 'grub-install --boot-directory=%s --modules=part_msdos %s' % (
-          bootdir, voldev ) )
+    srun( 'mount %s %s' % ( part, mnt  ) )
+    kernsrc = glob( '%s/boot/vmlinuz*generic' % mnt )[ 0 ]
+    run( 'cp %s %s' % ( kernsrc, kernel ) )
     srun( 'umount ' + mnt )
     run( 'rmdir ' + mnt )
+    detachNBD( image )
+
+
+def findBaseImage( flavor, size='8G' ):
+    "Return base VM image and kernel, creating them if needed"
+    image = path.join( VMImageDir, flavor + '-base.img' )
+    kernel = path.join( VMImageDir, flavor + '-vmlinuz' )
+    if path.exists( image ):
+        # Detect race condition with multiple builds
+        perms = stat( image )[ ST_MODE ] & 0777
+        if perms != 0444:
+            raise Exception( 'Error - %s is writable ' % image +
+                            '; are multiple builds running?' )
+    else:
+        # We create VMImageDir here since we are called first
+        run( 'mkdir -p %s' % VMImageDir )
+        iso = findiso( flavor )
+        log( '* Creating image file', image )
+        run( 'qemu-img create %s %s' % ( image, size ) )
+        installUbuntu( iso, image )
+        log( '* Extracting kernel to', kernel )
+        extractKernel( image, kernel )
+        # Write-protect image, also signaling it is complete
+        log( '* Write-protecting image', image)
+        os.chmod( image, 0444 )
+    log( '* Using base image', image )
+    return image, kernel
+
+
+def makeKickstartFloppy():
+    "Create and return kickstart floppy, kickstart, preseed"
+    kickstart = 'ks.cfg'
+    kstext = '\n'.join( [ '#Generated by Kickstart Configurator',
+                         '#platform=x86',
+                         '#System language',
+                         'lang en_US',
+                         '#Language modules to install',
+                         'langsupport en_US',
+                         '#System keyboard',
+                         'keyboard us',
+                         '#System mouse',
+                         'mouse',
+                         '#System timezone',
+                         'timezone America/Los_Angeles',
+                         '#Root password',
+                         'rootpw --disabled',
+                         '#Initial user'
+                         'user mininet --fullname "mininet" --password "mininet"',
+                         '#Use text mode install',
+                         'text',
+                         '#Install OS instead of upgrade',
+                         'install',
+                         '#Use CDROM installation media',
+                         'cdrom',
+                         '#System bootloader configuration',
+                         'bootloader --location=mbr',
+                         '#Clear the Master Boot Record',
+                         'zerombr yes',
+                         '#Partition clearing information',
+                         'clearpart --all --initlabel',
+                         '#Automatic partitioning',
+                         'autopart',
+                         '#System authorization infomation',
+                         'auth  --useshadow  --enablemd5',
+                         '#Firewall configuration',
+                         'firewall --disabled',
+                         '#Do not configure the X Window System',
+                         'skipx', '' ] )
+    with open( kickstart, 'w' ) as f:
+        f.write( kstext )
+    preseed = 'ks.preseed'
+    pstext = '\n'.join( [ 'd-i partman/confirm_write_new_label boolean true',
+                         'd-i partman/choose_partition select finish',
+                         'd-i partman/confirm boolean true',
+                         'd-i partman/confirm_nooverwrite boolean true',
+                         'd-i user-setup/allow-password-weak boolean true' ] )
+    with open( preseed, 'w' ) as f:
+        f.write( pstext )
+    # Create floppy and copy files to it
+    floppy = 'ksfloppy.img'
+    run( 'qemu-img create %s 1M' % floppy )
+    run( 'mcopy -i %s %s ::/' % ( floppy, kickstart ) )
+    run( 'mcopy -i %s %s ::/' % ( floppy, preseed ) )
+    log( '* Created floppy image %s containing %s and %s' %
+         ( floppy, kickstart, preseed ) )
+    return floppy, kickstart, preseed
+
+
+def kvmFor( name ):
+    "Guess kvm version for file name"
+    if 'amd64' in name:
+        kvm = 'qemu-system-x86_64'
+    elif 'i386' in name:
+        kvm = 'qemu-system-i386'
+    else:
+        log( "Error: can't discern CPU for file name", name )
+        exit( 1 )
+    return kvm
 
 
-def initPartition( partition, volume ):
-    """Copy partition to volume-p1 and initialize everything"""
-    srcdev = attachNBD( partition, flags='-r' )
-    voldev = attachNBD( volume )
-    log( srun( 'fdisk -l ' + voldev ) )
-    log( srun( 'partx ' + voldev ) )
-    dstdev = voldev + 'p1'
-    log( "* Copying partition from", srcdev, "to", dstdev )
-    log( srun( 'dd if=%s of=%s bs=1M' % ( srcdev, dstdev ) ) )
-    log( '* Resizing file system' )
-    srun( 'resize2fs ' + dstdev )
-    srun( 'e2fsck -y ' + dstdev )
-    log( '* Adding mininet user' )
-    addMininetUser( dstdev )
-    log( '* Installing grub2' )
-    installGrub( voldev, partnum=1 )
-    detachNBD( voldev )
-    detachNBD( srcdev )
-
-
-def boot( cow, kernel, tap ):
+def installUbuntu( iso, image ):
+    "Install Ubuntu from iso onto image"
+    kvm = kvmFor( iso )
+    floppy, kickstart, preseed = makeKickstartFloppy()
+    # Mount iso so we can use its kernel
+    mnt = mkdtemp()
+    srun( 'mount %s %s' % ( iso, mnt ) )
+    kernel = mnt + 'install/vmlinuz'
+    cmd = [ 'sudo', kvm,
+           '-machine accel=kvm',
+           '-nographic',
+           '-netdev user,id=mnbuild',
+           '-device virtio-net,netdev=mnbuild',
+           '-m 1024',
+           '-k en-us',
+           '-cdrom', iso,
+           '-drive file=%s,if=virtio' % image,
+           '-fda', floppy,
+           '-kernel', kernel,
+           '-append "root=/dev/vda1 init=/sbin/init console=ttyS0' +
+           'ks=floppy:/' + kickstart +
+           'preseed/file=floppy://' + preseed + '"' ]
+    cmd = ' '.join( cmd )
+    log( '* INSTALLING UBUNTU FROM', iso, 'ONTO', image )
+    log( cmd )
+    run( cmd )
+    # Unmount iso and clean up
+    srun( 'umount ' + mnt )
+    run( 'rmdir ' + mnt )
+    log( '* UBUNTU INSTALLATION COMPLETED FOR', image )
+
+
+def boot( cow, kernel, logfile ):
     """Boot qemu/kvm with a COW disk and local/user data store
        cow: COW disk path
        kernel: kernel path
-       tap: tap device to connect to VM
+       logfile: log file for pexpect object
        returns: pexpect object to qemu process"""
     # pexpect might not be installed until after depend() is called
     global pexpect
     import pexpect
-    if 'amd64' in kernel:
-        kvm = 'qemu-system-x86_64'
-    elif 'i386' in kernel:
-        kvm = 'qemu-system-i386'
-    else:
-        log( "Error: can't discern CPU for image", cow )
-        exit( 1 )
+    kvm = kvmFor( kernel )
     cmd = [ 'sudo', kvm,
             '-machine accel=kvm',
             '-nographic',
@@ -367,9 +326,9 @@ def boot( cow, kernel, tap ):
             '-drive file=%s,if=virtio' % cow,
             '-append "root=/dev/vda1 init=/sbin/init console=ttyS0" ' ]
     cmd = ' '.join( cmd )
-    log( '* STARTING VM' )
+    log( '* BOOTING VM FROM', cow )
     log( cmd )
-    vm = pexpect.spawn( cmd, timeout=TIMEOUT )
+    vm = pexpect.spawn( cmd, timeout=TIMEOUT, logfile=logfile )
     return vm
 
 
@@ -444,15 +403,13 @@ def build( flavor='raring32server' ):
     dir = mkdtemp( prefix=flavor + '-result-', dir='.' )
     os.chdir( dir )
     log( '* Created working directory', dir )
-    image, kernel = fetchImage( flavor )
+    image, kernel = findBaseImage( flavor )
     volume = flavor + '.qcow2'
-    makeVolume( volume )
-    initPartition( image, volume )
+    run( 'qemu-img create -f qcow2 -b %s %s' % ( image, volume ) )
     log( '* VM image for', flavor, 'created as', volume )
     logfile = open( flavor + '.log', 'w+' )
     log( '* Logging results to', abspath( logfile.name ) )
     vm = boot( volume, kernel, logfile )
-    vm.logfile_read = logfile
     interact( vm )
     vmdk = convert( volume, basename=flavor )
     log( '* Converted VM image stored as', vmdk )
@@ -467,7 +424,8 @@ def build( flavor='raring32server' ):
 
 def listFlavors():
     "List valid build flavors"
-    print '\nvalid build flavors:', ' '.join( ImageURLBase ), '\n'
+    print '\nvalid build flavors:', ' '.join( isoURLs ), '\n'
+
 
 def parseArgs():
     "Parse command line arguments and run"
@@ -489,7 +447,7 @@ def parseArgs():
         cleanup()
     flavors = args.flavor[ 1: ]
     for flavor in flavors:
-        if flavor not in ImageURLBase:
+        if flavor not in isoURLs:
             parser.print_help()
             listFlavors()
             break
-- 
GitLab