Final gasp of cloud image version.

This commit is contained in:
Bob Lantz
2013-08-22 18:40:10 -07:00
parent 94954177e5
commit 14903d6a05
+136 -98
View File
@@ -36,41 +36,42 @@ Notes du jour:
- and use pexpect to interact with it on the serial console - 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: More notes:
We really want a full, partitioned disk image! - 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.
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 - grub-install fails miserably unless you load part_msdos !!
configuratin to use a serial console.
/etc/default/grub: - Installing TexLive is just painful - I would like to avoid it
GRUB_TERMINAL=serial if we could... wireshark plugin build is also slow and painful...
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 - Maybe we want to install our own packages for these things...
that would make the whole installation process a lot easier,
by the way, we should use wget -c but it would mean that we don't automatically get upstream
updates
""" """
import os import os
from os import stat from os import stat
from stat import ST_MODE 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 sys import exit, argv
from glob import glob from glob import glob
from urllib import urlretrieve from urllib import urlretrieve
from subprocess import check_output, call, Popen, PIPE from subprocess import check_output, call, Popen, PIPE
from tempfile import mkdtemp from tempfile import mkdtemp
from time import time from time import time, strftime, localtime
import argparse import argparse
pexpect = None # For code check - imported dynamically pexpect = None # For code check - imported dynamically
@@ -90,10 +91,25 @@ ImageURLBase = {
'raring-server-cloudimg-amd64' '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 ): def run( cmd, **kwargs ):
"Convenient interface to check_output" "Convenient interface to check_output"
print cmd log( '-', cmd )
cmd = cmd.split() cmd = cmd.split()
return check_output( cmd, **kwargs ) return check_output( cmd, **kwargs )
@@ -105,7 +121,7 @@ def srun( cmd, **kwargs ):
def depend(): def depend():
"Install packagedependencies" "Install packagedependencies"
print '* Installing package dependencies' log( '* Installing package dependencies' )
run( 'sudo apt-get -y update' ) run( 'sudo apt-get -y update' )
run( 'sudo apt-get install -y' run( 'sudo apt-get install -y'
' kvm cloud-utils genisoimage qemu-kvm qemu-utils' ' kvm cloud-utils genisoimage qemu-kvm qemu-utils'
@@ -117,7 +133,7 @@ def depend():
def popen( cmd ): def popen( cmd ):
"Convenient interface to popen" "Convenient interface to popen"
print cmd log( cmd )
cmd = cmd.split() cmd = cmd.split()
return Popen( cmd ) return Popen( cmd )
@@ -148,8 +164,8 @@ def fetchImage( image, path=None ):
tgz = path + '.disk1.img' tgz = path + '.disk1.img'
disk = path + '.img' disk = path + '.img'
kernel = path + '-vmlinuz-generic' kernel = path + '-vmlinuz-generic'
if exists( disk ) and exists( kernel ): if exists( disk ):
print '* Found', disk, 'and', kernel log( '* Found', disk )
# Detect race condition with multiple builds # Detect race condition with multiple builds
perms = stat( disk )[ ST_MODE ] & 0777 perms = stat( disk )[ ST_MODE ] & 0777
if perms != 0444: if perms != 0444:
@@ -160,13 +176,13 @@ def fetchImage( image, path=None ):
run( 'mkdir -p %s' % dir ) run( 'mkdir -p %s' % dir )
if not os.path.exists( tgz ): if not os.path.exists( tgz ):
url = imageURL( image ) + '.tar.gz' url = imageURL( image ) + '.tar.gz'
print '* Retrieving', url log( '* Retrieving', url )
urlretrieve( url, tgz ) urlretrieve( url, tgz )
print '* Extracting', tgz log( '* Extracting', tgz )
run( 'tar -C %s -xzf %s' % ( dir, tgz ) ) run( 'tar -C %s -xzf %s' % ( dir, tgz ) )
# Write-protect disk image so it remains pristine; # Write-protect disk image so it remains pristine;
# We will not use it directly but will use a COW disk # 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 ) os.chmod( disk, 0444 )
return disk, kernel return disk, kernel
@@ -174,22 +190,23 @@ def fetchImage( image, path=None ):
def addTo( file, line ): def addTo( file, line ):
"Add line to file if it's not there already" "Add line to file if it's not there already"
if call( [ 'sudo', 'grep', line, file ] ) != 0: 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 ): def disableCloud( bind ):
"Disable cloud junk for disk mounted at 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 ) modules = glob( '%s/etc/init/cloud*.conf' % bind )
for module in modules: for module in modules:
path, ext = splitext( module ) path, ext = splitext( module )
override = path + '.override' call( 'echo manual | sudo tee %s.override > /dev/null' % path,
call( 'echo manual | sudo tee ' + override, shell=True ) shell=True )
def addMininetUser( nbd ): def addMininetUser( nbd ):
"Add mininet user/group to filesystem" "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 # 1. We bind-mount / into a temporary directory, and
# then mount the volume's /etc and /home on top of it! # then mount the volume's /etc and /home on top of it!
mnt = mkdtemp() mnt = mkdtemp()
@@ -205,8 +222,8 @@ def addMininetUser( nbd ):
addTo( bind + '/etc/hosts', '127.0.1.1 mininet-vm' ) addTo( bind + '/etc/hosts', '127.0.1.1 mininet-vm' )
# 2. Next, we delete any old mininet user and add a new one # 2. Next, we delete any old mininet user and add a new one
chroot( 'deluser mininet' ) chroot( 'deluser mininet' )
chroot( 'useradd --create-home mininet' ) chroot( 'useradd --create-home --shell /bin/bash mininet' )
print '* Setting password' log( '* Setting password' )
call( 'echo mininet:mininet | sudo chroot %s chpasswd -c SHA512' call( 'echo mininet:mininet | sudo chroot %s chpasswd -c SHA512'
% bind, shell=True ) % bind, shell=True )
# 2a. Add mininet to sudoers # 2a. Add mininet to sudoers
@@ -215,7 +232,7 @@ def addMininetUser( nbd ):
disableCloud( bind ) disableCloud( bind )
chroot( 'sudo update-rc.d landscape-client disable' ) chroot( 'sudo update-rc.d landscape-client disable' )
# 2c. Add serial getty # 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( 'cp /etc/init/tty1.conf /etc/init/ttyS0.conf' )
chroot( 'sed -i "s/tty1/ttyS0/g" /etc/init/ttyS0.conf' ) chroot( 'sed -i "s/tty1/ttyS0/g" /etc/init/ttyS0.conf' )
# 3. Lastly, we umount and clean up everything # 3. Lastly, we umount and clean up everything
@@ -226,8 +243,6 @@ def addMininetUser( nbd ):
srun( 'umount ' + mnt ) srun( 'umount ' + mnt )
run( 'rmdir ' + bind ) run( 'rmdir ' + bind )
run( 'rmdir ' + mnt ) run( 'rmdir ' + mnt )
# 4. Just to make sure, we check the filesystem
srun( 'e2fsck -y ' + nbd )
def attachNBD( cow, flags='' ): def attachNBD( cow, flags='' ):
@@ -235,16 +250,15 @@ def attachNBD( cow, flags='' ):
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 # qemu-nbd requires an absolute path
cow = abspath( cow ) cow = abspath( cow )
print '* Checking for unused /dev/nbdX device ', log( '* Checking for unused /dev/nbdX device ' )
for i in range ( 0, 63 ): for i in range ( 0, 63 ):
nbd = '/dev/nbd%d' % i nbd = '/dev/nbd%d' % i
print i,
# Check whether someone's already messing with that device # Check whether someone's already messing with that device
if call( [ 'pgrep', '-f', nbd ] ) == 0: if call( [ 'pgrep', '-f', nbd ] ) == 0:
continue continue
# Fails without -v for some annoying reason... srun( 'modprobe nbd max-part=64' )
print
srun( 'qemu-nbd %s -c %s %s' % ( flags, nbd, cow ) ) srun( 'qemu-nbd %s -c %s %s' % ( flags, nbd, cow ) )
print
return nbd return nbd
raise Exception( "Error: could not find unused /dev/nbdX device" ) raise Exception( "Error: could not find unused /dev/nbdX device" )
@@ -258,11 +272,10 @@ def makeCOWDisk( image, dir='.' ):
"Create new COW disk for image" "Create new COW disk for image"
disk, kernel = fetchImage( image ) disk, kernel = fetchImage( image )
cow = '%s/%s.qcow2' % ( dir, 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 ) ) 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 ) run( 'qemu-img resize %s +8G' % cow )
srun( 'modprobe nbd max-part=64')
nbd = attachNBD( cow ) nbd = attachNBD( cow )
srun( 'e2fsck -y ' + nbd ) srun( 'e2fsck -y ' + nbd )
srun( 'resize2fs ' + nbd ) srun( 'resize2fs ' + nbd )
@@ -271,39 +284,58 @@ def makeCOWDisk( image, dir='.' ):
return cow, kernel return cow, kernel
def makeVolume( volume, cylinders=1000 ): def makeVolume( volume, size='8G' ):
"""Create volume as a qcow2 and add a single boot partition """Create volume as a qcow2 and add a single boot partition"""
cylinders: number of ~8MB (255*63*512) cylinders in volume""" log( '* Creating volume of size', size )
heads, sectors, bytes = 255, 63, 512
size = cylinders * heads * sectors * bytes
print '* Creating volume of size', size
run( 'qemu-img create -f qcow2 %s %s' % ( volume, 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!! # We need to mount it using qemu-nbd!!
nbd = attachNBD( volume ) nbd = attachNBD( volume )
# A bit hacky - we may change this to use parted(8) later parted = Popen( [ 'sudo', 'parted', nbd ], stdin=PIPE )
fdisk = Popen( [ 'sudo', 'fdisk', nbd ], stdin=PIPE ) cmds = [ 'mklabel msdos',
cmds = 'x\nc\n%d\nr\no\nn\np\n1\n\n\na\n1\nw\n' % cylinders 'mkpart primary ext4 1 %s' % size,
fdisk.stdin.write( cmds ) 'set 1 boot on',
fdisk.wait() 'quit' ]
print '* Volume partition table:' parted.stdin.write( '\n'.join( cmds ) + '\n' )
print srun( 'fdisk -l ' + nbd ) parted.wait()
log( '* Volume partition table:' )
log( srun( 'fdisk -l ' + nbd ) )
detachNBD( 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 ): 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' ) srcdev = attachNBD( partition, flags='-r' )
voldev = attachNBD( volume ) voldev = attachNBD( volume )
print srun( 'fdisk -l ' + voldev ) log( srun( 'fdisk -l ' + voldev ) )
print srun( 'partx ' + voldev ) log( srun( 'partx ' + voldev ) )
dstdev = voldev + 'p1' dstdev = voldev + 'p1'
print "* Copying partition from", srcdev, "to", dstdev log( "* Copying partition from", srcdev, "to", dstdev )
print srun( 'time dd if=%s of=%s bs=1M' % ( srcdev, dstdev ) ) log( srun( 'dd if=%s of=%s bs=1M' % ( srcdev, dstdev ) ) )
print '* Resizing and adding Mininet user' log( '* Resizing file system' )
srun( 'resize2fs ' + dstdev ) srun( 'resize2fs ' + dstdev )
srun( 'e2fsck -y ' + dstdev ) srun( 'e2fsck -y ' + dstdev )
log( '* Adding mininet user' )
addMininetUser( dstdev ) addMininetUser( dstdev )
log( '* Installing grub2' )
installGrub( voldev, partnum=1 )
detachNBD( voldev ) detachNBD( voldev )
detachNBD( srcdev ) detachNBD( srcdev )
@@ -322,7 +354,7 @@ def boot( cow, kernel, tap ):
elif 'i386' in kernel: elif 'i386' in kernel:
kvm = 'qemu-system-i386' kvm = 'qemu-system-i386'
else: else:
print "Error: can't discern CPU for image", cow log( "Error: can't discern CPU for image", cow )
exit( 1 ) exit( 1 )
cmd = [ 'sudo', kvm, cmd = [ 'sudo', kvm,
'-machine accel=kvm', '-machine accel=kvm',
@@ -335,8 +367,8 @@ def boot( cow, kernel, tap ):
'-drive file=%s,if=virtio' % cow, '-drive file=%s,if=virtio' % cow,
'-append "root=/dev/vda1 init=/sbin/init console=ttyS0" ' ] '-append "root=/dev/vda1 init=/sbin/init console=ttyS0" ' ]
cmd = ' '.join( cmd ) cmd = ' '.join( cmd )
print '* STARTING VM' log( '* STARTING VM' )
print cmd log( cmd )
vm = pexpect.spawn( cmd, timeout=TIMEOUT ) vm = pexpect.spawn( cmd, timeout=TIMEOUT )
return vm return vm
@@ -344,64 +376,64 @@ def boot( cow, kernel, tap ):
def interact( vm ): def interact( vm ):
"Interact with vm, which is a pexpect object" "Interact with vm, which is a pexpect object"
prompt = '\$ ' prompt = '\$ '
print '* Waiting for login prompt' log( '* Waiting for login prompt' )
vm.expect( 'login: ' ) vm.expect( 'login: ' )
print '* Logging in' log( '* Logging in' )
vm.sendline( 'mininet' ) vm.sendline( 'mininet' )
print '* Waiting for password prompt' log( '* Waiting for password prompt' )
vm.expect( 'Password: ' ) vm.expect( 'Password: ' )
print '* Sending password' log( '* Sending password' )
vm.sendline( 'mininet' ) vm.sendline( 'mininet' )
print '* Waiting for login...' log( '* Waiting for login...' )
vm.expect( prompt ) vm.expect( prompt )
print '* Sending hostname command' log( '* Sending hostname command' )
vm.sendline( 'hostname' ) vm.sendline( 'hostname' )
print '* Waiting for output' log( '* Waiting for output' )
vm.expect( prompt ) vm.expect( prompt )
print '* Fetching Mininet VM install script' log( '* Fetching Mininet VM install script' )
vm.sendline( 'wget ' vm.sendline( 'wget '
'https://raw.github.com/mininet/mininet/master/util/vm/' 'https://raw.github.com/mininet/mininet/master/util/vm/'
'install-mininet-vm.sh' ) 'install-mininet-vm.sh' )
vm.expect( prompt ) vm.expect( prompt )
print '* Running VM install script' log( '* Running VM install script' )
vm.sendline( 'bash install-mininet-vm.sh' ) vm.sendline( 'bash install-mininet-vm.sh' )
print '* Waiting for script to complete... ' log( '* Waiting for script to complete... ' )
# Gigantic timeout for now ;-( # Gigantic timeout for now ;-(
vm.expect( 'Done preparing Mininet', timeout=3600 ) vm.expect( 'Done preparing Mininet', timeout=3600 )
print '* Completed successfully' log( '* Completed successfully' )
vm.expect( prompt ) vm.expect( prompt )
print '* Testing Mininet' log( '* Testing Mininet' )
vm.sendline( 'sudo mn --test pingall' ) vm.sendline( 'sudo mn --test pingall' )
if vm.expect( [ ' 0% dropped', pexpect.TIMEOUT ], timeout=45 ): if vm.expect( [ ' 0% dropped', pexpect.TIMEOUT ], timeout=45 ) == 0:
print '* Sanity check succeeded' log( '* Sanity check succeeded' )
else: else:
print '* Sanity check FAILED' log( '* Sanity check FAILED' )
vm.expect( prompt ) vm.expect( prompt )
print '* Making sure cgroups are mounted' log( '* Making sure cgroups are mounted' )
vm.sendline( 'sudo service cgroup-lite restart' ) vm.sendline( 'sudo service cgroup-lite restart' )
vm.expect( prompt ) vm.expect( prompt )
vm.sendline( 'sudo cgroups-mount' ) vm.sendline( 'sudo cgroups-mount' )
vm.expect( prompt ) vm.expect( prompt )
print '* Running make test' log( '* Running make test' )
vm.sendline( 'cd ~/mininet; sudo make test' ) vm.sendline( 'cd ~/mininet; sudo make test' )
vm.expect( prompt ) vm.expect( prompt )
print '* Shutting down' log( '* Shutting down' )
vm.sendline( 'sync; sudo shutdown -h now' ) vm.sendline( 'sync; sudo shutdown -h now' )
print '* Waiting for EOF/shutdown' log( '* Waiting for EOF/shutdown' )
vm.read() vm.read()
print '* Interaction complete' log( '* Interaction complete' )
def cleanup(): def cleanup():
"Clean up leftover qemu-nbd processes and other junk" "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 ): def convert( cow, basename ):
"""Convert a qcow2 disk to a vmdk and put it a new directory """Convert a qcow2 disk to a vmdk and put it a new directory
basename: base name for output vmdk file""" basename: base name for output vmdk file"""
vmdk = basename + '.vmdk' 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 ) ) run( 'qemu-img convert -f qcow2 -O vmdk %s %s' % ( cow, vmdk ) )
return vmdk return vmdk
@@ -411,28 +443,32 @@ def build( flavor='raring32server' ):
start = time() start = time()
dir = mkdtemp( prefix=flavor + '-result-', dir='.' ) dir = mkdtemp( prefix=flavor + '-result-', dir='.' )
os.chdir( dir ) os.chdir( dir )
print '* Created working directory', dir log( '* Created working directory', dir )
image, kernel = fetchImage( flavor ) image, kernel = fetchImage( flavor )
volume = flavor + '.qcow2' volume = flavor + '.qcow2'
makeVolume( volume ) makeVolume( volume )
initPartition( image, 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+' ) 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 = boot( volume, kernel, logfile )
vm.logfile_read = logfile vm.logfile_read = logfile
interact( vm ) interact( vm )
vmdk = convert( volume, basename=flavor ) vmdk = convert( volume, basename=flavor )
print '* Converted VM image stored as', vmdk log( '* Converted VM image stored as', vmdk )
end = time() end = time()
elapsed = end - start elapsed = end - start
print '* Results logged to', abspath( logfile.name ) log( '* Results logged to', abspath( logfile.name ) )
print '* Completed in %.2f seconds' % elapsed log( '* Completed in %.2f seconds' % elapsed )
print '* %s VM build DONE!!!!! :D' % flavor log( '* %s VM build DONE!!!!! :D' % flavor )
print log( '* ' )
os.chdir( '..' ) os.chdir( '..' )
def listFlavors():
"List valid build flavors"
print '\nvalid build flavors:', ' '.join( ImageURLBase ), '\n'
def parseArgs(): def parseArgs():
"Parse command line arguments and run" "Parse command line arguments and run"
parser = argparse.ArgumentParser( description='Mininet VM build script' ) parser = argparse.ArgumentParser( description='Mininet VM build script' )
@@ -448,17 +484,19 @@ def parseArgs():
if args.depend: if args.depend:
depend() depend()
if args.list: if args.list:
print 'valid build flavors:', ' '.join( ImageURLBase ) listFlavors()
if args.clean: if args.clean:
cleanup() cleanup()
flavors = args.flavor[ 1: ] flavors = args.flavor[ 1: ]
for flavor in flavors: for flavor in flavors:
if flavor not in ImageURLBase: if flavor not in ImageURLBase:
parser.print_help() parser.print_help()
listFlavors()
break
# try: # try:
build( flavor ) build( flavor )
# except Exception as e: # except Exception as e:
# print '* BUILD FAILED with exception: ', e # log( '* BUILD FAILED with exception: ', e )
# exit( 1 ) # exit( 1 )
if not ( args.depend or args.list or args.clean or flavors ): if not ( args.depend or args.list or args.clean or flavors ):
parser.print_help() parser.print_help()