Statistics
| Branch: | Tag: | Revision:

mininet / util / vm / build.py @ 1dfa7776

History | View | Annotate | Download (15.5 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
import argparse
39

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

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

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

    
47
isoURLs = {
48
    'quetzal32server':
49
    'http://mirrors.kernel.org/ubuntu-releases/12.10/'
50
    'ubuntu-12.10-server-i386.iso',
51
    'quetzal64server':
52
    'http://mirrors.kernel.org/ubuntu-releases/13.04/'
53
    'ubuntu-12.04-server-amd64.iso',
54
    'raring32server':
55
    'http://mirrors.kernel.org/ubuntu-releases/13.04/'
56
    'ubuntu-13.04-server-i386.iso',
57
    'raring64server':
58
    'http://mirrors.kernel.org/ubuntu-releases/13.04/'
59
    'ubuntu-13.04-server-amd64.iso',
60
}
61

    
62
logStartTime = time()
63

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

    
77

    
78
def run( cmd, **kwargs ):
79
    "Convenient interface to check_output"
80
    log( '-', cmd )
81
    cmd = cmd.split()
82
    return check_output( cmd, **kwargs )
83

    
84

    
85
def srun( cmd, **kwargs ):
86
    "Run + sudo"
87
    return run( 'sudo ' + cmd, **kwargs )
88

    
89

    
90
def depend():
91
    "Install package dependencies"
92
    log( '* Installing package dependencies' )
93
    run( 'sudo apt-get -y update' )
94
    run( 'sudo apt-get install -y'
95
         ' kvm cloud-utils genisoimage qemu-kvm qemu-utils'
96
         ' e2fsprogs '
97
         ' landscape-client'
98
         ' python-setuptools' )
99
    run( 'sudo easy_install pexpect' )
100

    
101

    
102
def popen( cmd ):
103
    "Convenient interface to popen"
104
    log( cmd )
105
    cmd = cmd.split()
106
    return Popen( cmd )
107

    
108

    
109
def remove( fname ):
110
    "rm -f fname"
111
    return run( 'rm -f %s' % fname )
112

    
113

    
114
def findiso( flavor ):
115
    "Find iso, fetching it if it's not there already"
116
    url = isoURLs[ flavor ]
117
    name = path.basename( url )
118
    iso = path.join( VMImageDir, name )
119
    if not path.exists( iso ) or ( stat( iso )[ ST_MODE ] & 0777 != 0444 ):
120
        log( '* Retrieving', url )
121
        run( 'curl -C - -o %s %s' % ( iso, url ) )
122
        # Write-protect iso, signaling it is complete
123
        log( '* Write-protecting iso', iso)
124
        os.chmod( iso, 0444 )
125
    log( '* Using iso', iso )
126
    return iso
127

    
128

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

    
146

    
147
def detachNBD( nbd ):
148
    "Detatch an nbd device"
149
    srun( 'qemu-nbd -d ' + nbd )
150

    
151

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

    
177

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

    
201

    
202
# Kickstart and Preseed files for Ubuntu/Debian installer
203
#
204
# Comments: this is really clunky and painful. If Ubuntu
205
# gets their act together and supports kickstart a bit better
206
# then we can get rid of preseed and even use this as a
207
# Fedora installer as well.
208
#
209
# Another annoying thing about Ubuntu is that it can't just
210
# install a normal system from the iso - it has to download
211
# junk from the internet, making this house of cards even
212
# more precarious.
213

    
214
KickstartText ="""
215
#Generated by Kickstart Configurator
216
#platform=x86
217

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

    
254
# Tell the Ubuntu/Debian installer to stop asking stupid questions
255

    
256
PreseedText = """
257
d-i mirror/country string manual
258
d-i mirror/http/hostname string mirrors.kernel.org
259
d-i mirror/http/directory string /ubuntu
260
d-i mirror/http/proxy string
261
d-i partman/confirm_write_new_label boolean true
262
d-i partman/choose_partition select finish
263
d-i partman/confirm boolean true
264
d-i partman/confirm_nooverwrite boolean true
265
d-i user-setup/allow-password-weak boolean true
266
d-i finish-install/reboot_in_progress note
267
d-i debian-installer/exit/poweroff boolean true
268
"""
269

    
270
def makeKickstartFloppy():
271
    "Create and return kickstart floppy, kickstart, preseed"
272
    kickstart = 'ks.cfg'
273
    with open( kickstart, 'w' ) as f:
274
        f.write( KickstartText )
275
    preseed = 'ks.preseed'
276
    with open( preseed, 'w' ) as f:
277
        f.write( PreseedText )
278
    # Create floppy and copy files to it
279
    floppy = 'ksfloppy.img'
280
    run( 'qemu-img create %s 1440k' % floppy )
281
    run( 'mkfs -t msdos ' + floppy )
282
    run( 'mcopy -i %s %s ::/' % ( floppy, kickstart ) )
283
    run( 'mcopy -i %s %s ::/' % ( floppy, preseed ) )
284
    return floppy, kickstart, preseed
285

    
286

    
287
def kvmFor( filepath ):
288
    "Guess kvm version for file path"
289
    name = path.basename( filepath )
290
    if '64' in name:
291
        kvm = 'qemu-system-x86_64'
292
    elif 'i386' in name or '32' in name:
293
        kvm = 'qemu-system-i386'
294
    else:
295
        log( "Error: can't discern CPU for file name", name )
296
        exit( 1 )
297
    return kvm
298

    
299

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

    
342

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

    
370

    
371
def interact( vm ):
372
    "Interact with vm, which is a pexpect object"
373
    prompt = '\$ '
374
    log( '* Waiting for login prompt' )
375
    vm.expect( 'login: ' )
376
    log( '* Logging in' )
377
    vm.sendline( 'mininet' )
378
    log( '* Waiting for password prompt' )
379
    vm.expect( 'Password: ' )
380
    log( '* Sending password' )
381
    vm.sendline( 'mininet' )
382
    log( '* Waiting for login...' )
383
    vm.expect( prompt )
384
    log( '* Sending hostname command' )
385
    vm.sendline( 'hostname' )
386
    log( '* Waiting for output' )
387
    vm.expect( prompt )
388
    log( '* Fetching Mininet VM install script' )
389
    vm.sendline( 'wget '
390
                 'https://raw.github.com/mininet/mininet/master/util/vm/'
391
                 'install-mininet-vm.sh' )
392
    vm.expect( prompt )
393
    log( '* Running VM install script' )
394
    vm.sendline( 'bash install-mininet-vm.sh' )
395
    vm.expect ( 'password for mininet: ' )
396
    vm.sendline( 'mininet' )
397
    log( '* Waiting for script to complete... ' )
398
    # Gigantic timeout for now ;-(
399
    vm.expect( 'Done preparing Mininet', timeout=3600 )
400
    log( '* Completed successfully' )
401
    vm.expect( prompt )
402
    log( '* Testing Mininet' )
403
    vm.sendline( 'sudo mn --test pingall' )
404
    if vm.expect( [ ' 0% dropped', pexpect.TIMEOUT ], timeout=45 ) == 0:
405
        log( '* Sanity check succeeded' )
406
    else:
407
        log( '* Sanity check FAILED' )
408
    vm.expect( prompt )
409
    log( '* Making sure cgroups are mounted' )
410
    vm.sendline( 'sudo service cgroup-lite restart' )
411
    vm.expect( prompt )
412
    vm.sendline( 'sudo cgroups-mount' )
413
    vm.expect( prompt )
414
    log( '* Running make test' )
415
    vm.sendline( 'cd ~/mininet; sudo make test' )
416
    vm.expect( prompt )
417
    log( '* Shutting down' )
418
    vm.sendline( 'sync; sudo shutdown -h now' )
419
    log( '* Waiting for EOF/shutdown' )
420
    vm.read()
421
    log( '* Interaction complete' )
422

    
423

    
424
def cleanup():
425
    "Clean up leftover qemu-nbd processes and other junk"
426
    call( [ 'sudo', 'pkill', '-9', 'qemu-nbd' ] )
427

    
428

    
429
def convert( cow, basename ):
430
    """Convert a qcow2 disk to a vmdk and put it a new directory
431
       basename: base name for output vmdk file"""
432
    vmdk = basename + '.vmdk'
433
    log( '* Converting qcow2 to vmdk' )
434
    run( 'qemu-img convert -f qcow2 -O vmdk %s %s' % ( cow, vmdk ) )
435
    return vmdk
436

    
437

    
438
def build( flavor='raring32server' ):
439
    "Build a Mininet VM"
440
    start = time()
441
    date = strftime( '%y%m%d-%H-%M-%S', localtime())
442
    dir = 'mn-%s-%s' % ( flavor, date )
443
    try:
444
        os.mkdir( dir )
445
    except:
446
        raise Exception( "Failed to create build directory %s" % dir )
447
    os.chdir( dir )
448
    log( '* Created working directory', dir )
449
    image, kernel, initrd = findBaseImage( flavor )
450
    volume = flavor + '.qcow2'
451
    run( 'qemu-img create -f qcow2 -b %s %s' % ( image, volume ) )
452
    log( '* VM image for', flavor, 'created as', volume )
453
    logfile = open( flavor + '.log', 'w+' )
454
    log( '* Logging results to', abspath( logfile.name ) )
455
    vm = boot( volume, kernel, initrd, logfile )
456
    interact( vm )
457
    vmdk = convert( volume, basename=flavor )
458
    log( '* Converted VM image stored as', abspath( vmdk ) )
459
    end = time()
460
    elapsed = end - start
461
    log( '* Results logged to', abspath( logfile.name ) )
462
    log( '* Completed in %.2f seconds' % elapsed )
463
    log( '* %s VM build DONE!!!!! :D' % flavor )
464
    os.chdir( '..' )
465

    
466

    
467
def listFlavors():
468
    "List valid build flavors"
469
    print '\nvalid build flavors:', ' '.join( isoURLs ), '\n'
470

    
471

    
472
def parseArgs():
473
    "Parse command line arguments and run"
474
    parser = argparse.ArgumentParser( description='Mininet VM build script' )
475
    parser.add_argument( '--depend', action='store_true',
476
                         help='install dependencies for this script' )
477
    parser.add_argument( '--list', action='store_true',
478
                         help='list valid build flavors' )
479
    parser.add_argument( '--clean', action='store_true',
480
                         help='clean up leftover build junk (e.g. qemu-nbd)' )
481
    parser.add_argument( 'flavor', nargs='*',
482
                         help='VM flavor to build (e.g. raring32server)' )
483
    args = parser.parse_args( argv )
484
    if args.depend:
485
        depend()
486
    if args.list:
487
        listFlavors()
488
    if args.clean:
489
        cleanup()
490
    flavors = args.flavor[ 1: ]
491
    for flavor in flavors:
492
        if flavor not in isoURLs:
493
            parser.print_help()
494
            listFlavors()
495
            break
496
        # try:
497
        build( flavor )
498
        # except Exception as e:
499
        # log( '* BUILD FAILED with exception: ', e )
500
        # exit( 1 )
501
    if not ( args.depend or args.list or args.clean or flavors ):
502
        parser.print_help()
503

    
504
if __name__ == '__main__':
505
    parseArgs()