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 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   
40 -class VM(object):
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 """
56 - def __init__(self, conf=None):
57 self.hypervisor = None #: hypervisor object, representing the hypervisor the vm is destined for 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 #: final destination for the disk images 68 self.destdir = None 69 #: tempdir where we do all the work 70 self.workdir = None 71 #: mount point where the disk images will be mounted 72 self.rootmnt = None 73 #: directory where we build up the guest filesystem 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
93 - def cleanup(self):
94 logging.info("Cleaning up") 95 while len(self._cleanup_cbs) > 0: 96 self._cleanup_cbs.pop(0)()
97
98 - def add_clean_cb(self, cb):
99 self._cleanup_cbs.insert(0, cb)
100
101 - def add_clean_cmd(self, *argv, **kwargs):
102 cb = lambda : util.run_cmd(*argv, **kwargs) 103 self.add_clean_cb(cb) 104 return cb
105
106 - def cancel_cleanup(self, cb):
107 try: 108 self._cleanup_cbs.remove(cb) 109 except ValueError, e: 110 # Wasn't in there. No worries. 111 pass
112
113 - def distro_help(self):
114 return 'Distro. Valid options: %s' % " ".join(VMBuilder.distros.keys())
115
116 - def hypervisor_help(self):
117 return 'Hypervisor. Valid options: %s' % " ".join(VMBuilder.hypervisors.keys())
118
119 - def register_setting(self, *args, **kwargs):
120 return self.optparser.add_option(*args, **kwargs)
121
122 - def register_setting_group(self, group):
123 return self.optparser.add_option_group(group)
124
125 - def setting_group(self, *args, **kwargs):
126 return optparse.OptionGroup(self.optparser, *args, **kwargs)
127
128 - def _register_base_settings(self):
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
154 - def add_disk(self, *args, **kwargs):
155 """Adds a disk image to the virtual machine""" 156 disk = Disk(self, *args, **kwargs) 157 self.disks.append(disk) 158 return disk
159
160 - def add_filesystem(self, *args, **kwargs):
161 """Adds a filesystem to the virtual machine""" 162 fs = Filesystem(self, *args, **kwargs) 163 self.filesystems.append(fs) 164 return fs
165
166 - def call_hooks(self, func):
167 for plugin in self.plugins: 168 getattr(plugin, func)() 169 getattr(self.hypervisor, func)() 170 getattr(self.distro, func)()
171
172 - def deploy(self):
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
186 - def set_distro(self, arg):
187 if arg in VMBuilder.distros.keys(): 188 self.distro = VMBuilder.distros[arg](self) 189 self.set_defaults() 190 else: 191 raise VMBuilderUserError("Invalid distro. Valid distros: %s" % " ".join(VMBuilder.distros.keys()))
192
193 - def set_hypervisor(self, arg):
194 if arg in VMBuilder.hypervisors.keys(): 195 self.hypervisor = VMBuilder.hypervisors[arg](self) 196 self.set_defaults() 197 else: 198 raise VMBuilderUserError("Invalid hypervisor. Valid hypervisors: %s" % " ".join(VMBuilder.hypervisors.keys()))
199
200 - def get_conf_value(self, key):
201 # This is horrible. Did I mention I hate people who (ab)use exceptions 202 # to handle non-exceptional events? 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
235 - def set_defaults(self):
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
265 - def ip_defaults(self):
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 # destdir is where the user's files will land when they're done 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
363 - def create_workdir(self):
364 """Creates the working directory for this vm and returns its path""" 365 return tempfile.mkdtemp('', 'vmbuilder', self.tmp)
366
367 - def mount_partitions(self):
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
377 - def umount_partitions(self):
378 """Unmounts all the vm's partitions and filesystems""" 379 logging.info('Unmounting target filesystem') 380 fss = VMBuilder.disk.get_ordered_filesystems(self) 381 fss.reverse() 382 for fs in fss: 383 fs.umount() 384 for disk in self.disks: 385 disk.unmap() 386 387 self.fsmounted = False
388
389 - def install(self):
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
408 - def preflight_check(self):
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
428 - def create(self):
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
475 -class _MyOptParser(optparse.OptionParser):
476 - def format_arg_help(self, formatter):
477 result = [] 478 for arg in self.arg_help: 479 result.append(self.format_arg(formatter, arg)) 480 return "".join(result)
481
482 - def format_arg(self, formatter, arg):
483 result = [] 484 arghelp = arg[1]() 485 arg = arg[0] 486 width = formatter.help_position - formatter.current_indent - 2 487 if len(arg) > width: 488 arg = "%*s%s\n" % (self.current_indent, "", arg) 489 indent_first = formatter.help_position 490 else: # start help on same line as opts 491 arg = "%*s%-*s " % (formatter.current_indent, "", width, arg) 492 indent_first = 0 493 result.append(arg) 494 help_lines = textwrap.wrap(arghelp, formatter.help_width) 495 result.append("%*s%s\n" % (indent_first, "", help_lines[0])) 496 result.extend(["%*s%s\n" % (formatter.help_position, "", line) 497 for line in help_lines[1:]]) 498 return "".join(result)
499
500 - def format_option_help(self, formatter=None):
501 if formatter is None: 502 formatter = self.formatter 503 formatter.store_option_strings(self) 504 result = [] 505 if self.arg_help: 506 result.append(formatter.format_heading(_("Arguments"))) 507 formatter.indent() 508 result.append(self.format_arg_help(formatter)) 509 result.append("\n") 510 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") 511 result.append("\n") 512 formatter.dedent() 513 result.append(formatter.format_heading(_("Options"))) 514 formatter.indent() 515 if self.option_list: 516 result.append(optparse.OptionContainer.format_option_help(self, formatter)) 517 result.append("\n") 518 for group in self.option_groups: 519 result.append(group.format_help(formatter)) 520 result.append("\n") 521 formatter.dedent() 522 # Drop the last "\n", or the header if no options or option groups: 523 return "".join(result[:-1])
524