1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 import ConfigParser
22 from gettext import gettext
23 import logging
24 import re
25 import os
26 import optparse
27 import shutil
28 import tempfile
29 import textwrap
30 import socket
31 import struct
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-2008 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
94 logging.info("Cleaning up")
95 while len(self._cleanup_cbs) > 0:
96 self._cleanup_cbs.pop(0)()
97
99 self._cleanup_cbs.insert(0, cb)
100
105
107 try:
108 self._cleanup_cbs.remove(cb)
109 except ValueError, e:
110
111 pass
112
115
118
120 return self.optparser.add_option(*args, **kwargs)
121
123 return self.optparser.add_option_group(group)
124
126 return optparse.OptionGroup(self.optparser, *args, **kwargs)
127
129 self.register_setting('-d', '--dest', dest='destdir', help='Specify the destination directory. [default: <hypervisor>-<distro>].')
130 self.register_setting('-c', '--config', type='string', help='Specify a additional configuration file')
131 self.register_setting('--debug', action='callback', callback=log.set_verbosity, help='Show debug information')
132 self.register_setting('-v', '--verbose', action='callback', callback=log.set_verbosity, help='Show progress information')
133 self.register_setting('-q', '--quiet', action='callback', callback=log.set_verbosity, help='Silent operation')
134 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]')
135 self.register_setting('--templates', metavar='DIR', help='Prepend DIR to template search path.')
136 self.register_setting('-o', '--overwrite', action='store_true', default=False, help='Force overwrite of destination directory if it already exist. [default: %default]')
137 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.')
138 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).')
139 self.register_setting('-m', '--mem', type='int', default=128, help='Assign MEM megabytes of memory to the guest vm. [default: %default]')
140 self.register_setting('--cpus', type='int', default=1, help='Number of virtual CPU\'s. [default: %default]')
141
142 group = self.setting_group('Network related options')
143 domainname = '.'.join(socket.gethostbyname_ex(socket.gethostname())[0].split('.')[1:])
144 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].')
145 group.add_option('--ip', metavar='ADDRESS', default='dhcp', help='IP address in dotted form [default: %default].')
146 group.add_option('--mac', metavar='VALUE', help='MAC address of the guest [default: one will be automatically generated on first run].')
147 group.add_option('--mask', metavar='VALUE', help='IP mask in dotted form [default: based on ip setting]. Ignored if --ip is not specified.')
148 group.add_option('--net', metavar='ADDRESS', help='IP net address in dotted form [default: based on ip setting]. Ignored if --ip is not specified.')
149 group.add_option('--bcast', metavar='VALUE', help='IP broadcast in dotted form [default: based on ip setting]. Ignored if --ip is not specified.')
150 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.')
151 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.')
152 self.register_setting_group(group)
153
155 """Adds a disk image to the virtual machine"""
156 disk = Disk(self, *args, **kwargs)
157 self.disks.append(disk)
158 return disk
159
161 """Adds a filesystem to the virtual machine"""
162 fs = Filesystem(self, *args, **kwargs)
163 self.filesystems.append(fs)
164 return fs
165
167 for plugin in self.plugins:
168 getattr(plugin, func)()
169 getattr(self.hypervisor, func)()
170 getattr(self.distro, func)()
171
173 """
174 "Deploy" the VM, by asking the plugins in turn to deploy it.
175
176 If no non-hypervior and non-distro plugin accepts to deploy
177 the image, thfe hypervisor's default deployment is used.
178
179 Returns when the first True is returned.
180 """
181 for plugin in self.plugins:
182 if getattr(plugin, 'deploy')():
183 return True
184 getattr(self.hypervisor, 'deploy')()
185
192
199
201
202
203 confvalue = None
204 try:
205 confvalue = self.confparser.get('DEFAULT', key)
206 except ConfigParser.NoSectionError, e:
207 pass
208 except ConfigParser.NoOptionError, e:
209 pass
210
211 try:
212 confvalue = self.confparser.get(self.hypervisor.arg, key)
213 except ConfigParser.NoSectionError, e:
214 pass
215 except ConfigParser.NoOptionError, e:
216 pass
217
218 try:
219 confvalue = self.confparser.get(self.distro.arg, key)
220 except ConfigParser.NoSectionError, e:
221 pass
222 except ConfigParser.NoOptionError, e:
223 pass
224
225 try:
226 confvalue = self.confparser.get('%s/%s' % (self.hypervisor.arg, self.distro.arg), key)
227 except ConfigParser.NoSectionError, e:
228 pass
229 except ConfigParser.NoOptionError, e:
230 pass
231
232 logging.debug('Returning value %s for configuration key %s' % (repr(confvalue), key))
233 return confvalue
234
236 """
237 is called to give all the plugins and the distro and hypervisor plugin a chance to set
238 some reasonable defaults, which the frontend then can inspect and present
239 """
240
241 if self.distro and self.hypervisor:
242 for plugin in VMBuilder._plugins:
243 self.plugins.append(plugin(self))
244
245 self.optparser.set_defaults(destdir='%s-%s' % (self.distro.arg, self.hypervisor.arg))
246
247 (settings, dummy) = self.optparser.parse_args([])
248 for (k,v) in settings.__dict__.iteritems():
249 confvalue = self.get_conf_value(k)
250 if confvalue:
251 if self.optparser.get_option('--%s' % k):
252 if self.optparser.get_option('--%s' % k).action == 'append':
253 setattr(self, k, confvalue.split(', '))
254 else:
255 setattr(self, k, confvalue)
256 else:
257 setattr(self, k, confvalue)
258 else:
259 setattr(self, k, v)
260
261 self.distro.set_defaults()
262 self.hypervisor.set_defaults()
263
264
266 """
267 is called to validate the ip configuration given and set defaults
268 """
269
270 logging.debug("ip: %s" % self.ip)
271
272 if self.mac:
273 valid_mac_address = re.compile("([0-9a-f]{2}:){5}([0-9a-f]{2})", re.IGNORECASE)
274 if not valid_mac_address.search(self.mac):
275 raise VMBuilderUserError("Malformed MAC address entered: %s" % self.mac)
276 else:
277 logging.debug("Valid mac given: %s" % self.mac)
278
279 if self.ip != 'dhcp':
280 if self.domain == '':
281 raise VMBuilderUserError('Domain is undefined and host has no domain set.')
282
283 try:
284 numip = struct.unpack('I', socket.inet_aton(self.ip))[0]
285 except socket.error:
286 raise VMBuilderUserError('%s is not a valid ip address' % self.ip)
287
288 if not self.mask:
289 ipclass = numip & 0xFF
290 if (ipclass > 0) and (ipclass <= 127):
291 mask = 0xFF
292 elif (ipclass > 128) and (ipclass < 192):
293 mask = OxFFFF
294 elif (ipclass < 224):
295 mask = 0xFFFFFF
296 else:
297 raise VMBuilderUserError('The class of the ip address specified (%s) does not seem right' % self.ip)
298 else:
299 mask = struct.unpack('I', socket.inet_aton(self.mask))[0]
300
301 numnet = numip & mask
302
303 if not self.net:
304 self.net = socket.inet_ntoa( struct.pack('I', numnet ) )
305 if not self.bcast:
306 self.bcast = socket.inet_ntoa( struct.pack('I', numnet + (mask ^ 0xFFFFFFFF)))
307 if not self.gw:
308 self.gw = socket.inet_ntoa( struct.pack('I', numnet + 0x01000000 ) )
309 if not self.dns:
310 self.dns = self.gw
311
312 self.mask = socket.inet_ntoa( struct.pack('I', mask ) )
313
314 logging.debug("net: %s" % self.net)
315 logging.debug("netmask: %s" % self.mask)
316 logging.debug("broadcast: %s" % self.bcast)
317 logging.debug("gateway: %s" % self.gw)
318 logging.debug("dns: %s" % self.dns)
319
321 """Creates the directory structure where we'll be doing all the work
322
323 When create_directory_structure returns, the following attributes will be set:
324
325 - L{VM.destdir}: The final destination for the disk images
326 - L{VM.workdir}: The temporary directory where we'll do all the work
327 - L{VM.rootmnt}: The root mount point where all the target filesystems will be mounted
328 - L{VM.tmproot}: The directory where we build up the guest filesystem
329
330 ..and the corresponding directories are created.
331
332 Additionally, L{VM.destdir} is created, which is where the files (disk images, filesystem
333 images, run scripts, etc.) will eventually be placed.
334 """
335
336 self.workdir = self.create_workdir()
337 self.add_clean_cmd('rm', '-rf', self.workdir)
338
339 logging.debug('Temporary directory: %s', self.workdir)
340
341 self.rootmnt = '%s/target' % self.workdir
342 logging.debug('Creating the root mount directory: %s', self.rootmnt)
343 os.mkdir(self.rootmnt)
344
345 self.tmproot = '%s/root' % self.workdir
346 logging.debug('Creating temporary root: %s', self.tmproot)
347 os.mkdir(self.tmproot)
348
349
350 if os.path.exists(self.destdir):
351 if self.overwrite:
352 logging.info('%s exists, and --overwrite specified. Removing..' % (self.destdir, ))
353 shutil.rmtree(self.destdir)
354 else:
355 raise VMBuilderUserError('%s already exists' % (self.destdir,))
356
357 logging.debug('Creating destination directory: %s', self.destdir)
358 os.mkdir(self.destdir)
359 self.add_clean_cmd('rmdir', self.destdir, ignore_fail=True)
360
361 self.result_files.append(self.destdir)
362
364 """Creates the working directory for this vm and returns its path"""
365 return tempfile.mkdtemp('', 'vmbuilder', self.tmp)
366
368 """Mounts all the vm's partitions and filesystems below .rootmnt"""
369 logging.info('Mounting target filesystems')
370 fss = disk.get_ordered_filesystems(self)
371 for fs in fss:
372 fs.mount()
373 self.distro.post_mount(fs)
374
375 self.fsmounted = True
376
388
390 if self.in_place:
391 self.installdir = self.rootmnt
392 else:
393 self.installdir = self.tmproot
394
395 logging.info("Installing guest operating system. This might take some time...")
396 self.distro.install(self.installdir)
397
398 self.call_hooks('post_install')
399
400 if not self.in_place:
401 logging.info("Copying to disk images")
402 util.run_cmd('rsync', '-aHA', '%s/' % self.tmproot, self.rootmnt)
403
404 if self.hypervisor.needs_bootloader:
405 logging.info("Installing bootloader")
406 self.distro.install_bootloader()
407
409 for opt in sum([self.confparser.options(section) for section in self.confparser.sections()], []) + [k for (k,v) in self.confparser.defaults().iteritems()]:
410 if '-' in opt:
411 raise VMBuilderUserError('You specified a "%s" config option in a config file, but that is not valid. Perhaps you meant "%s"?' % (opt, opt.replace('-', '_')))
412
413 self.ip_defaults()
414 self.call_hooks('preflight_check')
415
416 - def install_file(self, path, contents=None, source=None, mode=None):
417 fullpath = '%s%s' % (self.installdir, path)
418 if source and not contents:
419 shutil.copy(source, fullpath)
420 else:
421 fp = open(fullpath, 'w')
422 fp.write(contents)
423 fp.close()
424 if mode:
425 os.chmod(fullpath, mode)
426 return fullpath
427
429 """
430 The core vm creation method
431
432 The VM creation happens in the following steps:
433
434 A series of preliminary checks are performed:
435 - We check if we're being run as root, since
436 the filesystem handling requires root priv's
437 - Each plugin's preflight_check method is called.
438 See L{VMBuilder.plugins.Plugin} documentation for details
439 - L{create_directory_structure} is called
440 - VMBuilder.disk.create_partitions is called
441 - VMBuilder.disk.create_filesystems is called
442 - .mount_partitions is called
443 - .install is called
444
445 """
446 util.checkroot()
447
448 finished = False
449 try:
450 self.preflight_check()
451 self.create_directory_structure()
452
453 disk.create_partitions(self)
454 disk.create_filesystems(self)
455 self.mount_partitions()
456
457 self.install()
458
459 self.umount_partitions()
460
461 self.hypervisor.finalize()
462
463 self.deploy()
464
465 util.fix_ownership(self.result_files)
466
467 finished = True
468 except VMBuilderException,e:
469 raise e
470 finally:
471 if not finished:
472 logging.debug("Oh, dear, an exception occurred")
473 self.cleanup()
474
524