Update and rename MantenerFIFO to MantenerFIFO.md
[vsorcdistro/.git] / mininet / util / vm / build.py
1 #!/usr/bin/python
2
3 """
4 build.py: build a Mininet VM
5
6 Basic idea:
7
8     prepare
9     -> create base install image if it's missing
10         - download iso if it's missing
11         - install from iso onto image
12
13     build
14     -> create cow disk for new VM, based on base image
15     -> boot it in qemu/kvm with text /serial console
16     -> install Mininet
17
18     test
19     -> sudo mn --test pingall
20     -> make test
21
22     release
23     -> shut down VM
24     -> shrink-wrap VM
25     -> upload to storage
26
27 """
28
29 import os
30 from os import stat, path
31 from stat import ST_MODE, ST_SIZE
32 from os.path import abspath
33 from sys import exit, stdout, argv, modules
34 import re
35 from glob import glob
36 from subprocess import check_output, call, Popen
37 from tempfile import mkdtemp, NamedTemporaryFile
38 from time import time, strftime, localtime
39 import argparse
40 from distutils.spawn import find_executable
41 import inspect
42
43 pexpect = None  # For code check - imported dynamically
44
45 # boot can be slooooow!!!! need to debug/optimize somehow
46 TIMEOUT=600
47
48 # Some configuration options
49 # Possibly change this to use the parsed arguments instead!
50
51 LogToConsole = False        # VM output to console rather than log file
52 SaveQCOW2 = False           # Save QCOW2 image rather than deleting it
53 NoKVM = False               # Don't use kvm and use emulation instead
54 Branch = None               # Branch to update and check out before testing
55 Zip = False                 # Archive .ovf and .vmdk into a .zip file
56 Forward = []                # VM port forwarding options (-redir)
57 Chown = ''                  # Build directory owner
58
59 VMImageDir = os.environ[ 'HOME' ] + '/vm-images'
60
61 Prompt = '\$ '              # Shell prompt that pexpect will wait for
62
63 isoURLs = {
64     'precise32server':
65     'http://mirrors.kernel.org/ubuntu-releases/12.04/'
66     'ubuntu-12.04.5-server-i386.iso',
67     'precise64server':
68     'http://mirrors.kernel.org/ubuntu-releases/12.04/'
69     'ubuntu-12.04.5-server-amd64.iso',
70     'trusty32server':
71     'http://mirrors.kernel.org/ubuntu-releases/14.04/'
72     'ubuntu-14.04.4-server-i386.iso',
73     'trusty64server':
74     'http://mirrors.kernel.org/ubuntu-releases/14.04/'
75     'ubuntu-14.04.4-server-amd64.iso',
76     'wily32server':
77     'http://mirrors.kernel.org/ubuntu-releases/15.10/'
78     'ubuntu-15.10-server-i386.iso',
79     'wily64server':
80     'http://mirrors.kernel.org/ubuntu-releases/15.10/'
81     'ubuntu-15.10-server-amd64.iso',
82     'xenial32server':
83     'http://mirrors.kernel.org/ubuntu-releases/16.04/'
84     'ubuntu-16.04.1-server-i386.iso',
85     'xenial64server':
86     'http://mirrors.kernel.org/ubuntu-releases/16.04/'
87     'ubuntu-16.04.1-server-amd64.iso',
88 }
89
90
91 def OSVersion( flavor ):
92     "Return full OS version string for build flavor"
93     urlbase = path.basename( isoURLs.get( flavor, 'unknown' ) )
94     return path.splitext( urlbase )[ 0 ]
95
96 def OVFOSNameID( flavor ):
97     "Return OVF-specified ( OS Name, ID ) for flavor"
98     version = OSVersion( flavor )
99     arch = archFor( flavor )
100     if 'ubuntu' in version:
101         map = { 'i386': ( 'Ubuntu', 93 ),
102                 'x86_64': ( 'Ubuntu 64-bit', 94 ) }
103     else:
104         map = { 'i386': ( 'Linux', 36 ),
105                 'x86_64': ( 'Linux 64-bit', 101 ) }
106     osname, osid = map[ arch ]
107     return osname, osid
108
109 LogStartTime = time()
110 LogFile = None
111
112 def log( *args, **kwargs ):
113     """Simple log function: log( message along with local and elapsed time
114        cr: False/0 for no CR"""
115     cr = kwargs.get( 'cr', True )
116     elapsed = time() - LogStartTime
117     clocktime = strftime( '%H:%M:%S', localtime() )
118     msg = ' '.join( str( arg ) for arg in args )
119     output = '%s [ %.3f ] %s' % ( clocktime, elapsed, msg )
120     if cr:
121         print( output )
122     else:
123         print( output, )
124     # Optionally mirror to LogFile
125     if type( LogFile ) is file:
126         if cr:
127             output += '\n'
128         LogFile.write( output )
129         LogFile.flush()
130
131
132 def run( cmd, **kwargs ):
133     "Convenient interface to check_output"
134     log( '-', cmd )
135     cmd = cmd.split()
136     arg0 = cmd[ 0 ]
137     if not find_executable( arg0 ):
138         raise Exception( 'Cannot find executable "%s";' % arg0 +
139                          'you might try %s --depend' % argv[ 0 ] )
140     return check_output( cmd, **kwargs )
141
142
143 def srun( cmd, **kwargs ):
144     "Run + sudo"
145     return run( 'sudo ' + cmd, **kwargs )
146
147
148 # BL: we should probably have a "checkDepend()" which
149 # checks to make sure all dependencies are satisfied!
150
151 def depend():
152     "Install package dependencies"
153     log( '* Installing package dependencies' )
154     run( 'sudo apt-get -qy update' )
155     run( 'sudo apt-get -qy install'
156          ' kvm cloud-utils genisoimage qemu-kvm qemu-utils'
157          ' e2fsprogs curl'
158          ' python-setuptools mtools zip' )
159     run( 'sudo easy_install pexpect' )
160
161
162 def popen( cmd ):
163     "Convenient interface to popen"
164     log( cmd )
165     cmd = cmd.split()
166     return Popen( cmd )
167
168
169 def remove( fname ):
170     "Remove a file, ignoring errors"
171     try:
172         os.remove( fname )
173     except OSError:
174         pass
175
176
177 def findiso( flavor ):
178     "Find iso, fetching it if it's not there already"
179     url = isoURLs[ flavor ]
180     name = path.basename( url )
181     iso = path.join( VMImageDir, name )
182     if not path.exists( iso ) or ( stat( iso )[ ST_MODE ] & 0777 != 0444 ):
183         log( '* Retrieving', url )
184         run( 'curl -C - -o %s %s' % ( iso, url ) )
185         # Make sure the file header/type is something reasonable like
186         # 'ISO' or 'x86 boot sector', and not random html or text
187         result = run( 'file ' + iso )
188         if 'ISO' not in result and 'boot' not in result:
189             os.remove( iso )
190             raise Exception( 'findiso: could not download iso from ' + url )
191         # Write-protect iso, signaling it is complete
192         log( '* Write-protecting iso', iso)
193         os.chmod( iso, 0444 )
194     log( '* Using iso', iso )
195     return iso
196
197
198 def attachNBD( cow, flags='' ):
199     """Attempt to attach a COW disk image and return its nbd device
200         flags: additional flags for qemu-nbd (e.g. -r for readonly)"""
201     # qemu-nbd requires an absolute path
202     cow = abspath( cow )
203     log( '* Checking for unused /dev/nbdX device ' )
204     for i in range ( 0, 63 ):
205         nbd = '/dev/nbd%d' % i
206         # Check whether someone's already messing with that device
207         if call( [ 'pgrep', '-f', nbd ] ) == 0:
208             continue
209         srun( 'modprobe nbd max-part=64' )
210         srun( 'qemu-nbd %s -c %s %s' % ( flags, nbd, cow ) )
211         print()
212         return nbd
213     raise Exception( "Error: could not find unused /dev/nbdX device" )
214
215
216 def detachNBD( nbd ):
217     "Detatch an nbd device"
218     srun( 'qemu-nbd -d ' + nbd )
219
220
221 def extractKernel( image, flavor, imageDir=VMImageDir ):
222     "Extract kernel and initrd from base image"
223     kernel = path.join( imageDir, flavor + '-vmlinuz' )
224     initrd = path.join( imageDir, flavor + '-initrd' )
225     if path.exists( kernel ) and ( stat( image )[ ST_MODE ] & 0777 ) == 0444:
226         # If kernel is there, then initrd should also be there
227         return kernel, initrd
228     log( '* Extracting kernel to', kernel )
229     nbd = attachNBD( image, flags='-r' )
230     try:
231         print( srun( 'partx ' + nbd ) )
232     except:
233         log( 'Warning - partx failed with error' )
234     # Assume kernel is in partition 1/boot/vmlinuz*generic for now
235     part = nbd + 'p1'
236     mnt = mkdtemp()
237     srun( 'mount -o ro,noload %s %s' % ( part, mnt  ) )
238     kernsrc = glob( '%s/boot/vmlinuz*generic' % mnt )[ 0 ]
239     initrdsrc = glob( '%s/boot/initrd*generic' % mnt )[ 0 ]
240     srun( 'cp %s %s' % ( initrdsrc, initrd ) )
241     srun( 'chmod 0444 ' + initrd )
242     srun( 'cp %s %s' % ( kernsrc, kernel ) )
243     srun( 'chmod 0444 ' + kernel )
244     srun( 'umount ' + mnt )
245     run( 'rmdir ' + mnt )
246     detachNBD( nbd )
247     return kernel, initrd
248
249
250 def findBaseImage( flavor, size='8G' ):
251     "Return base VM image and kernel, creating them if needed"
252     image = path.join( VMImageDir, flavor + '-base.qcow2' )
253     if path.exists( image ):
254         # Detect race condition with multiple builds
255         perms = stat( image )[ ST_MODE ] & 0777
256         if perms != 0444:
257             raise Exception( 'Error - base image %s is writable.' % image +
258                              ' Are multiple builds running? if not,'
259                              ' remove %s and try again.' % image )
260     else:
261         # We create VMImageDir here since we are called first
262         run( 'mkdir -p %s' % VMImageDir )
263         iso = findiso( flavor )
264         log( '* Creating image file', image )
265         run( 'qemu-img create -f qcow2 %s %s' % ( image, size ) )
266         installUbuntu( iso, image )
267         # Write-protect image, also signaling it is complete
268         log( '* Write-protecting image', image)
269         os.chmod( image, 0444 )
270     kernel, initrd = extractKernel( image, flavor )
271     log( '* Using base image', image, 'and kernel', kernel )
272     return image, kernel, initrd
273
274
275 # Kickstart and Preseed files for Ubuntu/Debian installer
276 #
277 # Comments: this is really clunky and painful. If Ubuntu
278 # gets their act together and supports kickstart a bit better
279 # then we can get rid of preseed and even use this as a
280 # Fedora installer as well.
281 #
282 # Another annoying thing about Ubuntu is that it can't just
283 # install a normal system from the iso - it has to download
284 # junk from the internet, making this house of cards even
285 # more precarious.
286
287 KickstartText ="""
288 #Generated by Kickstart Configurator
289 #platform=x86
290
291 #System language
292 lang en_US
293 #Language modules to install
294 langsupport en_US
295 #System keyboard
296 keyboard us
297 #System mouse
298 mouse
299 #System timezone
300 timezone America/Los_Angeles
301 #Root password
302 rootpw --disabled
303 #Initial user
304 user mininet --fullname "mininet" --password "mininet"
305 #Use text mode install
306 text
307 #Install OS instead of upgrade
308 install
309 #Use CDROM installation media
310 cdrom
311 #System bootloader configuration
312 bootloader --location=mbr
313 #Clear the Master Boot Record
314 zerombr yes
315 #Partition clearing information
316 clearpart --all --initlabel
317 #Automatic partitioning
318 autopart
319 #System authorization information
320 auth  --useshadow  --enablemd5
321 #Firewall configuration
322 firewall --disabled
323 #Do not configure the X Window System
324 skipx
325 """
326
327 # Tell the Ubuntu/Debian installer to stop asking stupid questions
328
329 PreseedText = ( """
330 """
331 #d-i mirror/country string manual
332 #d-i mirror/http/hostname string mirrors.kernel.org
333 """
334 d-i mirror/http/directory string /ubuntu
335 d-i mirror/http/proxy string
336 d-i partman/confirm_write_new_label boolean true
337 d-i partman/choose_partition select finish
338 d-i partman/confirm boolean true
339 d-i partman/confirm_nooverwrite boolean true
340 d-i user-setup/allow-password-weak boolean true
341 d-i finish-install/reboot_in_progress note
342 d-i debian-installer/exit/poweroff boolean true
343 """ )
344
345 def makeKickstartFloppy():
346     "Create and return kickstart floppy, kickstart, preseed"
347     kickstart = 'ks.cfg'
348     with open( kickstart, 'w' ) as f:
349         f.write( KickstartText )
350     preseed = 'ks.preseed'
351     with open( preseed, 'w' ) as f:
352         f.write( PreseedText )
353     # Create floppy and copy files to it
354     floppy = 'ksfloppy.img'
355     run( 'qemu-img create %s 1440k' % floppy )
356     run( 'mkfs -t msdos ' + floppy )
357     run( 'mcopy -i %s %s ::/' % ( floppy, kickstart ) )
358     run( 'mcopy -i %s %s ::/' % ( floppy, preseed ) )
359     return floppy, kickstart, preseed
360
361
362 def archFor( filepath ):
363     "Guess architecture for file path"
364     name = path.basename( filepath )
365     if 'amd64' in name or 'x86_64' in name:
366         arch = 'x86_64'
367     # Beware of version 64 of a 32-bit OS
368     elif 'i386' in name or '32' in name or 'x86' in name:
369         arch = 'i386'
370     elif '64' in name:
371         arch = 'x86_64'
372     else:
373         log( "Error: can't discern CPU for name", name )
374         exit( 1 )
375     return arch
376
377
378 def installUbuntu( iso, image, logfilename='install.log', memory=1024 ):
379     "Install Ubuntu from iso onto image"
380     kvm = 'qemu-system-' + archFor( iso )
381     floppy, kickstart, preseed = makeKickstartFloppy()
382     # Mount iso so we can use its kernel
383     mnt = mkdtemp()
384     srun( 'mount %s %s' % ( iso, mnt ) )
385     kernel = path.join( mnt, 'install/vmlinuz' )
386     initrd = path.join( mnt, 'install/initrd.gz' )
387     if NoKVM:
388         accel = 'tcg'
389     else:
390         accel = 'kvm'
391         try:
392             run( 'kvm-ok' )
393         except:
394             raise Exception( 'kvm-ok failed; try using --nokvm' )
395     cmd = [ 'sudo', kvm,
396            '-machine', 'accel=%s' % accel,
397            '-nographic',
398            '-netdev', 'user,id=mnbuild',
399            '-device', 'virtio-net,netdev=mnbuild',
400            '-m', str( memory ),
401            '-k', 'en-us',
402            '-fda', floppy,
403            '-drive', 'file=%s,if=virtio' % image,
404            '-cdrom', iso,
405            '-kernel', kernel,
406            '-initrd', initrd,
407            '-append',
408            ' ks=floppy:/' + kickstart +
409            ' preseed/file=floppy://' + preseed +
410            ' console=ttyS0' ]
411     ubuntuStart = time()
412     log( '* INSTALLING UBUNTU FROM', iso, 'ONTO', image )
413     log( ' '.join( cmd ) )
414     log( '* logging to', abspath( logfilename ) )
415     params = {}
416     if not LogToConsole:
417         logfile = open( logfilename, 'w' )
418         params = { 'stdout': logfile, 'stderr': logfile }
419     vm = Popen( cmd, **params )
420     log( '* Waiting for installation to complete')
421     vm.wait()
422     if not LogToConsole:
423         logfile.close()
424     elapsed = time() - ubuntuStart
425     # Unmount iso and clean up
426     srun( 'umount ' + mnt )
427     run( 'rmdir ' + mnt )
428     if vm.returncode != 0:
429         raise Exception( 'Ubuntu installation returned error %d' %
430                           vm.returncode )
431     log( '* UBUNTU INSTALLATION COMPLETED FOR', image )
432     log( '* Ubuntu installation completed in %.2f seconds' % elapsed )
433
434
435 def boot( cow, kernel, initrd, logfile, memory=1024, cpuCores=1 ):
436     """Boot qemu/kvm with a COW disk and local/user data store
437        cow: COW disk path
438        kernel: kernel path
439        logfile: log file for pexpect object
440        memory: memory size in MB
441        cpuCores: number of CPU cores to use
442        returns: pexpect object to qemu process"""
443     # pexpect might not be installed until after depend() is called
444     global pexpect
445     if not pexpect:
446         import pexpect
447     class Spawn( pexpect.spawn ):
448         "Subprocess is sudo, so we have to sudo kill it"
449         def close( self, force=False ):
450             srun( 'kill %d' % self.pid )
451     arch = archFor( kernel )
452     log( '* Detected kernel architecture', arch )
453     if NoKVM:
454         accel = 'tcg'
455     else:
456         accel = 'kvm'
457     cmd = [ 'sudo', 'qemu-system-' + arch,
458             '-machine accel=%s' % accel,
459             '-nographic',
460             '-netdev user,id=mnbuild',
461             '-device virtio-net,netdev=mnbuild',
462             '-m %s' % memory,
463             '-k en-us',
464             '-kernel', kernel,
465             '-initrd', initrd,
466             '-drive file=%s,if=virtio' % cow,
467             '-append "root=/dev/vda1 init=/sbin/init console=ttyS0" ' ]
468     if Forward:
469         cmd += sum( [ [ '-redir', f ] for f in Forward ], [] )
470     if cpuCores > 1:
471         cmd += [ '-smp cores=%s' % cpuCores ]
472     cmd = ' '.join( cmd )
473     log( '* BOOTING VM FROM', cow )
474     log( cmd )
475     vm = Spawn( cmd, timeout=TIMEOUT, logfile=logfile )
476     return vm
477
478
479 def login( vm, user='mininet', password='mininet' ):
480     "Log in to vm (pexpect object)"
481     log( '* Waiting for login prompt' )
482     vm.expect( 'login: ' )
483     log( '* Logging in' )
484     vm.sendline( user )
485     log( '* Waiting for password prompt' )
486     vm.expect( 'Password: ' )
487     log( '* Sending password' )
488     vm.sendline( password )
489     log( '* Waiting for login...' )
490
491
492 def removeNtpd( vm, prompt=Prompt, ntpPackage='ntp' ):
493     "Remove ntpd and set clock immediately"
494     log( '* Removing ntpd' )
495     vm.sendline( 'sudo -n apt-get -qy remove ' + ntpPackage )
496     vm.expect( prompt )
497     # Try to make sure that it isn't still running
498     vm.sendline( 'sudo -n pkill ntpd' )
499     vm.expect( prompt )
500     log( '* Getting seconds since epoch from this server' )
501     # Note r'date +%s' specifies a format for 'date', not python!
502     seconds = int( run( r'date +%s' ) )
503     log( '* Setting VM clock' )
504     vm.sendline( 'sudo -n date -s @%d' % seconds )
505
506
507 def sanityTest( vm ):
508     "Run Mininet sanity test (pingall) in vm"
509     vm.sendline( 'sudo -n mn --test pingall' )
510     if vm.expect( [ ' 0% dropped', pexpect.TIMEOUT ], timeout=45 ) == 0:
511         log( '* Sanity check OK' )
512     else:
513         log( '* Sanity check FAILED' )
514         log( '* Sanity check output:' )
515         log( vm.before )
516
517
518 def coreTest( vm, prompt=Prompt ):
519     "Run core tests (make test) in VM"
520     log( '* Making sure cgroups are mounted' )
521     vm.sendline( 'sudo -n service cgroup-lite restart' )
522     vm.expect( prompt )
523     vm.sendline( 'sudo -n cgroups-mount' )
524     vm.expect( prompt )
525     log( '* Running make test' )
526     vm.sendline( 'cd ~/mininet; sudo make test' )
527     # We should change "make test" to report the number of
528     # successful and failed tests. For now, we have to
529     # know the time for each test, which means that this
530     # script will have to change as we add more tests.
531     for test in range( 0, 2 ):
532         if vm.expect( [ 'OK.*\r\n', 'FAILED.*\r\n', pexpect.TIMEOUT ], timeout=180 ) == 0:
533             log( '* Test', test, 'OK' )
534         else:
535             log( '* Test', test, 'FAILED' )
536             log( '* Test', test, 'output:' )
537             log( vm.before )
538
539
540 def installPexpect( vm, prompt=Prompt ):
541     "install pexpect"
542     vm.sendline( 'sudo -n apt-get -qy install python-pexpect' )
543     vm.expect( prompt )
544
545
546 def noneTest( vm, prompt=Prompt ):
547     "This test does nothing"
548     installPexpect( vm, prompt )
549     vm.sendline( 'echo' )
550
551
552 def examplesquickTest( vm, prompt=Prompt ):
553     "Quick test of mininet examples"
554     installPexpect( vm, prompt )
555     vm.sendline( 'sudo -n python ~/mininet/examples/test/runner.py -v -quick' )
556
557
558 def examplesfullTest( vm, prompt=Prompt ):
559     "Full (slow) test of mininet examples"
560     installPexpect( vm, prompt )
561     vm.sendline( 'sudo -n python ~/mininet/examples/test/runner.py -v' )
562
563
564 def walkthroughTest( vm, prompt=Prompt ):
565     "Test mininet walkthrough"
566     installPexpect( vm, prompt )
567     vm.sendline( 'sudo -n python ~/mininet/mininet/test/test_walkthrough.py -v' )
568
569
570 def useTest( vm, prompt=Prompt ):
571     "Use VM interactively - exit by pressing control-]"
572     old = vm.logfile
573     if old == stdout:
574         # Avoid doubling every output character!
575         log( '* Temporarily disabling logging to stdout' )
576         vm.logfile = None
577     log( '* Switching to interactive use - press control-] to exit' )
578     vm.interact()
579     if old == stdout:
580         log( '* Restoring logging to stdout' )
581         vm.logfile = stdout
582
583 # A convenient alias for use - 'run'; we might want to allow
584 # 'run' to take a parameter
585 runTest = useTest
586
587 def checkOutBranch( vm, branch, prompt=Prompt ):
588     # This is a bit subtle; it will check out an existing branch (e.g. master)
589     # if it exists; otherwise it will create a detached branch.
590     # The branch will be rebased to its parent on origin.
591     # This probably doesn't matter since we're running on a COW disk
592     # anyway.
593     vm.sendline( 'cd ~/mininet; git fetch --all; git checkout '
594                  + branch + '; git pull --rebase origin ' + branch )
595     vm.expect( prompt )
596     vm.sendline( 'sudo -n make install' )
597
598
599 def interact( vm, tests, pre='', post='', prompt=Prompt ):
600     "Interact with vm, which is a pexpect object"
601     login( vm )
602     log( '* Waiting for login...' )
603     vm.expect( prompt )
604     log( '* Sending hostname command' )
605     vm.sendline( 'hostname' )
606     log( '* Waiting for output' )
607     vm.expect( prompt )
608     log( '* Fetching Mininet VM install script' )
609     branch = Branch if Branch else 'master'
610     vm.sendline( 'wget '
611                  'https://raw.github.com/mininet/mininet/%s/util/vm/'
612                  'install-mininet-vm.sh' % branch )
613     vm.expect( prompt )
614     log( '* Running VM install script' )
615     installcmd = 'bash -v install-mininet-vm.sh'
616     if Branch:
617         installcmd += ' ' + Branch
618     vm.sendline( installcmd )
619     vm.expect ( 'password for mininet: ' )
620     vm.sendline( 'mininet' )
621     log( '* Waiting for script to complete... ' )
622     # Gigantic timeout for now ;-(
623     vm.expect( 'Done preparing Mininet', timeout=3600 )
624     log( '* Completed successfully' )
625     vm.expect( prompt )
626     version = getMininetVersion( vm )
627     vm.expect( prompt )
628     log( '* Mininet version: ', version )
629     log( '* Testing Mininet' )
630     runTests( vm, tests=tests, pre=pre, post=post )
631     # Ubuntu adds this because we install via a serial console,
632     # but we want the VM to boot via the VM console. Otherwise
633     # we get the message 'error: terminal "serial" not found'
634     log( '* Disabling serial console' )
635     vm.sendline( "sudo sed -i -e 's/^GRUB_TERMINAL=serial/#GRUB_TERMINAL=serial/' "
636                 "/etc/default/grub; sudo update-grub" )
637     vm.expect( prompt )
638     log( '* Shutting down' )
639     vm.sendline( 'sync; sudo shutdown -h now' )
640     log( '* Waiting for EOF/shutdown' )
641     vm.read()
642     log( '* Interaction complete' )
643     return version
644
645
646 def cleanup():
647     "Clean up leftover qemu-nbd processes and other junk"
648     call( [ 'sudo', 'pkill', '-9', 'qemu-nbd' ] )
649
650
651 def convert( cow, basename ):
652     """Convert a qcow2 disk to a vmdk and put it a new directory
653        basename: base name for output vmdk file"""
654     vmdk = basename + '.vmdk'
655     log( '* Converting qcow2 to vmdk' )
656     run( 'qemu-img convert -f qcow2 -O vmdk %s %s' % ( cow, vmdk ) )
657     return vmdk
658
659
660 # Template for OVF - a very verbose format!
661 # In the best of all possible worlds, we might use an XML
662 # library to generate this, but a template is easier and
663 # possibly more concise!
664 # Warning: XML file cannot begin with a newline!
665
666 OVFTemplate = """<?xml version="1.0"?>
667 <Envelope ovf:version="1.0" xml:lang="en-US"
668     xmlns="http://schemas.dmtf.org/ovf/envelope/1"
669     xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1"
670     xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"
671     xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"
672     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
673 <References>
674 <File ovf:href="%(diskname)s" ovf:id="file1" ovf:size="%(filesize)d"/>
675 </References>
676 <DiskSection>
677 <Info>Virtual disk information</Info>
678 <Disk ovf:capacity="%(disksize)d" ovf:capacityAllocationUnits="byte"
679     ovf:diskId="vmdisk1" ovf:fileRef="file1"
680     ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized"/>
681 </DiskSection>
682 <NetworkSection>
683 <Info>The list of logical networks</Info>
684 <Network ovf:name="nat">
685 <Description>The nat  network</Description>
686 </Network>
687 </NetworkSection>
688 <VirtualSystem ovf:id="%(vmname)s">
689 <Info>%(vminfo)s (%(name)s)</Info>
690 <Name>%(vmname)s</Name>
691 <OperatingSystemSection ovf:id="%(osid)d">
692 <Info>The kind of installed guest operating system</Info>
693 <Description>%(osname)s</Description>
694 </OperatingSystemSection>
695 <VirtualHardwareSection>
696 <Info>Virtual hardware requirements</Info>
697 <Item>
698 <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
699 <rasd:Description>Number of Virtual CPUs</rasd:Description>
700 <rasd:ElementName>%(cpus)s virtual CPU(s)</rasd:ElementName>
701 <rasd:InstanceID>1</rasd:InstanceID>
702 <rasd:ResourceType>3</rasd:ResourceType>
703 <rasd:VirtualQuantity>%(cpus)s</rasd:VirtualQuantity>
704 </Item>
705 <Item>
706 <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
707 <rasd:Description>Memory Size</rasd:Description>
708 <rasd:ElementName>%(mem)dMB of memory</rasd:ElementName>
709 <rasd:InstanceID>2</rasd:InstanceID>
710 <rasd:ResourceType>4</rasd:ResourceType>
711 <rasd:VirtualQuantity>%(mem)d</rasd:VirtualQuantity>
712 </Item>
713 <Item>
714 <rasd:Address>0</rasd:Address>
715 <rasd:Caption>scsiController0</rasd:Caption>
716 <rasd:Description>SCSI Controller</rasd:Description>
717 <rasd:ElementName>scsiController0</rasd:ElementName>
718 <rasd:InstanceID>4</rasd:InstanceID>
719 <rasd:ResourceSubType>lsilogic</rasd:ResourceSubType>
720 <rasd:ResourceType>6</rasd:ResourceType>
721 </Item>
722 <Item>
723 <rasd:AddressOnParent>0</rasd:AddressOnParent>
724 <rasd:ElementName>disk1</rasd:ElementName>
725 <rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
726 <rasd:InstanceID>11</rasd:InstanceID>
727 <rasd:Parent>4</rasd:Parent>
728 <rasd:ResourceType>17</rasd:ResourceType>
729 </Item>
730 <Item>
731 <rasd:AddressOnParent>2</rasd:AddressOnParent>
732 <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
733 <rasd:Connection>nat</rasd:Connection>
734 <rasd:Description>E1000 ethernet adapter on nat</rasd:Description>
735 <rasd:ElementName>ethernet0</rasd:ElementName>
736 <rasd:InstanceID>12</rasd:InstanceID>
737 <rasd:ResourceSubType>E1000</rasd:ResourceSubType>
738 <rasd:ResourceType>10</rasd:ResourceType>
739 </Item>
740 <Item>
741 <rasd:Address>0</rasd:Address>
742 <rasd:Caption>usb</rasd:Caption>
743 <rasd:Description>USB Controller</rasd:Description>
744 <rasd:ElementName>usb</rasd:ElementName>
745 <rasd:InstanceID>9</rasd:InstanceID>
746 <rasd:ResourceType>23</rasd:ResourceType>
747 </Item>
748 </VirtualHardwareSection>
749 </VirtualSystem>
750 </Envelope>
751 """
752
753
754 def generateOVF( name, osname, osid, diskname, disksize, mem=1024, cpus=1,
755                  vmname='Mininet-VM', vminfo='A Mininet Virtual Machine' ):
756     """Generate (and return) OVF file "name.ovf"
757        name: root name of OVF file to generate
758        osname: OS name for OVF (Ubuntu | Ubuntu 64-bit)
759        osid: OS ID for OVF (93 | 94 )
760        diskname: name of disk file
761        disksize: size of virtual disk in bytes
762        mem: VM memory size in MB
763        cpus: # of virtual CPUs
764        vmname: Name for VM (default name when importing)
765        vmimfo: Brief description of VM for OVF"""
766     ovf = name + '.ovf'
767     filesize = stat( diskname )[ ST_SIZE ]
768     params = dict( osname=osname, osid=osid, diskname=diskname,
769                    filesize=filesize, disksize=disksize, name=name,
770                    mem=mem, cpus=cpus, vmname=vmname, vminfo=vminfo )
771     xmltext = OVFTemplate % params
772     with open( ovf, 'w+' ) as f:
773         f.write( xmltext )
774     return ovf
775
776
777 def qcow2size( qcow2 ):
778     "Return virtual disk size (in bytes) of qcow2 image"
779     output = check_output( [ 'qemu-img', 'info', qcow2 ] )
780     try:
781         assert 'format: qcow' in output
782         bytes = int( re.findall( '(\d+) bytes', output )[ 0 ] )
783     except:
784         raise Exception( 'Could not determine size of %s' % qcow2 )
785     return bytes
786
787
788 def build( flavor='raring32server', tests=None, pre='', post='', memory=1024 ):
789     """Build a Mininet VM; return vmdk and vdisk size
790        tests: tests to run
791        pre: command line to run in VM before tests
792        post: command line to run in VM after tests
793        prompt: shell prompt (default '$ ')
794        memory: memory size in MB"""
795     global LogFile, Zip, Chown
796     start = time()
797     lstart = localtime()
798     date = strftime( '%y%m%d-%H-%M-%S', lstart)
799     ovfdate = strftime( '%y%m%d', lstart )
800     dir = 'mn-%s-%s' % ( flavor, date )
801     if Branch:
802         dirname = 'mn-%s-%s-%s' % ( Branch, flavor, date )
803     try:
804         os.mkdir( dir)
805     except:
806         raise Exception( "Failed to create build directory %s" % dir )
807     if Chown:
808         run( 'chown %s %s' % ( Chown, dir ) )
809     os.chdir( dir )
810     LogFile = open( 'build.log', 'w' )
811     log( '* Logging to', abspath( LogFile.name ) )
812     log( '* Created working directory', dir )
813     image, kernel, initrd = findBaseImage( flavor )
814     basename = 'mininet-' + flavor
815     volume = basename + '.qcow2'
816     run( 'qemu-img create -f qcow2 -b %s %s' % ( image, volume ) )
817     log( '* VM image for', flavor, 'created as', volume )
818     if LogToConsole:
819         logfile = stdout
820     else:
821         logfile = open( flavor + '.log', 'w+' )
822     log( '* Logging results to', abspath( logfile.name ) )
823     vm = boot( volume, kernel, initrd, logfile, memory=memory )
824     version = interact( vm, tests=tests, pre=pre, post=post )
825     size = qcow2size( volume )
826     arch = archFor( flavor )
827     vmdk = convert( volume, basename='mininet-vm-' + arch )
828     if not SaveQCOW2:
829         log( '* Removing qcow2 volume', volume )
830         os.remove( volume )
831     log( '* Converted VM image stored as', abspath( vmdk ) )
832     ovfname = 'mininet-%s-%s-%s' % ( version, ovfdate, OSVersion( flavor ) )
833     osname, osid = OVFOSNameID( flavor )
834     ovf = generateOVF( name=ovfname, osname=osname, osid=osid,
835                        diskname=vmdk, disksize=size )
836     log( '* Generated OVF descriptor file', ovf )
837     if Zip:
838         log( '* Generating .zip file' )
839         run( 'zip %s-ovf.zip %s %s' % ( ovfname, ovf, vmdk ) )
840     end = time()
841     elapsed = end - start
842     log( '* Results logged to', abspath( logfile.name ) )
843     log( '* Completed in %.2f seconds' % elapsed )
844     log( '* %s VM build DONE!!!!! :D' % flavor )
845     os.chdir( '..' )
846
847
848 def runTests( vm, tests=None, pre='', post='', prompt=Prompt, uninstallNtpd=False ):
849     "Run tests (list) in vm (pexpect object)"
850     # We disable ntpd and set the time so that ntpd won't be
851     # messing with the time during tests. Set to true for a COW
852     # disk and False for a non-COW disk.
853     if uninstallNtpd:
854         removeNtpd( vm )
855         vm.expect( prompt )
856     if Branch:
857         checkOutBranch( vm, branch=Branch )
858         vm.expect( prompt )
859     if not tests:
860         tests = []
861     if pre:
862         log( '* Running command', pre )
863         vm.sendline( pre )
864         vm.expect( prompt )
865     testfns = testDict()
866     if tests:
867         log( '* Running tests' )
868     for test in tests:
869         if test not in testfns:
870             raise Exception( 'Unknown test: ' + test )
871         log( '* Running test', test )
872         fn = testfns[ test ]
873         fn( vm )
874         vm.expect( prompt )
875     if post:
876         log( '* Running post-test command', post )
877         vm.sendline( post )
878         vm.expect( prompt )
879
880 def getMininetVersion( vm ):
881     "Run mn to find Mininet version in VM"
882     vm.sendline( '~/mininet/bin/mn --version' )
883     # Eat command line echo, then read output line
884     vm.readline()
885     version = vm.readline().strip()
886     return version
887
888
889 def bootAndRun( image, prompt=Prompt, memory=1024, cpuCores=1, outputFile=None,
890                 runFunction=None, **runArgs ):
891     """Boot and test VM
892        tests: list of tests to run
893        pre: command line to run in VM before tests
894        post: command line to run in VM after tests
895        prompt: shell prompt (default '$ ')
896        memory: VM memory size in MB
897        cpuCores: number of CPU cores to use"""
898     bootTestStart = time()
899     basename = path.basename( image )
900     image = abspath( image )
901     tmpdir = mkdtemp( prefix='test-' + basename )
902     log( '* Using tmpdir', tmpdir )
903     cow = path.join( tmpdir, basename + '.qcow2' )
904     log( '* Creating COW disk', cow )
905     run( 'qemu-img create -f qcow2 -b %s %s' % ( image, cow ) )
906     log( '* Extracting kernel and initrd' )
907     kernel, initrd = extractKernel( image, flavor=basename, imageDir=tmpdir )
908     if LogToConsole:
909         logfile = stdout
910     else:
911         logfile = NamedTemporaryFile( prefix=basename,
912                                       suffix='.testlog', delete=False )
913     log( '* Logging VM output to', logfile.name )
914     vm = boot( cow=cow, kernel=kernel, initrd=initrd, logfile=logfile,
915                memory=memory, cpuCores=cpuCores )
916     login( vm )
917     log( '* Waiting for prompt after login' )
918     vm.expect( prompt )
919     # runFunction should begin with sendline and should eat its last prompt
920     if runFunction:
921         runFunction( vm, **runArgs )
922     log( '* Shutting down' )
923     vm.sendline( 'sudo -n shutdown -h now ' )
924     log( '* Waiting for shutdown' )
925     vm.wait()
926     if outputFile:
927         log( '* Saving temporary image to %s' % outputFile )
928         convert( cow, outputFile )
929     log( '* Removing temporary dir', tmpdir )
930     srun( 'rm -rf ' + tmpdir )
931     elapsed = time() - bootTestStart
932     log( '* Boot and test completed in %.2f seconds' % elapsed )
933
934
935 def buildFlavorString():
936     "Return string listing valid build flavors"
937     return 'valid build flavors: %s' % ' '.join( sorted( isoURLs ) )
938
939
940 def testDict():
941     "Return dict of tests in this module"
942     suffix = 'Test'
943     trim = len( suffix )
944     fdict = dict( [ ( fname[ : -trim ], f ) for fname, f in
945                     inspect.getmembers( modules[ __name__ ],
946                                     inspect.isfunction )
947                   if fname.endswith( suffix ) ] )
948     return fdict
949
950
951 def testString():
952     "Return string listing valid tests"
953     tests = [ '%s <%s>' % ( name, func.__doc__ )
954               for name, func in testDict().iteritems() ]
955     return 'valid tests: %s' % ', '.join( tests )
956
957
958 def parseArgs():
959     "Parse command line arguments and run"
960     global LogToConsole, NoKVM, Branch, Zip, TIMEOUT, Forward, Chown
961     parser = argparse.ArgumentParser( description='Mininet VM build script',
962                                       epilog='' )
963     parser.add_argument( '-v', '--verbose', action='store_true',
964                         help='send VM output to console rather than log file' )
965     parser.add_argument( '-d', '--depend', action='store_true',
966                          help='install dependencies for this script' )
967     parser.add_argument( '-l', '--list', action='store_true',
968                          help='list valid build flavors and tests' )
969     parser.add_argument( '-c', '--clean', action='store_true',
970                          help='clean up leftover build junk (e.g. qemu-nbd)' )
971     parser.add_argument( '-q', '--qcow2', action='store_true',
972                          help='save qcow2 image rather than deleting it' )
973     parser.add_argument( '-n', '--nokvm', action='store_true',
974                          help="Don't use kvm - use tcg emulation instead" )
975     parser.add_argument( '-m', '--memory', metavar='MB', type=int,
976                         default=1024, help='VM memory size in MB' )
977     parser.add_argument( '-i', '--image', metavar='image', default=[],
978                          action='append',
979                          help='Boot and test an existing VM image' )
980     parser.add_argument( '-t', '--test', metavar='test', default=[],
981                          action='append',
982                          help='specify a test to run; ' + testString() )
983     parser.add_argument( '-w', '--timeout', metavar='timeout', type=int,
984                             default=0, help='set expect timeout' )
985     parser.add_argument( '-r', '--run', metavar='cmd', default='',
986                          help='specify a command line to run before tests' )
987     parser.add_argument( '-p', '--post', metavar='cmd', default='',
988                          help='specify a command line to run after tests' )
989     parser.add_argument( '-b', '--branch', metavar='branch',
990                          help='branch to install and/or check out and test' )
991     parser.add_argument( 'flavor', nargs='*',
992                          help='VM flavor(s) to build; ' + buildFlavorString() )
993     parser.add_argument( '-z', '--zip', action='store_true',
994                          help='archive .ovf and .vmdk into .zip file' )
995     parser.add_argument( '-o', '--out',
996                          help='output file for test image (vmdk)' )
997     parser.add_argument( '-f', '--forward', default=[], action='append',
998                          help='forward VM ports to local server, e.g. tcp:5555::22' )
999     parser.add_argument( '-u', '--chown', metavar='user',
1000                          help='specify an owner for build directory' )
1001     args = parser.parse_args()
1002     if args.depend:
1003         depend()
1004     if args.list:
1005         print( buildFlavorString() )
1006     if args.clean:
1007         cleanup()
1008     if args.verbose:
1009         LogToConsole = True
1010     if args.nokvm:
1011         NoKVM = True
1012     if args.branch:
1013         Branch = args.branch
1014     if args.zip:
1015         Zip = True
1016     if args.timeout:
1017         TIMEOUT = args.timeout
1018     if args.forward:
1019         Forward = args.forward
1020     if not args.test and not args.run and not args.post:
1021         args.test = [ 'sanity', 'core' ]
1022     if args.chown:
1023         Chown = args.chown
1024     for flavor in args.flavor:
1025         if flavor not in isoURLs:
1026             print( "Unknown build flavor:", flavor )
1027             print( buildFlavorString() )
1028             break
1029         try:
1030             build( flavor, tests=args.test, pre=args.run, post=args.post,
1031                    memory=args.memory )
1032         except Exception as e:
1033             log( '* BUILD FAILED with exception: ', e )
1034             exit( 1 )
1035     for image in args.image:
1036         bootAndRun( image, runFunction=runTests, tests=args.test, pre=args.run,
1037                     post=args.post, memory=args.memory, outputFile=args.out,
1038                     uninstallNtpd=True  )
1039     if not ( args.depend or args.list or args.clean or args.flavor
1040              or args.image ):
1041         parser.print_help()
1042
1043
1044 if __name__ == '__main__':
1045     parseArgs()