~soren/vmbuilder/vmbuilder.refactoring

« back to all changes in this revision

Viewing changes to VMBuilder/vm.py

  • Committer: Soren Hansen
  • Date: 2009-12-03 11:34:24 UTC
  • Revision ID: soren@ubuntu.com-20091203113424-sqb2p1518k2oxw75
Development snapshot of the refactoring work, I'm doing. Feel free to look around now, but I'll finish this up and divide it into digestible chunks and submit it in a day or so.

Show diffs side-by-side

added added

removed removed

Lines of Context:
28
28
import textwrap
29
29
import socket
30
30
import struct
 
31
import sys
31
32
import urllib
32
33
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
 
34
import VMBuilder.util              as util
 
35
import VMBuilder.log               as log
 
36
from   VMBuilder.exception         import VMBuilderException, VMBuilderUserError
38
37
_ = gettext
39
38
 
40
39
class VM(object):
45
44
    disks: The disk images for the vm.
46
45
    filesystems: The filesystem images for the vm.
47
46
 
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
47
    optparser: Will be of interest mostly to frontends. Any sort of option
52
48
               a plugin accepts will be represented in the optparser.
53
 
    
54
49
 
55
50
    """
56
51
    def __init__(self, conf=None):
57
 
        self.hypervisor = None #: hypervisor object, representing the hypervisor the vm is destined for
58
52
        self.distro = None
59
53
 
60
54
        self.disks = []
61
55
        self.filesystems = []
62
56
 
63
 
        self.result_files = []
64
 
        self.plugins  = []
 
57
        self.plugins = []
65
58
        self._cleanup_cbs = []
66
59
 
67
60
        #: final destination for the disk images
68
61
        self.destdir = None
69
62
        #: tempdir where we do all the work
70
63
        self.workdir = None
71
 
        #: mount point where the disk images will be mounted
72
 
        self.rootmnt = None
73
64
        #: directory where we build up the guest filesystem
74
65
        self.tmproot = None
75
66
 
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]')
 
67
        self.optparser = _MyOptParser(epilog="VMBuilder is Copyright (C) 2007-2009 Canonical Ltd. and written by Soren Hansen <soren@canonical.com>.", usage='%prog hypervisor distro [options]')
79
68
        self.optparser.arg_help = (('hypervisor', self.hypervisor_help), ('distro', self.distro_help))
80
69
 
81
70
        self.confparser = ConfigParser.SafeConfigParser()
82
71
 
 
72
        configuration_files = ['/etc/vmbuilder.cfg', os.path.expanduser('~/.vmbuilder.cfg')]
 
73
 
83
74
        if conf:
84
 
            if not(os.path.isfile(conf)):
 
75
            if not os.path.isfile(conf):
85
76
                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()
 
77
            configuration_files += [conf]
 
78
 
 
79
        self.confparser.read(configuration_files)
92
80
 
93
81
        self.add_clean_cmd('rm', log.logfile)
94
82
 
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
83
    def cleanup(self):
104
84
        logging.info("Cleaning up")
105
85
        while len(self._cleanup_cbs) > 0:
135
115
    def setting_group(self, *args, **kwargs):
136
116
        return optparse.OptionGroup(self.optparser, *args, **kwargs)
137
117
 
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
118
    def add_disk(self, *args, **kwargs):
165
119
        """Adds a disk image to the virtual machine"""
166
 
        disk = Disk(self, *args, **kwargs)
 
120
        from VMBuilder.disk import Disk
 
121
        disk = Disk(*args, **kwargs)
167
122
        self.disks.append(disk)
168
123
        return disk
169
124
 
170
125
    def add_filesystem(self, *args, **kwargs):
171
126
        """Adds a filesystem to the virtual machine"""
172
 
        fs = Filesystem(self, *args, **kwargs)
 
127
        from VMBuilder.disk import Filesystem
 
128
        fs = Filesystem(*args, **kwargs)
173
129
        self.filesystems.append(fs)
174
130
        return fs
175
131
 
176
132
    def call_hooks(self, func):
 
133
        logging.info('Calling hook: %s' % (func,))
177
134
        for plugin in self.plugins:
 
135
            logging.info('Calling %s method in %s plugin.' % (func, plugin.__module__))
 
136
            cbs = list(self._cleanup_cbs)
178
137
            getattr(plugin, func)()
179
 
        getattr(self.hypervisor, func)()
 
138
        logging.info('Calling %s method in distro plugin.' % (func,))
180
139
        getattr(self.distro, func)()
181
140
        
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
141
    def set_distro(self, arg):
197
142
        if arg in VMBuilder.distros.keys():
198
143
            self.distro = VMBuilder.distros[arg](self)
200
145
        else:
201
146
            raise VMBuilderUserError("Invalid distro. Valid distros: %s" % " ".join(VMBuilder.distros.keys()))
202
147
 
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
148
    def get_conf_value(self, key):
211
149
        # This is horrible. Did I mention I hate people who (ab)use exceptions
212
150
        # to handle non-exceptional events?
218
156
        except ConfigParser.NoOptionError, e:
219
157
            pass
220
158
 
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
 
159
#        try:
 
160
#            confvalue = self.confparser.get(self.hypervisor.arg, key)
 
161
#        except ConfigParser.NoSectionError, e:
 
162
#            pass
 
163
#        except ConfigParser.NoOptionError, e:
 
164
#            pass
227
165
 
228
166
        try:
229
167
            confvalue = self.confparser.get(self.distro.arg, key)
232
170
        except ConfigParser.NoOptionError, e:
233
171
            pass
234
172
 
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
 
173
#        try:
 
174
#            confvalue = self.confparser.get('%s/%s' % (self.hypervisor.arg, self.distro.arg), key)
 
175
#        except ConfigParser.NoSectionError, e:
 
176
#            pass
 
177
#        except ConfigParser.NoOptionError, e:
 
178
#            pass
241
179
 
242
180
        logging.debug('Returning value %s for configuration key %s' % (repr(confvalue), key))
243
181
        return confvalue
244
182
    
245
183
    def set_defaults(self):
246
184
        """
