Statistics
| Branch: | Tag: | Revision:

mininet / util / vm / build.py @ 86af067e

History | View | Annotate | Download (35.7 KB)

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

    
58
VMImageDir = os.environ[ 'HOME' ] + '/vm-images'
59

    
60
Prompt = '\$ '              # Shell prompt that pexpect will wait for
61

    
62
isoURLs = {
63
    'precise32server':
64
    'http://mirrors.kernel.org/ubuntu-releases/12.04/'
65
    'ubuntu-12.04.5-server-i386.iso',
66
    'precise64server':
67
    'http://mirrors.kernel.org/ubuntu-releases/12.04/'
68
    'ubuntu-12.04.5-server-amd64.iso',
69
    'quantal32server':
70
    'http://mirrors.kernel.org/ubuntu-releases/12.10/'
71
    'ubuntu-12.10-server-i386.iso',
72
    'quantal64server':
73
    'http://mirrors.kernel.org/ubuntu-releases/12.10/'
74
    'ubuntu-12.10-server-amd64.iso',
75
    'raring32server':
76
    'http://mirrors.kernel.org/ubuntu-releases/13.04/'
77
    'ubuntu-13.04-server-i386.iso',
78
    'raring64server':
79
    'http://mirrors.kernel.org/ubuntu-releases/13.04/'
80
    'ubuntu-13.04-server-amd64.iso',
81
    'saucy32server':
82
    'http://mirrors.kernel.org/ubuntu-releases/13.10/'
83
    'ubuntu-13.10-server-i386.iso',
84
    'saucy64server':
85
    'http://mirrors.kernel.org/ubuntu-releases/13.10/'
86
    'ubuntu-13.10-server-amd64.iso',
87
    'trusty32server':
88
    'http://mirrors.kernel.org/ubuntu-releases/14.04/'
89
    'ubuntu-14.04-server-i386.iso',
90
    'trusty64server':
91
    'http://mirrors.kernel.org/ubuntu-releases/14.04/'
92
    'ubuntu-14.04-server-amd64.iso',
93
    'utopic32server':
94
    'http://mirrors.kernel.org/ubuntu-releases/14.10/'
95
    'ubuntu-14.10-server-i386.iso',
96
    'utopic64server':
97
    'http://mirrors.kernel.org/ubuntu-releases/14.10/'
98
    'ubuntu-14.10-server-amd64.iso',
99
}
100

    
101

    
102
def OSVersion( flavor ):
103
    "Return full OS version string for build flavor"
104
    urlbase = path.basename( isoURLs.get( flavor, 'unknown' ) )
105
    return path.splitext( urlbase )[ 0 ]
106

    
107
def OVFOSNameID( flavor ):
108
    "Return OVF-specified ( OS Name, ID ) for flavor"
109
    version = OSVersion( flavor )
110
    arch = archFor( flavor )
111
    if 'ubuntu' in version:
112
        map = { 'i386': ( 'Ubuntu', 93 ),
113
                'x86_64': ( 'Ubuntu 64-bit', 94 ) }
114
    else:
115
        map = { 'i386': ( 'Linux', 36 ),
116
                'x86_64': ( 'Linux 64-bit', 101 ) }
117
    osname, osid = map[ arch ]
118
    return osname, osid
119

    
120
LogStartTime = time()
121
LogFile = None
122

    
123
def log( *args, **kwargs ):
124
    """Simple log function: log( message along with local and elapsed time
125
       cr: False/0 for no CR"""
126
    cr = kwargs.get( 'cr', True )
127
    elapsed = time() - LogStartTime
128
    clocktime = strftime( '%H:%M:%S', localtime() )
129
    msg = ' '.join( str( arg ) for arg in args )
130
    output = '%s [ %.3f ] %s' % ( clocktime, elapsed, msg )
131
    if cr:
132
        print output
133
    else:
134
        print output,
135
    # Optionally mirror to LogFile
136
    if type( LogFile ) is file:
137
        if cr:
138
            output += '\n'
139
        LogFile.write( output )
140
        LogFile.flush()
141

    
142

    
143
def run( cmd, **kwargs ):
144
    "Convenient interface to check_output"
145
    log( '-', cmd )
