diff --git a/util/vm/build.py b/util/vm/build.py index 33f960a..eb54c94 100755 --- a/util/vm/build.py +++ b/util/vm/build.py @@ -46,13 +46,16 @@ it is an actual usable/bootable image??? import os +from os import stat +from stat import ST_MODE from os.path import exists, splitext +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, NamedTemporaryFile -from sys import exit from time import time +import argparse # boot can be slooooow!!!! need to debug/optimize somehow TIMEOUT=600 @@ -60,7 +63,10 @@ TIMEOUT=600 VMImageDir = os.environ[ 'HOME' ] + '/vm-images' ImageURLBase = { - 'raring-server-amd64': + 'raring32server': + 'http://cloud-images.ubuntu.com/raring/current/' + 'raring-server-cloudimg-i386', + 'raring64server': 'http://cloud-images.ubuntu.com/raring/current/' 'raring-server-cloudimg-amd64' } @@ -78,17 +84,17 @@ def srun( cmd, **kwargs ): return run( 'sudo ' + cmd, **kwargs ) -# Install necessary packages -print '* Installing package dependencies' -packages = ( ) -run( 'sudo apt-get -y update' ) -run( 'sudo apt-get install -y' - ' kvm cloud-utils genisoimage qemu-kvm qemu-utils' - ' e2fsprogs ' - ' landscape-client' - ' python-setuptools' ) -run( 'sudo easy_install pexpect' ) -import pexpect +def depend(): + "Install packagedependencies" + print '* Installing package dependencies' + packages = ( ) + run( 'sudo apt-get -y update' ) + run( 'sudo apt-get install -y' + ' kvm cloud-utils genisoimage qemu-kvm qemu-utils' + ' e2fsprogs ' + ' landscape-client' + ' python-setuptools' ) + run( 'sudo easy_install pexpect' ) def popen( cmd ): @@ -127,6 +133,11 @@ def fetchImage( image, path=None ): floppy = path + '-floppy' if exists( disk ) and exists( kernel ): print '* Found', disk, 'and', kernel + # Detect race condition with multiple builds + perms = stat( disk )[ ST_MODE ] & 0777 + if perms != 0444: + raise Exception( 'Error - %s is writable ' % disk + + '; are multiple builds running?' ) else: dir = os.path.dirname( path ) run( 'mkdir -p %s' % dir ) @@ -136,13 +147,10 @@ def fetchImage( image, path=None ): urlretrieve( url, tgz ) print '* Extracting', tgz run( 'tar -C %s -xzf %s' % ( dir, tgz ) ) - # Make sure Mininet user is there - os.chmod( disk, 0664 ) - addMininetUser( disk ) - # Write-protect disk image so it remains somewhat pristine; - # We will not use it directly but will use a COW disk - print '* Write-protecting disk image', disk - os.chmod( disk, 0444 ) + # 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 + os.chmod( disk, 0444 ) return disk, kernel @@ -162,14 +170,14 @@ def disableCloud( bind ): call( 'echo manual | sudo tee ' + override, shell=True ) -def addMininetUser( img ): - "Add mininet user/group to root filesystem image" - print '* Adding mininet user to', img +def addMininetUser( nbd ): + "Add mininet user/group to filesystem" + print '* 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' % ( img, mnt ) ) + 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 ) ) @@ -202,66 +210,79 @@ def addMininetUser( img ): run( 'rmdir ' + bind ) run( 'rmdir ' + mnt ) # 4. Just to make sure, we check the filesystem - run( 'e2fsck -y ' + img ) + srun( 'e2fsck -y ' + nbd ) + + +def connectCOWdevice( cow ): + "Attempt to connect a COW disk and return its nbd device" + print '* 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( 'qemu-nbd -c %s %s' % ( nbd, cow ) ) + return nbd + raise Exception( "Error: could not find unused /dev/nbdX device" ) + + +def disconnectCOWdevice( nbd ): + srun( 'qemu-nbd -d ' + nbd ) def makeCOWDisk( image ): "Create new COW disk for image" disk, kernel = fetchImage( image ) - cow = disk + '.qcow2' + cow = NamedTemporaryFile( prefix=image + '-', suffix='.qcow2', + dir='.' ).name print '* Creating COW disk', cow - remove( cow ) run( 'qemu-img create -f qcow2 -b %s %s' % ( disk, cow ) ) print '* Resizing COW disk and file system' run( 'qemu-img resize %s +8G' % cow ) - srun( 'modprobe nbd') - # Ideally we should check if it's being used... - srun( 'qemu-nbd -d /dev/nbd0' ) - srun( 'qemu-nbd -c /dev/nbd0 ' + cow ) - srun( 'e2fsck -fy /dev/nbd0' ) - srun( 'resize2fs /dev/nbd0' ) - srun( 'qemu-nbd -d /dev/nbd0' ) + srun( 'modprobe nbd max-part=64') + nbd = connectCOWdevice( cow ) + srun( 'e2fsck -y ' + nbd ) + srun( 'resize2fs ' + nbd ) + addMininetUser( nbd ) + disconnectCOWdevice( nbd ) return cow, kernel -def boot( cow, kernel ): +def boot( cow, kernel, tap ): """Boot qemu/kvm with a COW disk and local/user data store - returns: popen object to qemu process""" - if 'amd64' in cow: + cow: COW disk path + kernel: kernel path + tap: tap device to connect to VM + 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 cow: + elif 'i386' in kernel: kvm = 'qemu-system-i386' else: print "Error: can't discern CPU for image", cow - exit - # was -nographic - # was -net=%net - # ' -net nic,model=virtio' - # ' -netdev tap,id=mininet0,ifname=%s,script=no ' % tap + + exit( 1 ) cmd = [ 'sudo', kvm, - '-machine', 'accel=kvm', + '-machine accel=kvm', '-nographic', - '-m', '512', - '-k', 'en-us', + '-netdev user,id=mnbuild', + '-device virtio-net,netdev=mnbuild', + '-m 1024', + '-k en-us', '-kernel', kernel, - '-drive', 'file=%s,if=virtio' % cow, - '-append', - ' "root=/dev/vda' - ' init=/sbin/init' -# ' init=/usr/lib/cloud-init/uncloud-init' -# ' ds=nocloud' -# ' --verbose' - ' console=ttyS0"' - ] - + '-drive file=%s,if=virtio' % cow, + '-append "root=/dev/vda init=/sbin/init console=ttyS0" ' ] cmd = ' '.join( cmd ) print '* STARTING VM' print cmd vm = pexpect.spawn( cmd, timeout=TIMEOUT ) - logfile = NamedTemporaryFile( prefix='mn-build-expect' ) - print '* Logging results to', logfile.name - vm.logfile_read = logfile - return vm, logfile + return vm + def interact( vm ): "Interact with vm, which is a pexpect object" @@ -288,7 +309,8 @@ def interact( vm ): print '* Running VM install script' vm.sendline( 'bash install-mininet-vm.sh' ) print '* Waiting for script to complete... ' - vm.expect( 'Done preparing Mininet' ) + # Gigantic timeout for now ;-( + vm.expect( 'Done preparing Mininet', timeout=3600 ) print '* Completed successfully' vm.expect( prompt ) print '* Testing Mininet' @@ -312,18 +334,74 @@ def interact( vm ): vm.read() print '* Interaction complete' -image = 'raring-server-amd64' -start = time() -cow, kernel = makeCOWDisk( image ) -print '* VM image for', image, 'created as', cow -vm, logfile = boot( cow, kernel ) -interact( vm ) -logfile.close() -end = time() -elapsed = end - start -print '* Results logged to', logfile.name -print '* Completed in %.2f seconds' % elapsed -print '* DONE!!!!! :D' +def cleanup(): + "Clean up leftover qemu-nbd processes and other junk" + call( 'sudo pkill -9 qemu-nbd', shell=True ) +def convert( cow, basename ): + """Convert a qcow2 disk to a vmdk and put it a new directory + basename: base name for output vmdk file""" + dir = mkdtemp( prefix=basename, dir='.' ) + vmdk = '%s/%s.vmdk' % ( dir, basename ) + print '* Converting qcow2 to vmdk' + run( 'qemu-img convert -f qcow2 -O vmdk %s %s' % ( cow, vmdk ) ) + return vmdk + + +def build( flavor='raring-server-amd64' ): + "Build a Mininet VM" + start = time() + cow, kernel = makeCOWDisk( flavor ) + print '* VM image for', flavor, 'created as', cow + with NamedTemporaryFile( + prefix='mn-build-%s-' % flavor, suffix='.log', dir='.' ) as logfile: + print '* Logging results to', logfile.name + vm = boot( cow, kernel, logfile ) + vm.logfile_read = logfile + interact( vm ) + # cow is a temporary file and will go away when we quit! + # We convert it to a .vmdk which can be used in most VMMs + vmdk = convert( cow, basename=flavor ) + print '* Converted VM image stored as', vmdk + end = time() + elapsed = end - start + print '* Results logged to', logfile.name + print '* Completed in %.2f seconds' % elapsed + print '* %s VM build DONE!!!!! :D' % flavor + print + + +def parseArgs(): + "Parse command line arguments and run" + parser = argparse.ArgumentParser( description='Mininet VM build script' ) + parser.add_argument( '--depend', action='store_true', + help='Install dependencies for this script' ) + parser.add_argument( '--list', action='store_true', + help='list valid build flavors' ) + parser.add_argument( '--clean', action='store_true', + help='clean up leftover build junk (e.g. qemu-nbd)' ) + parser.add_argument( 'flavor', nargs='*', + help='VM flavor to build (e.g. raring32server)' ) + args = parser.parse_args( argv ) + if args.depend: + depend() + if args.list: + print 'valid build flavors:', ' '.join( ImageURLBase ) + if args.clean: + cleanup() + flavors = args.flavor[ 1: ] + for flavor in flavors: + if flavor not in ImageURLBase: + parser.print_help() + # try: + build( flavor ) + # except Exception as e: + # print '* BUILD FAILED with exception: ', e + # exit( 1 ) + if not ( args.depend or args.list or args.clean or flavors ): + parser.print_help() + +if __name__ == '__main__': + parseArgs()