322 lines
9.0 KiB
Python
Executable File
322 lines
9.0 KiB
Python
Executable File
#!/usr/bin/python
|
|
|
|
"""
|
|
build.py: build a Mininet VM
|
|
|
|
Basic idea:
|
|
|
|
prepare
|
|
- download cloud image if it's missing
|
|
- write-protect it
|
|
|
|
build
|
|
-> create cow disk for vm
|
|
-> boot it in qemu/kvm with text /serial console
|
|
-> install Mininet
|
|
|
|
test
|
|
-> make codecheck
|
|
-> make test
|
|
|
|
release
|
|
-> shut down VM
|
|
-> 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
|
|
|
|
Something to think about:
|
|
|
|
Maybe download the cloud image and customize it so that
|
|
it is an actual usable/bootable image???
|
|
|
|
|
|
"""
|
|
|
|
|
|
import os
|
|
from os.path import exists, splitext
|
|
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
|
|
|
|
# boot can be slooooow!!!! need to debug/optimize somehow
|
|
TIMEOUT=120
|
|
|
|
VMImageDir = os.environ[ 'HOME' ] + '/vm-images'
|
|
|
|
ImageURLBase = {
|
|
'raring-server-amd64':
|
|
'http://cloud-images.ubuntu.com/raring/current/'
|
|
'raring-server-cloudimg-amd64'
|
|
}
|
|
|
|
|
|
def run( cmd, **kwargs ):
|
|
"Convenient interface to check_output"
|
|
print cmd
|
|
cmd = cmd.split()
|
|
return check_output( cmd, **kwargs )
|
|
|
|
|
|
def srun( cmd, **kwargs ):
|
|
"Run + sudo"
|
|
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 popen( cmd ):
|
|
"Convenient interface to popen"
|
|
print cmd
|
|
cmd = cmd.split()
|
|
return Popen( cmd )
|
|
|
|
|
|
def remove( fname ):
|
|
"rm -f 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 + '.tar.gz'
|
|
disk = path + '.img'
|
|
kernel = path + '-vmlinuz-generic'
|
|
floppy = path + '-floppy'
|
|
if exists( disk ) and exists( kernel ):
|
|
print '* Found', disk, 'and', kernel
|
|
else:
|
|
dir = os.path.dirname( path )
|
|
run( 'mkdir -p %s' % dir )
|
|
if not os.path.exists( tgz ):
|
|
url = imageURL( image ) + '.tar.gz'
|
|
print '* Retrieving', url
|
|
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 )
|
|
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' % ( line, file ), shell=True )
|
|
|
|
|
|
def disableCloud( bind ):
|
|
"Disable cloud junk for disk mounted at bind"
|
|
print '* 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 )
|
|
|
|
|
|
def addMininetUser( img ):
|
|
"Add mininet user/group to root filesystem image"
|
|
print '* Adding mininet user to', img
|
|
# 1. We bind-mount / into a temporary directory, and
|
|
# then mount the volume's /etc and /home on top of it!
|
|
bind = mkdtemp()
|
|
mnt = mkdtemp()
|
|
srun( 'mount -B / ' + bind )
|
|
srun( 'mount %s %s' % ( img, mnt ) )
|
|
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 )
|
|
# 2. Next, we delete any old mininet user and add a new one
|
|
chroot( 'deluser mininet' )
|
|
chroot( 'useradd --create-home mininet' )
|
|
print '* 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
|
|
print '* 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/etc ' % bind )
|
|
srun( 'umount %s/home ' % bind )
|
|
srun( 'umount %s' % bind )
|
|
srun( 'umount ' + mnt )
|
|
run( 'rmdir ' + mnt )
|
|
# 4. Just to make sure, we check the filesystem
|
|
run( 'e2fsck -y ' + img )
|
|
|
|
|
|
def makeCOWDisk( image ):
|
|
"Create new COW disk for image"
|
|
disk, kernel = fetchImage( image )
|
|
cow = disk + '.qcow2'
|
|
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')
|
|
srun( 'qemu-nbd -c /dev/nbd0 ' + cow )
|
|
srun( 'e2fsck -fy /dev/nbd0' )
|
|
srun( 'resize2fs /dev/nbd0' )
|
|
srun( 'qemu-nbd -d /dev/nbd0' )
|
|
return cow, kernel
|
|
|
|
|
|
def boot( cow, kernel ):
|
|
"""Boot qemu/kvm with a COW disk and local/user data store
|
|
returns: popen object to qemu process"""
|
|
if 'amd64' in cow:
|
|
kvm = 'qemu-system-x86_64'
|
|
elif 'i386' in cow:
|
|
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 +
|
|
cmd = [ 'sudo', kvm,
|
|
'-machine', 'accel=kvm',
|
|
'-nographic',
|
|
'-m', '512',
|
|
'-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"'
|
|
]
|
|
|
|
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
|
|
|
|
def interact( vm ):
|
|
"Interact with vm, which is a pexpect object"
|
|
prompt = '\$ '
|
|
print '* Waiting for login prompt'
|
|
vm.expect( 'login: ' )
|
|
print '* Logging in'
|
|
vm.sendline( 'mininet' )
|
|
print '* Waiting for password prompt'
|
|
vm.expect( 'Password: ' )
|
|
print '* Sending password'
|
|
vm.sendline( 'mininet' )
|
|
print '* Waiting for login...'
|
|
vm.expect( prompt )
|
|
print '* Sending hostname command'
|
|
vm.sendline( 'hostname' )
|
|
print '* Waiting for output'
|
|
vm.expect( prompt )
|
|
print '* 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'
|
|
vm.sendline( 'bash install-mininet-vm.sh' )
|
|
print '* Waiting for script to complete... '
|
|
vm.expect( 'Done preparing Mininet' )
|
|
print '* Completed successfully'
|
|
vm.expect( prompt )
|
|
print '* Testing Mininet'
|
|
vm.sendline( 'sudo mn --test pingall' )
|
|
vm.expect( '0% dropped' )
|
|
print '* Basic test succeeded'
|
|
vm.expect( prompt )
|
|
print '* Running make test'
|
|
vm.sendline( 'cd ~/mininet; sudo make test' )
|
|
vm.expect( 'OK' )
|
|
print '* Basic test succeeded'
|
|
vm.expect( 'OK' )
|
|
print '* Hi-Fi test succeeded'
|
|
vm.expect( prompt )
|
|
print '* Shutting down'
|
|
vm.sendline( 'sync; sudo shutdown -h now' )
|
|
print '* Waiting for EOF/shutdown'
|
|
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'
|
|
|
|
|