146
    cmd = cmd.split()
147
    arg0 = cmd[ 0 ]
148
    if not find_executable( arg0 ):
149
        raise Exception( 'Cannot find executable "%s";' % arg0 +
150
                         'you might try %s --depend' % argv[ 0 ] )
151
    return check_output( cmd, **kwargs )
152

    
153

    
154
def srun( cmd, **kwargs ):
155
    "Run + sudo"
156
    return run( 'sudo ' + cmd, **kwargs )
157

    
158

    
159
# BL: we should probably have a "checkDepend()" which
160
# checks to make sure all dependencies are satisfied!
161

    
162
def depend():
163
    "Install package dependencies"
164
    log( '* Installing package dependencies' )
165
    run( 'sudo apt-get -qy update' )
166
    run( 'sudo apt-get -qy install'
167
         ' kvm cloud-utils genisoimage qemu-kvm qemu-utils'
168
         ' e2fsprogs dnsmasq curl'
169
         ' python-setuptools mtools zip' )
170
    run( 'sudo easy_install pexpect' )
171

    
172

    
173
def popen( cmd ):
174
    "Convenient interface to popen"
175
    log( cmd )
176
    cmd = cmd.split()
177
    return Popen( cmd )
178

    
179

    
180
def remove( fname ):
181
    "Remove a file, ignoring errors"
182
    try:
183
        os.remove( fname )
184
    except OSError:
185
        pass
186

    
187

    
188
def findiso( flavor ):
189
    "Find iso, fetching it if it's not there already"
190
    url = isoURLs[ flavor ]
191
    name = path.basename( url )
192
    iso = path.join( VMImageDir, name )
193
    if not path.exists( iso ) or ( stat( iso )[ ST_MODE ] & 0777 != 0444 ):
194
        log( '* Retrieving', url )
195
        run( 'curl -C - -o %s %s' % ( iso, url ) )
196
        # Make sure the file header/type is something reasonable like
197
        # 'ISO' or 'x86 boot sector', and not random html or text
198
        result = run( 'file ' + iso )
199
        if 'ISO' not in result and 'boot' not in result:
200
            os.remove( iso )
201
            raise Exception( 'findiso: could not download iso from ' + url )
202
        # Write-protect iso, signaling it is complete
203
        log( '* Write-protecting iso', iso)
204
        os.chmod( iso, 0444 )
205
    log( '* Using iso', iso )
206
    return iso
207

    
208

    
209
def attachNBD( cow, flags='' ):
210
    """Attempt to attach a COW disk image and return its nbd device
211
        flags: additional flags for qemu-nbd (e.g. -r for readonly)"""
212
    # qemu-nbd requires an absolute path
213
    cow = abspath( cow )
214
    log( '* Checking for unused /dev/nbdX device ' )
215
    for i in range ( 0, 63 ):
216
        nbd = '/dev/nbd%d' % i
217
        # Check whether someone's already messing with that device
218
        if call( [ 'pgrep', '-f', nbd ] ) == 0:
219
            continue
220
        srun( 'modprobe nbd max-part=64' )
221
        srun( 'qemu-nbd %s -c %s %s' % ( flags, nbd, cow ) )
222
        print
223
        return nbd
224
    raise Exception( "Error: could not find unused /dev/nbdX device" )
225

    
226

    
227
def detachNBD( nbd ):
228
    "Detatch an nbd device"
229
    srun( 'qemu-nbd -d ' + nbd )
230

    
231

    
232
def extractKernel( image, flavor, imageDir=VMImageDir ):
233
    "Extract kernel and initrd from base image"
234
    kernel = path.join( imageDir, flavor + '-vmlinuz' )
235
    initrd = path.join( imageDir, flavor + '-initrd' )
236
    if path.exists( kernel ) and ( stat( image )[ ST_MODE ] & 0777 ) == 0444:
237
        # If kernel is there, then initrd should also be there
238
        return kernel, initrd
239
    log( '* Extracting kernel to', kernel )
240
    nbd = attachNBD( image, flags='-r' )
241
    print srun( 'partx ' + nbd )
242
    # Assume kernel is in partition 1/boot/vmlinuz*generic for now
243
    part = nbd + 'p1'
