X-Git-Url: https://git.siccegge.de//index.cgi?p=forks%2Fvmdebootstrap.git;a=blobdiff_plain;f=vmdebootstrap;h=a5537b7abd6f6191ddc2cf8704470db9082184f5;hp=19238c7dab98deacb8d4aaa2bff42d3d9314d32f;hb=0bc3dbfc1fc101a372572aab597175aad45c0be7;hpb=4947c4df8dc3e8a7cafb5d5b0f017f37d4d1ec10 diff --git a/vmdebootstrap b/vmdebootstrap index 19238c7..a5537b7 100755 --- a/vmdebootstrap +++ b/vmdebootstrap @@ -1,5 +1,6 @@ #!/usr/bin/python -# Copyright 2011 Lars Wirzenius +# Copyright 2011, 2012 Lars Wirzenius +# Copyright 2012 Codethink Limited # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,80 +25,111 @@ import subprocess import tempfile +__version__ = '0.1.0' + + class VmDebootstrap(cliapp.Application): def add_settings(self): default_arch = 'amd64' - self.settings.add_boolean_setting(['verbose'], - 'report what is going on') - self.settings.add_string_setting(['image'], - 'put created disk image in FILE', - metavar='FILE') - self.settings.add_bytesize_setting(['size'], - 'create a disk image of size SIZE ' - '(%default)', - metavar='SIZE', - default='1G') - self.settings.add_string_setting(['mirror'], - 'use MIRROR as package source ' - '(%default)', - metavar='URL', - default='http://cdn.debian.net/debian/') - self.settings.add_string_setting(['arch'], - 'architecture to use ' - '(%default)', - metavar='ARCH', - default=default_arch) - self.settings.add_string_setting(['distribution'], - 'release to use (%default)', - metavar='NAME', - default='stable') - self.settings.add_string_list_setting(['package'], - 'install PACKAGE onto system') - self.settings.add_boolean_setting(['enable-dhcp'], - 'enable DHCP on eth0') - self.settings.add_string_setting(['root-password'], - 'set root password', - metavar='PASSWORD') - self.settings.add_string_setting(['customize'], - 'run SCRIPT after setting up system', - metavar='SCRIPT') - self.settings.add_string_setting(['hostname'], - 'set name to HOSTNAME (%default)', - metavar='HOSTNAME', - default='debian') + self.settings.boolean(['verbose'], 'report what is going on') + self.settings.string(['image'], 'put created disk image in FILE', + metavar='FILE') + self.settings.bytesize(['size'], + 'create a disk image of size SIZE (%default)', + metavar='SIZE', + default='1G') + self.settings.string(['tarball'], "tar up the disk's contents in FILE", + metavar='FILE') + self.settings.string(['mirror'], + 'use MIRROR as package source (%default)', + metavar='URL', + default='http://cdn.debian.net/debian/') + self.settings.string(['arch'], 'architecture to use (%default)', + metavar='ARCH', + default=default_arch) + self.settings.string(['distribution'], + 'release to use (%default)', + metavar='NAME', + default='stable') + self.settings.string_list(['package'], 'install PACKAGE onto system') + self.settings.string_list(['custom-package'], + 'install package in DEB file onto system ' + '(not from mirror)', + metavar='DEB') + self.settings.boolean(['no-kernel'], 'do not install a linux package') + self.settings.boolean(['enable-dhcp'], 'enable DHCP on eth0') + self.settings.string(['root-password'], 'set root password', + metavar='PASSWORD') + self.settings.boolean(['lock-root-password'], + 'lock root account so they cannot login?') + self.settings.string(['customize'], + 'run SCRIPT after setting up system', + metavar='SCRIPT') + self.settings.string(['hostname'], + 'set name to HOSTNAME (%default)', + metavar='HOSTNAME', + default='debian') + self.settings.string_list(['user'], + 'create USER with PASSWORD', + metavar='USER/PASSWORD') + self.settings.boolean(['serial-console'], + 'configure image to use a serial console') + self.settings.string(['serial-console-command'], + 'command to manage the serial console, appended ' + 'to /etc/inittab (%default)', + metavar='COMMAND', + default='/sbin/getty -L ttyS0 115200 vt100') + self.settings.boolean(['sudo'], + 'install sudo, and if user is created, add them ' + 'to sudo group') def process_args(self, args): - if not self.settings['image']: - raise cliapp.AppException('You must give image filename.') - if not self.settings['size']: - raise cliapp.AppException('You must give image size.') + if not self.settings['image'] and not self.settings['tarball']: + raise cliapp.AppException('You must give disk image filename, ' + 'or tarball filename') + if self.settings['image'] and not self.settings['size']: + raise cliapp.AppException('If disk image is specified, ' + 'You must give image size.') self.remove_dirs = [] self.mount_points = [] try: - self.create_empty_image() - self.partition_image() - self.install_mbr() - rootdev = self.setup_kpartx() - self.mkfs(rootdev) - rootdir = self.mount(rootdev) + if self.settings['image']: + self.create_empty_image() + self.partition_image() + self.install_mbr() + rootdev = self.setup_kpartx() + self.mkfs(rootdev) + rootdir = self.mount(rootdev) + else: + rootdir = self.mkdtemp() self.debootstrap(rootdir) self.set_hostname(rootdir) + self.create_fstab(rootdir) + self.install_debs(rootdir) + self.cleanup_apt_cache(rootdir) self.set_root_password(rootdir) + self.create_users(rootdir) self.remove_udev_persistent_rules(rootdir) self.setup_networking(rootdir) - self.install_extlinux(rootdev, rootdir) self.customize(rootdir) - except: - self.cleanup() + if self.settings['image']: + self.install_extlinux(rootdev, rootdir) + self.optimize_image(rootdir) + if self.settings['tarball']: + self.create_tarball(rootdir) + except BaseException, e: + self.message('EEEK! Something bad happened...') + self.cleanup_system() raise else: - self.cleanup() + self.cleanup_system() def message(self, msg): + logging.info(msg) if self.settings['verbose']: print msg @@ -163,37 +195,105 @@ class VmDebootstrap(cliapp.Application): def debootstrap(self, rootdir): self.message('Debootstrapping') - if self.settings['arch'] == 'i386': - kernel_arch = 'i686' - else: - kernel_arch = self.settings['arch'] - kernel_image = 'linux-image-2.6-%s' % kernel_arch + necessary_packages = ['acpid'] + + include = self.settings['package'] - include = [kernel_image] + self.settings['package'] + if not self.settings['no-kernel']: + if self.settings['arch'] == 'i386': + kernel_arch = '486' + else: + kernel_arch = self.settings['arch'] + kernel_image = 'linux-image-%s' % kernel_arch + include.append(kernel_image) - self.runcmd(['debootstrap', - '--arch=%s' % self.settings['arch'], - '--include=%s' % ','.join(include), - self.settings['distribution'], - rootdir, - self.settings['mirror']]) + if self.settings['sudo'] and 'sudo' not in include: + include.append('sudo') + + args = ['debootstrap', '--arch=%s' % self.settings['arch']] + args.append( + '--include=%s' % ','.join(necessary_packages + include)) + args += [self.settings['distribution'], + rootdir, self.settings['mirror']] + self.runcmd(args) def set_hostname(self, rootdir): hostname = self.settings['hostname'] - f = open(os.path.join(rootdir, 'etc', 'hostname'), 'w') - f.write(hostname) - f.close() + with open(os.path.join(rootdir, 'etc', 'hostname'), 'w') as f: + f.write('%s\n' % hostname) + + etc_hosts = os.path.join(rootdir, 'etc', 'hosts') + with open(etc_hosts, 'r') as f: + data = f.read() + with open(etc_hosts, 'w') as f: + for line in data.splitlines(): + if line.startswith('127.0.0.1'): + line += ' %s' % hostname + f.write('%s\n' % line) + + def create_fstab(self, rootdir): + fstab = os.path.join(rootdir, 'etc', 'fstab') + with open(fstab, 'w') as f: + f.write('proc /proc proc defaults 0 0\n') + f.write('/dev/sda1 / ext4 errors=remount-ro 0 1\n') + + def install_debs(self, rootdir): + if not self.settings['custom-package']: + return + self.message('Installing custom packages') + tmp = os.path.join(rootdir, 'tmp', 'install_debs') + os.mkdir(tmp) + for deb in self.settings['custom-package']: + shutil.copy(deb, tmp) + filenames = [os.path.join('/tmp/install_debs', os.path.basename(deb)) + for deb in self.settings['custom-package']] + out, err, exit = \ + self.runcmd_unchecked(['chroot', rootdir, 'dpkg', '-i'] + filenames) + logging.debug('stdout:\n%s' % out) + logging.debug('stderr:\n%s' % err) + out = self.runcmd(['chroot', rootdir, + 'apt-get', '-f', '--no-remove', 'install']) + logging.debug('stdout:\n%s' % out) + shutil.rmtree(tmp) + + def cleanup_apt_cache(self, rootdir): + out = self.runcmd(['chroot', rootdir, 'apt-get', 'clean']) + logging.debug('stdout:\n%s' % out) def set_root_password(self, rootdir): if self.settings['root-password']: self.message('Setting root password') - encrypted = crypt.crypt(self.settings['root-password'], '..') - self.runcmd(['chroot', rootdir, 'usermod', '-p', encrypted, - 'root']) - else: + self.set_password(rootdir, 'root', self.settings['root-password']) + elif self.settings['lock-root-password']: self.message('Locking root password') self.runcmd(['chroot', rootdir, 'passwd', '-l', 'root']) - + else: + self.message('Give root an empty password') + self.delete_password(rootdir, 'root') + + def create_users(self, rootdir): + def create_user(user): + self.runcmd(['chroot', rootdir, 'adduser', '--gecos', user, + '--disabled-password', user]) + if self.settings['sudo']: + self.runcmd(['chroot', rootdir, 'adduser', user, 'sudo']) + + for userpass in self.settings['user']: + if '/' in userpass: + user, password = userpass.split('/', 1) + create_user(user) + self.set_password(rootdir, user, password) + else: + create_user(userpass) + self.delete_password(rootdir, userpass) + + def set_password(self, rootdir, user, password): + encrypted = crypt.crypt(password, '..') + self.runcmd(['chroot', rootdir, 'usermod', '-p', encrypted, user]) + + def delete_password(self, rootdir, user): + self.runcmd(['chroot', rootdir, 'passwd', '-d', user]) + def remove_udev_persistent_rules(self, rootdir): self.message('Removing udev persistent cd and net rules') for x in ['70-persistent-cd.rules', '70-persistent-net.rules']: @@ -213,7 +313,7 @@ class VmDebootstrap(cliapp.Application): if self.settings['enable-dhcp']: f.write('\n') - f.write('allow-hotplug eth0\n') + f.write('auto eth0\n') f.write('iface eth0 inet dhcp\n') f.close() @@ -246,27 +346,48 @@ timeout 1 label linux kernel %(kernel)s -append initrd=%(initrd)s root=UUID=%(uuid)s ro quiet +append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s +%(extserial)s ''' % { 'kernel': kernel_image, 'initrd': initrd_image, 'uuid': uuid, + 'kserial': + 'console=ttyS0,115200' if self.settings['serial-console'] else '', + 'extserial': 'serial 0 115200' if self.settings['serial-console'] else '', }) f.close() + + if self.settings['serial-console']: + serial_command = self.settings['serial-console-command'] + logging.debug('adding getty to serial console') + inittab = os.path.join(rootdir, 'etc/inittab') + with open(inittab, 'a') as f: + f.write('\nS0:23:respawn:%s\n' % serial_command) self.runcmd(['extlinux', '--install', rootdir]) self.runcmd(['sync']) import time; time.sleep(2) - - def cleanup(self): + + def optimize_image(self, rootdir): + """ + Filing up the image with zeros will increase its compression rate + """ + zeros = os.path.join(rootdir, 'ZEROS') + self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M']) + self.runcmd(['rm', '-f', zeros]) + + + def cleanup_system(self): # Clean up after any errors. self.message('Cleaning up') - for mount_point in self.mount_points: - self.runcmd(['umount', mount_point], ignore_fail=True) + if self.settings['image']: + for mount_point in self.mount_points: + self.runcmd(['umount', mount_point], ignore_fail=True) - self.runcmd(['kpartx', '-d', self.settings['image']], ignore_fail=True) + self.runcmd(['kpartx', '-d', self.settings['image']], ignore_fail=True) for dirname in self.remove_dirs: shutil.rmtree(dirname) @@ -275,9 +396,16 @@ append initrd=%(initrd)s root=UUID=%(uuid)s ro quiet script = self.settings['customize'] if script: self.message('Running customize script %s' % script) - self.runcmd([script, rootdir]) + with open('/dev/tty', 'w') as tty: + cliapp.runcmd([script, rootdir], stdout=tty, stderr=tty) + + def create_tarball(self, rootdir): + # Create a tarball of the disk's contents + # shell out to runcmd since it more easily handles rootdir + self.message('Creating tarball of disk contents') + self.runcmd(['tar', '-cf', self.settings['tarball'], '-C', rootdir, '.']) if __name__ == '__main__': - VmDebootstrap().run() + VmDebootstrap(version=__version__).run()