From 14903d6a053022772494d6fb1a40b8cc213d5d56 Mon Sep 17 00:00:00 2001
From: Bob Lantz <rlantz@cs.stanford.edu>
Date: Thu, 22 Aug 2013 18:40:10 -0700
Subject: [PATCH] Final gasp of cloud image version.

---
 util/vm/build.py | 236 +++++++++++++++++++++++++++--------------------
 1 file changed, 137 insertions(+), 99 deletions(-)

diff --git a/util/vm/build.py b/util/vm/build.py
index 9accde19..9b02da09 100755
--- a/util/vm/build.py
+++ b/util/vm/build.py
@@ -36,41 +36,42 @@
  
 - and use pexpect to interact with it on the serial console
 
-Something to think about:
-
-Maybe download the cloud image and customize it so that
-it is an actual usable/bootable image???
-
 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 really want a full, partitioned disk image!
-
-This means we want to use the disk1.image file ???
+- 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.
 
-However, this means that we will need to change the grub2
-configuratin to use a serial console.
+- grub-install fails miserably unless you load part_msdos !!
 
-/etc/default/grub:
-    GRUB_TERMINAL=serial
-    GRUB_SERIAL_COMMAND="serial --unit=0 --speed=38400 --word=8 --parity=no --stop=1"
-    BOOT_IMAGE="console=ttyS0"
-    
-# grub2-mkconfig -o /boot/grub2/grub.cfg
+- Installing TexLive is just painful - I would like to avoid it
+  if we could... wireshark plugin build is also slow and painful...
 
-by the way, we should use wget -c
+- 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 stat import ST_MODE
-from os.path import exists, splitext, abspath, realpath
+from os.path import exists, splitext, abspath
 from sys import exit, argv
 from glob import glob
 from urllib import urlretrieve
 from subprocess import check_output, call, Popen, PIPE
 from tempfile import mkdtemp
-from time import time
+from time import time, strftime, localtime
 import argparse
 
 pexpect = None  # For code check - imported dynamically
@@ -90,10 +91,25 @@
     'raring-server-cloudimg-amd64'
 }
 
+logStartTime = time()
+
+def log( *args, **kwargs ):
+    """Simple log function: log( message along with local and elapsed time
+       cr: False/0 for no CR"""
+    cr = kwargs.get( 'cr', True )
+    elapsed = time() - logStartTime
+    clocktime = strftime( '%H:%M:%S', localtime() )
+    msg = ' '.join( str( arg ) for arg in args )
+    output = '%s [ %.3f ] %s' % ( clocktime, elapsed, msg )
+    if cr:
+        print output
+    else:
+        print output,
+
 
 def run( cmd, **kwargs ):
     "Convenient interface to check_output"
-    print cmd
+    log( '-', cmd )
     cmd = cmd.split()
     return check_output( cmd, **kwargs )
 
@@ -105,7 +121,7 @@ def srun( cmd, **kwargs ):
 
 def depend():
     "Install packagedependencies"
