diff options
author | Neil Williams <codehelp@debian.org> | 2015-11-05 15:50:15 +0000 |
---|---|---|
committer | Neil Williams <codehelp@debian.org> | 2015-11-05 15:50:15 +0000 |
commit | 0100446b4febb76cf96cfd9559d1d22d8dcd83aa (patch) | |
tree | d23233fcda466939e0172069bc07b7e22268dca7 /vmdebootstrap | |
parent | 5413ccc44b905facd55fbb0812b7c9e350deb44f (diff) | |
parent | 92b873ab69757662bec66618c06aa69d3b06814f (diff) | |
download | vmdebootstrap-0100446b4febb76cf96cfd9559d1d22d8dcd83aa.tar.gz |
Merge branch 'modules'
Split the single script into modules and add
documentation.
Diffstat (limited to 'vmdebootstrap')
-rwxr-xr-x | vmdebootstrap | 1095 | ||||
-rw-r--r-- | vmdebootstrap/__init__.py | 0 | ||||
-rw-r--r-- | vmdebootstrap/base.py | 217 | ||||
-rw-r--r-- | vmdebootstrap/codenames.py | 79 | ||||
-rw-r--r-- | vmdebootstrap/constants.py | 52 | ||||
-rw-r--r-- | vmdebootstrap/extlinux.py | 107 | ||||
-rw-r--r-- | vmdebootstrap/filesystem.py | 273 | ||||
-rw-r--r-- | vmdebootstrap/grub.py | 123 | ||||
-rw-r--r-- | vmdebootstrap/uefi.py | 167 |
9 files changed, 1018 insertions, 1095 deletions
diff --git a/vmdebootstrap b/vmdebootstrap deleted file mode 100755 index 67ea2c8..0000000 --- a/vmdebootstrap +++ /dev/null @@ -1,1095 +0,0 @@ -#! /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 cliapp -import crypt -import logging -import os -import re -import sys -import shutil -import datetime -import subprocess -import tempfile -import time -from distro_info import DebianDistroInfo, UbuntuDistroInfo - - -__version__ = '0.11' - -# pylint: disable=invalid-name,line-too-long,missing-docstring,too-many-branches - - -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.debian_info = DebianDistroInfo() - self.ubuntu_info = UbuntuDistroInfo() - self.bootdir = None - self.efi_arch_table = { - 'amd64': { - 'removable': '/EFI/boot/bootx64.efi', # destination location - 'install': '/EFI/debian/grubx64.efi', # package location - 'package': 'grub-efi-amd64', # bootstrap package - 'bin_package': 'grub-efi-amd64-bin', # binary only - 'extra': 'i386', # architecture to add binary package - 'exclusive': False, # only EFI supported for this arch. - 'target': 'x86_64-efi', # grub target name - }, - 'i386': { - 'removable': '/EFI/boot/bootia32.efi', - 'install': '/EFI/debian/grubia32.efi', - 'package': 'grub-efi-ia32', - 'bin_package': 'grub-efi-ia32-bin', - 'extra': None, - 'exclusive': False, - 'target': 'i386-efi', - }, - 'arm64': { - 'removable': '/EFI/boot/bootaa64.efi', - 'install': '/EFI/debian/grubaa64.efi', - 'package': 'grub-efi-arm64', - 'bin_package': 'grub-efi-arm64-bin', - 'extra': None, - 'exclusive': True, - 'target': 'arm64-efi', - } - } - - 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 if not using the default [deprecated]') - self.settings.string_list( - ['debootstrapopts'], - 'pass additional options to debootstrap'), - 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 - 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 self.debian_info.valid(self.settings['distribution']): - if not self.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 self.settings['arch'] in self.efi_arch_table and\ - self.efi_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 self.efi_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 self.was_oldstable(datetime.date(2015, 4, 26)): - if self.settings['use-uefi'] and 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.") - rootdir = None - try: - rootdev = None - roottype = self.settings['roottype'] - bootdev = None - boottype = None - if self.settings['image']: - self.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") - self.runcmd(['mkswap', swapdev]) - self.mkfs(rootdev, fstype=roottype) - rootdir = self.mount(rootdev) - if self.settings['use-uefi']: - self.bootdir = '%s/%s/%s' % (rootdir, 'boot', 'efi') - logging.debug("bootdir:%s", self.bootdir) - self.mkfs(bootdev, fstype='vfat') - os.makedirs(self.bootdir) - self.mount(bootdev, self.bootdir) - 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) - else: - rootdir = self.mkdtemp() - self.debootstrap(rootdir) - self.set_hostname(rootdir) - self.create_fstab(rootdir, rootdev, roottype, bootdev, boottype) - self.install_debs(rootdir) - self.set_root_password(rootdir) - self.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) - self.cleanup_apt_cache(rootdir) - self.update_initramfs(rootdir) - - if self.settings['image']: - if self.settings['use-uefi']: - self.install_grub_uefi(rootdir) - elif self.settings['grub']: - self.install_grub2(rootdev, rootdir) - elif self.settings['extlinux']: - self.install_extlinux(rootdev, rootdir) - self.append_serial_console(rootdir) - self.optimize_image(rootdir) - if self.settings['squash']: - self.squash() - if self.settings['pkglist']: - 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']: - self.create_tarball(rootdir) - - if self.settings['owner']: - 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)) - self.runcmd(['mount', device, mount_point]) - self.mount_points.append(mount_point) - logging.debug('mounted %s on %s', device, mount_point) - return mount_point - - def create_empty_image(self): - self.message('Creating disk image') - self.runcmd(['qemu-img', 'create', '-f', 'raw', - self.settings['image'], - str(self.settings['size'])]) - - 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') - self.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']: - espsize = self.settings['esp-size'] / (1024 * 1024) - self.message("Using ESP size: %smib %s bytes" % (espsize, self.settings['esp-size'])) - self.runcmd(['parted', '-s', self.settings['image'], - 'mkpart', 'primary', 'fat32', - '1', str(espsize)]) - self.runcmd(['parted', '-s', self.settings['image'], - 'set', '1', 'boot', 'on']) - self.runcmd(['parted', '-s', self.settings['image'], - 'set', '1', 'esp', 'on']) - - 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) - self.runcmd(['parted', '-s', self.settings['image'], - 'mkpart', 'primary', 'fat16', str(partoffset), str(bootsize)]) - logging.debug("Starting root partition at %sMb", partoffset) - self.runcmd(['parted', '-s', self.settings['image'], - 'mkpart', 'primary', str(bootsize), extent]) - elif self.settings['use-uefi']: - bootsize = self.settings['esp-size'] / (1024 * 1024) + 1 - self.runcmd(['parted', '-s', self.settings['image'], - 'mkpart', 'primary', str(bootsize), extent]) - else: - self.runcmd(['parted', '-s', self.settings['image'], - 'mkpart', 'primary', '0%', extent]) - self.runcmd(['parted', '-s', self.settings['image'], - 'set', '1', 'boot', 'on']) - if self.settings['swap'] > 0: - logging.debug("Creating swap partition") - self.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") - self.runcmd(['chroot', rootdir, cmd, '-u']) - - def install_mbr(self): - if os.path.exists("/sbin/install-mbr"): - self.message('Installing MBR') - self.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 = self.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", self.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 _efi_packages(self): - packages = [] - pkg = self.efi_arch_table[self.settings['arch']]['package'] - self.message("Adding %s" % pkg) - packages.append(pkg) - extra = self.efi_arch_table[self.settings['arch']]['extra'] - if extra and isinstance(extra, str): - bin_pkg = self.efi_arch_table[str(extra)]['bin_package'] - self.message("Adding support for %s using %s" % (extra, bin_pkg)) - packages.append(bin_pkg) - return packages - - def _copy_efi_binary(self, efi_removable, efi_install): - logging.debug("using bootdir=%s", self.bootdir) - logging.debug("moving %s to %s", efi_removable, efi_install) - if efi_removable.startswith('/'): - efi_removable = efi_removable[1:] - if efi_install.startswith('/'): - efi_install = efi_install[1:] - efi_output = os.path.join(self.bootdir, efi_removable) - efi_input = os.path.join(self.bootdir, efi_install) - if not os.path.exists(efi_input): - logging.warning("%s does not exist (%s)", efi_input, efi_install) - raise cliapp.AppException("Missing %s" % efi_install) - if not os.path.exists(os.path.dirname(efi_output)): - os.makedirs(os.path.dirname(efi_output)) - logging.debug( - 'Moving UEFI support: %s -> %s', efi_input, efi_output) - if os.path.exists(efi_output): - os.unlink(efi_output) - os.rename(efi_input, efi_output) - - def configure_efi(self): - """ - Copy the bootloader file from the package into the location - so needs to be after grub and kernel already installed. - """ - self.message('Configuring EFI') - efi_removable = str(self.efi_arch_table[self.settings['arch']]['removable']) - efi_install = str(self.efi_arch_table[self.settings['arch']]['install']) - self.message('Installing UEFI support binary') - self._copy_efi_binary(efi_removable, efi_install) - - def configure_extra_efi(self): - extra = str(self.efi_arch_table[self.settings['arch']]['extra']) - if extra: - efi_removable = str(self.efi_arch_table[extra]['removable']) - efi_install = str(self.efi_arch_table[extra]['install']) - self.message('Copying UEFI support binary for %s' % extra) - self._copy_efi_binary(efi_removable, efi_install) - - def mkfs(self, device, fstype): - self.message('Creating filesystem %s' % fstype) - self.runcmd(['mkfs', '-t', fstype, device]) - - def suite_to_codename(self, distro): - suite = self.debian_info.codename(distro, datetime.date.today()) - if not suite: - return distro - return suite - - def was_oldstable(self, limit): - suite = self.suite_to_codename(self.settings['distribution']) - # this check is only for debian - if not self.debian_info.valid(suite): - return False - return suite == self.debian_info.old(limit) - - def was_stable(self, limit): - suite = self.suite_to_codename(self.settings['distribution']) - # this check is only for debian - if not self.debian_info.valid(suite): - return False - return suite == self.debian_info.stable(limit) - - def debootstrap(self, rootdir): # pylint: disable=too-many-statements - 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.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(self._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 self.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') - - 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['debootstrapopts']: - for opt in self.settings['debootstrapopts']: - for part in opt.split(' '): - args.append('--%s' % part) - elif self.settings['variant']: - args.append('--variant') - args.append(self.settings['variant']) - args += [self.settings['distribution'], - rootdir, self.settings['mirror']] - logging.debug(" ".join(args)) - self.runcmd(args) - if self.settings['foreign']: - # 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') - self.runcmd(['chroot', rootdir, - '/debootstrap/debootstrap', '--second-stage'], - env=env) - - 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 = self.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 = 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') - 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(vmuser): - self.runcmd(['chroot', rootdir, 'adduser', '--gecos', vmuser, - '--disabled-password', vmuser]) - if self.settings['sudo']: - self.runcmd(['chroot', rootdir, 'adduser', vmuser, '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']: - 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 mask_udev_predictable_rules(self, rootdir): - """ - This can be reset later but to get networking working immediately - on boot, the interface we're going to use must be known without - reference to the eventual machine. - http://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames/ - """ - self.message('Disabling systemd predictable interface names') - udev_path = os.path.join( - 'etc', 'udev', 'rules.d', '80-net-setup-link.rules') - self.runcmd(['chroot', rootdir, 'ln', '-s', '/dev/null', udev_path]) - - def setup_networking(self, rootdir): - self.message('Setting up networking') - ifc_file = os.path.join(rootdir, 'etc', 'network', 'interfaces') - ifc_d = os.path.join(rootdir, 'etc', 'network', 'interfaces.d') - - # unconditionally write for wheezy (which became oldstable 2015.04.25) - if self.was_oldstable(datetime.date(2015, 4, 26)): - with open(ifc_file, 'w') as netfile: - netfile.write('source /etc/network/interfaces.d/*\n') - elif not os.path.exists(ifc_file): - with open(ifc_file, 'a') as netfile: - netfile.write('source-directory /etc/network/interfaces.d\n') - - if not os.path.exists(ifc_d): - os.mkdir(ifc_d) - ethpath = os.path.join(ifc_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') - # force predictable interface names - self.mask_udev_predictable_rules(rootdir) - - 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) - - # pylint: disable=no-self-use - def _grub_serial_console(self, rootdir): - cmdline = 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=tty1 console=ttyS0,38400n8"' - terminal = 'GRUB_TERMINAL="serial gfxterm"' - command = 'GRUB_SERIAL_COMMAND="serial --speed=38400 --unit=0 --parity=no --stop=1"' - grub_cfg = os.path.join(rootdir, 'etc', 'default', 'grub') - logging.debug("Allowing serial output in grub config %s", grub_cfg) - with open(grub_cfg, 'a+') as cfg: - cfg.write("# %s serial support\n" % os.path.basename(__file__)) - cfg.write("%s\n" % cmdline) - cfg.write("%s\n" % terminal) - cfg.write("%s\n" % command) - - def _mount_wrapper(self, rootdir): - self.runcmd(['mount', '/dev', '-t', 'devfs', '-obind', - '%s' % os.path.join(rootdir, 'dev')]) - self.runcmd(['mount', '/proc', '-t', 'proc', '-obind', - '%s' % os.path.join(rootdir, 'proc')]) - self.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind', - '%s' % os.path.join(rootdir, 'sys')]) - - def _umount_wrapper(self, rootdir): - self.runcmd(['umount', os.path.join(rootdir, 'sys')]) - self.runcmd(['umount', os.path.join(rootdir, 'proc')]) - self.runcmd(['umount', os.path.join(rootdir, 'dev')]) - - def install_grub_uefi(self, rootdir): - self.message("Configuring grub-uefi") - target = self.efi_arch_table[self.settings['arch']]['target'] - grub_opts = "--target=%s" % target - logging.debug("Running grub-install with options: %s", grub_opts) - self._mount_wrapper(rootdir) - try: - self.runcmd(['chroot', rootdir, 'update-grub']) - self.runcmd(['chroot', rootdir, 'grub-install', grub_opts]) - except cliapp.AppException as exc: - logging.warning(exc) - self.message( - "Failed to configure grub-uefi for %s" % - self.settings['arch']) - self._umount_wrapper(rootdir) - self.configure_efi() - extra = str(self.efi_arch_table[self.settings['arch']]['extra']) - if extra: - target = self.efi_arch_table[extra]['target'] - grub_opts = "--target=%s" % target - try: - self.runcmd(['chroot', rootdir, 'update-grub']) - self.runcmd(['chroot', rootdir, 'grub-install', grub_opts]) - except cliapp.AppException as exc: - logging.warning(exc) - self.message( - "Failed to configure grub-uefi for %s" % extra) - self.configure_extra_efi() - self._umount_wrapper(rootdir) - - def install_grub2(self, rootdev, rootdir): - self.message("Configuring grub2") - # rely on kpartx using consistent naming to map loop0p1 to loop0 - grub_opts = os.path.join('/dev', os.path.basename(rootdev)[:-2]) - if self.settings['serial-console']: - self._grub_serial_console(rootdir) - logging.debug("Running grub-install with options: %s", grub_opts) - self._mount_wrapper(rootdir) - try: - self.runcmd(['chroot', rootdir, 'update-grub']) - self.runcmd(['chroot', rootdir, 'grub-install', grub_opts]) - except cliapp.AppException as exc: - logging.warning(exc) - self.message("Failed. Is grub2-common installed? Using extlinux.") - self.install_extlinux(rootdev, rootdir) - self._umount_wrapper(rootdir) - - def install_extlinux(self, rootdev, rootdir): - if not os.path.exists("/usr/bin/extlinux"): - self.message("extlinux not installed, skipping.") - return - self.message('Installing extlinux') - - def find(pattern): - dirname = os.path.join(rootdir, 'boot') - basenames = os.listdir(dirname) - logging.debug('find: %s', basenames) - for basename in basenames: - if re.search(pattern, basename): - return os.path.join('boot', basename) - raise cliapp.AppException('Cannot find match: %s' % pattern) - - try: - kernel_image = find('vmlinuz-.*') - initrd_image = find('initrd.img-.*') - except cliapp.AppException as e: - self.message("Unable to find kernel. Not installing extlinux.") - logging.debug("No kernel found. %s. Skipping install of extlinux.", e) - return - - out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value', - '-s', 'UUID', rootdev]) - uuid = out.splitlines()[0].strip() - - conf = os.path.join(rootdir, 'extlinux.conf') - logging.debug('configure extlinux %s', conf) - kserial = 'console=ttyS0,115200' if self.settings['serial-console'] else '' - extserial = 'serial 0 115200' if self.settings['serial-console'] else '' - msg = ''' -default linux -timeout 1 - -label linux -kernel %(kernel)s -append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s -%(extserial)s -''' % { - 'kernel': kernel_image, # pylint: disable=bad-continuation - 'initrd': initrd_image, # pylint: disable=bad-continuation - 'uuid': uuid, # pylint: disable=bad-continuation - 'kserial': kserial, # pylint: disable=bad-continuation - 'extserial': extserial, # pylint: disable=bad-continuation - } # pylint: disable=bad-continuation - logging.debug("extlinux config:\n%s", msg) - - # python multiline string substitution is just ugly. - # use an external file or live with the mangling, no point in - # mangling the string to remove spaces just to keep it pretty in source. - f = open(conf, 'w') - f.write(msg) - - self.runcmd(['extlinux', '--install', rootdir]) - self.runcmd(['sync']) - time.sleep(2) - - 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']) - self.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'], - self.runcmd(['du', self.settings['image']])) - self.message("Running mksquashfs") - suffixed = "%s.squashfs" % self.settings['image'] - if os.path.exists(suffixed): - os.unlink(suffixed) - msg = self.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'], - self.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: - self.runcmd(['umount', mount_point], ignore_fail=False) - except cliapp.AppException: - logging.debug("umount failed, sleeping and trying again") - time.sleep(5) - self.runcmd(['umount', mount_point], ignore_fail=False) - - self.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'], - self.runcmd(['du', self.settings['image']])) - try: - with open('/dev/tty', 'w') as tty: - cliapp.runcmd([script, rootdir, self.settings['image']], stdout=tty, stderr=tty) - except IOError: - logging.debug('tty unavailable, trying in headless mode.') - subprocess.call([script, rootdir, self.settings['image']]) - - 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, '.']) - - def chown(self): - # 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): - # output the list of installed packages for sources identification - self.message("Creating a list of installed binary package names") - out = self.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 - self.runcmd(['chroot', rootdir, 'apt-get', '-qq', 'update']) - -if __name__ == '__main__': - VmDebootstrap(version=__version__).run() diff --git a/vmdebootstrap/__init__.py b/vmdebootstrap/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/vmdebootstrap/__init__.py diff --git a/vmdebootstrap/base.py b/vmdebootstrap/base.py new file mode 100644 index 0000000..0a302a4 --- /dev/null +++ b/vmdebootstrap/base.py @@ -0,0 +1,217 @@ +""" + Base for common utility functions +""" +# -*- coding: utf-8 -*- +# +# base.py +# +# Copyright 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 crypt +import shutil +import cliapp +import logging +import subprocess + +# pylint: disable=missing-docstring + + +def runcmd(argv, stdin='', ignore_fail=False, env=None, **kwargs): + logging.debug('runcmd: %s %s %s', argv, env, kwargs) + proc = subprocess.Popen( + argv, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=env, **kwargs) + out, err = proc.communicate(stdin) + if proc.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 + + +# FIXME: use contextmanager +def mount_wrapper(rootdir): + runcmd(['mount', '/dev', '-t', 'devfs', '-obind', + '%s' % os.path.join(rootdir, 'dev')]) + runcmd(['mount', '/proc', '-t', 'proc', '-obind', + '%s' % os.path.join(rootdir, 'proc')]) + runcmd(['mount', '/sys', '-t', 'sysfs', '-obind', + '%s' % os.path.join(rootdir, 'sys')]) + + +def umount_wrapper(rootdir): + runcmd(['umount', os.path.join(rootdir, 'sys')]) + runcmd(['umount', os.path.join(rootdir, 'proc')]) + runcmd(['umount', os.path.join(rootdir, 'dev')]) + + +def cleanup_apt_cache(rootdir): + out = runcmd(['chroot', rootdir, 'apt-get', 'clean']) + logging.debug('stdout:\n%s', out) + + +def set_password(rootdir, user, password): + encrypted = crypt.crypt(password, '..') + runcmd(['chroot', rootdir, 'usermod', '-p', encrypted, user]) + + +def delete_password(rootdir, user): + runcmd(['chroot', rootdir, 'passwd', '-d', user]) + + +def copy_files(src, dest): + for filename in os.listdir(src): + if os.path.isdir(filename) or os.path.islink(filename): + continue + shutil.copyfile( + os.path.join(src, filename), + os.path.join(dest, filename)) + + +class Base(object): + + name = 'base' + + def __init__(self): + super(Base, self).__init__() + self.settings = None + + def define_settings(self, settings): + self.settings = settings + + def message(self, msg): + logging.info(msg) + if self.settings['verbose']: + print msg + + def create_empty_image(self): + self.message('Creating disk image') + runcmd(['qemu-img', 'create', '-f', 'raw', + self.settings['image'], + str(self.settings['size'])]) + + 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') + runcmd(['tar', '-cf', self.settings['tarball'], '-C', rootdir, '.']) + + def mkfs(self, device, fstype): + self.message('Creating filesystem %s' % fstype) + runcmd(['mkfs', '-t', fstype, device]) + + def set_root_password(self, rootdir): + if self.settings['root-password']: + self.message('Setting root password') + set_password(rootdir, 'root', self.settings['root-password']) + elif self.settings['lock-root-password']: + self.message('Locking root password') + runcmd(['chroot', rootdir, 'passwd', '-l', 'root']) + else: + self.message('Give root an empty password') + delete_password(rootdir, 'root') + + def create_users(self, rootdir): + def create_user(vmuser): + runcmd(['chroot', rootdir, 'adduser', '--gecos', vmuser, + '--disabled-password', vmuser]) + if self.settings['sudo']: + runcmd(['chroot', rootdir, 'adduser', vmuser, 'sudo']) + + for userpass in self.settings['user']: + if '/' in userpass: + user, password = userpass.split('/', 1) + create_user(user) + set_password(rootdir, user, password) + else: + create_user(userpass) + delete_password(rootdir, userpass) + + 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']])) + try: + with open('/dev/tty', 'w') as tty: + cliapp.runcmd([script, rootdir, self.settings['image']], stdout=tty, stderr=tty) + except IOError: + logging.debug('tty unavailable, trying in headless mode.') + subprocess.call([script, rootdir, self.settings['image']]) + + 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 ftab: + ftab.write('\nS0:23:respawn:%s\n' % serial_command) + + def check_swap_size(self): + # swap - modifies extent + 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']) + return extent + + def make_swap(self, extent): + if self.settings['swap'] > 0: + logging.debug("Creating swap partition") + runcmd([ + 'parted', '-s', self.settings['image'], + 'mkpart', 'primary', 'linux-swap', extent, '100%']) + + def base_packages(self): + packages = [] + if not self.settings['foreign'] and not self.settings['no-acpid']: + packages.append('acpid') + if self.settings['sudo']: + packages.append('sudo') + if not self.settings['no-kernel']: + if self.settings['kernel-package']: + packages.append(self.settings['kernel-package']) + return packages + + def mask_udev_predictable_rules(self, rootdir): + """ + This can be reset later but to get networking using immediately + on boot, the interface we're going to use must be known + http://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames/ + """ + self.message('Disabling systemd predictable interface names') + udev_path = os.path.join( + 'etc', 'udev', 'rules.d', '80-net-setup-link.rules') + runcmd(['chroot', rootdir, 'ln', '-s', '/dev/null', udev_path]) diff --git a/vmdebootstrap/codenames.py b/vmdebootstrap/codenames.py new file mode 100644 index 0000000..d0f71fe --- /dev/null +++ b/vmdebootstrap/codenames.py @@ -0,0 +1,79 @@ +""" + Wrapper for distro information +""" +# -*- coding: utf-8 -*- +# +# codenames.py +# +# Copyright 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 datetime +from vmdebootstrap.base import Base +from distro_info import DebianDistroInfo, UbuntuDistroInfo + +# pylint: disable=missing-docstring + + +class Codenames(Base): + + name = 'codenames' + + def __init__(self): + super(Codenames, self).__init__() + self.debian_info = DebianDistroInfo() + self.ubuntu_info = UbuntuDistroInfo() + self.settings = None + + def define_settings(self, settings): + self.settings = settings + + def suite_to_codename(self, distro): + suite = self.debian_info.codename(distro, datetime.date.today()) + if not suite: + return distro + return suite + + def was_oldstable(self, limit): + suite = self.suite_to_codename(self.settings['distribution']) + # this check is only for debian + if not self.debian_info.valid(suite): + return False + return suite == self.debian_info.old(limit) + + def was_stable(self, limit): + suite = self.suite_to_codename(self.settings['distribution']) + # this check is only for debian + if not self.debian_info.valid(suite): + return False + return suite == self.debian_info.stable(limit) + + def kernel_package(self): + packages = [] + if not self.settings['no-kernel']: + if self.settings['kernel-package']: + return packages + if self.settings['arch'] == 'i386': + # wheezy (which became oldstable on 04/25/2015) used '486' + if self.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'] + packages.append('linux-image-%s' % kernel_arch) + return packages diff --git a/vmdebootstrap/constants.py b/vmdebootstrap/constants.py new file mode 100644 index 0000000..9f39415 --- /dev/null +++ b/vmdebootstrap/constants.py @@ -0,0 +1,52 @@ +""" + Constants which can be used by any handler +""" +# -*- coding: utf-8 -*- +# +# constants.py +# +# Copyright 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/>. + + +arch_table = { # pylint: disable=invalid-name + 'amd64': { + 'removable': '/EFI/boot/bootx64.efi', # destination location + 'install': '/EFI/debian/grubx64.efi', # package location + 'package': 'grub-efi-amd64', # bootstrap package + 'bin_package': 'grub-efi-amd64-bin', # binary only + 'extra': 'i386', # architecture to add binary package + 'exclusive': False, # only EFI supported for this arch. + 'target': 'x86_64-efi', # grub target name + }, + 'i386': { + 'removable': '/EFI/boot/bootia32.efi', + 'install': '/EFI/debian/grubia32.efi', + 'package': 'grub-efi-ia32', + 'bin_package': 'grub-efi-ia32-bin', + 'extra': None, + 'exclusive': False, + 'target': 'i386-efi', + }, + 'arm64': { + 'removable': '/EFI/boot/bootaa64.efi', + 'install': '/EFI/debian/grubaa64.efi', + 'package': 'grub-efi-arm64', + 'bin_package': 'grub-efi-arm64-bin', + 'extra': None, + 'exclusive': True, + 'target': 'arm64-efi', + } +} diff --git a/vmdebootstrap/extlinux.py b/vmdebootstrap/extlinux.py new file mode 100644 index 0000000..5698f6c --- /dev/null +++ b/vmdebootstrap/extlinux.py @@ -0,0 +1,107 @@ +""" + Wrapper for Extlinux support +""" +# -*- coding: utf-8 -*- +# +# extlinux.py +# +# Copyright 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 re +import os +import time +import cliapp +import logging +from vmdebootstrap.base import Base, runcmd + +# pylint: disable=missing-docstring + + +class ExtLinux(Base): + + name = 'extlinux' + + def __init__(self): + super(ExtLinux, self).__init__() + + def install_extlinux(self, rootdev, rootdir): + if not os.path.exists("/usr/bin/extlinux"): + self.message("extlinux not installed, skipping.") + return + self.message('Installing extlinux') + + def find(pattern): + dirname = os.path.join(rootdir, 'boot') + basenames = os.listdir(dirname) + logging.debug('find: %s', basenames) + for basename in basenames: + if re.search(pattern, basename): + return os.path.join('boot', basename) + raise cliapp.AppException('Cannot find match: %s' % pattern) + + try: + kernel_image = find('vmlinuz-.*') + initrd_image = find('initrd.img-.*') + except cliapp.AppException as exc: + self.message("Unable to find kernel. Not installing extlinux.") + logging.debug("No kernel found. %s. Skipping install of extlinux.", exc) + return + + out = runcmd(['blkid', '-c', '/dev/null', '-o', 'value', + '-s', 'UUID', rootdev]) + uuid = out.splitlines()[0].strip() + + conf = os.path.join(rootdir, 'extlinux.conf') + logging.debug('configure extlinux %s', conf) + kserial = 'console=ttyS0,115200' if self.settings['serial-console'] else '' + extserial = 'serial 0 115200' if self.settings['serial-console'] else '' + msg = ''' +default linux +timeout 1 + +label linux +kernel %(kernel)s +append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s +%(extserial)s +''' % { + 'kernel': kernel_image, # pylint: disable=bad-continuation + 'initrd': initrd_image, # pylint: disable=bad-continuation + 'uuid': uuid, # pylint: disable=bad-continuation + 'kserial': kserial, # pylint: disable=bad-continuation + 'extserial': extserial, # pylint: disable=bad-continuation + } # pylint: disable=bad-continuation + logging.debug("extlinux config:\n%s", msg) + + # python multiline string substitution is just ugly. + # use an external file or live with the mangling, no point in + # mangling the string to remove spaces just to keep it pretty in source. + ext_f = open(conf, 'w') + ext_f.write(msg) + + runcmd(['extlinux', '--install', rootdir]) + runcmd(['sync']) + time.sleep(2) + + def install_mbr(self): + if not self.settings['mbr'] and not self.settings['extlinux']: + return + 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) diff --git a/vmdebootstrap/filesystem.py b/vmdebootstrap/filesystem.py new file mode 100644 index 0000000..df3e7b5 --- /dev/null +++ b/vmdebootstrap/filesystem.py @@ -0,0 +1,273 @@ +""" + Wrapper for filesystem utilities +""" +# -*- coding: utf-8 -*- +# +# filesystem.py +# +# Copyright 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 cliapp +import logging +from vmdebootstrap.base import ( + Base, + runcmd, + copy_files +) + +# pylint: disable=missing-docstring + + +class Filesystem(Base): + + name = 'filesystem' + + def __init__(self): + super(Filesystem, self).__init__() + self.settings = None + self.devices = { + 'rootdir': None, + 'rootdev': None, + 'bootdev': None, + 'boottype': None, + 'roottype': None, + 'swapdev': None, + } + + def define_settings(self, settings): + self.settings = settings + self.devices['roottype'] = self.settings['roottype'] + + 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'] + elif self.settings['squash']: + filename = self.settings['squash'] + else: + return + self.message("Changing owner to %s" % self.settings["owner"]) + runcmd(["chown", "-R", self.settings["owner"], filename]) + + def update_initramfs(self): + rootdir = self.devices['rootdir'] + if not rootdir: + raise cliapp.AppException("rootdir not set") + cmd = os.path.join('usr', 'sbin', 'update-initramfs') + if os.path.exists(os.path.join(str(rootdir), cmd)): + self.message("Updating the initramfs") + runcmd(['chroot', rootdir, cmd, '-u']) + + 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] + self.devices['rootdev'] = root + self.devices['bootdev'] = boot + self.devices['swap'] = swap + + def mkfs(self, device, fstype): + self.message('Creating filesystem %s' % fstype) + runcmd(['mkfs', '-t', fstype, device]) + + def create_fstab(self): + rootdir = self.devices['rootdir'] + rootdev = self.devices['rootdev'] + bootdev = self.devices['bootdev'] + boottype = self.devices['boottype'] + roottype = self.devices['roottype'] + + 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 + + if not rootdir: + raise cliapp.AppException("rootdir not set") + + fstab = os.path.join(str(rootdir), 'etc', 'fstab') + with open(fstab, 'w') as fstab: + fstab.write('proc /proc proc defaults 0 0\n') + fstab.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr, roottype)) + if bootdevstr: + fstab.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr, boottype)) + if self.settings['swap'] > 0: + fstab.write("/dev/sda3 swap swap defaults 0 0\n") + elif self.settings['swap'] > 0: + fstab.write("/dev/sda2 swap swap defaults 0 0\n") + + def squash_rootfs(self): + """ + Run squashfs on the rootfs within the image. + Copy the initrd and the kernel out, squashfs the rest. + Also UEFI files, if enabled, ESP partition as a vfat image. TBD. + """ + if not self.settings['squash']: + return + if not os.path.exists('/usr/bin/mksquashfs'): + logging.warning("Squash selected but mksquashfs not found!") + return + if not os.path.exists(self.settings['squash']): + os.mkdir(self.settings['squash']) + suffixed = os.path.join(self.settings['squash'], "filesystem.squashfs") + if os.path.exists(suffixed): + os.unlink(suffixed) + self.message("Running mksquashfs on rootfs.") + msg = runcmd( + ['mksquashfs', self.devices['rootdir'], suffixed, + '-no-progress', '-comp', 'xz'], ignore_fail=False) + logging.debug(msg) + check_size = os.path.getsize(suffixed) + logging.debug("Created squashfs: %s", 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) + bootdir = os.path.join(self.devices['rootdir'], 'boot') + # copying the boot/* files + self.message("Copying boot files out of squashfs") + copy_files(bootdir, self.settings['squash']) + + def configure_apt(self): + rootdir = self.devices['rootdir'] + if not self.settings['configure-apt'] or not self.settings['apt-mirror']: + return + if not rootdir: + raise cliapp.AppException("rootdir not set") + # use the distribution and mirror to create an apt source + self.message("Configuring apt to use distribution and mirror") + conf = os.path.join(str(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(str(rootdir), 'etc', 'apt', 'sources.list')) + source = open(conf, 'w') + line = 'deb %s %s main\n' % (mirror, self.settings['distribution']) + source.write(line) + line = '#deb-src %s %s main\n' % (mirror, self.settings['distribution']) + source.write(line) + source.close() + # ensure the apt sources have valid lists + runcmd(['chroot', rootdir, 'apt-get', '-qq', 'update']) + + def list_installed_pkgs(self): + if not self.settings['pkglist']: + return + rootdir = self.devices['rootdir'] + # 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 remove_udev_persistent_rules(self): + rootdir = self.devices['rootdir'] + if not rootdir: + raise cliapp.AppException("rootdir not set") + self.message('Removing udev persistent cd and net rules') + for xrule in ['70-persistent-cd.rules', '70-persistent-net.rules']: + pathname = os.path.join(str(rootdir), 'etc', 'udev', 'rules.d', xrule) + if os.path.exists(pathname): + logging.debug('rm %s', pathname) + os.remove(pathname) + else: + logging.debug('not removing non-existent %s', pathname) + + def set_hostname(self): + rootdir = self.devices['rootdir'] + hostname = self.settings['hostname'] + if not rootdir: + raise cliapp.AppException("rootdir not set") + with open(os.path.join(str(rootdir), 'etc', 'hostname'), 'w') as fhost: + fhost.write('%s\n' % hostname) + + etc_hosts = os.path.join(str(rootdir), 'etc', 'hosts') + try: + with open(etc_hosts, 'r') as fhost: + data = fhost.read() + with open(etc_hosts, 'w') as fhosts: + for line in data.splitlines(): + if line.startswith('127.0.0.1'): + line += ' %s' % hostname + fhosts.write('%s\n' % line) + except IOError: + pass + + def make_rootfs_part(self, extent): + bootsize = self.settings['esp-size'] / (1024 * 1024) + 1 + runcmd(['parted', '-s', self.settings['image'], + 'mkpart', 'primary', str(bootsize), extent]) diff --git a/vmdebootstrap/grub.py b/vmdebootstrap/grub.py new file mode 100644 index 0000000..95b17eb --- /dev/null +++ b/vmdebootstrap/grub.py @@ -0,0 +1,123 @@ +""" + Wrapper for Grub operations +""" +# -*- coding: utf-8 -*- +# +# grub.py +# +# Copyright 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/>. + +# pylint: disable=missing-docstring,duplicate-code + + +import os +import cliapp +import logging +from vmdebootstrap.base import ( + Base, + runcmd, + mount_wrapper, + umount_wrapper +) +from vmdebootstrap.uefi import arch_table + + +def grub_serial_console(rootdir): + cmdline = 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=tty1 console=ttyS0,38400n8"' + terminal = 'GRUB_TERMINAL="serial gfxterm"' + command = 'GRUB_SERIAL_COMMAND="serial --speed=38400 --unit=0 --parity=no --stop=1"' + grub_cfg = os.path.join(rootdir, 'etc', 'default', 'grub') + logging.debug("Allowing serial output in grub config %s", grub_cfg) + with open(grub_cfg, 'a+') as cfg: + cfg.write("# %s serial support\n" % os.path.basename(__file__)) + cfg.write("%s\n" % cmdline) + cfg.write("%s\n" % terminal) + cfg.write("%s\n" % command) + + +class GrubHandler(Base): + + name = 'grub' + + def __init__(self): + super(GrubHandler, self).__init__() + + def install_grub2(self, rootdev, rootdir): + self.message("Configuring grub2") + # rely on kpartx using consistent naming to map loop0p1 to loop0 + grub_opts = os.path.join('/dev', os.path.basename(rootdev)[:-2]) + if self.settings['serial-console']: + grub_serial_console(rootdir) + logging.debug("Running grub-install with options: %s", grub_opts) + mount_wrapper(rootdir) + try: + runcmd(['chroot', rootdir, 'update-grub']) + runcmd(['chroot', rootdir, 'grub-install', grub_opts]) + except cliapp.AppException as exc: + logging.warning(exc) + self.message("Failed. Is grub2-common installed? Using extlinux.") + umount_wrapper(rootdir) + return False + umount_wrapper(rootdir) + return True + + def install_grub_uefi(self, rootdir): + ret = True + self.message("Configuring grub-uefi") + target = arch_table[self.settings['arch']]['target'] + grub_opts = "--target=%s" % target + logging.debug("Running grub-install with options: %s", grub_opts) + mount_wrapper(rootdir) + try: + runcmd(['chroot', rootdir, 'update-grub']) + runcmd(['chroot', rootdir, 'grub-install', grub_opts]) + except cliapp.AppException as exc: + logging.warning(exc) + ret = False + self.message( + "Failed to configure grub-uefi for %s" % + self.settings['arch']) + finally: + umount_wrapper(rootdir) + if not ret: + raise cliapp.AppException("Failed to install grub uefi") + + def install_extra_grub_uefi(self, rootdir): + ret = True + extra = arch_table[self.settings['arch']]['extra'] + if extra: + logging.debug("Installing extra grub support for %s", extra) + mount_wrapper(rootdir) + target = arch_table[extra]['target'] + grub_opts = "--target=%s" % target + self.message("Adding grub target %s" % grub_opts) + try: + runcmd(['chroot', rootdir, 'update-grub']) + runcmd(['chroot', rootdir, 'grub-install', grub_opts]) + except cliapp.AppException as exc: + logging.warning(exc) + ret = False + self.message( + "Failed to configure grub-uefi for %s" % extra) + finally: + umount_wrapper(rootdir) + if not ret: + raise cliapp.AppException("Failed to install extra grub uefi") + + def grub_packages(self): + if self.settings['grub'] and not self.settings['use-uefi']: + return ['grub-pc'] + return [] diff --git a/vmdebootstrap/uefi.py b/vmdebootstrap/uefi.py new file mode 100644 index 0000000..bf04ae8 --- /dev/null +++ b/vmdebootstrap/uefi.py @@ -0,0 +1,167 @@ +""" + Wrapper for UEFI operations +""" +# -*- coding: utf-8 -*- +# +# uefi.py +# +# Copyright 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/>. + + +# pylint: disable=missing-docstring,duplicate-code + + +import os +import cliapp +import logging +from vmdebootstrap.base import ( + Base, + runcmd, + mount_wrapper, + umount_wrapper, +) +from vmdebootstrap.constants import arch_table + + +class Uefi(Base): + + name = 'uefi' + + def __init__(self): + super(Uefi, self).__init__() + self.bootdir = None + + def check_settings(self, oldstable=False): + 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 self.settings['arch'] in arch_table and\ + 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 oldstable: + if self.settings['arch'] != 'amd64' and self.settings['use-uefi']: + raise cliapp.AppException( + 'Only amd64 supports UEFI in Wheezy') + + def efi_packages(self): + packages = [] + if not self.settings['use-uefi'] or\ + self.settings['arch'] not in arch_table: + return packages + pkg = arch_table[self.settings['arch']]['package'] + self.message("Adding %s to debootstrap" % pkg) + packages.append(pkg) + extra = arch_table[self.settings['arch']]['extra'] + if extra and isinstance(extra, str): + bin_pkg = arch_table[str(extra)]['bin_package'] + self.message("Adding support for %s using %s" % (extra, bin_pkg)) + packages.append(bin_pkg) + return packages + + def copy_efi_binary(self, efi_removable, efi_install): + if self.settings['arch'] not in arch_table: + return + logging.debug("using bootdir=%s", self.bootdir) + if efi_removable.startswith('/'): + efi_removable = efi_removable[1:] + if efi_install.startswith('/'): + efi_install = efi_install[1:] + efi_output = os.path.join(self.bootdir, efi_removable) + efi_input = os.path.join(self.bootdir, efi_install) + logging.debug("moving %s to %s", efi_output, efi_input) + if not os.path.exists(efi_input): + logging.warning("%s does not exist (%s)", efi_input, efi_install) + raise cliapp.AppException("Missing %s" % efi_input) + if not os.path.exists(os.path.dirname(efi_output)): + os.makedirs(os.path.dirname(efi_output)) + logging.debug( + 'Moving UEFI support: %s -> %s', efi_input, efi_output) + if os.path.exists(efi_output): + os.unlink(efi_output) + os.rename(efi_input, efi_output) + + def configure_efi(self, rootdir): + """ + Copy the bootloader file from the package into the location + so needs to be after grub and kernel already installed. + """ + if self.settings['arch'] not in arch_table: + return + self.message('Configuring EFI') + mount_wrapper(rootdir) + efi_removable = str(arch_table[self.settings['arch']]['removable']) + efi_install = str(arch_table[self.settings['arch']]['install']) + self.message('Installing UEFI support binary') + logging.debug("moving %s to %s", efi_removable, efi_install) + try: + self.copy_efi_binary(efi_removable, efi_install) + finally: + umount_wrapper(rootdir) + + def configure_extra_efi(self, rootdir): + if self.settings['arch'] not in arch_table: + return + extra = arch_table[self.settings['arch']]['extra'] + if extra: + mount_wrapper(rootdir) + efi_removable = str(arch_table[extra]['removable']) + efi_install = str(arch_table[extra]['install']) + self.message('Copying UEFI support binary for %s' % extra) + try: + self.copy_efi_binary(efi_removable, efi_install) + finally: + umount_wrapper(rootdir) + + def partition_esp(self): + if not self.settings['use-uefi']: + return + espsize = self.settings['esp-size'] / (1024 * 1024) + self.message("Using ESP size: %smib %s bytes" % (espsize, self.settings['esp-size'])) + runcmd(['parted', '-s', self.settings['image'], + 'mkpart', 'primary', 'fat32', + '1', str(espsize)]) + runcmd(['parted', '-s', self.settings['image'], + 'set', '1', 'boot', 'on']) + runcmd(['parted', '-s', self.settings['image'], + 'set', '1', 'esp', 'on']) + + def prepare_esp(self, rootdir, bootdev): + self.bootdir = '%s/%s/%s' % (rootdir, 'boot', 'efi') + logging.debug("bootdir:%s", self.bootdir) + self.mkfs(bootdev, fstype='vfat') + os.makedirs(self.bootdir) + return self.bootdir + + def make_root(self, extent): + bootsize = self.settings['esp-size'] / (1024 * 1024) + 1 + runcmd(['parted', '-s', self.settings['image'], + 'mkpart', 'primary', str(bootsize), extent]) |