244
    mnt = mkdtemp()
245
    srun( 'mount -o ro,noload %s %s' % ( part, mnt  ) )
246
    kernsrc = glob( '%s/boot/vmlinuz*generic' % mnt )[ 0 ]
247
    initrdsrc = glob( '%s/boot/initrd*generic' % mnt )[ 0 ]
248
    srun( 'cp %s %s' % ( initrdsrc, initrd ) )
249
    srun( 'chmod 0444 ' + initrd )
250
    srun( 'cp %s %s' % ( kernsrc, kernel ) )
251
    srun( 'chmod 0444 ' + kernel )
252
    srun( 'umount ' + mnt )
253
    run( 'rmdir ' + mnt )
254
    detachNBD( nbd )
255
    return kernel, initrd
256

    
257

    
258
def findBaseImage( flavor, size='8G' ):
259
    "Return base VM image and kernel, creating them if needed"
260
    image = path.join( VMImageDir, flavor + '-base.qcow2' )
261
    if path.exists( image ):
262
        # Detect race condition with multiple builds
263
        perms = stat( image )[ ST_MODE ] & 0777
264
        if perms != 0444:
265
            raise Exception( 'Error - %s is writable ' % image +
266
                            '; are multiple builds running?' )
267
    else:
268
        # We create VMImageDir here since we are called first
269
        run( 'mkdir -p %s' % VMImageDir )
270
        iso = findiso( flavor )
271
        log( '* Creating image file', image )
272
        run( 'qemu-img create -f qcow2 %s %s' % ( image, size ) )
273
        installUbuntu( iso, image )
274
        # Write-protect image, also signaling it is complete
275
        log( '* Write-protecting image', image)
276
        os.chmod( image, 0444 )
277
    kernel, initrd = extractKernel( image, flavor )
278
    log( '* Using base image', image, 'and kernel', kernel )
279
    return image, kernel, initrd
280

    
281

    
282
# Kickstart and Preseed files for Ubuntu/Debian installer
283
#
284
# Comments: this is really clunky and painful. If Ubuntu
285
# gets their act together and supports kickstart a bit better
286
# then we can get rid of preseed and even use this as a
287
# Fedora installer as well.
288
#
289
# Another annoying thing about Ubuntu is that it can't just
290
# install a normal system from the iso - it has to download
291
# junk from the internet, making this house of cards even
292
# more precarious.
293

    
294
KickstartText ="""
295
#Generated by Kickstart Configurator
296
#platform=x86
297

298
#System language
299
lang en_US
300
#Language modules to install
301
langsupport en_US
302
#System keyboard
303
keyboard us
304
#System mouse
305
mouse
306
#System timezone
307
timezone America/Los_Angeles
308
#Root password
309
rootpw --disabled
310
#Initial user
311
user mininet --fullname "mininet" --password "mininet"
312
#Use text mode install
313
text
314
#Install OS instead of upgrade
315
install
316
#Use CDROM installation media
317
cdrom
318
#System bootloader configuration
319
bootloader --location=mbr
320
#Clear the Master Boot Record
321
zerombr yes
322
#Partition clearing information
323
clearpart --all --initlabel
324
#Automatic partitioning
325
autopart
326
#System authorization information
327
auth  --useshadow  --enablemd5
328
#Firewall configuration
329
firewall --disabled
330
#Do not configure the X Window System
331
skipx
332
"""
333

    
334
# Tell the Ubuntu/Debian installer to stop asking stupid questions
335

    
336
PreseedText = ( """
337
"""
338
#d-i mirror/country string manual
339
#d-i mirror/http/hostname string mirrors.kernel.org
340
"""
341
d-i mirror/http/directory string /ubuntu
342
d-i mirror/http/proxy string
343
d-i partman/confirm_write_new_label boolean true
344
d-i partman/choose_partition select finish
345
d-i partman/confirm boolean true
346
d-i partman/confirm_nooverwrite boolean true
347
d-i user-setup/allow-password-weak boolean true
348
d-i finish-install/reboot_in_progress note
349
d-i debian-installer/exit/poweroff boolean true
350
""" )
351

    
352
def makeKickstartFloppy():
353
    "Create and return kickstart floppy, kickstart, preseed"
