forked from Mirror/pmbootstrap
* pmb.helpers.run: support running processes in background * enable QXL driver support in the linux kernel configurations so that we can also use SPICE to connect to the VM. QXL is a paravirtual graphics driver with 2D support The SPICE project aims to provide a complete open source solution for remote access to virtual machines in a seamless way. Both DRM_QXL and DRM_BOCHS are enabled as modules. According to [1], on Linux guests, the qxl and bochs_drm kernel modules must be loaded in order to gain a decent performance * qemu: add new option --spice to connect to VM using a SPICE client If specified, 'pmbootstrap qemu' will look for some SPICE client in the user's PATH and run qemu using the QXL driver. Currently supported spice clients are 'spicy' and 'remote-viewer' but adding support for more clients can be easily done. qemu with qxl support will run on port 8077/tcp, which doesn't belong to any well-known service and represents 'PM' in decimal. References: [0] https://www.linux-kvm.org/page/SPICE [1] https://wiki.archlinux.org/index.php/QEMU#qxl [2] https://wiki.archlinux.org/index.php/QEMU#SPICE [3] https://github.com/postmarketOS/pmbootstrap/issues/453 (partially fixed)
281 lines
9.8 KiB
Python
281 lines
9.8 KiB
Python
"""
|
|
Copyright 2017 Pablo Castellano
|
|
|
|
This file is part of pmbootstrap.
|
|
|
|
pmbootstrap 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.
|
|
|
|
pmbootstrap 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 pmbootstrap. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import re
|
|
|
|
import pmb.build
|
|
import pmb.chroot
|
|
import pmb.chroot.apk
|
|
import pmb.chroot.other
|
|
import pmb.chroot.initfs
|
|
import pmb.config
|
|
import pmb.helpers.devices
|
|
import pmb.helpers.run
|
|
import pmb.parse.arch
|
|
|
|
|
|
def system_image(args, device):
|
|
"""
|
|
Returns path to system image for specified device. In case that it doesn't
|
|
exist, raise and exception explaining how to generate it.
|
|
"""
|
|
path = args.work + "/chroot_native/home/user/rootfs/" + device + ".img"
|
|
if not os.path.exists(path):
|
|
logging.debug("Could not find system image: " + path)
|
|
img_command = "pmbootstrap install"
|
|
if device != args.device:
|
|
img_command = ("pmbootstrap config device " + device +
|
|
"' and '" + img_command)
|
|
message = "The system image '{0}' has not been generated yet, please" \
|
|
" run '{1}' first.".format(device, img_command)
|
|
raise RuntimeError(message)
|
|
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 which_spice(args):
|
|
"""
|
|
Finds some SPICE executable or raises an exception otherwise
|
|
:returns: tuple (spice_was_found, path_to_spice_executable)
|
|
"""
|
|
executables = ["remote-viewer", "spicy"]
|
|
for executable in executables:
|
|
if shutil.which(executable):
|
|
return executable
|
|
return None
|
|
|
|
|
|
def spice_command(args):
|
|
"""
|
|
Generate the full SPICE command with arguments connect to
|
|
the virtual machine
|
|
:returns: tuple (dict, list), configuration parameters and spice command
|
|
"""
|
|
parameters = {
|
|
"spice_addr": "127.0.0.1",
|
|
"spice_port": "8077"
|
|
}
|
|
if not args.use_spice:
|
|
parameters["enable_spice"] = False
|
|
return parameters, []
|
|
spice_binary = which_spice(args)
|
|
if not spice_binary:
|
|
parameters["enable_spice"] = False
|
|
return parameters, []
|
|
spice_addr = parameters["spice_addr"]
|
|
spice_port = parameters["spice_port"]
|
|
commands = {
|
|
"spicy": ["spicy", "-h", spice_addr, "-p", spice_port],
|
|
"remote-viewer": [
|
|
"remote-viewer",
|
|
"spice://" + spice_addr + "?port=" + spice_port
|
|
]
|
|
}
|
|
parameters["enable_spice"] = True
|
|
return parameters, commands[spice_binary]
|
|
|
|
|
|
def qemu_command(args, arch, device, img_path, config):
|
|
"""
|
|
Generate the full qemu command with arguments to run postmarketOS
|
|
"""
|
|
qemu_bin = which_qemu(args, arch)
|
|
deviceinfo = pmb.parse.deviceinfo(args, device=device)
|
|
cmdline = deviceinfo["kernel_cmdline"]
|
|
if args.cmdline:
|
|
cmdline = args.cmdline
|
|
logging.info("cmdline: " + cmdline)
|
|
|
|
ssh_port = str(args.port)
|
|
telnet_port = str(args.port + 1)
|
|
telnet_debug_port = str(args.port + 2)
|
|
|
|
rootfs = args.work + "/chroot_rootfs_" + device
|
|
command = [qemu_bin]
|
|
command += ["-kernel", rootfs + "/boot/vmlinuz-postmarketos"]
|
|
command += ["-initrd", rootfs + "/boot/initramfs-postmarketos"]
|
|
command += ["-append", '"' + cmdline + '"']
|
|
command += ["-m", str(args.memory)]
|
|
command += ["-netdev",
|
|
"user,id=net0,"
|
|
"hostfwd=tcp::" + ssh_port + "-:22,"
|
|
"hostfwd=tcp::" + telnet_port + "-:23,"
|
|
"hostfwd=tcp::" + telnet_debug_port + "-:24"
|
|
",net=172.16.42.0/24,dhcpstart=" + pmb.config.default_ip
|
|
]
|
|
|
|
if deviceinfo["dtb"] != "":
|
|
dtb_image = rootfs + "/usr/share/dtb/" + deviceinfo["dtb"] + ".dtb"
|
|
if not os.path.exists(dtb_image):
|
|
raise RuntimeError("DTB file not found: " + dtb_image)
|
|
command += ["-dtb", dtb_image]
|
|
|
|
if arch == "x86_64":
|
|
command += ["-serial", "stdio"]
|
|
command += ["-drive", "file=" + img_path + ",format=raw"]
|
|
command += ["-device", "e1000,netdev=net0"]
|
|
|
|
elif arch == "arm":
|
|
command += ["-M", "vexpress-a9"]
|
|
command += ["-sd", img_path]
|
|
command += ["-device", "virtio-net-device,netdev=net0"]
|
|
|
|
elif arch == "aarch64":
|
|
command += ["-M", "virt"]
|
|
command += ["-cpu", "cortex-a57"]
|
|
command += ["-device", "virtio-gpu-pci"]
|
|
command += ["-device", "virtio-net-device,netdev=net0"]
|
|
|
|
# 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
|
|
enable_kvm = True
|
|
if args.arch:
|
|
arch1 = pmb.parse.arch.uname_to_qemu(args.arch_native)
|
|
arch2 = pmb.parse.arch.uname_to_qemu(args.arch)
|
|
enable_kvm = (arch1 == arch2)
|
|
if enable_kvm and os.path.exists("/dev/kvm"):
|
|
command += ["-enable-kvm"]
|
|
else:
|
|
logging.info("Warning: qemu is not using KVM and will run slower!")
|
|
|
|
# QXL / SPICE (2D acceleration support)
|
|
if config["enable_spice"]:
|
|
command += ["-vga", "qxl"]
|
|
command += ["-spice",
|
|
"port={spice_port},addr={spice_addr}".format(**config) +
|
|
",disable-ticketing"]
|
|
|
|
return command
|
|
|
|
|
|
def resize_image(args, img_size_new, img_path):
|
|
"""
|
|
Truncates the system image 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 system image
|
|
"""
|
|
# 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 system image 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 system image 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 system image size must be " + img_size_str + " or greater")
|
|
|
|
|
|
def run(args):
|
|
"""
|
|
Run a postmarketOS image in qemu
|
|
"""
|
|
arch = pmb.parse.arch.uname_to_qemu(args.arch_native)
|
|
if args.arch:
|
|
arch = pmb.parse.arch.uname_to_qemu(args.arch)
|
|
|
|
device = pmb.parse.arch.qemu_to_pmos_device(arch)
|
|
img_path = system_image(args, device)
|
|
spice_parameters, command_spice = spice_command(args)
|
|
|
|
# Workaround: qemu runs as local user and needs write permissions in the
|
|
# system image, which is owned by root
|
|
if not os.access(img_path, os.W_OK):
|
|
pmb.helpers.run.root(args, ["chmod", "666", img_path])
|
|
|
|
run_spice = spice_parameters["enable_spice"]
|
|
command = qemu_command(args, arch, device, img_path, spice_parameters)
|
|
|
|
logging.info("Running postmarketOS in QEMU VM (" + arch + ")")
|
|
logging.info("Command: " + " ".join(command))
|
|
|
|
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 system image size when you run out of space!")
|
|
|
|
print()
|
|
logging.info("You can connect to the virtual machine using the"
|
|
" following services:")
|
|
logging.info("(ssh) ssh -p " + str(args.port) + " user@localhost")
|
|
logging.info("(telnet) telnet localhost " + str(args.port + 1))
|
|
logging.info("(telnet debug) telnet localhost " + str(args.port + 2))
|
|
|
|
# SPICE related messages
|
|
if not run_spice:
|
|
if args.use_spice:
|
|
logging.warning("WARNING: Could not find any SPICE client (spicy,"
|
|
" remote-viewer) in your PATH, starting without"
|
|
" SPICE support!")
|
|
else:
|
|
logging.info("NOTE: Consider using --spice for potential"
|
|
" performance improvements (2d acceleration)")
|
|
|
|
try:
|
|
process = pmb.helpers.run.user(args, command, background=run_spice)
|
|
|
|
# Launch SPICE client
|
|
if run_spice:
|
|
logging.info("Command: " + " ".join(command_spice))
|
|
pmb.helpers.run.user(args, command_spice)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
if process:
|
|
process.terminate()
|