247
 
        is called to give all the plugins and the distro and hypervisor plugin a chance to set
 
185
        is called to give all the plugins and the distro plugin a chance to set
248
186
        some reasonable defaults, which the frontend then can inspect and present
249
187
        """
250
188
        multiline_split = re.compile("\s*,\s*")
251
 
        if self.distro and self.hypervisor:
 
189
        if self.distro:
252
190
            for plugin in VMBuilder._plugins:
253
191
                self.plugins.append(plugin(self))
254
192
 
255
 
            self.optparser.set_defaults(destdir='%s-%s' % (self.distro.arg, self.hypervisor.arg))
 
193
            self.optparser.set_defaults(destdir='%s' % (self.distro.arg,))
256
194
 
257
195
            (settings, dummy) = self.optparser.parse_args([])
258
196
            for (k,v) in settings.__dict__.iteritems():
269
207
                else:
270
208
                    setattr(self, k, v)
271
209
 
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
 
 
331
 
    def create_directory_structure(self):
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)
 
210
            self.call_hooks('set_defaults')
377
211
 
378
212
    def mount_partitions(self):
379
213
        """Mounts all the vm's partitions and filesystems below .rootmnt"""
397
231
 
398
232
        self.fsmounted = False
399
233
 
 
234
    def installdir(self):
 
235
        if self.in_place:
 
236
            return self.rootmnt
 
237
        else:
 
238
            return self.tmproot
 
239
 
400
240
    def install(self):
401
 
        if self.in_place:
402
 
            self.installdir = self.rootmnt
403
 
        else:
404
 
            self.installdir = self.tmproot
405
 
 
406
241
        logging.info("Installing guest operating system. This might take some time...")
407
 
        self.distro.install(self.installdir)
408
242
 
409
243
        self.call_hooks('post_install')
410
244
    
418
252
 
419
253
        self.distro.install_vmbuilder_log(log.logfile, self.rootmnt)
420
254
 
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
255
    def install_file(self, path, contents=None, source=None, mode=None):
444
 
        fullpath = '%s%s' % (self.installdir, path)
 
256
        fullpath = '%s%s' % (self.installdir(), path)
445
257
        if source and not contents:
446
258
            shutil.copy(source, fullpath) 
447
259
        else:
472
284
        """
473
285
        util.checkroot()
474
286
 
475
 
        finished = False
 
287
        failed = False
476
288
        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
 
289
            hooks = ['preflight_check',
 
290
                     'bootstrap',
 
291
                     'customise',
 
292
                     'stop',
 
293
                     'create_partitions',
 
294
                     'create_filesystems',
 
295
                     'mount_partitions',
 
296
                     'umount_partitions',
 
297
                     'finalize',
 
298
                     'deploy']
 
299
 
 
300
            for hook in hooks:
 
301
                if hook == 'stop':
 
302
                    sys.exit(0)
 
303
                self.call_hooks(hook)
 
304
 
 
305
#            util.fix_ownership(self.result_files)
 
306
 
495
307
        except VMBuilderException,e:
 
308
            failed = True
496
309
            raise
497
310
        finally:
498
 
            if not finished:
 
311
            if failed:
499
312
                logging.debug("Oh, dear, an exception occurred")
500
313
            self.cleanup()
501
314