354
    kickstart = 'ks.cfg'
355
    with open( kickstart, 'w' ) as f:
356
        f.write( KickstartText )
357
    preseed = 'ks.preseed'
358
    with open( preseed, 'w' ) as f:
359
        f.write( PreseedText )
360
    # Create floppy and copy files to it
361
    floppy = 'ksfloppy.img'
362
    run( 'qemu-img create %s 1440k' % floppy )
363
    run( 'mkfs -t msdos ' + floppy )
364
    run( 'mcopy -i %s %s ::/' % ( floppy, kickstart ) )
365
    run( 'mcopy -i %s %s ::/' % ( floppy, preseed ) )
366
    return floppy, kickstart, preseed
367

    
368

    
369
def archFor( filepath ):
370
    "Guess architecture for file path"
371
    name = path.basename( filepath )
372
    if 'amd64' in name or 'x86_64' in name:
373
        arch = 'x86_64'
374
    # Beware of version 64 of a 32-bit OS
375
    elif 'i386' in name or '32' in name or 'x86' in name:
376
        arch = 'i386'
377
    elif '64' in name:
378
        arch = 'x86_64'
379
    else:
380
        log( "Error: can't discern CPU for name", name )
381
        exit( 1 )
382
    return arch
383

    
384

    
385
def installUbuntu( iso, image, logfilename='install.log', memory=1024 ):
386
    "Install Ubuntu from iso onto image"
387
    kvm = 'qemu-system-' + archFor( iso )
388
    floppy, kickstart, preseed = makeKickstartFloppy()
389
    # Mount iso so we can use its kernel
390
    mnt = mkdtemp()
391
    srun( 'mount %s %s' % ( iso, mnt ) )
392
    kernel = path.join( mnt, 'install/vmlinuz' )
393
    initrd = path.join( mnt, 'install/initrd.gz' )
394
    if NoKVM:
395
        accel = 'tcg'
396
    else:
397
        accel = 'kvm'
398
    cmd = [ 'sudo', kvm,
399
           '-machine', 'accel=%s' % accel,
400
           '-nographic',
401
           '-netdev', 'user,id=mnbuild',
402
           '-device', 'virtio-net,netdev=mnbuild',
403
           '-m', str( memory ),
404
           '-k', 'en-us',
405
           '-fda', floppy,
406
           '-drive', 'file=%s,if=virtio' % image,
407
           '-cdrom', iso,
408
           '-kernel', kernel,
409
           '-initrd', initrd,
410
           '-append',
411
           ' ks=floppy:/' + kickstart +
412
           ' preseed/file=floppy://' + preseed +
413
           ' console=ttyS0' ]
414
    ubuntuStart = time()
415
    log( '* INSTALLING UBUNTU FROM', iso, 'ONTO', image )
416
    log( ' '.join( cmd ) )
417
    log( '* logging to', abspath( logfilename ) )
418
    params = {}
419
    if not LogToConsole:
420
        logfile = open( logfilename, 'w' )
421
        params = { 'stdout': logfile, 'stderr': logfile }
422
    vm = Popen( cmd, **params )
423
    log( '* Waiting for installation to complete')
424
    vm.wait()
425
    if not LogToConsole:
426
        logfile.close()
427
    elapsed = time() - ubuntuStart
428
    # Unmount iso and clean up
429
    srun( 'umount ' + mnt )
430
    run( 'rmdir ' + mnt )
431
    if vm.returncode != 0:
432
        raise Exception( 'Ubuntu installation returned error %d' %
433
                          vm.returncode )
434
    log( '* UBUNTU INSTALLATION COMPLETED FOR', image )
435
    log( '* Ubuntu installation completed in %.2f seconds' % elapsed )
436

    
437

    
438
def boot( cow, kernel, initrd, logfile, memory=1024, cpuCores=1 ):
439
    """Boot qemu/kvm with a COW disk and local/user data store
440
       cow: COW disk path
441
       kernel: kernel path
442
       logfile: log file for pexpect object
443
       memory: memory size in MB
444
       cpuCores: number of CPU cores to use
445
       returns: pexpect object to qemu process"""
446
    # pexpect might not be installed until after depend() is called
