1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import ConfigParser
21 from gettext import gettext
22 import logging
23 import re
24 import os
25 import optparse
26 import shutil
27 import tempfile
28 import textwrap
29 import socket
30 import struct
31 import urllib
32 import VMBuilder
33 import VMBuilder.util as util
34 import VMBuilder.log as log
35 import VMBuilder.disk as disk
36 from VMBuilder.disk import Disk, Filesystem
37 from VMBuilder.exception import VMBuilderException, VMBuilderUserError
38 _ = gettext
39
41 """The VM object has the following attributes of relevance to plugins:
42
43 distro: A distro object, representing the distro running in the vm
44
45 disks: The disk images for the vm.
46 filesystems: The filesystem images for the vm.
47
48 result_files: A list of the files that make up the entire vm.
49 The ownership of these files will be fixed up.
50
51 optparser: Will be of interest mostly to frontends. Any sort of option
52 a plugin accepts will be represented in the optparser.
53
54
55 """
57 self.hypervisor = None
58 self.distro = None
59
60 self.disks = []
61 self.filesystems = []
62
63 self.result_files = []
64 self.plugins = []
65 self._cleanup_cbs = []
66
67
68 self.destdir = None
69
70 self.workdir = None
71
72 self.rootmnt = None
73
74 self.tmproot = None
75
76 self.fsmounted = False
77
78 self.optparser = _MyOptParser(epilog="ubuntu-vm-builder is Copyright (C) 2007-2009 Canonical Ltd. and written by Soren Hansen <soren@canonical.com>.", usage='%prog hypervisor distro [options]')
79 self.optparser.arg_help = (('hypervisor', self.hypervisor_help), ('distro', self.distro_help))
80
81 self.confparser = ConfigParser.SafeConfigParser()
82
83 if conf:
84 if not(os.path.isfile(conf)):
85 raise VMBuilderUserError('The path to the configuration file is not valid: %s.' % conf)
86 else:
87 conf = ''
88
89 self.confparser.read(['/etc/vmbuilder.cfg', os.path.expanduser('~/.vmbuilder.cfg'), conf])
90
91 self._register_base_settings()
92
93 self.add_clean_cmd('rm', log.logfile)
94
102
104 logging.info("Cleaning up")
105 while len(self._cleanup_cbs) > 0:
106 self._cleanup_cbs.pop(0)()
107
109 self._cleanup_cbs.insert(0, cb)
110
115
117 try:
118 self._cleanup_cbs.remove(cb)
119 except ValueError, e:
120
121 pass
122
125
128
130 return self.optparser.add_option(*args, **kwargs)
131
133 return self.optparser.add_option_group(group)
134
136 return optparse.OptionGroup(self.optparser, *args, **kwargs)
137
139 self.register_setting('-d', '--dest', dest='destdir', help='Specify the destination directory. [default: <hypervisor>-<distro>].')
140 self.register_setting('-c', '--config', type='string', help='Specify a additional configuration file')
141 self.register_setting('--debug', action='callback', callback=log.set_verbosity, help='Show debug information')
142 self.register_setting('-v', '--verbose', action='callback', callback=log.set_verbosity, help='Show progress information')
143 self.register_setting('-q', '--quiet', action='callback', callback=log.set_verbosity, help='Silent operation')
144 self.register_setting('-t', '--tmp', default=os.environ.get('TMPDIR', '/tmp'), help='Use TMP as temporary working space for image generation. Defaults to $TMPDIR if it is defined or /tmp otherwise. [default: %default]')
145 self.register_setting('--templates', metavar='DIR', help='Prepend DIR to template search path.')
146 self.register_setting('-o', '--overwrite', action='store_true', default=False, help='Force overwrite of destination directory if it already exist. [default: %default]')
147 self.register_setting('--in-place', action='store_true', default=False, help='Install directly into the filesystem images. This is needed if your $TMPDIR is nodev and/or nosuid, but will result in slightly larger file system images.')
148 self.register_setting('--tmpfs', metavar="OPTS", help='Use a tmpfs as the working directory, specifying its size or "-" to use tmpfs default (suid,dev,size=1G).')
149 self.register_setting('-m', '--mem', type='int', default=128, help='Assign MEM megabytes of memory to the guest vm. [default: %default]')
150 self.register_setting('--cpus', type='int', default=1, help='Number of virtual CPU\'s. [default: %default]')
151
152 group = self.setting_group('Network related options')
153 domainname = '.'.join(socket.gethostbyname_ex(socket.gethostname())[0].split('.')[1:]) or "defaultdomain"
154 group.add_option('--domain', metavar='DOMAIN', default=domainname, help='Set DOMAIN as the domain name of the guest [default: The domain of the machine running this script: %default].')
155 group.add_option('--ip', metavar='ADDRESS', default='dhcp', help='IP address in dotted form [default: %default].')
156 group.add_option('--mac', metavar='VALUE', help='MAC address of the guest [default: one will be automatically generated on first run].')
157 group.add_option('--mask', metavar='VALUE', help='IP mask in dotted form [default: based on ip setting]. Ignored if --ip is not specified.')
158 group.add_option('--net', metavar='ADDRESS', help='IP net address in dotted form [default: based on ip setting]. Ignored if --ip is not specified.')
159 group.add_option('--bcast', metavar='VALUE', help='IP broadcast in dotted form [default: based on ip setting]. Ignored if --ip is not specified.')
160 group.add_option('--gw', metavar='ADDRESS', help='Gateway (router) address in dotted form [default: based on ip setting (first valid address in the network)]. Ignored if --ip is not specified.')
161 group.add_option('--dns', metavar='ADDRESS', help='DNS address in dotted form [default: based on ip setting (first valid address in the network)] Ignored if --ip is not specified.')
162 self.register_setting_group(group)
163
165 """Adds a disk image to the virtual machine"""
166 disk = Disk(self, *args, **kwargs)
167 self.disks.append(disk)
168 return disk
169
171 """Adds a filesystem to the virtual machine"""
172 fs = Filesystem(self, *args, **kwargs)
173 self.filesystems.append(fs)
174 return fs
175
177 for plugin in self.plugins:
178 getattr(plugin, func)()
179 getattr(self.hypervisor, func)()
180 getattr(self.distro, func)()
181
183 """
184 "Deploy" the VM, by asking the plugins in turn to deploy it.
185
186 If no non-hypervior and non-distro plugin accepts to deploy
187 the image, thfe hypervisor's default deployment is used.
188
189 Returns when the first True is returned.
190 """
191 for plugin in self.plugins:
192 if getattr(plugin, 'deploy')():
193 return True
194 getattr(self.hypervisor, 'deploy')()
195
202
209
211
212
213 confvalue = None
214 try:
215 confvalue = self.confparser.get('DEFAULT', key)
216 except ConfigParser.NoSectionError, e:
217 pass
218 except ConfigParser.NoOptionError, e:
219 pass
220
221 try:
222 confvalue = self.confparser.get(self.hypervisor.arg, key)
223 except ConfigParser.NoSectionError, e:
224 pass
225 except ConfigParser.NoOptionError, e:
226 pass
227
228 try:
229 confvalue = self.confparser.get(self.distro.arg, key)
230 except ConfigParser.NoSectionError, e:
231 pass
232 except ConfigParser.NoOptionError, e:
233 pass
234
235 try:
236 confvalue = self.confparser.get('%s/%s' % (self.hypervisor.arg, self.distro.arg), key)
237 except ConfigParser.NoSectionError, e:
238 pass
239 except ConfigParser.NoOptionError, e:
240 pass
241
242 logging.debug('Returning value %s for configuration key %s' % (repr(confvalue), key))
243 return confvalue
244
246 """
247 is called to give all the plugins and the distro and hypervisor plugin a chance to set
248 some reasonable defaults, which the frontend then can inspect and present
249 """
250 multiline_split = re.compile("\s*,\s*")
251 if self.distro and self.hypervisor:
252 for plugin in VMBuilder._plugins:
253 self.plugins.append(plugin(self))
254
255 self.optparser.set_defaults(destdir='%s-%s' % (self.distro.arg, self.hypervisor.arg))
256
257 (settings, dummy) = self.optparser.parse_args([])
258 for (k,v) in settings.__dict__.iteritems():
259 confvalue = self.get_conf_value(k)
260 if confvalue:
261 if self.optparser.get_option('--%s' % k):
262 if self.optparser.get_option('--%s' % k).action == 'append':
263 values = multiline_split.split(confvalue)
264 setattr(self, k, values)
265 else:
266 setattr(self, k, confvalue)
267 else:
268 setattr(self, k, confvalue)
269 else:
270 setattr(self, k, v)
271
272 self.distro.set_defaults()
273 self.hypervisor.set_defaults()
274
275
277 """
278 is called to validate the ip configuration given and set defaults
279 """
280
281 logging.debug("ip: %s" % self.ip)
282
283 if self.mac:
284 valid_mac_address = re.compile("([0-9a-f]{2}:){5}([0-9a-f]{2})", re.IGNORECASE)
285 if not valid_mac_address.search(self.mac):
286 raise VMBuilderUserError("Malformed MAC address entered: %s" % self.mac)
287 else:
288 logging.debug("Valid mac given: %s" % self.mac)
289
290 if self.ip != 'dhcp':
291 if self.domain == '':
292 raise VMBuilderUserError('Domain is undefined and host has no domain set.')
293
294 try:
295 numip = struct.unpack('I', socket.inet_aton(self.ip))[0]
296 except socket.error:
297 raise VMBuilderUserError('%s is not a valid ip address' % self.ip)
298
299 if not self.mask:
300 ipclass = numip & 0xFF
301 if (ipclass > 0) and (ipclass <= 127):
302 mask = 0xFF
303 elif (ipclass > 128) and (ipclass < 192):
304 mask = OxFFFF
305 elif (ipclass < 224):
306 mask = 0xFFFFFF
307 else:
308 raise VMBuilderUserError('The class of the ip address specified (%s) does not seem right' % self.ip)
309 else:
310 mask = struct.unpack('I', socket.inet_aton(self.mask))[0]
311
312 numnet = numip & mask
313
314 if not self.net:
315 self.net = socket.inet_ntoa( struct.pack('I', numnet ) )
316 if not self.bcast:
317 self.bcast = socket.inet_ntoa( struct.pack('I', numnet + (mask ^ 0xFFFFFFFF)))
318 if not self.gw:
319 self.gw = socket.inet_ntoa( struct.pack('I', numnet + 0x01000000 ) )
320 if not self.dns:
321 self.dns = self.gw
322
323 self.mask = socket.inet_ntoa( struct.pack('I', mask ) )
324
325 logging.debug("net: %s" % self.net)
326 logging.debug("netmask: %s" % self.mask)
327 logging.debug("broadcast: %s" % self.bcast)
328 logging.debug("gateway: %s" % self.gw)
329 logging.debug("dns: %s" % self.dns)
330
332 """Creates the directory structure where we'll be doing all the work
333
334 When create_directory_structure returns, the following attributes will be set:
335
336 - L{VM.destdir}: The final destination for the disk images
337 - L{VM.workdir}: The temporary directory where we'll do all the work
338 - L{VM.rootmnt}: The root mount point where all the target filesystems will be mounted
339 - L{VM.tmproot}: The directory where we build up the guest filesystem
340
341 ..and the corresponding directories are created.
342
343 Additionally, L{VM.destdir} is created, which is where the files (disk images, filesystem
344 images, run scripts, etc.) will eventually be placed.
345 """
346
347 self.workdir = self.create_workdir()
348 self.add_clean_cmd('rm', '-rf', self.workdir)
349
350 logging.debug('Temporary directory: %s', self.workdir)
351
352 self.rootmnt = '%s/target' % self.workdir
353 logging.debug('Creating the root mount directory: %s', self.rootmnt)
354 os.mkdir(self.rootmnt)
355
356 self.tmproot = '%s/root' % self.workdir
357 logging.debug('Creating temporary root: %s', self.tmproot)
358 os.mkdir(self.tmproot)
359
360
361 if os.path.exists(self.destdir):
362 if self.overwrite:
363 logging.info('%s exists, and --overwrite specified. Removing..' % (self.destdir, ))
364 shutil.rmtree(self.destdir)
365 else:
366 raise VMBuilderUserError('%s already exists' % (self.destdir,))
367
368 logging.debug('Creating destination directory: %s', self.destdir)
369 os.mkdir(self.destdir)
370 self.add_clean_cmd('rmdir', self.destdir, ignore_fail=True)
371
372 self.result_files.append(self.destdir)
373
375 """Creates the working directory for this vm and returns its path"""
376 return tempfile.mkdtemp('', 'vmbuilder', self.tmp)
377
379 """Mounts all the vm's partitions and filesystems below .rootmnt"""
380 logging.info('Mounting target filesystems')
381 fss = disk.get_ordered_filesystems(self)
382 for fs in fss:
383 fs.mount()
384 self.distro.post_mount(fs)
385
386 self.fsmounted = True
387
399
401 if self.in_place:
402 self.installdir = self.rootmnt
403 else:
404 self.installdir = self.tmproot
405
406 logging.info("Installing guest operating system. This might take some time...")
407 self.distro.install(self.installdir)
408
409 self.call_hooks('post_install')
410
411 if not self.in_place:
412 logging.info("Copying to disk images")
413 util.run_cmd('rsync', '-aHA', '%s/' % self.tmproot, self.rootmnt)
414
415 if self.hypervisor.needs_bootloader:
416 logging.info("Installing bootloader")
417 self.distro.install_bootloader()
418
419 self.distro.install_vmbuilder_log(log.logfile, self.rootmnt)
420
422 for opt in sum([self.confparser.options(section) for section in self.confparser.sections()], []) + [k for (k,v) in self.confparser.defaults().iteritems()]:
423 if '-' in opt:
424 raise VMBuilderUserError('You specified a "%s" config option in a config file, but that is not valid. Perhaps you meant "%s"?' % (opt, opt.replace('-', '_')))
425
426 self.ip_defaults()
427 self.call_hooks('preflight_check')
428
429
430 if self.mirror:
431 testurl = self.mirror
432 else:
433 testurl = 'http://archive.ubuntu.com/'
434
435 try:
436 logging.debug('Testing access to %s' % testurl)
437 testnet = urllib.urlopen(testurl)
438 except IOError:
439 raise VMBuilderUserError('Could not connect to %s. Please check your connectivity and try again.' % testurl)
440
441 testnet.close()
442
443 - def install_file(self, path, contents=None, source=None, mode=None):
444 fullpath = '%s%s' % (self.installdir, path)
445 if source and not contents:
446 shutil.copy(source, fullpath)
447 else:
448 fp = open(fullpath, 'w')
449 fp.write(contents)
450 fp.close()
451 if mode:
452 os.chmod(fullpath, mode)
453 return fullpath
454
456 """
457 The core vm creation method
458
459 The VM creation happens in the following steps:
460
461 A series of preliminary checks are performed:
462 - We check if we're being run as root, since
463 the filesystem handling requires root priv's
464 - Each plugin's preflight_check method is called.
465 See L{VMBuilder.plugins.Plugin} documentation for details
466 - L{create_directory_structure} is called
467 - VMBuilder.disk.create_partitions is called
468 - VMBuilder.disk.create_filesystems is called
469 - .mount_partitions is called
470 - .install is called
471
472 """
473 util.checkroot()
474
475 finished = False
476 try:
477 self.preflight_check()
478 self.create_directory_structure()
479
480 disk.create_partitions(self)
481 disk.create_filesystems(self)
482 self.mount_partitions()
483
484 self.install()
485
486 self.umount_partitions()
487
488 self.hypervisor.finalize()
489
490 self.deploy()
491
492 util.fix_ownership(self.result_files)
493
494 finished = True
495 except VMBuilderException,e:
496 raise
497 finally:
498 if not finished:
499 logging.debug("Oh, dear, an exception occurred")
500 self.cleanup()
501
551