Statistics
| Branch: | Tag: | Revision:

mininet / util / vm / build.py @ 662fb712

History | View | Annotate | Download (17.3 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
    -> make codecheck
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
32
from os.path import abspath
33
from sys import exit, argv
34
from glob import glob
35
from subprocess import check_output, call, Popen
36
from tempfile import mkdtemp
37
from time import time, strftime, localtime
38
from lxml import etree
39
import argparse
40

    
41
pexpect = None  # For code check - imported dynamically
42

    
43
# boot can be slooooow!!!! need to debug/optimize somehow
44
TIMEOUT=600
45

    
46
VMImageDir = os.environ[ 'HOME' ] + '/vm-images'
47

    
48
isoURLs = {
49
    'precise32server':
50
    'http://mirrors.kernel.org/ubuntu-releases/12.04/'
51
    'ubuntu-12.04.3-server-i386.iso',
52
    'precise64server':
53
    'http://mirrors.kernel.org/ubuntu-releases/12.04/'
54
    'ubuntu-12.04.3-server-amd64.iso',
55
    'quetzal32server':
56
    'http://mirrors.kernel.org/ubuntu-releases/12.10/'
57
    'ubuntu-12.10-server-i386.iso',
58
    'quetzal64server':
59
    'http://mirrors.kernel.org/ubuntu-releases/12.10/'
60
    'ubuntu-12.10-server-amd64.iso',
61
    'raring32server':
62
    'http://mirrors.kernel.org/ubuntu-releases/13.04/'
63
    'ubuntu-13.04-server-i386.iso',
64
    'raring64server':
65
    'http://mirrors.kernel.org/ubuntu-releases/13.04/'
66
    'ubuntu-13.04-server-amd64.iso',
67
}
68

    
69
logStartTime = time()
70

    
71
def log( *args, **kwargs ):
72
    """Simple log function: log( message along with local and elapsed time
73
       cr: False/0 for no CR"""
74
    cr = kwargs.get( 'cr', True )
75
    elapsed = time() - logStartTime
76
    clocktime = strftime( '%H:%M:%S', localtime() )
77
    msg = ' '.join( str( arg ) for arg in args )
78
    output = '%s [ %.3f ] %s' % ( clocktime, elapsed, msg )
79
    if cr:
80
        print output
81
    else:
82
        print output,
83

    
84

    
85
def run( cmd, **kwargs ):
86
    "Convenient interface to check_output"
87
    log( '-', cmd )
88
    cmd = cmd.split()
89
    return check_output( cmd, **kwargs )
90

    
91

    
92
def srun( cmd, **kwargs ):
93
    "Run + sudo"
94
    return run( 'sudo ' + cmd, **kwargs )
95

    
96

    
97
def depend():
98
    "Install package dependencies"
99
    log( '* Installing package dependencies' )
100
    run( 'sudo apt-get -y update' )
101
    run( 'sudo apt-get install -y'
102
         ' kvm cloud-utils genisoimage qemu-kvm qemu-utils'
103
         ' e2fsprogs '
104
         ' landscape-client'
105
         ' python-setuptools' )
106
    run( 'sudo easy_install pexpect' )
107

    
108

    
109
def popen( cmd ):
110
    "Convenient interface to popen"
111
    log( cmd )
112
    cmd = cmd.split()
113
    return Popen( cmd )
114

    
115

    
116
def remove( fname ):
117
    "rm -f fname"
118
    return run( 'rm -f %s' % fname )
119

    
120

    
121
def findiso( flavor ):
122
    "Find iso, fetching it if it's not there already"
123
    url = isoURLs[ flavor ]
124
    name = path.basename( url )
125
    iso = path.join( VMImageDir, name )
126
    if not path.exists( iso ) or ( stat( iso )[ ST_MODE ] & 0777 != 0444 ):
127
        log( '* Retrieving', url )
128
        run( 'curl -C - -o %s %s' % ( iso, url ) )
129
        if 'ISO' not in run( 'file ' + iso ):
130
            os.remove( iso )
131
            raise Exception( 'findiso: could not download iso from ' + url )
132
        # Write-protect iso, signaling it is complete
133
        log( '* Write-protecting iso', iso)
134
        os.chmod( iso, 0444 )
135
    log( '* Using iso', iso )
136
    return iso
137

    
138

    
139
def attachNBD( cow, flags='' ):
140
    """Attempt to attach a COW disk image and return its nbd device
141
        flags: additional flags for qemu-nbd (e.g. -r for readonly)"""
142
    # qemu-nbd requires an absolute path
143
    cow = abspath( cow )
144
    log( '* Checking for unused /dev/nbdX device ' )
145
    for i in range ( 0, 63 ):
146
        nbd = '/dev/nbd%d' % i
147
        # Check whether someone's already messing with that device
148
        if call( [ 'pgrep', '-f', nbd ] ) == 0:
149
            continue
150
        srun( 'modprobe nbd max-part=64' )
151
        srun( 'qemu-nbd %s -c %s %s' % ( flags, nbd, cow ) )
152
        print
153
        return nbd
154
    raise Exception( "Error: could not find unused /dev/nbdX device" )
155

    
156

    
157
def detachNBD( nbd ):
158
    "Detatch an nbd device"
159
    srun( 'qemu-nbd -d ' + nbd )
160

    
161

    
162
def extractKernel( image, flavor ):
163
    "Extract kernel and initrd from base image"
164
    kernel = path.join( VMImageDir, flavor + '-vmlinuz' )
165
    initrd = path.join( VMImageDir, flavor + '-initrd' )
166
    if path.exists( kernel ) and ( stat( image )[ ST_MODE ] & 0777 ) == 0444:
167
        # If kernel is there, then initrd should also be there
168
        return kernel, initrd
169
    log( '* Extracting kernel to', kernel )
170
    nbd = attachNBD( image, flags='-r' )
171
    print srun( 'partx ' + nbd )
172
    # Assume kernel is in partition 1/boot/vmlinuz*generic for now
173
    part = nbd + 'p1'
174
    mnt = mkdtemp()
175
    srun( 'mount -o ro %s %s' % ( part, mnt  ) )
176
    kernsrc = glob( '%s/boot/vmlinuz*generic' % mnt )[ 0 ]
177
    initrdsrc = glob( '%s/boot/initrd*generic' % mnt )[ 0 ]
178
    srun( 'cp %s %s' % ( initrdsrc, initrd ) )
179
    srun( 'chmod 0444 ' + initrd )
180
    srun( 'cp %s %s' % ( kernsrc, kernel ) )
181
    srun( 'chmod 0444 ' + kernel )
182
    srun( 'umount ' + mnt )
183
    run( 'rmdir ' + mnt )
184
    detachNBD( nbd )
185
    return kernel, initrd
186

    
187

    
188
def findBaseImage( flavor, size='8G' ):
189
    "Return base VM image and kernel, creating them if needed"
190
    image = path.join( VMImageDir, flavor + '-base.img' )
191
    if path.exists( image ):
192
        # Detect race condition with multiple builds
193
        perms = stat( image )[ ST_MODE ] & 0777
194
        if perms != 0444:
195
            raise Exception( 'Error - %s is writable ' % image +
196
                            '; are multiple builds running?' )
197
    else:
198
        # We create VMImageDir here since we are called first
199
        run( 'mkdir -p %s' % VMImageDir )
200
        iso = findiso( flavor )
201
        log( '* Creating image file', image )
202
        run( 'qemu-img create %s %s' % ( image, size ) )
203
        installUbuntu( iso, image )
204
        # Write-protect image, also signaling it is complete
205
        log( '* Write-protecting image', image)
206
        os.chmod( image, 0444 )
207
    kernel, initrd = extractKernel( image, flavor )
208
    log( '* Using base image', image, 'and kernel', kernel )
209
    return image, kernel, initrd
210

    
211

    
212
# Kickstart and Preseed files for Ubuntu/Debian installer
213
#
214
# Comments: this is really clunky and painful. If Ubuntu
215
# gets their act together and supports kickstart a bit better
216
# then we can get rid of preseed and even use this as a
217
# Fedora installer as well.
218
#
219
# Another annoying thing about Ubuntu is that it can't just
220
# install a normal system from the iso - it has to download
221
# junk from the internet, making this house of cards even
222
# more precarious.
223

    
224
KickstartText ="""
225
#Generated by Kickstart Configurator
226
#platform=x86
227

228
#System language
229
lang en_US
230
#Language modules to install
231
langsupport en_US
232
#System keyboard
233
keyboard us
234
#System mouse
235
mouse
236
#System timezone
237
timezone America/Los_Angeles
238
#Root password
239
rootpw --disabled
240
#Initial user
241
user mininet --fullname "mininet" --password "mininet"
242
#Use text mode install
243
text
244
#Install OS instead of upgrade
245
install
246
#Use CDROM installation media
247
cdrom
248
#System bootloader configuration
249
bootloader --location=mbr
250
#Clear the Master Boot Record
251
zerombr yes
252
#Partition clearing information
253
clearpart --all --initlabel
254
#Automatic partitioning
255
autopart
256
#System authorization infomation
257
auth  --useshadow  --enablemd5
258
#Firewall configuration
259
firewall --disabled
260
#Do not configure the X Window System
261
skipx
262
"""
263

    
264
# Tell the Ubuntu/Debian installer to stop asking stupid questions
265

    
266
PreseedText = """
267
d-i mirror/country string manual
268
d-i mirror/http/hostname string mirrors.kernel.org
269
d-i mirror/http/directory string /ubuntu
270
d-i mirror/http/proxy string
271
d-i partman/confirm_write_new_label boolean true
272
d-i partman/choose_partition select finish
273
d-i partman/confirm boolean true
274
d-i partman/confirm_nooverwrite boolean true
275
d-i user-setup/allow-password-weak boolean true
276
d-i finish-install/reboot_in_progress note
277
d-i debian-installer/exit/poweroff boolean true
278
"""
279

    
280
def makeKickstartFloppy():
281
    "Create and return kickstart floppy, kickstart, preseed"
282
    kickstart = 'ks.cfg'
283
    with open( kickstart, 'w' ) as f:
284
        f.write( KickstartText )
285
    preseed = 'ks.preseed'
286
    with open( preseed, 'w' ) as f:
287
        f.write( PreseedText )
288
    # Create floppy and copy files to it
289
    floppy = 'ksfloppy.img'
290
    run( 'qemu-img create %s 1440k' % floppy )
291
    run( 'mkfs -t msdos ' + floppy )
292
    run( 'mcopy -i %s %s ::/' % ( floppy, kickstart ) )
293
    run( 'mcopy -i %s %s ::/' % ( floppy, preseed ) )
294
    return floppy, kickstart, preseed
295

    
296

    
297
def kvmFor( filepath ):
298
    "Guess kvm version for file path"
299
    name = path.basename( filepath )
300
    if '64' in name:
301
        kvm = 'qemu-system-x86_64'
302
    elif 'i386' in name or '32' in name:
303
        kvm = 'qemu-system-i386'
304
    else:
305
        log( "Error: can't discern CPU for file name", name )
306
        exit( 1 )
307
    return kvm
308

    
309

    
310
def installUbuntu( iso, image, logfilename='install.log' ):
311
    "Install Ubuntu from iso onto image"
312
    kvm = kvmFor( iso )
313
    floppy, kickstart, preseed = makeKickstartFloppy()
314
    # Mount iso so we can use its kernel
315
    mnt = mkdtemp()
316
    srun( 'mount %s %s' % ( iso, mnt ) )
317
    kernel = path.join( mnt, 'install/vmlinuz' )
318
    initrd = path.join( mnt, 'install/initrd.gz' )
319
    cmd = [ 'sudo', kvm,
320
           '-machine', 'accel=kvm',
321
           '-nographic',
322
           '-netdev', 'user,id=mnbuild',
323
           '-device', 'virtio-net,netdev=mnbuild',
324
           '-m', '1024',
325
           '-k', 'en-us',
326
           '-fda', floppy,
327
           '-drive', 'file=%s,if=virtio' % image,
328
           '-cdrom', iso,
329
           '-kernel', kernel,
330
           '-initrd', initrd,
331
           '-append',
332
           ' ks=floppy:/' + kickstart +
333
           ' preseed/file=floppy://' + preseed +
334
           ' console=ttyS0' ]
335
    ubuntuStart = time()
336
    log( '* INSTALLING UBUNTU FROM', iso, 'ONTO', image )
337
    log( ' '.join( cmd ) )
338
    log( '* logging to', abspath( logfilename ) )
339
    logfile = open( logfilename, 'w' )
340
    vm = Popen( cmd, stdout=logfile, stderr=logfile )
341
    log( '* Waiting for installation to complete')
342
    vm.wait()
343
    logfile.close()
344
    elapsed = time() - ubuntuStart
345
    # Unmount iso and clean up
346
    srun( 'umount ' + mnt )
347
    run( 'rmdir ' + mnt )
348
    log( '* UBUNTU INSTALLATION COMPLETED FOR', image )
349
    log( '* Ubuntu installation completed in %.2f seconds ' % elapsed )
350

    
351

    
352
def boot( cow, kernel, initrd, logfile ):
353
    """Boot qemu/kvm with a COW disk and local/user data store
354
       cow: COW disk path
355
       kernel: kernel path
356
       logfile: log file for pexpect object
357
       returns: pexpect object to qemu process"""
358
    # pexpect might not be installed until after depend() is called
359
    global pexpect
360
    import pexpect
361
    kvm = kvmFor( kernel )
362
    cmd = [ 'sudo', kvm,
363
            '-machine accel=kvm',
364
            '-nographic',
365
            '-netdev user,id=mnbuild',
366
            '-device virtio-net,netdev=mnbuild',
367
            '-m 1024',
368
            '-k en-us',
369
            '-kernel', kernel,
370
            '-initrd', initrd,
371
            '-drive file=%s,if=virtio' % cow,
372
            '-append "root=/dev/vda1 init=/sbin/init console=ttyS0" ' ]
373
    cmd = ' '.join( cmd )
374
    log( '* BOOTING VM FROM', cow )
375
    log( cmd )
376
    vm = pexpect.spawn( cmd, timeout=TIMEOUT, logfile=logfile )
377
    return vm
378

    
379

    
380
def interact( vm ):
381
    "Interact with vm, which is a pexpect object"
382
    prompt = '\$ '
383
    log( '* Waiting for login prompt' )
384
    vm.expect( 'login: ' )
385
    log( '* Logging in' )
386
    vm.sendline( 'mininet' )
387
    log( '* Waiting for password prompt' )
388
    vm.expect( 'Password: ' )
389
    log( '* Sending password' )
390
    vm.sendline( 'mininet' )
391
    log( '* Waiting for login...' )
392
    vm.expect( prompt )
393
    log( '* Sending hostname command' )
394
    vm.sendline( 'hostname' )
395
    log( '* Waiting for output' )
396
    vm.expect( prompt )
397
    log( '* Fetching Mininet VM install script' )
398
    vm.sendline( 'wget '
399
                 'https://raw.github.com/mininet/mininet/master/util/vm/'
400
                 'install-mininet-vm.sh' )
401
    vm.expect( prompt )
402
    log( '* Running VM install script' )
403
    vm.sendline( 'bash install-mininet-vm.sh' )
404
    vm.expect ( 'password for mininet: ' )
405
    vm.sendline( 'mininet' )
406
    log( '* Waiting for script to complete... ' )
407
    # Gigantic timeout for now ;-(
408
    vm.expect( 'Done preparing Mininet', timeout=3600 )
409
    log( '* Completed successfully' )
410
    vm.expect( prompt )
411
    log( '* Testing Mininet' )
412
    vm.sendline( 'sudo mn --test pingall' )
413
    if vm.expect( [ ' 0% dropped', pexpect.TIMEOUT ], timeout=45 ) == 0:
414
        log( '* Sanity check OK' )
415
    else:
416
        log( '* Sanity check FAILED' )
417
    vm.expect( prompt )
418
    log( '* Making sure cgroups are mounted' )
419
    vm.sendline( 'sudo service cgroup-lite restart' )
420
    vm.expect( prompt )
421
    vm.sendline( 'sudo cgroups-mount' )
422
    vm.expect( prompt )
423
    log( '* Running make test' )
424
    vm.sendline( 'cd ~/mininet; sudo make test' )
425
    # We should change "make test" to report the number of
426
    # successful and failed tests. For now, we have to
427
    # know the time for each test, which means that this
428
    # script will have to change as we add more tests.
429
    for test in range( 0, 2 ):
430
        if vm.expect( [ 'OK', 'FAILED', pexpect.timeout ], timeout=60 ) == 0:
431
            log( '* Test', test, 'OK' )
432
        else:
433
            log( '* Test', test, 'FAILED' )
434
    vm.expect( prompt )
435
    log( '* Shutting down' )
436
    vm.sendline( 'sync; sudo shutdown -h now' )
437
    log( '* Waiting for EOF/shutdown' )
438
    vm.read()
439
    log( '* Interaction complete' )
440

    
441

    
442
def cleanup():
443
    "Clean up leftover qemu-nbd processes and other junk"
444
    call( [ 'sudo', 'pkill', '-9', 'qemu-nbd' ] )
445

    
446

    
447
def convert( cow, basename ):
448
    """Convert a qcow2 disk to a vmdk and put it a new directory
449
       basename: base name for output vmdk file"""
450
    vmdk = basename + '.vmdk'
451
    log( '* Converting qcow2 to vmdk' )
452
    run( 'qemu-img convert -f qcow2 -O vmdk %s %s' % ( cow, vmdk ) )
453
    return vmdk
454

    
455

    
456
# Template for virt-image(5) file
457

    
458
VirtImageXML = """
459
<?xml version="1.0" encoding="UTF-8"?>
460
<image>
461
    <name>%s</name>
462
    <domain>
463
        <boot type="hvm">
464
            <guest>
465
                <arch>%s/arch>
466
            </guest>
467
            <os>
468
                <loader dev="hd"/>
469
            </os>
470
            <drive disk="root.raw" target="hda"/>
471
        </boot>
472
        <devices>
473
            <vcpu>1</vcpu>
474
            <memory>%s</memory>
475
            <interface/>
476
            <graphics/>
477
        </devices>
478
    </domain>
479
        <storage>
480
            <disk file="%s" size="%s" format="vmdk"/>
481
        </storage>
482
</image>
483
"""
484

    
485
def genVirtImage( name, mem, diskname, disksize ):
486
    "Generate and return virt-image file name.xml"
487
    # Our strategy is going to be: create a
488
    # virt-image file and then use virt-convert to convert
489
    # it to an .ovf file
490
    xmlfile = name + '.xml'
491
    xmltext = VirtImageXML % ( name, mem, diskname, disksize )
492
    with open( xmlfile, 'w+' ) as f:
493
        f.write( xmltext )
494
    return xmlfile
495

    
496

    
497
def build( flavor='raring32server' ):
498
    "Build a Mininet VM"
499
    start = time()
500
    date = strftime( '%y%m%d-%H-%M-%S', localtime())
501
    dir = 'mn-%s-%s' % ( flavor, date )
502
    try:
503
        os.mkdir( dir )
504
    except:
505
        raise Exception( "Failed to create build directory %s" % dir )
506
    os.chdir( dir )
507
    log( '* Created working directory', dir )
508
    image, kernel, initrd = findBaseImage( flavor )
509
    volume = flavor + '.qcow2'
510
    run( 'qemu-img create -f qcow2 -b %s %s' % ( image, volume ) )
511
    log( '* VM image for', flavor, 'created as', volume )
512
    logfile = open( flavor + '.log', 'w+' )
513
    log( '* Logging results to', abspath( logfile.name ) )
514
    vm = boot( volume, kernel, initrd, logfile )
515
    interact( vm )
516
    vmdk = convert( volume, basename=flavor )
517
    log( '* Converted VM image stored as', abspath( vmdk ) )
518
    end = time()
519
    elapsed = end - start
520
    log( '* Results logged to', abspath( logfile.name ) )
521
    log( '* Completed in %.2f seconds' % elapsed )
522
    log( '* %s VM build DONE!!!!! :D' % flavor )
523
    os.chdir( '..' )
524

    
525

    
526
def listFlavors():
527
    "List valid build flavors"
528
    print '\nvalid build flavors:', ' '.join( isoURLs ), '\n'
529

    
530

    
531
def parseArgs():
532
    "Parse command line arguments and run"
533
    parser = argparse.ArgumentParser( description='Mininet VM build script' )
534
    parser.add_argument( '--depend', action='store_true',
535
                         help='install dependencies for this script' )
536
    parser.add_argument( '--list', action='store_true',
537
                         help='list valid build flavors' )
538
    parser.add_argument( '--clean', action='store_true',
539
                         help='clean up leftover build junk (e.g. qemu-nbd)' )
540
    parser.add_argument( 'flavor', nargs='*',
541
                         help='VM flavor to build (e.g. raring32server)' )
542
    args = parser.parse_args( argv )
543
    if args.depend:
544
        depend()
545
    if args.list:
546
        listFlavors()
547
    if args.clean:
548
        cleanup()
549
    flavors = args.flavor[ 1: ]
550
    for flavor in flavors:
551
        if flavor not in isoURLs:
552
            parser.print_help()
553
            listFlavors()
554
            break
555
        # try:
556
        build( flavor )
557
        # except Exception as e:
558
        # log( '* BUILD FAILED with exception: ', e )
559
        # exit( 1 )
560
    if not ( args.depend or args.list or args.clean or flavors ):
561
        parser.print_help()
562
        listFlavors()
563

    
564
if __name__ == '__main__':
565
    parseArgs()