447
    global pexpect
448
    if not pexpect:
449
        import pexpect
450
    class Spawn( pexpect.spawn ):
451
        "Subprocess is sudo, so we have to sudo kill it"
452
        def close( self, force=False ):
453
            srun( 'kill %d' % self.pid )
454
    arch = archFor( kernel )
455
    log( '* Detected kernel architecture', arch )
456
    if NoKVM:
457
        accel = 'tcg'
458
    else:
459
        accel = 'kvm'
460
    cmd = [ 'sudo', 'qemu-system-' + arch,
461
            '-machine accel=%s' % accel,
462
            '-nographic',
463
            '-netdev user,id=mnbuild',
464
            '-device virtio-net,netdev=mnbuild',
465
            '-m %s' % memory,
466
            '-k en-us',
467
            '-kernel', kernel,
468
            '-initrd', initrd,
469
            '-drive file=%s,if=virtio' % cow,
470
            '-append "root=/dev/vda1 init=/sbin/init console=ttyS0" ' ]
471
    if Forward:
472
        cmd += sum( [ [ '-redir', f ] for f in Forward ], [] )
473
    if cpuCores > 1:
474
        cmd += [ '-smp cores=%s' % cpuCores ]
475
    cmd = ' '.join( cmd )
476
    log( '* BOOTING VM FROM', cow )
477
    log( cmd )
478
    vm = Spawn( cmd, timeout=TIMEOUT, logfile=logfile )
479
    return vm
480

    
481

    
482
def login( vm, user='mininet', password='mininet' ):
483
    "Log in to vm (pexpect object)"
484
    log( '* Waiting for login prompt' )
485
    vm.expect( 'login: ' )
486
    log( '* Logging in' )
487
    vm.sendline( user )
488
    log( '* Waiting for password prompt' )
489
    vm.expect( 'Password: ' )
490
    log( '* Sending password' )
491
    vm.sendline( password )
492
    log( '* Waiting for login...' )
493

    
494

    
495
def removeNtpd( vm, prompt=Prompt, ntpPackage='ntp' ):
496
    "Remove ntpd and set clock immediately"
497
    log( '* Removing ntpd' )
498
    vm.sendline( 'sudo -n apt-get -qy remove ' + ntpPackage )
499
    vm.expect( prompt )
500
    # Try to make sure that it isn't still running
501
    vm.sendline( 'sudo -n pkill ntpd' )
502
    vm.expect( prompt )
503
    log( '* Getting seconds since epoch from this server' )
504
    # Note r'date +%s' specifies a format for 'date', not python!
505
    seconds = int( run( r'date +%s' ) )
506
    log( '* Setting VM clock' )
507
    vm.sendline( 'sudo -n date -s @%d' % seconds )
508

    
509

    
510
def sanityTest( vm ):
511
    "Run Mininet sanity test (pingall) in vm"
512
    vm.sendline( 'sudo -n mn --test pingall' )
513
    if vm.expect( [ ' 0% dropped', pexpect.TIMEOUT ], timeout=45 ) == 0:
514
        log( '* Sanity check OK' )
515
    else:
516
        log( '* Sanity check FAILED' )
517
        log( '* Sanity check output:' )
518
        log( vm.before )
519

    
520

    
521
def coreTest( vm, prompt=Prompt ):
522
    "Run core tests (make test) in VM"
523
    log( '* Making sure cgroups are mounted' )
524
    vm.sendline( 'sudo -n service cgroup-lite restart' )
525
    vm.expect( prompt )
526
    vm.sendline( 'sudo -n cgroups-mount' )
527
    vm.expect( prompt )
528
    log( '* Running make test' )
529
    vm.sendline( 'cd ~/mininet; sudo make test' )
530
    # We should change "make test" to report the number of
531
    # successful and failed tests. For now, we have to
532
    # know the time for each test, which means that this
533
    # script will have to change as we add more tests.
534
    for test in range( 0, 2 ):
535
        if vm.expect( [ 'OK.*\r\n', 'FAILED.*\r\n', pexpect.TIMEOUT ], timeout=180 ) == 0:
536
            log( '* Test', test, 'OK' )
537
        else:
538
            log( '* Test', test, 'FAILED' )
539
            log( '* Test', test, 'output:' )
540
            log( vm.before )
541

    
542

    
543
def installPexpect( vm, prompt=Prompt ):
544
    "install pexpect"
545
    vm.sendline( 'sudo -n apt-get -qy install python-pexpect' )
546
    vm.expect( prompt )
547

    
548

    
549
def noneTest( vm, prompt=Prompt ):
550
    "This test does nothing"
551
    installPexpect( vm, prompt )
552
    vm.sendline( 'echo' )
553

    
554

    
555
def examplesquickTest( vm, prompt=Prompt ):
556
    "Quick test of mininet examples"
557
    installPexpect( vm, prompt )
558
    vm.sendline( 'sudo -n python ~/mininet/examples/test/runner.py -v -quick' )
559

    
560

    
561
def examplesfullTest( vm, prompt=Prompt ):
562
    "Full (slow) test of mininet examples"
563
    installPexpect( vm, prompt )
564
    vm.sendline( 'sudo -n python ~/mininet/examples/test/runner.py -v' )
565

    
566

    
567
def walkthroughTest( vm, prompt=Prompt ):
568
    "Test mininet walkthrough"
569
    installPexpect( vm, prompt )
570
    vm.sendline( 'sudo -n python ~/mininet/mininet/test/test_walkthrough.py -v' )
571

    
572

    
573
def useTest( vm, prompt=Prompt ):
574
    "Use VM interactively - exit by pressing control-]"
575
    old = vm.logfile
576
    if old == stdout:
577
        # Avoid doubling every output character!
578
        log( '* Temporarily disabling logging to stdout' )
579
        vm.logfile = None
580
    log( '* Switching to interactive use - press control-] to exit' )
581
    vm.interact()
582
    if old == stdout:
583
        log( '* Restoring logging to stdout' )
584
        vm.logfile = stdout
585

    
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
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
        dir = '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
    os.chdir( dir )
808
    LogFile = open( 'build.log', 'w' )
809
    log( '* Logging to', abspath( LogFile.name ) )
810
    log( '* Created working directory', dir )
811
    image, kernel, initrd = findBaseImage( flavor )
812
    basename = 'mininet-' + flavor
813
    volume = basename + '.qcow2'
814
    run( 'qemu-img create -f qcow2 -b %s %s' % ( image, volume ) )
815
    log( '* VM image for', flavor, 'created as', volume )
816
    if LogToConsole:
817
        logfile = stdout
818
    else:
819
        logfile = open( flavor + '.log', 'w+' )
820
    log( '* Logging results to', abspath( logfile.name ) )
821
    vm = boot( volume, kernel, initrd, logfile, memory=memory )
822
    version = interact( vm, tests=tests, pre=pre, post=post )
823
    size = qcow2size( volume )
824
    arch = archFor( flavor )
825
    vmdk = convert( volume, basename='mininet-vm-' + arch )
826
    if not SaveQCOW2:
827
        log( '* Removing qcow2 volume', volume )
828
        os.remove( volume )
829
    log( '* Converted VM image stored as', abspath( vmdk ) )
830
    ovfname = 'mininet-%s-%s-%s' % ( version, ovfdate, OSVersion( flavor ) )
831
    osname, osid = OVFOSNameID( flavor )
832
    ovf = generateOVF( name=ovfname, osname=osname, osid=osid,
833
                       diskname=vmdk, disksize=size )
834
    log( '* Generated OVF descriptor file', ovf )
835
    if Zip:
836
        log( '* Generating .zip file' )
837
        run( 'zip %s-ovf.zip %s %s' % ( ovfname, ovf, vmdk ) )
838
    end = time()
839
    elapsed = end - start
840
    log( '* Results logged to', abspath( logfile.name ) )
841
    log( '* Completed in %.2f seconds' % elapsed )
842
    log( '* %s VM build DONE!!!!! :D' % flavor )
843
    os.chdir( '..' )
844

    
845

    
846
def runTests( vm, tests=None, pre='', post='', prompt=Prompt, uninstallNtpd=False ):
847
    "Run tests (list) in vm (pexpect object)"
848
    # We disable ntpd and set the time so that ntpd won't be
