Package VMBuilder :: Module vm
[frames] | no frames]

Source Code for Module VMBuilder.vm

  1  # 
  2  #    Uncomplicated VM Builder 
  3  #    Copyright (C) 2007-2008 Canonical Ltd. 
  4  #     
  5  #    See AUTHORS for list of contributors 
  6  # 
  7  #    This program is free software: you can redistribute it and/or modify 
  8  #    it under the terms of the GNU General Public License as published by 
  9  #    the Free Software Foundation, either version 3 of the License, or 
 10  #    (at your option) any later version. 
 11  # 
 12  #    This program is distributed in the hope that it will be useful, 
 13  #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 14  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 15  #    GNU General Public License for more details. 
 16  # 
 17  #    You should have received a copy of the GNU General Public License 
 18  #    along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 19  # 
 20  #    The VM class 
 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   
39 -class VM(object):
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 """
55 - def __init__(self, conf=None):
56 self.hypervisor = None #: hypervisor object, representing the hypervisor the vm is destined for 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 #: final destination for the disk images 67 self.destdir = None 68 #: tempdir where we do all the work 69 self.workdir = None 70 #: mount point where the disk images will be mounted 71 self.rootmnt = None 72 #: directory where we build up the guest filesystem 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
90 - def cleanup(self):
91 logging.info("Cleaning up") 92 while len(self._cleanup_cbs) > 0: 93 self._cleanup_cbs.pop(0)()
94
95 - def add_clean_cb(self, cb):
96 self._cleanup_cbs.insert(0, cb)
97
98 - def add_clean_cmd(self, *argv, **kwargs):
99 cb = lambda : util.run_cmd(*argv, **kwargs) 100 self.add_clean_cb(cb) 101 return cb
102
103 - def cancel_cleanup(self, cb):
104 try: 105 self._cleanup_cbs.remove(cb) 106 except ValueError, e: 107 # Wasn't in there. No worries. 108 pass
109
110 - def distro_help(self):
111 return 'Distro. Valid options: %s' % " ".join(VMBuilder.distros.keys())
112
113 - def hypervisor_help(self):
114 return 'Hypervisor. Valid options: %s' % " ".join(VMBuilder.hypervisors.keys())
115
116 - def register_setting(self, *args, **kwargs):
117 return self.optparser.add_option(*args, **kwargs)
118
119 - def register_setting_group(self, group):
120 return self.optparser.add_option_group(group)
121
122 - def setting_group(self, *args, **kwargs):
123 return optparse.OptionGroup(self.optparser, *args, **kwargs)
124
125 - def _register_base_settings(self):
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
149 - def add_disk(self, *args, **kwargs):
150 """Adds a disk image to the virtual machine""" 151 disk = Disk(self, *args, **kwargs) 152 self.disks.append(disk) 153 return disk
154
155 - def add_filesystem(self, *args, **kwargs):
156 """Adds a filesystem to the virtual machine""" 157 fs = Filesystem(self, *args, **kwargs) 158 self.filesystems.append(fs) 159 return fs
160
161 - def call_hooks(self, func):
162 for plugin in self.plugins: 163 getattr(plugin, func)() 164 getattr(self.hypervisor, func)() 165 getattr(self.distro, func)()
166
167 - def deploy(self):
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
181 - def set_distro(self, arg):
182 if arg in VMBuilder.distros.keys(): 183 self.distro = VMBuilder.distros[arg](self) 184 self.set_defaults() 185 else: 186 raise VMBuilderUserError("Invalid distro. Valid distros: %s" % " ".join(VMBuilder.distros.keys()))
187
188 - def set_hypervisor(self, arg):
189 if arg in VMBuilder.hypervisors.keys(): 190 self.hypervisor = VMBuilder.hypervisors[arg](self) 191 self.set_defaults() 192 else: 193 raise VMBuilderUserError("Invalid hypervisor. Valid hypervisors: %s" % " ".join(VMBuilder.hypervisors.keys()))
194
195 - def get_conf_value(self, key):
196 # This is horrible. Did I mention I hate people who (ab)use exceptions 197 # to handle non-exceptional events? 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
230 - def set_defaults(self):
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
260 - def ip_defaults(self):
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 # destdir is where the user's files will land when they're done 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
351 - def create_workdir(self):
352 """Creates the working directory for this vm and returns its path""" 353 return tempfile.mkdtemp('', 'vmbuilder', self.tmp)
354
355 - def mount_partitions(self):
356 """Mounts all the vm's partitions and filesystems below .rootmnt""" 357 logging.info('Mounting target filesystems') 358 fss = disk.get_ordered_filesystems(self) 359 for fs in fss: 360 fs.mount() 361 self.distro.post_mount(fs)
362
363 - def umount_partitions(self):
364 """Unmounts all the vm's partitions and filesystems""" 365 logging.info('Unmounting target filesystem') 366 fss = VMBuilder.disk.get_ordered_filesystems(self) 367 fss.reverse() 368 for fs in fss: 369 fs.umount() 370 for disk in self.disks: 371 disk.unmap()
372
373 - def install(self):
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
392 - def preflight_check(self):
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
412 - def create(self):
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
459 -class _MyOptParser(optparse.OptionParser):
460 - def format_arg_help(self, formatter):
461 result = [] 462 for arg in self.arg_help: 463 result.append(self.format_arg(formatter, arg)) 464 return "".join(result)
465
466 - def format_arg(self, formatter, arg):
467 result = [] 468 arghelp = arg[1]() 469 arg = arg[0] 470 width = formatter.help_position - formatter.current_indent - 2 471 if len(arg) > width: 472 arg = "%*s%s\n" % (self.current_indent, "", arg) 473 indent_first = formatter.help_position 474 else: # start help on same line as opts 475 arg = "%*s%-*s " % (formatter.current_indent, "", width, arg) 476 indent_first = 0 477 result.append(arg) 478 help_lines = textwrap.wrap(arghelp, formatter.help_width) 479 result.append("%*s%s\n" % (indent_first, "", help_lines[0])) 480 result.extend(["%*s%s\n" % (formatter.help_position, "", line) 481 for line in help_lines[1:]]) 482 return "".join(result)
483
484 - def format_option_help(self, formatter=None):
485 if formatter is None: 486 formatter = self.formatter 487 formatter.store_option_strings(self) 488 result = [] 489 if self.arg_help: 490 result.append(formatter.format_heading(_("Arguments"))) 491 formatter.indent() 492 result.append(self.format_arg_help(formatter)) 493 result.append("\n") 494 result.append("*** Use vmbuilder <hypervisor> <distro> --help to get more options. Hypervisor, distro, and plugins specific help is only available when the first two arguments are supplied.\n") 495 result.append("\n") 496 formatter.dedent() 497 result.append(formatter.format_heading(_("Options"))) 498 formatter.indent() 499 if self.option_list: 500 result.append(optparse.OptionContainer.format_option_help(self, formatter)) 501 result.append("\n") 502 for group in self.option_groups: 503 result.append(group.format_help(formatter)) 504 result.append("\n") 505 formatter.dedent() 506 # Drop the last "\n", or the header if no options or option groups: 507 return "".join(result[:-1])
508