pmbootstrap-meow/pmb/qemu/run.py
Minecrell 79a8d04835
pmb.qemu: do not try to change default IP range (!1886)
The current network setup has weird side effects.
Normally, QEMU would automatically make the guest set up necessary
IP routes through its integrated DHCP server.
When running QEMU through pmbootstrap they are missing.

First, we change the DHCP range in a way that could potentially
conflict with default IPs used for QEMU's own services:
QEMU has the default gateway at <network>.2, and DNS at <network>.3.
We set the DHCP range to start at <network>.1, and will therefore
potentially give out one of these addresses (QEMU's default starts at
<network>.15).
See: https://wiki.qemu.org/Documentation/Networking#User_Networking_.28SLIRP.29

In practice this does not cause immediate problems because there is
just one guest in the network, and it will get <network>.1, which is
not used by QEMU.

More problematic is that we start a DHCP server from postmarketOS
at the same time (normally used for the USB network) and there are
actually two DHCP servers running at the same time.

QEMU's user networking is local to the process, therefore it is not
possible to access the QEMU guest through its IP from the host.
That's why we have the port forwardings so you can access SSH at
localhost:2222 for example.

In practice the network interface in the QEMU guest is only used to
access the Internet. For that, we don't care which IP address we get,
we just want to get a working setup (IP + routes + DNS) automatically
through DHCP.

To make this work nicely we just need to stop trying to fit QEMU's
network setup into our usual setup for USB networking. When we remove
the custom DHCP option, and avoid starting a DHCP server from postmarketOS
(deviceinfo_disable_dhcpd) everything is suddenly working fine. :)
2020-03-14 08:05:32 +01:00

265 lines
9.5 KiB
Python