849
    # messing with the time during tests. Set to true for a COW
850
    # disk and False for a non-COW disk.
851
    if uninstallNtpd:
852
        removeNtpd( vm )
853
        vm.expect( prompt )
854
    if Branch:
855
        checkOutBranch( vm, branch=Branch )
856
        vm.expect( prompt )
857
    if not tests:
858
        tests = []
859
    if pre:
860
        log( '* Running command', pre )
861
        vm.sendline( pre )
862
        vm.expect( prompt )
863
    testfns = testDict()
864
    if tests:
865
        log( '* Running tests' )
866
    for test in tests:
867
        if test not in testfns:
868
            raise Exception( 'Unknown test: ' + test )
869
        log( '* Running test', test )
870
        fn = testfns[ test ]
871
        fn( vm )
872
        vm.expect( prompt )
873
    if post:
874
        log( '* Running post-test command', post )
875
        vm.sendline( post )
876
        vm.expect( prompt )
877

    
878
def getMininetVersion( vm ):
879
    "Run mn to find Mininet version in VM"
880
    vm.sendline( '~/mininet/bin/mn --version' )
881
    # Eat command line echo, then read output line
882
    vm.readline()
883
    version = vm.readline().strip()
884
    return version
885

    
886

    
887
def bootAndRun( image, prompt=Prompt, memory=1024, cpuCores=1, outputFile=None,
888
                runFunction=None, **runArgs ):
889
    """Boot and test VM
890
       tests: list of tests to run
891
       pre: command line to run in VM before tests
892
       post: command line to run in VM after tests
893
       prompt: shell prompt (default '$ ')
894
       memory: VM memory size in MB
895
       cpuCores: number of CPU cores to use"""
896
    bootTestStart = time()
897
    basename = path.basename( image )
898
    image = abspath( image )
899
    tmpdir = mkdtemp( prefix='test-' + basename )
900
    log( '* Using tmpdir', tmpdir )
901
    cow = path.join( tmpdir, basename + '.qcow2' )
902
    log( '* Creating COW disk', cow )
903
    run( 'qemu-img create -f qcow2 -b %s %s' % ( image, cow ) )
904
    log( '* Extracting kernel and initrd' )
905
    kernel, initrd = extractKernel( image, flavor=basename, imageDir=tmpdir )
906
    if LogToConsole:
907
        logfile = stdout
908
    else:
909
        logfile = NamedTemporaryFile( prefix=basename,
910
                                      suffix='.testlog', delete=False )
911
    log( '* Logging VM output to', logfile.name )
912
    vm = boot( cow=cow, kernel=kernel, initrd=initrd, logfile=logfile,
913
               memory=memory, cpuCores=cpuCores )
914
    login( vm )
915
    log( '* Waiting for prompt after login' )
916
    vm.expect( prompt )
917
    # runFunction should begin with sendline and should eat its last prompt
918
    if runFunction:
919
        runFunction( vm, **runArgs )
920
    log( '* Shutting down' )
921
    vm.sendline( 'sudo -n shutdown -h now ' )
922
    log( '* Waiting for shutdown' )
923
    vm.wait()
924
    if outputFile:
925
        log( '* Saving temporary image to %s' % outputFile )
926
        convert( cow, outputFile )
927
    log( '* Removing temporary dir', tmpdir )
928
    srun( 'rm -rf ' + tmpdir )
929
    elapsed = time() - bootTestStart
930
    log( '* Boot and test completed in %.2f seconds' % elapsed )
931

    
932

    
933
def buildFlavorString():
934
    "Return string listing valid build flavors"
935
    return 'valid build flavors: ( %s )' % ' '.join( sorted( isoURLs ) )
936

    
937

    
938
def testDict():
939
    "Return dict of tests in this module"
940
    suffix = 'Test'
941
    trim = len( suffix )
942
    fdict = dict( [ ( fname[ : -trim ], f ) for fname, f in
943
                    inspect.getmembers( modules[ __name__ ],
944
                                    inspect.isfunction )
945
                  if fname.endswith( suffix ) ] )
946
    return fdict
947

    
948

    
949
def testString():
950
    "Return string listing valid tests"
