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

Source Code for Module VMBuilder.vm

  1  # 
  2  #    Uncomplicated VM Builder 
  3  #    Copyright (C) 2007-2009 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 version 3, as 
  9  #    published by the Free Software Foundation. 
 10  # 
 11  #    This program is distributed in the hope that it will be useful, 
 12  #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  #    GNU General Public License for more details. 
 15  # 
 16  #    You should have received a copy of the GNU General Public License 
 17  #    along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 18  # 
 19  #    The VM class 
 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   
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-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
95 - def get_version_info(self):
96 import vcsversion 97 info = vcsversion.version_info 98 info['major'] = 0 99 info['minor'] = 11 100 info['micro'] = 3 101 return info
102
103 - def cleanup(self):
104 logging.info("Cleaning up") 105 while len(self._cleanup_cbs) > 0: 106 self._cleanup_cbs.pop(0)()
107
108 - def add_clean_cb(self, cb):
109 self._cleanup_cbs.insert(0, cb)
110
111 - def add_clean_cmd(self, *argv, **kwargs):
112 cb = lambda : util.run_cmd(*argv, **kwargs) 113 self.add_clean_cb(cb) 114 return cb
115
116 - def cancel_cleanup(self, cb):
117 try: 118 self._cleanup_cbs.remove(cb) 119 except ValueError, e: 120 # Wasn't in there. No worries. 121 pass
122
123 - def distro_help(self):
124 return 'Distro. Valid options: %s' % " ".join(VMBuilder.distros.keys())
125
126 - def hypervisor_help(self):
127 return 'Hypervisor. Valid options: %s' % " ".join(VMBuilder.hypervisors.keys())
128
129 - def register_setting(self, *args, **kwargs):
130 return self.optparser.add_option(*args, **kwargs)
131
132 - def register_setting_group(self, group):
133 return self.optparser.add_option_group(group)
134
135 - def setting_group(self, *args, **kwargs):
136 return optparse.OptionGroup(self.optparser, *args, **kwargs)
137
138 - def _register_base_settings(self):
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
164 - def add_disk(self, *args, **kwargs):
165 """Adds a disk image to the virtual machine""" 166 disk = Disk(self, *args, **kwargs) 167 self.disks.append(disk) 168 return disk
169
170 - def add_filesystem(self, *args, **kwargs):
171 """Adds a filesystem to the virtual machine""" 172 fs = Filesystem(self, *args, **kwargs) 173 self.filesystems.append(fs) 174 return fs
175
176 - def call_hooks(self, func):
177 for plugin in self.plugins: 178 getattr(plugin, func)() 179 getattr(self.hypervisor, func)() 180 getattr(self.distro, func)()
181
182 - def deploy(self):
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
196 - def set_distro(self, arg):
197 if arg in VMBuilder.distros.keys(): 198 self.distro = VMBuilder.distros[arg](self) 199 self.set_defaults() 200 else: 201 raise VMBuilderUserError("Invalid distro. Valid distros: %s" % " ".join(VMBuilder.distros.keys()))
202
203 - def set_hypervisor(self, arg):
204 if arg in VMBuilder.hypervisors.keys(): 205 self.hypervisor = VMBuilder.hypervisors[arg](self) 206 self.set_defaults() 207 else: 208 raise VMBuilderUserError("Invalid hypervisor. Valid hypervisors: %s" % " ".join(VMBuilder.hypervisors.keys()))
209
210 - def get_conf_value(self, key):
211 # This is horrible. Did I mention I hate people who (ab)use exceptions 212 # to handle non-exceptional events? 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
245 - def set_defaults(self):
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
276 - def ip_defaults(self):
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 # destdir is where the user's files will land when they're done 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
374 - def create_workdir(self):
375 """Creates the working directory for this vm and returns its path""" 376 return tempfile.mkdtemp('', 'vmbuilder', self.tmp)
377
378 - def mount_partitions(self):
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
388 - def umount_partitions(self):
389 """Unmounts all the vm's partitions and filesystems""" 390 logging.info('Unmounting target filesystem') 391 fss = VMBuilder.disk.get_ordered_filesystems(self) 392 fss.reverse() 393 for fs in fss: 394 fs.umount() 395 for disk in self.disks: 396 disk.unmap() 397 398 self.fsmounted = False
399
400 - def install(self):
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
421 - def preflight_check(self):
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 # Check repository availability 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
455 - def create(self):
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
502 -class _MyOptParser(optparse.OptionParser):
503 - def format_arg_help(self, formatter):
504 result = [] 505 for arg in self.arg_help: 506 result.append(self.format_arg(formatter, arg)) 507 return "".join(result)
508
509 - def format_arg(self, formatter, arg):
510 result = [] 511 arghelp = arg[1]() 512 arg = arg[0] 513 width = formatter.help_position - formatter.current_indent - 2 514 if len(arg) > width: 515 arg = "%*s%s\n" % (self.current_indent, "", arg) 516 indent_first = formatter.help_position 517 else: # start help on same line as opts 518 arg = "%*s%-*s " % (formatter.current_indent, "", width, arg) 519 indent_first = 0 520 result.append(arg) 521 help_lines = textwrap.wrap(arghelp, formatter.help_width) 522 result.append("%*s%s\n" % (indent_first, "", help_lines[0])) 523 result.extend(["%*s%s\n" % (formatter.help_position, "", line) 524 for line in help_lines[1:]]) 525 return "".join(result)
526
527 - def format_option_help(self, formatter=None):
528 if formatter is None: 529 formatter = self.formatter 530 formatter.store_option_strings(self) 531 result = [] 532 if self.arg_help: 533 result.append(formatter.format_heading(_("Arguments"))) 534 formatter.indent() 535 result.append(self.format_arg_help(formatter)) 536 result.append("\n") 537 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") 538 result.append("\n") 539 formatter.dedent() 540 result.append(formatter.format_heading(_("Options"))) 541 formatter.indent() 542 if self.option_list: 543 result.append(optparse.OptionContainer.format_option_help(self, formatter)) 544 result.append("\n") 545 for group in self.option_groups: 546 result.append(group.format_help(formatter)) 547 result.append("\n") 548 formatter.dedent() 549 # Drop the last "\n", or the header if no options or option groups: 550 return "".join(result[:-1])
551