diff options
author | Neil Williams <codehelp@debian.org> | 2015-08-15 14:33:59 +0200 |
---|---|---|
committer | Neil Williams <codehelp@debian.org> | 2015-08-16 14:28:35 +0200 |
commit | 54b1ab7ea71a857791189d7a2108c8b76cbfde2a (patch) | |
tree | 1515a90e7e8a8986277adc2654f001e8ecdbcce0 /bin | |
parent | ca8829488c49ae4801ef91eed6d17f7bad892eef (diff) | |
download | vmdebootstrap-54b1ab7ea71a857791189d7a2108c8b76cbfde2a.tar.gz |
move handlers into a module directory
Diffstat (limited to 'bin')
-rwxr-xr-x | bin/vmdebootstrap | 801 |
1 files changed, 801 insertions, 0 deletions
diff --git a/bin/vmdebootstrap b/bin/vmdebootstrap new file mode 100755 index 0000000..1bb9e2e --- /dev/null +++ b/bin/vmdebootstrap @@ -0,0 +1,801 @@ +#! /usr/bin/python +# Copyright 2011-2013 Lars Wirzenius +# Copyright 2012 Codethink Limited +# Copyright 2014-2015 Neil Williams <codehelp@debian.org> +# +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import time +import shutil +import cliapp +import logging +import datetime +import tempfile +import subprocess +from vmdebootstrap.base import ( + Base, + runcmd, + cleanup_apt_cache, +) +from vmdebootstrap.grub import GrubHandler +from vmdebootstrap.extlinux import ExtLinux +from vmdebootstrap.codenames import Codenames +from vmdebootstrap.filesystem import Filesystem +from vmdebootstrap.uefi import Uefi, arch_table + + +__version__ = '1.0' + +# pylint: disable=invalid-name,line-too-long,missing-docstring + + +class VmDebootstrap(cliapp.Application): # pylint: disable=too-many-public-methods + + def __init__(self, progname=None, version=__version__, description=None, epilog=None): + super(VmDebootstrap, self).__init__(progname, version, description, epilog) + self.remove_dirs = [] + self.mount_points = [] + self.bootdir = None + self.handlers = { + Base.name: Base(), + Uefi.name: Uefi(), + Codenames.name: Codenames(), + GrubHandler.name: GrubHandler(), + ExtLinux.name: ExtLinux(), + Filesystem.name: Filesystem(), + } + + def add_settings(self): + default_arch = subprocess.check_output( + ["dpkg", "--print-architecture"]).strip() + + 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.bytesize( + ['bootsize'], 'create boot partition of size ' + 'SIZE (%default)', + metavar='BOOTSIZE', default='0%') + self.settings.string( + ['boottype'], 'specify file system type for /boot/', + default='ext2') + self.settings.bytesize( + ['bootoffset'], 'Space to leave at start of the ' + 'image for bootloader', + default='0') + self.settings.boolean( + ['use-uefi'], 'Setup image for UEFI boot', + default=False) + self.settings.bytesize( + ['esp-size'], 'Size of EFI System Partition - ' + 'requires use-uefi', + default='5mib') + self.settings.string( + ['part-type'], 'Partition type to use for this image', + default='msdos') + self.settings.string( + ['roottype'], 'specify file system type for /', + default='ext4') + self.settings.bytesize( + ['swap'], 'create swap space of size SIZE (min 256Mb)') + self.settings.string( + ['foreign'], 'set up foreign debootstrap environment ' + 'using provided program (ie binfmt handler)') + self.settings.string( + ['variant'], 'select debootstrap variant it not using ' + 'the default') + self.settings.boolean( + ['extlinux'], 'install extlinux?', + default=True) + self.settings.string( + ['tarball'], "tar up the disk's contents in FILE", + metavar='FILE') + self.settings.string( + ['apt-mirror'], 'configure apt to use MIRROR', + metavar='URL') + self.settings.string( + ['mirror'], 'use MIRROR as package source (%default)', + metavar='URL', + default='http://http.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.string( + ['kernel-package'], 'install PACKAGE instead of ' + 'the default kernel package', + metavar='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') + self.settings.string( + ['owner'], 'the user who will own the image when ' + 'the build is complete.') + self.settings.boolean( + ['squash'], 'use squashfs on the final image.') + self.settings.boolean( + ['configure-apt'], 'Create an apt source based on ' + 'the distribution and mirror selected.') + self.settings.boolean( + ['mbr'], 'Run install-mbr (default if extlinux used)') + self.settings.boolean( + ['grub'], 'Install and configure grub2 - ' + 'disables extlinux.') + self.settings.boolean( + ['sparse'], 'Do not fill the image with zeros to ' + 'keep a sparse disk image', + default=False) + self.settings.boolean( + ['pkglist'], 'Create a list of package names ' + 'included in the image.') + self.settings.boolean( + ['no-acpid'], 'do not install the acpid package', + default=False) + + def process_args(self, args): # pylint: disable=too-many-branches,too-many-statements + + for _, handler in self.handlers.items(): + handler.define_settings(self.settings) + + distro = self.handlers[Codenames.name] + 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.') + if not distro.debian_info.valid(self.settings['distribution']): + if not distro.ubuntu_info.valid(self.settings['distribution']): + raise cliapp.AppException( + '%s is not a valid Debian or Ubuntu suite or codename.' + % self.settings['distribution']) + if not self.settings['use-uefi'] and self.settings['esp-size'] != 5242880: + raise cliapp.AppException( + 'You must specify use-uefi for esp-size to have effect') + if arch_table[self.settings['arch']]['exclusive'] and\ + not self.settings['use-uefi']: + raise cliapp.AppException( + 'Only UEFI is supported on %s' % self.settings['arch']) + elif self.settings['use-uefi'] and self.settings['arch'] not in arch_table: + raise cliapp.AppException( + '%s is not a supported UEFI architecture' % self.settings['arch']) + if self.settings['use-uefi'] and ( + self.settings['bootsize'] or + self.settings['bootoffset']): + raise cliapp.AppException( + 'A separate boot partition is not supported with UEFI') + + if self.settings['use-uefi'] and not self.settings['grub']: + raise cliapp.AppException( + 'UEFI without Grub is not supported.') + + # wheezy (which became oldstable on 04/25/2015) only had amd64 uefi + if distro.was_oldstable(datetime.date(2015, 4, 26)): + if self.settings['arch'] != 'amd64': + raise cliapp.AppException( + 'Only amd64 supports UEFI in Wheezy') + + if os.geteuid() != 0: + sys.exit("You need to have root privileges to run this script.") + self.start_ops() + + def _image_preparations(self, rootdir): + uefi = self.handlers[Uefi.name] + base = self.handlers[Base.name] + roottype = self.settings['roottype'] + base.create_empty_image() + self.partition_image() + if self.settings['mbr'] or self.settings['extlinux']: + self.install_mbr() + (rootdev, bootdev, swapdev) = self.setup_kpartx() + if self.settings['swap'] > 0: + self.message("Creating swap space") + runcmd(['mkswap', swapdev]) + self.mkfs(rootdev, fstype=roottype) + rootdir = self.mount(rootdev) + if self.settings['use-uefi']: + self.mount(bootdev, uefi.prepare_esp(rootdir, bootdev)) + elif bootdev: + if self.settings['boottype']: + boottype = self.settings['boottype'] + else: + boottype = 'ext2' + self.mkfs(bootdev, fstype=boottype) + self.bootdir = '%s/%s' % (rootdir, 'boot/') + os.mkdir(self.bootdir) + self.mount(bootdev, self.bootdir) + + def _image_operations(self, rootdir, rootdev): + if not self.settings['image']: + return + grub = self.handlers[GrubHandler.name] + extlinux = self.handlers[ExtLinux.name] + if self.settings['use-uefi']: + grub.install_grub_uefi(rootdir) + elif self.settings['grub']: + grub.install_grub2(rootdev, rootdir) + elif self.settings['extlinux']: + extlinux.install_extlinux(rootdev, rootdir) + self.append_serial_console(rootdir) + self.optimize_image(rootdir) + if self.settings['squash']: + self.squash() + + def start_ops(self): + base = self.handlers[Base.name] + rootdir = None + rootdev = None + roottype = self.settings['roottype'] + bootdev = None + boottype = None + try: + if self.settings['image']: + self._image_preparations(rootdir) + else: + rootdir = self.mkdtemp() + self.debootstrap(rootdir) + self.set_hostname(rootdir) + self.create_fstab(rootdir, rootdev, roottype, bootdev, boottype) + self.install_debs(rootdir) + base.set_root_password(rootdir) + base.create_users(rootdir) + self.remove_udev_persistent_rules(rootdir) + self.setup_networking(rootdir) + if self.settings['configure-apt'] or self.settings['apt-mirror']: + self.configure_apt(rootdir) + self.customize(rootdir) + cleanup_apt_cache(rootdir) + self.update_initramfs(rootdir) + self._image_operations(rootdir, rootdev) + self.list_installed_pkgs(rootdir) + + if self.settings['foreign']: + os.unlink( + '%s/usr/bin/%s' % + (rootdir, os.path.basename(self.settings['foreign']))) + + if self.settings['tarball']: + base.create_tarball(rootdir) + self.chown() + + except BaseException as e: + self.message('EEEK! Something bad happened...') + if rootdir: + db_log = os.path.join(rootdir, 'debootstrap', 'debootstrap.log') + if os.path.exists(db_log): + shutil.copy(db_log, os.getcwd()) + self.message(e) + self.cleanup_system() + raise + else: + self.cleanup_system() + + def message(self, msg): + logging.info(msg) + if self.settings['verbose']: + print msg + + def runcmd(self, argv, stdin='', ignore_fail=False, env=None, **kwargs): + logging.debug('runcmd: %s %s %s', argv, env, kwargs) + p = subprocess.Popen(argv, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=env, **kwargs) + out, err = p.communicate(stdin) + if p.returncode != 0: + msg = 'command failed: %s\n%s\n%s' % (argv, out, err) + logging.error(msg) + if not ignore_fail: + raise cliapp.AppException(msg) + return out + + def mkdtemp(self): + dirname = tempfile.mkdtemp() + self.remove_dirs.append(dirname) + logging.debug('mkdir %s', dirname) + return dirname + + def mount(self, device, path=None): + if not path: + mount_point = self.mkdtemp() + else: + mount_point = path + self.message('Mounting %s on %s' % (device, mount_point)) + runcmd(['mount', device, mount_point]) + self.mount_points.append(mount_point) + logging.debug('mounted %s on %s', device, mount_point) + return mount_point + + def partition_image(self): + """ + Uses fat16 (msdos) partitioning by default, use part-type to change. + If bootoffset is specified, the first actual partition + starts at that offset to allow customisation scripts to + put bootloader images into the space, e.g. u-boot. + """ + self.message('Creating partitions') + runcmd(['parted', '-s', self.settings['image'], + 'mklabel', self.settings['part-type']]) + partoffset = 0 + extent = '100%' + swap = 256 * 1024 * 1024 + if self.settings['swap'] > 0: + if self.settings['swap'] > swap: + swap = self.settings['swap'] + else: + # minimum 256Mb as default qemu ram is 128Mb + logging.debug("Setting minimum 256Mb swap space") + extent = "%s%%" % int(100 * (self.settings['size'] - swap) / self.settings['size']) + + if self.settings['use-uefi']: + uefi = self.handlers[Uefi.name] + uefi.partition_esp() + + if self.settings['bootoffset'] and self.settings['bootoffset'] is not '0': + # turn v.small offsets into something at least possible to create. + if self.settings['bootoffset'] < 1048576: + partoffset = 1 + logging.info( + "Setting bootoffset %smib to allow for %s bytes", + partoffset, self.settings['bootoffset']) + else: + partoffset = self.settings['bootoffset'] / (1024 * 1024) + self.message("Using bootoffset: %smib %s bytes" % (partoffset, self.settings['bootoffset'])) + if self.settings['bootsize'] and self.settings['bootsize'] is not '0%': + if self.settings['grub'] and not partoffset: + partoffset = 1 + bootsize = self.settings['bootsize'] / (1024 * 1024) + bootsize += partoffset + self.message("Using bootsize %smib: %s bytes" % (bootsize, self.settings['bootsize'])) + logging.debug("Starting boot partition at %sMb", bootsize) + runcmd(['parted', '-s', self.settings['image'], + 'mkpart', 'primary', 'fat16', str(partoffset), str(bootsize)]) + logging.debug("Starting root partition at %sMb", partoffset) + runcmd(['parted', '-s', self.settings['image'], + 'mkpart', 'primary', str(bootsize), extent]) + elif self.settings['use-uefi']: + bootsize = self.settings['esp-size'] / (1024 * 1024) + 1 + runcmd(['parted', '-s', self.settings['image'], + 'mkpart', 'primary', str(bootsize), extent]) + else: + runcmd(['parted', '-s', self.settings['image'], + 'mkpart', 'primary', '0%', extent]) + runcmd(['parted', '-s', self.settings['image'], + 'set', '1', 'boot', 'on']) + if self.settings['swap'] > 0: + logging.debug("Creating swap partition") + runcmd(['parted', '-s', self.settings['image'], + 'mkpart', 'primary', 'linux-swap', extent, '100%']) + + def update_initramfs(self, rootdir): + cmd = os.path.join('usr', 'sbin', 'update-initramfs') + if os.path.exists(os.path.join(rootdir, cmd)): + self.message("Updating the initramfs") + runcmd(['chroot', rootdir, cmd, '-u']) + + def install_mbr(self): + if os.path.exists("/sbin/install-mbr"): + self.message('Installing MBR') + runcmd(['install-mbr', self.settings['image']]) + else: + msg = "mbr enabled but /sbin/install-mbr not found" \ + " - please install the mbr package." + raise cliapp.AppException(msg) + + def setup_kpartx(self): + bootindex = None + swapindex = None + out = runcmd(['kpartx', '-avs', self.settings['image']]) + if self.settings['bootsize'] and self.settings['swap'] > 0: + bootindex = 0 + rootindex = 1 + swapindex = 2 + parts = 3 + elif self.settings['use-uefi']: + bootindex = 0 + rootindex = 1 + parts = 2 + elif self.settings['use-uefi'] and self.settings['swap'] > 0: + bootindex = 0 + rootindex = 1 + swapindex = 2 + parts = 3 + elif self.settings['bootsize']: + bootindex = 0 + rootindex = 1 + parts = 2 + elif self.settings['swap'] > 0: + rootindex = 0 + swapindex = 1 + parts = 2 + else: + rootindex = 0 + parts = 1 + boot = None + swap = None + devices = [line.split()[2] + for line in out.splitlines() + if line.startswith('add map ')] + if len(devices) != parts: + msg = 'Surprising number of partitions - check output of losetup -a' + logging.debug("%s", runcmd(['losetup', '-a'])) + logging.debug("%s: devices=%s parts=%s", msg, devices, parts) + raise cliapp.AppException(msg) + root = '/dev/mapper/%s' % devices[rootindex] + if self.settings['bootsize'] or self.settings['use-uefi']: + boot = '/dev/mapper/%s' % devices[bootindex] + if self.settings['swap'] > 0: + swap = '/dev/mapper/%s' % devices[swapindex] + return root, boot, swap + + def mkfs(self, device, fstype): + self.message('Creating filesystem %s' % fstype) + runcmd(['mkfs', '-t', fstype, device]) + + def _bootstrap_packages(self): # pylint: disable=too-many-branches + uefi = self.handlers[Uefi.name] + distro = self.handlers[Codenames.name] + include = self.settings['package'] + + if not self.settings['foreign'] and not self.settings['no-acpid']: + include.append('acpid') + + if self.settings['grub']: + if self.settings['use-uefi']: + include.extend(uefi.efi_packages()) + else: + include.append('grub-pc') + + if not self.settings['no-kernel']: + if self.settings['kernel-package']: + kernel_image = self.settings['kernel-package'] + else: + if self.settings['arch'] == 'i386': + # wheezy (which became oldstable on 04/25/2015) used '486' + if distro.was_oldstable(datetime.date(2015, 4, 26)): + kernel_arch = '486' + else: + kernel_arch = '586' + elif self.settings['arch'] == 'armhf': + kernel_arch = 'armmp' + else: + kernel_arch = self.settings['arch'] + kernel_image = 'linux-image-%s' % kernel_arch + include.append(kernel_image) + + if self.settings['sudo'] and 'sudo' not in include: + include.append('sudo') + + return include + + def _debootstrap_second_stage(self, rootdir): + # set a noninteractive debconf environment for secondstage + env = { + "DEBIAN_FRONTEND": "noninteractive", + "DEBCONF_NONINTERACTIVE_SEEN": "true", + "LC_ALL": "C" + } + # add the mapping to the complete environment. + env.update(os.environ) + # First copy the binfmt handler over + self.message('Setting up binfmt handler') + shutil.copy(self.settings['foreign'], '%s/usr/bin/' % rootdir) + # Next, run the package install scripts etc. + self.message('Running debootstrap second stage') + runcmd(['chroot', rootdir, + '/debootstrap/debootstrap', '--second-stage'], + env=env) + + def debootstrap(self, rootdir): + msg = "(%s)" % self.settings['variant'] if self.settings['variant'] else '' + self.message( + 'Debootstrapping %s [%s] %s' % ( + self.settings['distribution'], self.settings['arch'], msg)) + include = self._bootstrap_packages() + + args = ['debootstrap', '--arch=%s' % self.settings['arch']] + + if self.settings['package']: + args.append( + '--include=%s' % ','.join(include)) + if self.settings['foreign']: + args.append('--foreign') + if self.settings['variant']: + args.append('--variant') + args.append(self.settings['variant']) + args += [self.settings['distribution'], + rootdir, self.settings['mirror']] + logging.debug(" ".join(args)) + runcmd(args) + if self.settings['foreign']: + self._debootstrap_second_stage(rootdir) + + def set_hostname(self, rootdir): + hostname = self.settings['hostname'] + with open(os.path.join(rootdir, 'etc', 'hostname'), 'w') as f: + f.write('%s\n' % hostname) + + etc_hosts = os.path.join(rootdir, 'etc', 'hosts') + try: + 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) + except IOError: + pass + + def create_fstab(self, rootdir, rootdev, roottype, bootdev, boottype): # pylint: disable=too-many-arguments + def fsuuid(device): + out = runcmd(['blkid', '-c', '/dev/null', '-o', 'value', + '-s', 'UUID', device]) + return out.splitlines()[0].strip() + + if rootdev: + rootdevstr = 'UUID=%s' % fsuuid(rootdev) + else: + rootdevstr = '/dev/sda1' + + if bootdev and not self.settings['use-uefi']: + bootdevstr = 'UUID=%s' % fsuuid(bootdev) + else: + bootdevstr = None + + fstab = os.path.join(rootdir, 'etc', 'fstab') + with open(fstab, 'w') as f: + f.write('proc /proc proc defaults 0 0\n') + f.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr, roottype)) + if bootdevstr: + f.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr, boottype)) + if self.settings['swap'] > 0: + f.write("/dev/sda3 swap swap defaults 0 0\n") + elif self.settings['swap'] > 0: + f.write("/dev/sda2 swap swap defaults 0 0\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, _ = \ + self.runcmd_unchecked(['chroot', rootdir, 'dpkg', '-i'] + filenames) + logging.debug('stdout:\n%s', out) + logging.debug('stderr:\n%s', err) + out = runcmd(['chroot', rootdir, + 'apt-get', '-f', '--no-remove', 'install']) + logging.debug('stdout:\n%s', out) + shutil.rmtree(tmp) + + 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']: + pathname = os.path.join(rootdir, 'etc', 'udev', 'rules.d', x) + if os.path.exists(pathname): + logging.debug('rm %s', pathname) + os.remove(pathname) + else: + logging.debug('not removing non-existent %s', pathname) + + def setup_networking(self, rootdir): + self.message('Setting up networking') + distro = self.handlers[Codenames.name] + # unconditionally write for wheezy (which became oldstable on 04/25/2015) + if distro.was_oldstable(datetime.date(2015, 4, 26)): + with open(os.path.join(rootdir, 'etc', 'network', 'interfaces'), 'w') as netfile: + netfile.write('source /etc/network/interfaces.d/*\n') + os.mkdir(os.path.join(rootdir, 'etc', 'network', 'interfaces.d')) + + elif not os.path.exists(os.path.join(rootdir, 'etc', 'network', 'interfaces')): + iface_path = os.path.join(rootdir, 'etc', 'network', 'interfaces') + with open(iface_path, 'w') as netfile: + netfile.write('source-directory /etc/network/interfaces.d\n') + ethpath = os.path.join(rootdir, 'etc', 'network', 'interfaces.d', 'setup') + with open(ethpath, 'w') as eth: + eth.write('auto lo\n') + eth.write('iface lo inet loopback\n') + + if self.settings['enable-dhcp']: + eth.write('\n') + eth.write('auto eth0\n') + eth.write('iface eth0 inet dhcp\n') + + def append_serial_console(self, rootdir): + 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') + # to autologin, serial_command can contain '-a root' + with open(inittab, 'a') as f: + f.write('\nS0:23:respawn:%s\n' % serial_command) + + def optimize_image(self, rootdir): + """ + Filing up the image with zeros will increase its compression rate + """ + if not self.settings['sparse']: + zeros = os.path.join(rootdir, 'ZEROS') + self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M']) + runcmd(['rm', '-f', zeros]) + + def squash(self): + """ + Run squashfs on the image. + """ + if not os.path.exists('/usr/bin/mksquashfs'): + logging.warning("Squash selected but mksquashfs not found!") + return + logging.debug( + "%s usage: %s", self.settings['image'], + runcmd(['du', self.settings['image']])) + self.message("Running mksquashfs") + suffixed = "%s.squashfs" % self.settings['image'] + if os.path.exists(suffixed): + os.unlink(suffixed) + msg = runcmd( + ['mksquashfs', self.settings['image'], + suffixed, + '-no-progress', '-comp', 'xz'], ignore_fail=False) + logging.debug(msg) + check_size = os.path.getsize(suffixed) + if check_size < (1024 * 1024): + logging.warning( + "%s appears to be too small! %s bytes", + suffixed, check_size) + else: + logging.debug("squashed size: %s", check_size) + os.unlink(self.settings['image']) + self.settings['image'] = suffixed + logging.debug( + "%s usage: %s", self.settings['image'], + runcmd(['du', self.settings['image']])) + + def cleanup_system(self): + # Clean up after any errors. + + self.message('Cleaning up') + + # Umount in the reverse mount order + if self.settings['image']: + for i in range(len(self.mount_points) - 1, -1, -1): + mount_point = self.mount_points[i] + try: + runcmd(['umount', mount_point], ignore_fail=False) + except cliapp.AppException: + logging.debug("umount failed, sleeping and trying again") + time.sleep(5) + runcmd(['umount', mount_point], ignore_fail=False) + + runcmd(['kpartx', '-d', self.settings['image']], ignore_fail=True) + + for dirname in self.remove_dirs: + shutil.rmtree(dirname) + + def customize(self, rootdir): + script = self.settings['customize'] + if not script: + return + if not os.path.exists(script): + example = os.path.join("/usr/share/vmdebootstrap/examples/", script) + if not os.path.exists(example): + self.message("Unable to find %s" % script) + return + script = example + self.message('Running customize script %s' % script) + logging.info("rootdir=%s image=%s", rootdir, self.settings['image']) + logging.debug( + "%s usage: %s", self.settings['image'], + runcmd(['du', self.settings['image']])) + with open('/dev/tty', 'w') as tty: + try: + cliapp.runcmd([script, rootdir, self.settings['image']], stdout=tty, stderr=tty) + except IOError: + subprocess.call([script, rootdir, self.settings['image']]) + logging.debug( + "%s usage: %s", self.settings['image'], + runcmd(['du', self.settings['image']])) + + def chown(self): + if not self.settings['owner']: + return + # Change image owner after completed build + if self.settings['image']: + filename = self.settings['image'] + elif self.settings['tarball']: + filename = self.settings['tarball'] + else: + return + self.message("Changing owner to %s" % self.settings["owner"]) + subprocess.call(["chown", self.settings["owner"], filename]) + + def list_installed_pkgs(self, rootdir): + if not self.settings['pkglist']: + return + # output the list of installed packages for sources identification + self.message("Creating a list of installed binary package names") + out = runcmd(['chroot', rootdir, + 'dpkg-query', '-W', "-f='${Package}.deb\n'"]) + with open('dpkg.list', 'w') as dpkg: + dpkg.write(out) + + def configure_apt(self, rootdir): + # use the distribution and mirror to create an apt source + self.message("Configuring apt to use distribution and mirror") + conf = os.path.join(rootdir, 'etc', 'apt', 'sources.list.d', 'base.list') + logging.debug('configure apt %s', conf) + mirror = self.settings['mirror'] + if self.settings['apt-mirror']: + mirror = self.settings['apt-mirror'] + self.message("Setting apt mirror to %s" % mirror) + os.unlink(os.path.join(rootdir, 'etc', 'apt', 'sources.list')) + f = open(conf, 'w') + line = 'deb %s %s main\n' % (mirror, self.settings['distribution']) + f.write(line) + line = '#deb-src %s %s main\n' % (mirror, self.settings['distribution']) + f.write(line) + f.close() + # ensure the apt sources have valid lists + runcmd(['chroot', rootdir, 'apt-get', '-qq', 'update']) + +if __name__ == '__main__': + VmDebootstrap(version=__version__).run() |