# Copyright 2020 Pablo Castellano, Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import os
import re
import signal
import shlex
import shutil
import pmb.build
import pmb.chroot
import pmb.chroot.apk
import pmb.chroot.other
import pmb.chroot.initfs
import pmb.config
import pmb.helpers.run
import pmb.parse.arch
def system_image(args):
"""
Returns path to rootfs for specified device. In case that it doesn't
exist, raise and exception explaining how to generate it.
"""
path = args.work + "/chroot_native/home/pmos/rootfs/" + args.device + ".img"
if not os.path.exists(path):
logging.debug("Could not find rootfs: " + path)
raise RuntimeError("The rootfs has not been generated yet, please "
"run 'pmbootstrap install' first.")
return path
def which_qemu(args, arch):
"""
Finds the qemu executable or raises an exception otherwise
"""
executable = "qemu-system-" + arch
if shutil.which(executable):
return executable
else:
raise RuntimeError("Could not find the '" + executable + "' executable"
" in your PATH. Please install it in order to"
" run qemu.")
def create_gdk_loader_cache(args):
"""
Create a gdk loader cache that can be used for running GTK UIs outside of
the chroot.
"""
gdk_cache_dir = "/usr/lib/gdk-pixbuf-2.0/2.10.0/"
custom_cache_path = gdk_cache_dir + "loaders-pmos-chroot.cache"
rootfs_native = args.work + "/chroot_native"
if os.path.isfile(rootfs_native + custom_cache_path):
return rootfs_native + custom_cache_path
cache_path = gdk_cache_dir + "loaders.cache"
if not os.path.isfile(rootfs_native + cache_path):
raise RuntimeError("gdk pixbuf cache file not found: " + cache_path)
pmb.chroot.root(args, ["cp", cache_path, custom_cache_path])
cmd = ["sed", "-i", "-e",
"s@\"" + gdk_cache_dir + "@\"" + rootfs_native + gdk_cache_dir + "@",
custom_cache_path]
pmb.chroot.root(args, cmd)
return rootfs_native + custom_cache_path
def command_qemu(args, arch, img_path):
"""
Generate the full qemu command with arguments to run postmarketOS
"""
cmdline = args.deviceinfo["kernel_cmdline"]
if args.cmdline:
cmdline = args.cmdline
logging.debug("Kernel cmdline: " + cmdline)
port_ssh = str(args.port)
port_telnet = str(args.port + 1)
suffix = "rootfs_" + args.device
rootfs = args.work + "/chroot_" + suffix
if args.flavor:
flavor = args.flavor
else:
flavor = pmb.chroot.other.kernel_flavors_installed(args, suffix)[0]
if args.host_qemu:
qemu_bin = which_qemu(args, arch)
env = {}
command = [qemu_bin]
else:
rootfs_native = args.work + "/chroot_native"
env = {"QEMU_MODULE_DIR": rootfs_native + "/usr/lib/qemu",
"GBM_DRIVERS_PATH": rootfs_native + "/usr/lib/xorg/modules/dri",
"LIBGL_DRIVERS_PATH": rootfs_native + "/usr/lib/xorg/modules/dri"}
if "gtk" in args.qemu_display:
gdk_cache = create_gdk_loader_cache(args)
env.update({"GTK_THEME": "Default",
"GDK_PIXBUF_MODULE_FILE": gdk_cache,
"XDG_DATA_DIRS": rootfs_native + "/usr/local/share:" +
rootfs_native + "/usr/share"})
command = [rootfs_native + "/lib/ld-musl-" +
args.arch_native + ".so.1"]
command += ["--library-path=" + rootfs_native + "/lib:" +
rootfs_native + "/usr/lib:" +
rootfs_native + "/usr/lib/pulseaudio"]
command += [rootfs_native + "/usr/bin/qemu-system-" + arch]
command += ["-L", rootfs_native + "/usr/share/qemu/"]
command += ["-kernel", rootfs + "/boot/vmlinuz-" + flavor]
command += ["-initrd", rootfs + "/boot/initramfs-" + flavor]
command += ["-append", shlex.quote(cmdline)]
command += ["-smp", str(os.cpu_count())]
command += ["-m", str(args.memory)]
command += ["-netdev",
"user,id=net0,"
"hostfwd=tcp::" + port_ssh + "-:22,"
"hostfwd=tcp::" + port_telnet + "-:23"
]
command += ["-show-cursor"]
if arch == "x86_64":
command += ["-vga", "virtio"]
command += ["-serial", "stdio"]
command += ["-drive", "file=" + img_path + ",format=raw"]
command += ["-device", "e1000,netdev=net0"]
elif arch == "aarch64":
command += ["-M", "virt"]
command += ["-cpu", "cortex-a57"]
command += ["-device", "virtio-gpu-pci"]
command += ["-device", "virtio-net-device,netdev=net0"]
command += ["-usb", "-device", "usb-ehci",
"-device", "usb-kbd", "-device", "usb-mouse"]
# Add storage
command += ["-device", "virtio-blk-device,drive=system"]
command += ["-drive", "if=none,id=system,file={},id=hd0".format(img_path)]
else:
raise RuntimeError("Architecture {} not supported by this command yet.".format(arch))
# Kernel Virtual Machine (KVM) support
native = args.arch_native == args.deviceinfo["arch"]
if native and os.path.exists("/dev/kvm"):
command += ["-enable-kvm"]
else:
logging.info("WARNING: QEMU is not using KVM and will run slower!")
display = args.qemu_display
if display != "none":
display += ",gl=" + ("on" if args.qemu_gl else "off")
command += ["-display", display]
# Audio support
if args.qemu_audio:
command += ["-audiodev", args.qemu_audio + ",id=audio"]
command += ["-soundhw", "hda"]
return (command, env)
def resize_image(args, img_size_new, img_path):
"""
Truncates the rootfs to a specific size. The value must be larger than the
current image size, and it must be specified in MiB or GiB units (powers of 1024).
:param img_size_new: new image size in M or G
:param img_path: the path to the rootfs
"""
# Current image size in bytes
img_size = os.path.getsize(img_path)
# Make sure we have at least 1 integer followed by either M or G
pattern = re.compile("^[0-9]+[M|G]$")
if not pattern.match(img_size_new):
raise RuntimeError("You must specify the rootfs size in [M]iB or [G]iB, e.g. 2048M or 2G")
# Remove M or G and convert to bytes
img_size_new_bytes = int(img_size_new[:-1]) * 1024 * 1024
# Convert further for G
if (img_size_new[-1] == "G"):
img_size_new_bytes = img_size_new_bytes * 1024
if (img_size_new_bytes >= img_size):
logging.info("Setting the rootfs size to " + img_size_new)
pmb.helpers.run.root(args, ["truncate", "-s", img_size_new, img_path])
else:
# Convert to human-readable format
# NOTE: We convert to M here, and not G, so that we don't have to display
# a size like 1.25G, since decimal places are not allowed by truncate.
# We don't want users thinking they can use decimal numbers, and so in
# this example, they would need to use a size greater then 1280M instead.
img_size_str = str(round(img_size / 1024 / 1024)) + "M"
raise RuntimeError("The rootfs size must be " + img_size_str + " or greater")
def sigterm_handler(number, frame):
raise RuntimeError("pmbootstrap was terminated by another process,"
" and killed the QEMU VM it was running.")
def install_depends(args, arch):
"""
Install any necessary qemu dependencies in native chroot
"""
depends = ["qemu", "qemu-system-" + arch, "qemu-ui-sdl", "qemu-ui-gtk",
"mesa-gl", "mesa-egl", "mesa-dri-classic", "mesa-dri-gallium",
"qemu-audio-alsa", "qemu-audio-pa", "qemu-audio-sdl"]
pmb.chroot.apk.install(args, depends)
def run(args):
"""
Run a postmarketOS image in qemu
"""
if not args.device.startswith("qemu-"):
raise RuntimeError("'pmbootstrap qemu' can be only used with one of "
"the QEMU device packages. Run 'pmbootstrap init' "
"and select the 'qemu' vendor.")
arch = pmb.parse.arch.alpine_to_qemu(args.deviceinfo["arch"])
img_path = system_image(args)
if not args.host_qemu:
install_depends(args, arch)
logging.info("Running postmarketOS in QEMU VM (" + arch + ")")
qemu, env = command_qemu(args, arch, img_path)
# Workaround: QEMU runs as local user and needs write permissions in the
# rootfs, which is owned by root
if not os.access(img_path, os.W_OK):
pmb.helpers.run.root(args, ["chmod", "666", img_path])
# Resize the rootfs (or show hint)
if args.image_size:
resize_image(args, args.image_size, img_path)
else:
logging.info("NOTE: Run 'pmbootstrap qemu --image-size 2G' to set"
" the rootfs size when you run out of space!")
# SSH/telnet hints
logging.info("Connect to the VM (telnet requires 'pmbootstrap initfs"
" hook_add debug-shell'):")
logging.info("* (ssh) ssh -p {port} {user}@localhost".format(**vars(args)))
logging.info("* (telnet) telnet localhost " + str(args.port + 1))
# Run QEMU and kill it together with pmbootstrap
process = None
try:
signal.signal(signal.SIGTERM, sigterm_handler)
process = pmb.helpers.run.user(args, qemu, output="interactive", env=env)
except KeyboardInterrupt:
# Don't show a trace when pressing ^C
pass
finally:
if process:
process.terminate()