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() |