-    print '* Installing package dependencies'
+    log( '* Installing package dependencies' )
     run( 'sudo apt-get -y update' )
     run( 'sudo apt-get install -y'
          ' kvm cloud-utils genisoimage qemu-kvm qemu-utils'
@@ -117,7 +133,7 @@ def depend():
 
 def popen( cmd ):
     "Convenient interface to popen"
-    print cmd
+    log( cmd )
     cmd = cmd.split()
     return Popen( cmd )
 
@@ -148,8 +164,8 @@ def fetchImage( image, path=None ):
     tgz = path + '.disk1.img'
     disk = path + '.img'
     kernel = path + '-vmlinuz-generic'
-    if exists( disk ) and exists( kernel ):
-        print '* Found', disk, 'and', kernel
+    if exists( disk ):
+        log( '* Found', disk )
         # Detect race condition with multiple builds
         perms = stat( disk )[ ST_MODE ] & 0777
         if perms != 0444:
@@ -160,13 +176,13 @@ def fetchImage( image, path=None ):
         run( 'mkdir -p %s' % dir )
         if not os.path.exists( tgz ):
             url = imageURL( image ) + '.tar.gz'
-            print '* Retrieving', url
+            log( '* Retrieving', url )
             urlretrieve( url, tgz )
-        print '* Extracting', 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
-        print '* Write-protecting disk image', disk
+        log( '* Write-protecting disk image', disk )
         os.chmod( disk, 0444 )
     return disk, kernel
 
@@ -174,22 +190,23 @@ def fetchImage( image, path=None ):
 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' % ( line, file ), shell=True )
+        call( 'echo "%s" | sudo tee -a %s > /dev/null' % ( line, file ),
+              shell=True )
 
 
 def disableCloud( bind ):
     "Disable cloud junk for disk mounted at bind"
-    print '* Disabling cloud startup scripts'
+    log( '* Disabling cloud startup scripts' )
     modules = glob( '%s/etc/init/cloud*.conf' % bind )
     for module in modules:
         path, ext = splitext( module )
-        override = path + '.override'
-        call( 'echo manual | sudo tee ' + override, shell=True )
+        call( 'echo manual | sudo tee %s.override > /dev/null' % path,
+              shell=True )
 
 
 def addMininetUser( nbd ):
     "Add mininet user/group to filesystem"
-    print '* Adding mininet user to filesystem on device', nbd
+    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()
@@ -205,8 +222,8 @@ def chroot( cmd ):
     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 mininet' )
-    print '* Setting password'
+    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
@@ -215,7 +232,7 @@ def chroot( cmd ):
     disableCloud( bind )
     chroot( 'sudo update-rc.d landscape-client disable' )
     # 2c. Add serial getty
-    print '* Adding getty on ttyS0'
+    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
@@ -226,8 +243,6 @@ def chroot( cmd ):
     srun( 'umount ' + mnt )
     run( 'rmdir ' + bind )
     run( 'rmdir ' + mnt )
-    # 4. Just to make sure, we check the filesystem
-    srun( 'e2fsck -y ' + nbd )
 
 
 def attachNBD( cow, flags='' ):
@@ -235,16 +250,15 @@ def attachNBD( cow, flags='' ):
        flags: additional flags for qemu-nbd (e.g. -r for readonly)"""
     # qemu-nbd requires an absolute path
     cow = abspath( cow )
-    print '* Checking for unused /dev/nbdX device ',
+    log( '* Checking for unused /dev/nbdX device ' )
     for i in range ( 0, 63 ):
         nbd = '/dev/nbd%d' % i
-        print i,
         # Check whether someone's already messing with that device
         if call( [ 'pgrep', '-f', nbd ] ) == 0:
             continue
-        # Fails without -v for some annoying reason...
-        print
+        srun( 'modprobe nbd max-part=64' )
         srun( 'qemu-nbd %s -c %s %s' % ( flags, nbd, cow ) )
+        print
         return nbd
     raise Exception( "Error: could not find unused /dev/nbdX device" )
 
@@ -258,11 +272,10 @@ def makeCOWDisk( image, dir='.' ):
     "Create new COW disk for image"
     disk, kernel = fetchImage( image )
     cow = '%s/%s.qcow2' % ( dir, image )
-    print '* Creating COW disk', cow
+    log( '* Creating COW disk', cow )
     run( 'qemu-img create -f qcow2 -b %s %s' % ( disk, cow ) )
-    print '* Resizing COW disk and file system'
+    log( '* Resizing COW disk and file system' )
     run( 'qemu-img resize %s +8G' % cow )
-    srun( 'modprobe nbd max-part=64')
     nbd = attachNBD( cow )
     srun( 'e2fsck -y ' + nbd )
     srun( 'resize2fs ' + nbd )
@@ -271,39 +284,58 @@ def makeCOWDisk( image, dir='.' ):
     return cow, kernel
 
 
-def makeVolume( volume, cylinders=1000  ):
-    """Create volume as a qcow2 and add a single boot partition
-       cylinders: number of ~8MB (255*63*512) cylinders in volume"""
-    heads, sectors, bytes = 255, 63, 512
-    size = cylinders * heads * sectors * bytes
-    print '* Creating volume of size', size
+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 ) )
-    print '* Partitioning volume'
+    log( '* Partitioning volume' )
     # We need to mount it using qemu-nbd!!
     nbd = attachNBD( volume )
-    # A bit hacky - we may change this to use parted(8) later
-    fdisk = Popen( [ 'sudo', 'fdisk', nbd ], stdin=PIPE )
-    cmds = 'x\nc\n%d\nr\no\nn\np\n1\n\n\na\n1\nw\n' % cylinders
-    fdisk.stdin.write( cmds )
-    fdisk.wait()
-    print '* Volume partition table:'
-    print srun( 'fdisk -l ' + nbd )
+    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"
+    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( 'umount ' + mnt )
+    run( 'rmdir ' + mnt )
+
+
 def initPartition( partition, volume ):
-    """Copy partition to volume-p1 and call addMininetUser"""
+    """Copy partition to volume-p1 and initialize everything"""
     srcdev = attachNBD( partition, flags='-r' )
     voldev = attachNBD( volume )
-    print srun( 'fdisk -l ' + voldev )
-    print srun( 'partx ' + voldev )
+    log( srun( 'fdisk -l ' + voldev ) )
+    log( srun( 'partx ' + voldev ) )
     dstdev = voldev + 'p1'
-    print "* Copying partition from", srcdev, "to", dstdev
-    print srun( 'time dd if=%s of=%s bs=1M' % ( srcdev, dstdev ) )
-    print '* Resizing and adding Mininet user'
+    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 )
 
@@ -322,7 +354,7 @@ def boot( cow, kernel, tap ):
     elif 'i386' in kernel:
         kvm = 'qemu-system-i386'
     else:
-        print "Error: can't discern CPU for image", cow
+        log( "Error: can't discern CPU for image", cow )
         exit( 1 )
     cmd = [ 'sudo', kvm,
             '-machine accel=kvm',
@@ -335,8 +367,8 @@ def boot( cow, kernel, tap ):
             '-drive file=%s,if=virtio' % cow,
             '-append "root=/dev/vda1 init=/sbin/init console=ttyS0" ' ]
     cmd = ' '.join( cmd )
-    print '* STARTING VM'
-    print cmd
+    log( '* STARTING VM' )
+    log( cmd )
     vm = pexpect.spawn( cmd, timeout=TIMEOUT )
     return vm
 
@@ -344,64 +376,64 @@ def boot( cow, kernel, tap ):
 def interact( vm ):
     "Interact with vm, which is a pexpect object"
     prompt = '\$ '
-    print '* Waiting for login prompt'
+    log( '* Waiting for login prompt' )
     vm.expect( 'login: ' )
-    print '* Logging in'
+    log( '* Logging in' )
     vm.sendline( 'mininet' )
-    print '* Waiting for password prompt'
+    log( '* Waiting for password prompt' )
     vm.expect( 'Password: ' )
-    print '* Sending password'
+    log( '* Sending password' )
     vm.sendline( 'mininet' )
-    print '* Waiting for login...'
+    log( '* Waiting for login...' )
     vm.expect( prompt )
-    print '* Sending hostname command'
+    log( '* Sending hostname command' )
     vm.sendline( 'hostname' )
-    print '* Waiting for output'
+    log( '* Waiting for output' )
     vm.expect( prompt )
-    print '* Fetching Mininet VM install script'
+    log( '* Fetching Mininet VM install script' )
     vm.sendline( 'wget '
                  'https://raw.github.com/mininet/mininet/master/util/vm/'
                  'install-mininet-vm.sh' )
     vm.expect( prompt )
-    print '* Running VM install script'
+    log( '* Running VM install script' )
     vm.sendline( 'bash install-mininet-vm.sh' )
-    print '* Waiting for script to complete... '
+    log( '* Waiting for script to complete... ' )
     # Gigantic timeout for now ;-(
     vm.expect( 'Done preparing Mininet', timeout=3600 )
-    print '* Completed successfully'
+    log( '* Completed successfully' )
     vm.expect( prompt )
-    print '* Testing Mininet'
+    log( '* Testing Mininet' )
     vm.sendline( 'sudo mn --test pingall' )
-    if vm.expect( [ ' 0% dropped', pexpect.TIMEOUT ], timeout=45 ):
-        print '* Sanity check succeeded'
+    if vm.expect( [ ' 0% dropped', pexpect.TIMEOUT ], timeout=45 ) == 0:
+        log( '* Sanity check succeeded' )
     else:
-        print '* Sanity check FAILED'
+        log( '* Sanity check FAILED' )
     vm.expect( prompt )
-    print '* Making sure cgroups are mounted'
+    log( '* Making sure cgroups are mounted' )
     vm.sendline( 'sudo service cgroup-lite restart' )
     vm.expect( prompt )
     vm.sendline( 'sudo cgroups-mount' )
     vm.expect( prompt )
-    print '* Running make test'
+    log( '* Running make test' )
     vm.sendline( 'cd ~/mininet; sudo make test' )
     vm.expect( prompt )
-    print '* Shutting down'
+    log( '* Shutting down' )
     vm.sendline( 'sync; sudo shutdown -h now' )
-    print '* Waiting for EOF/shutdown'
+    log( '* Waiting for EOF/shutdown' )
     vm.read()
-    print '* Interaction complete'
+    log( '* Interaction complete' )
 
 
 def cleanup():
     "Clean up leftover qemu-nbd processes and other junk"
-    call( 'sudo pkill -9 qemu-nbd', shell=True )
+    call( [ 'sudo', 'pkill', '-9', 'qemu-nbd' ] )
 
 
 def convert( cow, basename ):
     """Convert a qcow2 disk to a vmdk and put it a new directory
        basename: base name for output vmdk file"""
     vmdk = basename + '.vmdk'
-    print '* Converting qcow2 to vmdk'
+    log( '* Converting qcow2 to vmdk' )
     run( 'qemu-img convert -f qcow2 -O vmdk %s %s' % ( cow, vmdk ) )
     return vmdk
 
@@ -411,28 +443,32 @@ def build( flavor='raring32server' ):
     start = time()
     dir = mkdtemp( prefix=flavor + '-result-', dir='.' )
     os.chdir( dir )
-    print '* Created working directory', dir
+    log( '* Created working directory', dir )
     image, kernel = fetchImage( flavor )
     volume = flavor + '.qcow2'
     makeVolume( volume )
     initPartition( image, volume )
-    print '* VM image for', flavor, 'created as', volume
+    log( '* VM image for', flavor, 'created as', volume )
     logfile = open( flavor + '.log', 'w+' )
-    print '* Logging results to', abspath( logfile.name )
+    log( '* Logging results to', abspath( logfile.name ) )
     vm = boot( volume, kernel, logfile )
     vm.logfile_read = logfile
     interact( vm )
     vmdk = convert( volume, basename=flavor )
-    print '* Converted VM image stored as', vmdk
+    log( '* Converted VM image stored as', vmdk )
     end = time()
     elapsed = end - start
-    print '* Results logged to', abspath( logfile.name )
-    print '* Completed in %.2f seconds' % elapsed
-    print '* %s VM build DONE!!!!! :D' % flavor
-    print
+    log( '* Results logged to', abspath( logfile.name ) )
+    log( '* Completed in %.2f seconds' % elapsed )
+    log( '* %s VM build DONE!!!!! :D' % flavor )
+    log( '* ' )
     os.chdir( '..' )
 
 
+def listFlavors():
+    "List valid build flavors"
+    print '\nvalid build flavors:', ' '.join( ImageURLBase ), '\n'
+
 def parseArgs():
     "Parse command line arguments and run"
     parser = argparse.ArgumentParser( description='Mininet VM build script' )
@@ -448,17 +484,19 @@ def parseArgs():
     if args.depend:
         depend()
     if args.list:
-        print 'valid build flavors:', ' '.join( ImageURLBase )
+        listFlavors()
     if args.clean:
         cleanup()
     flavors = args.flavor[ 1: ]
     for flavor in flavors:
         if flavor not in ImageURLBase:
             parser.print_help()
+            listFlavors()
+            break
         # try:
         build( flavor )
         # except Exception as e:
-        # print '* BUILD FAILED with exception: ', e
+        # log( '* BUILD FAILED with exception: ', e )
         # exit( 1 )
     if not ( args.depend or args.list or args.clean or flavors ):
         parser.print_help()
-- 
GitLab