diff options
Diffstat (limited to 'bin/sbuild-qemu-update')
-rwxr-xr-x | bin/sbuild-qemu-update | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/bin/sbuild-qemu-update b/bin/sbuild-qemu-update new file mode 100755 index 0000000..af5c31c --- /dev/null +++ b/bin/sbuild-qemu-update @@ -0,0 +1,282 @@ +#!/usr/bin/python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright © 2020-2024 Christian Kastner <ckk@debian.org> +# 2021 Simon McVittie <smcv@debian.org> +# 2024 Johannes Schauer Marin Rodrigues <josch@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 2 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/>. +# +####################################################################### + + +# Note that there is significant overlap between this program and +# sbuild-qemu-boot. Both are in their developmental stages and I'd prefer to +# wait and see where this goes before refactoring them. --ckk + + +import argparse +import datetime +import os +import subprocess +import sys + +import pexpect + + +SUPPORTED_ARCHS = [ + 'amd64', + 'arm64', + 'armhf', + 'i386', + 'ppc64el', +] + +IMAGEDIR = os.environ.get( + 'IMAGEDIR', + os.path.join(os.path.expanduser('~'), '.cache', 'sbuild'), +) + + +def make_snapshot(image): + iso_stamp = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S') + run = subprocess.run( + ['qemu-img', 'snapshot', '-l', image], + capture_output=True + ) + tags = [t.split()[1].decode('utf-8') for t in run.stdout.splitlines()[2:]] + + if iso_stamp in tags: + print( + f"Error: snapshot for {iso_stamp} already exists.", + file=sys.stderr + ) + return False + + run = subprocess.run(['qemu-img', 'snapshot', '-c', iso_stamp, image]) + return True if run.returncode == 0 else False + + +def get_qemu_base_args(image, guest_arch=None, boot="auto"): + host_arch = subprocess.check_output( + ['dpkg', '--print-architecture'], + text=True, + ).strip() + + if not guest_arch: + # This assumes that images are named foo-bar-ARCH.img + root, _ = os.path.splitext(os.path.basename(image)) + components = root.split('-') + for c in reversed(components): + if c in SUPPORTED_ARCHS: + guest_arch = c + break + if not guest_arch: + print( + f"Could not guess guest architecture, please use --arch", + file=sys.stderr, + ) + return + else: + if not guest_arch in SUPPORTED_ARCHS: + print(f"Unsupported architecture: {guest_arch}", file=sys.stderr) + print("Supported architectures are: ", file=sys.stderr, end="") + print(f"{', '.join(SUPPORTED_ARCHS)}", file=sys.stderr) + return + + if guest_arch == 'amd64' : + argv = ['qemu-system-x86_64'] + if host_arch == 'amd64': + argv.append('-enable-kvm') + elif guest_arch == 'i386': + argv = ['qemu-system-i386', '-machine', 'q35'] + if host_arch in ['amd64', 'i386']: + argv.append('-enable-kvm') + elif guest_arch == 'ppc64el': + argv = ['qemu-system-ppc64le'] + if host_arch == 'ppc64el': + argv.append('-enable-kvm') + elif guest_arch == 'arm64': + argv = [ + 'qemu-system-aarch64', + '-machine', 'virt', + ] + if host_arch == 'arm64': + argv.extend(['-cpu', 'host', '-enable-kvm']) + else: + argv.extend(['-cpu', 'cortex-a53']) + elif guest_arch == 'armhf': + if host_arch == 'arm64': + argv = [ + 'qemu-system-aarch64', + '-cpu', 'host,aarch64=off', + '-enable-kvm' + ] + else: + argv = ['qemu-system-arm'] + argv.extend([ + '-machine', 'virt', + ]) + + if boot == "auto": + match guest_arch: + case 'amd64'|'i386': + boot = "bios" + case 'arm64'|'armhf': + boot = "efi" + case 'ppc64el': + boot = "ieee1275" + + eficode = None + match boot: + case "bios"|"none": + pass + case "efi": + match guest_arch: + case 'amd64': + eficode = "/usr/share/OVMF/OVMF_CODE.fd" + case 'i386': + eficode = "/usr/share/OVMF/OVMF32_CODE_4M.secboot.fd" + case 'arm64': + eficode = "/usr/share/AAVMF/AAVMF_CODE.fd" + case 'armhf': + eficode = "/usr/share/AAVMF/AAVMF32_CODE.fd" + case 'ppc64el': + print("efi not supported on ppc64el") + if eficode: + argv.extend(["-drive", f"if=pflash,format=raw,unit=0,read-only=on,file={eficode}"]) + + + return argv + + +def update_interaction(child, quiet): + child.expect('host login: ') + child.sendline('root') + if not quiet: + child.logfile = sys.stdout + child.expect('root@host:~# ') + child.sendline('DEBIAN_FRONTEND=noninteractive apt-get --quiet update') + child.expect('root@host:~# ') + child.sendline('DEBIAN_FRONTEND=noninteractive apt-get --quiet --assume-yes dist-upgrade') + child.expect('root@host:~# ') + child.sendline('DEBIAN_FRONTEND=noninteractive apt-get --quiet --assume-yes clean') + child.expect('root@host:~# ') + child.sendline('DEBIAN_FRONTEND=noninteractive apt-get --quiet --assume-yes autoremove') + child.expect('root@host:~# ') + child.sendline('sync') + child.expect('root@host:~# ') + # Don't recall what issue this solves, but it solves it + child.sendline('sleep 1') + child.expect('root@host:~# ') + child.sendline('shutdown -h now') + + +def main(): + parser = argparse.ArgumentParser( + description='sbuild-update analog for QEMU images.', + ) + parser.add_argument( + '--snapshot', + action='store_true', + help="Create a snapshot of the image before changing it. Useful for " + "reproducibility purposes.", + ) + parser.add_argument( + '--arch', + help="Architecture to use (instead of attempting to auto-guess based " + "on the image name).", + ) + parser.add_argument( + '--timeout', + type=int, + default=600, + metavar='SECONDS', + action='store', + help="Maximum time to wait for command to finish with expected " + "result. Mostly relevant for foreign architectures, where " + "`apt-get update` can take quite a while. Default: 600s." + ) + parser.add_argument( + '--noexec', + action='store_true', + help="Don't actually do anything. Just print the command string that " + "would be executed, and then exit.", + ) + parser.add_argument( + '--boot', + choices=['auto', 'bios', 'efi', 'ieee1275', 'none'], + default='auto', + help="How to boot the image. Default is BIOS on amd64 and i386, EFI " + "on arm64 and armhf, and IEEE1275 on ppc64el.", + ) + parser.add_argument( + 'image', + help="Image. Will first be interpreted as a path. If no suitable " + "image exists at that location, then $IMAGEDIR\<image> is tried.", + ) + dbglvlgroup = parser.add_mutually_exclusive_group() + dbglvlgroup.add_argument("-v", "--verbose", action="store_true") + dbglvlgroup.add_argument("-q", "--quiet", action="store_true") + + parser.add_argument("extra_args", nargs='*') + + parsed_args = parser.parse_args() + + if os.path.exists(parsed_args.image): + image = parsed_args.image + elif os.path.exists(os.path.join(IMAGEDIR, parsed_args.image)): + image = os.path.join(IMAGEDIR, parsed_args.image) + else: + print("Image does not exist", file=sys.stderr) + sys.exit(1) + + args = get_qemu_base_args(parsed_args.image, parsed_args.arch, parsed_args.boot) + if not args: + sys.exit(1) + + args.extend([ + '-object', 'rng-random,filename=/dev/urandom,id=rng0', + '-device', 'virtio-rng-pci,rng=rng0,id=rng-device0', + '-device', 'virtio-serial', + '-nic', 'user,model=virtio', + '-m', '1024', + '-smp', '1', + '-nographic', + image, + ]) + if parsed_args.extra_args: + args.extend(parsed_args.extra_args) + + print(' '.join(str(a) for a in args)) + if parsed_args.noexec: + return + elif parsed_args.snapshot and not make_snapshot(image): + return + + logfile = None + if parsed_args.verbose: + logfile = sys.stdout + child = pexpect.spawn(args[0], args[1:], logfile=logfile, encoding="utf-8") + child.timeout = parsed_args.timeout + try: + update_interaction(child, parsed_args.quiet) + except pexpect.TIMEOUT: + print("Update timed out. Consider using --timeout.", file=sys.stderr) + child.terminate() + + +if __name__ == '__main__': + main() |