951
    return 'valid tests: ( %s )' % ' '.join( testDict().keys() )
952

    
953

    
954
def parseArgs():
955
    "Parse command line arguments and run"
956
    global LogToConsole, NoKVM, Branch, Zip, TIMEOUT, Forward
957
    parser = argparse.ArgumentParser( description='Mininet VM build script',
958
                                      epilog=buildFlavorString() + ' ' +
959
                                      testString() )
960
    parser.add_argument( '-v', '--verbose', action='store_true',
961
                        help='send VM output to console rather than log file' )
962
    parser.add_argument( '-d', '--depend', action='store_true',
963
                         help='install dependencies for this script' )
964
    parser.add_argument( '-l', '--list', action='store_true',
965
                         help='list valid build flavors and tests' )
966
    parser.add_argument( '-c', '--clean', action='store_true',
967
                         help='clean up leftover build junk (e.g. qemu-nbd)' )
968
    parser.add_argument( '-q', '--qcow2', action='store_true',
969
                         help='save qcow2 image rather than deleting it' )
970
    parser.add_argument( '-n', '--nokvm', action='store_true',
971
                         help="Don't use kvm - use tcg emulation instead" )
972
    parser.add_argument( '-m', '--memory', metavar='MB', type=int,
973
                        default=1024, help='VM memory size in MB' )
974
    parser.add_argument( '-i', '--image', metavar='image', default=[],
975
                         action='append',
976
                         help='Boot and test an existing VM image' )
977
    parser.add_argument( '-t', '--test', metavar='test', default=[],
978
                         action='append',
979
                         help='specify a test to run' )
980
    parser.add_argument( '-w', '--timeout', metavar='timeout', type=int,
981
                            default=0, help='set expect timeout' )
982
    parser.add_argument( '-r', '--run', metavar='cmd', default='',
983
                         help='specify a command line to run before tests' )
984
    parser.add_argument( '-p', '--post', metavar='cmd', default='',
985
                         help='specify a command line to run after tests' )
986
    parser.add_argument( '-b', '--branch', metavar='branch',
987
                         help='branch to install and/or check out and test' )
988
    parser.add_argument( 'flavor', nargs='*',
989
                         help='VM flavor(s) to build (e.g. raring32server)' )
990
    parser.add_argument( '-z', '--zip', action='store_true',
991
                         help='archive .ovf and .vmdk into .zip file' )
992
    parser.add_argument( '-o', '--out',
993
                         help='output file for test image (vmdk)' )
994
    parser.add_argument( '-f', '--forward', default=[], action='append',
995
                         help='forward VM ports to local server, e.g. tcp:5555::22' )
996
    args = parser.parse_args()
997
    if args.depend:
998
        depend()
999
    if args.list:
1000
        print buildFlavorString()
1001
    if args.clean:
1002
        cleanup()
1003
    if args.verbose:
1004
        LogToConsole = True
1005
    if args.nokvm:
1006
        NoKVM = True
1007
    if args.branch:
1008
        Branch = args.branch
1009
    if args.zip:
1010
        Zip = True
1011
    if args.timeout:
1012
        TIMEOUT = args.timeout
1013
    if args.forward:
1014
        Forward = args.forward
1015
    if not args.test and not args.run and not args.post:
1016
        args.test = [ 'sanity', 'core' ]
1017
    for flavor in args.flavor:
1018
        if flavor not in isoURLs:
1019
            print "Unknown build flavor:", flavor
1020
            print buildFlavorString()
1021
            break
1022
        try:
1023
            build( flavor, tests=args.test, pre=args.run, post=args.post,
1024
                   memory=args.memory )
1025
        except Exception as e:
1026
            log( '* BUILD FAILED with exception: ', e )
1027
            exit( 1 )
1028
    for image in args.image:
1029
        bootAndRun( image, runFunction=runTests, tests=args.test, pre=args.run,
1030
                    post=args.post, memory=args.memory, outputFile=args.out,
1031
                    uninstallNtpd=True  )
1032
    if not ( args.depend or args.list or args.clean or args.flavor
1033
             or args.image ):
1034
        parser.print_help()
1035

    
1036

    
1037
if __name__ == '__main__':
1038
    parseArgs()