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 os
25 import optparse
26 import shutil
27 import tempfile
28 import textwrap
29 import socket
30 import struct
31 import VMBuilder
32 import VMBuilder.util as util
33 import VMBuilder.log as log
34 import VMBuilder.disk as disk
35 from VMBuilder.disk import Disk, Filesystem
36 from VMBuilder.exception import VMBuilderException, VMBuilderUserError
37 _ = gettext
38
40 """The VM object has the following attributes of relevance to plugins:
41
42 distro: A distro object, representing the distro running in the vm
43
44 disks: The disk images for the vm.
45 filesystems: The filesystem images for the vm.
46
47 result_files: A list of the files that make up the entire vm.
48 The ownership of these files will be fixed up.
49
50 optparser: Will be of interest mostly to frontends. Any sort of option
51 a plugin accepts will be represented in the optparser.
52
53
54 """
56 self.hypervisor = None
57 self.distro = None
58
59 self.disks = []
60 self.filesystems = []
61
62 self.result_files = []
63 self.plugins = []
64 self._cleanup_cbs = []
65
66
67 self.destdir = None
68
69 self.workdir = None
70
71 self.rootmnt = None
72
73 self.tmproot = None
74
75 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]')
76 self.optparser.arg_help = (('hypervisor', self.hypervisor_help), ('distro', self.distro_help))
77
78 self.confparser = ConfigParser.SafeConfigParser()
79
80 if conf:
81 if not(os.path.isfile(conf)):
82 raise VMBuilderUserError('The path to the configuration file is not valid: %s.' % conf)
83 else:
84 conf = ''
85
86 self.confparser.read(['/etc/vmbuilder.cfg', os.path.expanduser('~/.vmbuilder.cfg'), conf])
87
88 self._register_base_settings()
89
91 logging.info("Cleaning up")
92 while len(self._cleanup_cbs) > 0:
93 self._cleanup_cbs.pop(0)()
94
96 self._cleanup_cbs.insert(0, cb)
97
102
104 try:
105 self._cleanup_cbs.remove(cb)
106 except ValueError, e:
107
108 pass
109
112
115
117 return self.optparser.add_option(*args, **kwargs)
118
120 return self.optparser.add_option_group(group)
121
123 return optparse.OptionGroup(self.optparser, *args, **kwargs)
124
126 self.register_setting('-d', '--dest', dest='destdir', help='Specify the destination directory. [default: <hypervisor>-<distro>].')
127 self.register_setting('-c', '--config', type='string', help='Specify a additional configuration file')
128 self.register_setting('--debug', action='callback', callback=log.set_verbosity, help='Show debug information')
129 self.register_setting('-v', '--verbose', action='callback', callback=log.set_verbosity, help='Show progress information')
130 self.register_setting('-q', '--quiet', action='callback', callback=log.set_verbosity, help='Silent operation')
131 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]')
132 self.register_setting('--templates', metavar='DIR', help='Prepend DIR to template search path.')
133 self.register_setting('-o', '--overwrite', action='store_true', default=False, help='Force overwrite of destination directory if it already exist. [default: %default]')
134 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.')
135 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).')
136 self.register_setting('-m', '--mem', type='int', default=128, help='Assign MEM megabytes of memory to the guest vm. [default: %default]')
137
138 group = self.setting_group('Network related options')
139 domainname = '.'.join(socket.gethostbyname_ex(socket.gethostname())[0].split('.')[1:])
140 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.')
141 group.add_option('--ip', metavar='ADDRESS', default='dhcp', help='IP address in dotted form [default: %default]')
142 group.add_option('--mask', metavar='VALUE', help='IP mask in dotted form [default: based on ip setting]. Ignored if --ip is not specified.')
143 group.add_option('--net', metavar='ADDRESS', help='IP net address in dotted form [default: based on ip setting]. Ignored if --ip is not specified.')
144 group.add_option('--bcast', metavar='VALUE', help='IP broadcast in dotted form [default: based on ip setting]. Ignored if --ip is not specified.')
145 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.')
146 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.')
147 self.register_setting_group(group)
148
150 """Adds a disk image to the virtual machine"""
151 disk = Disk(self, *args, **kwargs)
152 self.disks.append(disk)
153 return disk
154
156 """Adds a filesystem to the virtual machine"""
157 fs = Filesystem(self, *args, **kwargs)
158 self.filesystems.append(fs)
159 return fs
160
162 for plugin in self.plugins:
163 getattr(plugin, func)()
164 getattr(self.hypervisor, func)()
165 getattr(self.distro, func)()
166
168 """
169 "Deploy" the VM, by asking the plugins in turn to deploy it.
170
171 If no non-hypervior and non-distro plugin accepts to deploy
172 the image, thfe hypervisor's default deployment is used.
173
174 Returns when the first True is returned.
175 """
176 for plugin in self.plugins:
177 if getattr(plugin, 'deploy')():
178 return True
179 getattr(self.hypervisor, 'deploy')()
180
187
194
196
197
198 confvalue = None
199 try:
200 confvalue = self.confparser.get('DEFAULT', key)
201 except ConfigParser.NoSectionError, e:
202 pass
203 except ConfigParser.NoOptionError, e:
204 pass
205
206 try:
207 confvalue = self.confparser.get(self.hypervisor.arg, key)
208 except ConfigParser.NoSectionError, e:
209 pass
210 except ConfigParser.NoOptionError, e:
211 pass
212
213 try:
214 confvalue = self.confparser.get(self.distro.arg, key)
215 except ConfigParser.NoSectionError, e:
216 pass
217 except ConfigParser.NoOptionError, e:
218 pass
219
220 try:
221 confvalue = self.confparser.get('%s/%s' % (self.hypervisor.arg, self.distro.arg), key)
222 except ConfigParser.NoSectionError, e:
223 pass
224 except ConfigParser.NoOptionError, e:
225 pass
226
227 logging.debug('Returning value %s for configuration key %s' % (repr(confvalue), key))
228 return confvalue
229
231 """
232 is called to give all the plugins and the distro and hypervisor plugin a chance to set
233 some reasonable defaults, which the frontend then can inspect and present
234 """
235
236 if self.distro and self.hypervisor:
237 for plugin in VMBuilder._plugins:
238 self.plugins.append(plugin(self))
239
240 self.optparser.set_defaults(destdir='%s-%s' % (self.distro.arg, self.hypervisor.arg))
241
242 (settings, dummy) = self.optparser.parse_args([])
243 for (k,v) in settings.__dict__.iteritems():
244 confvalue = self.get_conf_value(k)
245 if confvalue:
246 if self.optparser.get_option('--%s' % k):
247 if self.optparser.get_option('--%s' % k).action == 'append':
248 setattr(self, k, confvalue.split(', '))
249 else:
250 setattr(self, k, confvalue)
251 else:
252 setattr(self, k, confvalue)
253 else:
254 setattr(self, k, v)
255
256 self.distro.set_defaults()
257 self.hypervisor.set_defaults()
258
259
261 """
262 is called to validate the ip configuration given and set defaults
263 """
264
265 logging.debug("ip: %s" % self.ip)
266
267 if self.ip != 'dhcp':
268 if self.domain == '':
269 raise VMBuilderUserError('Domain is undefined and host has no domain set.')
270
271 try:
272 numip = struct.unpack('I', socket.inet_aton(self.ip))[0]
273 except socket.error:
274 raise VMBuilderUserError('%s is not a valid ip address' % self.ip)
275
276 if not self.mask:
277 ipclass = numip & 0xFF
278 if (ipclass > 0) and (ipclass <= 127):
279 mask = 0xFF
280 elif (ipclass > 128) and (ipclass < 192):
281 mask = OxFFFF
282 elif (ipclass < 224):
283 mask = 0xFFFFFF
284 else:
285 raise VMBuilderUserError('The class of the ip address specified (%s) does not seem right' % self.ip)
286 else:
287 mask = struct.unpack('I', socket.inet_aton(self.mask))[0]
288
289 numnet = numip & mask
290
291 if not self.net:
292 self.net = socket.inet_ntoa( struct.pack('I', numnet ) )
293 if not self.bcast:
294 self.bcast = socket.inet_ntoa( struct.pack('I', numnet + (mask ^ 0xFFFFFFFF)))
295 if not self.gw:
296 self.gw = socket.inet_ntoa( struct.pack('I', numnet + 0x01000000 ) )
297 if not self.dns:
298 self.dns = self.gw
299
300 self.mask = socket.inet_ntoa( struct.pack('I', mask ) )
301
302 logging.debug("net: %s" % self.net)
303 logging.debug("netmask: %s" % self.mask)
304 logging.debug("broadcast: %s" % self.bcast)
305 logging.debug("gateway: %s" % self.gw)
306 logging.debug("dns: %s" % self.dns)
307
309 """Creates the directory structure where we'll be doing all the work
310
311 When create_directory_structure returns, the following attributes will be set:
312
313 - L{VM.destdir}: The final destination for the disk images
314 - L{VM.workdir}: The temporary directory where we'll do all the work
315 - L{VM.rootmnt}: The root mount point where all the target filesystems will be mounted
316 - L{VM.tmproot}: The directory where we build up the guest filesystem
317
318 ..and the corresponding directories are created.
319
320 Additionally, L{VM.destdir} is created, which is where the files (disk images, filesystem
321 images, run scripts, etc.) will eventually be placed.
322 """
323
324 self.workdir = self.create_workdir()
325 self.add_clean_cmd('rm', '-rf', self.workdir)
326
327 logging.debug('Temporary directory: %s', self.workdir)
328
329 self.rootmnt = '%s/target' % self.workdir
330 logging.debug('Creating the root mount directory: %s', self.rootmnt)
331 os.mkdir(self.rootmnt)
332
333 self.tmproot = '%s/root' % self.workdir
334 logging.debug('Creating temporary root: %s', self.tmproot)
335 os.mkdir(self.tmproot)
336
337
338 if os.path.exists(self.destdir):
339 if self.overwrite:
340 logging.info('%s exists, and --overwrite specified. Removing..' % (self.destdir, ))
341 shutil.rmtree(self.destdir)
342 else:
343 raise VMBuilderUserError('%s already exists' % (self.destdir,))
344
345 logging.debug('Creating destination directory: %s', self.destdir)
346 os.mkdir(self.destdir)
347 self.add_clean_cmd('rmdir', self.destdir, ignore_fail=True)
348
349 self.result_files.append(self.destdir)
350
352 """Creates the working directory for this vm and returns its path"""
353 return tempfile.mkdtemp('', 'vmbuilder', self.tmp)
354
362
372
374 if self.in_place:
375 self.installdir = self.rootmnt
376 else:
377 self.installdir = self.tmproot
378
379 logging.info("Installing guest operating system. This might take some time...")
380 self.distro.install(self.installdir)
381
382 self.call_hooks('post_install')
383
384 if not self.in_place:
385 logging.info("Copying to disk images")
386 util.run_cmd('rsync', '-aHA', '%s/' % self.tmproot, self.rootmnt)
387
388 if self.hypervisor.needs_bootloader:
389 logging.info("Installing bootloader")
390 self.distro.install_bootloader()
391
393 for opt in sum([self.confparser.options(section) for section in self.confparser.sections()], []) + [k for (k,v) in self.confparser.defaults().iteritems()]:
394 if '-' in opt:
395 raise VMBuilderUserError('You specified a "%s" config option in a config file, but that is not valid. Perhaps you meant "%s"?' % (opt, opt.replace('-', '_')))
396
397 self.ip_defaults()
398 self.call_hooks('preflight_check')
399
400 - def install_file(self, path, contents=None, source=None, mode=None):
401 fullpath = '%s%s' % (self.installdir, path)
402 if source and not contents:
403 shutil.copy(source, fullpath)
404 else:
405 fp = open(fullpath, 'w')
406 fp.write(contents)
407 fp.close()
408 if mode:
409 os.chmod(fullpath, mode)
410 return fullpath
411
413 """
414 The core vm creation method
415
416 The VM creation happens in the following steps:
417
418 A series of preliminary checks are performed:
419 - We check if we're being run as root, since
420 the filesystem handling requires root priv's
421 - Each plugin's preflight_check method is called.
422 See L{VMBuilder.plugins.Plugin} documentation for details
423 - L{create_directory_structure} is called
424 - VMBuilder.disk.create_partitions is called
425 - VMBuilder.disk.create_filesystems is called
426 - .mount_partitions is called
427 - .install is called
428
429 """
430 util.checkroot()
431
432 finished = False
433 try:
434 self.preflight_check()
435 self.create_directory_structure()
436
437 disk.create_partitions(self)
438 disk.create_filesystems(self)
439 self.mount_partitions()
440
441 self.install()
442
443 self.umount_partitions()
444
445 self.hypervisor.finalize()
446
447 self.deploy()
448
449 util.fix_ownership(self.result_files)
450
451 finished = True
452 except VMBuilderException,e:
453 raise e
454 finally:
455 if not finished:
456 logging.debug("Oh, dear, an exception occurred")
457 self.cleanup()
458
508