Initial Commit
This commit is contained in:
Executable
+418
@@ -0,0 +1,418 @@
|
||||
#!/bin/bash
|
||||
# =====================================================================
|
||||
# The one place a qemu command line is built.
|
||||
#
|
||||
# WHY A SCRIPT AND NOT ExecStart=
|
||||
#
|
||||
# The isolation argument in TUNING.md Change 59 rests on the qemu argv
|
||||
# being a CONSTANT - specifically that -netdev never grows a guestfwd=,
|
||||
# smb=, tftp= or bootfile=. Those four are handled inside the qemu
|
||||
# process and never become host sockets, so neither the cgroup filter in
|
||||
# owuivm.service nor the nft table in 60-owuivm.nft can see them. No
|
||||
# filter can catch what no filter can observe; the only defence is that
|
||||
# the string is fixed and a test asserts it.
|
||||
#
|
||||
# So everything security-relevant is literal text in this file. What
|
||||
# varies between sessions is a session id and two paths, and those are
|
||||
# re-validated HERE as well as in vmd - this is the point where a name
|
||||
# becomes a path, and one check at the boundary is worth more than any
|
||||
# number upstream (the same reasoning as matlab.py's helper filenames).
|
||||
#
|
||||
# WHAT IS DELIBERATELY ABSENT
|
||||
#
|
||||
# -sandbox QEMU's own seccomp layer. MEASURED TO BE MUTUALLY
|
||||
# EXCLUSIVE with systemd's SystemCallFilter= on this
|
||||
# build: under the unit's filter, qemu reports "There
|
||||
# is no option group 'sandbox'" and exits 1, while the
|
||||
# identical command line outside it starts fine.
|
||||
# libseccomp IS linked - the two layers simply cannot
|
||||
# both install a filter.
|
||||
# systemd's is the one kept: it is a broader allowlist
|
||||
# (@system-service excludes @mount, @swap, @module,
|
||||
# @reboot and @obsolete outright), it covers the whole
|
||||
# process tree rather than just qemu, and it is
|
||||
# visible to `systemd-analyze security`. What is given
|
||||
# up is spawn=deny, and the vector that mattered there
|
||||
# - `migrate exec:` over QMP - is already closed by
|
||||
# vmd exposing typed verbs and never a raw command.
|
||||
# -runas `-sandbox elevateprivileges=deny` would have blocked
|
||||
# setuid anyway; systemd's User= drops the uid before
|
||||
# exec, which is stronger and needs no cooperation
|
||||
# from qemu.
|
||||
# -daemonize systemd supervises the process; a double fork would
|
||||
# hide the exit status and break ExecStopPost=.
|
||||
# usb passthrough, no host device reaches the guest except /dev/kvm,
|
||||
# vfio, -virtfs which DevicePolicy=closed enforces.
|
||||
# romfile the virtio-net PXE option ROM is dropped: a boot
|
||||
# path we never use is a parser we never need.
|
||||
# =====================================================================
|
||||
set -euo pipefail
|
||||
|
||||
die() { printf 'owuivm-launch: %s\n' "$*" >&2; exit 78; } # EX_CONFIG
|
||||
|
||||
MODE="${OWUIVM_MODE:-session}"
|
||||
SESSION="${OWUIVM_SESSION:-}"
|
||||
DISK="${OWUIVM_DISK:-}"
|
||||
VARS="${OWUIVM_VARS:-}"
|
||||
SHARE="${OWUIVM_SHARE:-}"
|
||||
# A saved machine to resume instead of booting one. See the warm-start
|
||||
# comment further down; empty for every ordinary boot.
|
||||
INCOMING="${OWUIVM_INCOMING:-}"
|
||||
MEM_MB="${OWUIVM_MEM_MB:-8192}"
|
||||
CPUS="${OWUIVM_CPUS:-8}"
|
||||
RUN="/run/owuivm/$SESSION"
|
||||
|
||||
[[ "$SESSION" =~ ^[0-9a-f]{32}$ ]] || die "session id is not 32 hex characters"
|
||||
[[ "$MEM_MB" =~ ^[0-9]{3,6}$ ]] || die "memory is not a plain number of MB"
|
||||
[[ "$CPUS" =~ ^[0-9]{1,2}$ ]] || die "cpu count is not a plain number"
|
||||
case "$MODE" in session|maint) ;; *) die "mode must be session or maint" ;; esac
|
||||
|
||||
# A session boots a throwaway overlay; maintenance boots a base disk that
|
||||
# somebody has deliberately unfrozen. The read-only case is enforced by
|
||||
# the file mode (base images are 0444 owuivmadm) rather than by this
|
||||
# flag, so a bug here cannot make a base writable.
|
||||
case "$MODE:$DISK" in
|
||||
session:@POOL_ROOT@/vmrun/*.qcow2) ;;
|
||||
maint:/var/lib/owuivm/base/*.qcow2) ;;
|
||||
*) die "disk path is not permitted for mode $MODE: $DISK" ;;
|
||||
esac
|
||||
[[ "$DISK" != *..* ]] || die "disk path contains .."
|
||||
[[ -f "$DISK" ]] || die "disk does not exist: $DISK"
|
||||
|
||||
[[ "$VARS" =~ ^/run/owuivm/[0-9a-f]{32}/OVMF_VARS\.fd$ ]] || die "vars path is not a session file"
|
||||
[[ -f "$VARS" ]] || die "vars file does not exist: $VARS"
|
||||
|
||||
# INSTALL MEDIA, MAINTENANCE MODE ONLY.
|
||||
#
|
||||
# The operator installs a base image by hand: they need the machine
|
||||
# booted from an ISO with a console attached, which is the same thing
|
||||
# maintenance already does plus one CD-ROM. A session must never get one
|
||||
# - it runs from a frozen disk that is already installed, and a bootable
|
||||
# CD in front of it is just another thing that could change what boots.
|
||||
#
|
||||
# The request is a file beside the base image saying which ISO, rather
|
||||
# than an environment variable, because the desktop tool that writes it
|
||||
# runs as the operator and cannot write into /run/owuivm. The path is
|
||||
# re-validated here anyway: this is where a name becomes a device.
|
||||
# Which image this is, from the disk's own name. A session's overlay is
|
||||
# named after its session id, so the name comes from the backing file.
|
||||
if [[ "$MODE" == "maint" ]]; then
|
||||
name=$(basename "$DISK" .qcow2)
|
||||
else
|
||||
name=$(basename "$(qemu-img info --output=json "$DISK" 2>/dev/null \
|
||||
| sed -n 's/.*"backing-filename": "\([^"]*\)".*/\1/p' | head -1)" .qcow2)
|
||||
fi
|
||||
[[ "$name" =~ ^(ubuntu|windows)$ ]] || die "cannot tell which image this is: '$name'"
|
||||
|
||||
# A FROZEN MAINTENANCE BOOT IS A SESSION'S BOOT WITH A SCREEN ATTACHED.
|
||||
#
|
||||
# The operator's main reason to start a base image is to check that it
|
||||
# comes up the way the agent will find it - and doing that must not be
|
||||
# able to change it. So the disk's own mode decides what is booted:
|
||||
# frozen means a throwaway overlay on top of the untouched base, exactly
|
||||
# what session.py builds for an agent, and unfrozen means the base
|
||||
# itself because somebody deliberately made it writable.
|
||||
#
|
||||
# The overlay path is DERIVED from a name this script has already
|
||||
# validated, never taken from the caller - which is why the path rule
|
||||
# above still only permits a base image here.
|
||||
# `-w` is deliberately NOT the test. It asks "can THIS process write
|
||||
# it", and root can write a 0444 file - so a maintenance boot started by
|
||||
# root would silently take the other branch and open the base directly.
|
||||
# Frozen is the owner write bit and nothing else, which is the same
|
||||
# definition the desktop tool, vmd and the prepare script all use.
|
||||
frozen_disk() { (( 8#$(stat -c %a "$1" 2>/dev/null || echo 600) & 8#200 )) && return 1 || return 0; }
|
||||
|
||||
if [[ "$MODE" == "maint" ]] && frozen_disk "$DISK"; then
|
||||
overlay="@POOL_ROOT@/vmrun/maint-$name.qcow2"
|
||||
rm -f "$overlay"
|
||||
qemu-img create -f qcow2 -b "$DISK" -F qcow2 "$overlay" >/dev/null \
|
||||
|| die "could not create the throwaway disk for a frozen boot: $overlay"
|
||||
# qemu-img asks for 0644 and the unit's umask clears the group write
|
||||
# bit it never requested, leaving a file the qemu user can read and
|
||||
# not write. Here qemu IS this user, but the mode is set explicitly so
|
||||
# the two callers of this line agree.
|
||||
chmod 0660 "$overlay" 2>/dev/null || true
|
||||
DISK="$overlay"
|
||||
fi
|
||||
|
||||
ISO=""
|
||||
if [[ "$MODE" == "maint" ]]; then
|
||||
marker="/var/lib/owuivm/base/$name.install"
|
||||
if [[ -f "$marker" ]]; then
|
||||
ISO=$(head -c 400 "$marker" | tr -d '\r\n')
|
||||
[[ "$ISO" =~ ^/var/lib/owuivm/iso/[A-Za-z0-9._-]+\.iso$ ]] \
|
||||
|| die "install media must be an .iso in /var/lib/owuivm/iso: $ISO"
|
||||
[[ "$ISO" != *..* ]] || die "install media path contains .."
|
||||
[[ -f "$ISO" ]] || die "install media does not exist: $ISO"
|
||||
fi
|
||||
fi
|
||||
|
||||
# WARM START: RESUME A SAVED MACHINE INSTEAD OF BOOTING ONE.
|
||||
#
|
||||
# A Windows boot costs ninety seconds and the agent cannot hide all of it
|
||||
# behind its planning turn. Resuming a saved machine is reading a file:
|
||||
# measured, 3.92 GB of state written in 14.6s, and the guest comes back
|
||||
# with its desktop drawn and its PowerShell already warm - which is the
|
||||
# ninety seconds AND the first-command cold start, both gone.
|
||||
#
|
||||
# The path is checked hard because a migration stream is executable input
|
||||
# in every sense that matters: it sets the machine's registers, its
|
||||
# device state and its memory. It may only ever be a file this host
|
||||
# wrote, in the directory only the image owner can write.
|
||||
if [[ -n "$INCOMING" ]]; then
|
||||
[[ "$INCOMING" =~ ^/var/lib/owuivm/warm/(ubuntu|windows)/state$ ]] \
|
||||
|| die "resume state is not one of this host's own: $INCOMING"
|
||||
[[ "$INCOMING" != *..* ]] || die "resume path contains .."
|
||||
[[ -f "$INCOMING" ]] || die "resume state does not exist: $INCOMING"
|
||||
fi
|
||||
|
||||
if [[ -n "$SHARE" ]]; then
|
||||
[[ "$SHARE" =~ ^@POOL_ROOT@/vmprojects/[a-z0-9][a-z0-9-]{1,30}$ ]] || die "share path is not a project dataset"
|
||||
[[ "$SHARE" != *..* ]] || die "share path contains .."
|
||||
[[ -d "$SHARE" ]] || die "share does not exist: $SHARE"
|
||||
fi
|
||||
|
||||
# WINDOWS 11 WILL NOT INSTALL WITHOUT SECURE BOOT AND A TPM.
|
||||
#
|
||||
# Its setup checks for both and stops with "This PC can't run Windows 11"
|
||||
# - which reads like a hardware problem rather than a missing device
|
||||
# model. Ubuntu needs neither and is given neither, so the two machines
|
||||
# differ only where they have to.
|
||||
#
|
||||
# `smm=on` is already on the machine type above and is a prerequisite:
|
||||
# without it the secure-boot firmware loads but cannot protect its own
|
||||
# variable store, and the enrolled keys are decorative.
|
||||
OVMF_CODE=/usr/share/OVMF/OVMF_CODE_4M.fd
|
||||
TPM_ARGS=()
|
||||
CPU=host
|
||||
RTC_BASE=utc
|
||||
|
||||
if [[ "$name" == "windows" ]]; then
|
||||
# HYPER-V ENLIGHTENMENTS: THE DIFFERENCE BETWEEN "SLOW" AND "FINE".
|
||||
#
|
||||
# Windows assumes it may be a Hyper-V guest and looks for the
|
||||
# paravirtual interfaces that say so. Finding none, it behaves like a
|
||||
# kernel on bare metal that has been lied to about its hardware: it
|
||||
# spins on locks a hypervisor could have yielded, takes a VM exit for
|
||||
# every timer tick and every inter-processor interrupt, and drives its
|
||||
# APIC through a trap on each access. Measured on this host, an idle
|
||||
# Windows 11 installer held qemu at 347% of one 700% quota while the
|
||||
# cgroup was neither short of memory (memory.events max 0) nor
|
||||
# meaningfully throttled (0.35s of throttling in three hours) - the
|
||||
# work was real, it was simply wasted.
|
||||
#
|
||||
# Each of these tells Windows that somebody is listening:
|
||||
# relaxed stop watchdogging a clock that a hypervisor may pause
|
||||
# vapic/avic APIC through memory instead of a VM exit per access
|
||||
# time a paravirtual reference clock
|
||||
# spinlocks give up the CPU after N spins instead of burning them
|
||||
# synic/stimer synthetic interrupt controller and timers, with
|
||||
# stimer-direct delivering them without a round trip
|
||||
# tlbflush/ipi one hypercall instead of one IPI per target CPU
|
||||
# vpindex/runtime/frequencies/reset the identifiers and the reset
|
||||
# path the above are defined in terms of
|
||||
#
|
||||
# Listed explicitly rather than `hv-passthrough`, which hands over
|
||||
# whatever the host kernel happens to support: these images are frozen
|
||||
# and are meant to behave the same after a host upgrade as before it.
|
||||
# All of them verified accepted by this host's KVM before being added.
|
||||
CPU="host,hv-relaxed,hv-vapic,hv-avic,hv-time,hv-spinlocks=0x1fff"
|
||||
CPU="$CPU,hv-vpindex,hv-runtime,hv-synic,hv-stimer,hv-stimer-direct"
|
||||
CPU="$CPU,hv-reset,hv-frequencies,hv-tlbflush,hv-ipi"
|
||||
|
||||
# Windows reads the RTC as local time and has done since DOS. With the
|
||||
# host at UTC that is not a performance problem, it is a clock several
|
||||
# hours wrong - which breaks TLS verification, and therefore breaks
|
||||
# `pip`, `winget` and every script that fetches anything, in a way that
|
||||
# reads as a network failure.
|
||||
RTC_BASE=localtime
|
||||
|
||||
OVMF_CODE=/usr/share/OVMF/OVMF_CODE_4M.secboot.fd
|
||||
[[ -f "$OVMF_CODE" ]] || die "secure-boot firmware is missing: $OVMF_CODE"
|
||||
|
||||
# swtpm is a separate process holding the TPM's state. It is started
|
||||
# here rather than as its own unit because it must be listening before
|
||||
# qemu opens the chardev, and it exits on its own when qemu goes away
|
||||
# (--terminate), so there is nothing left to reap.
|
||||
mkdir -p "$RUN/tpm"
|
||||
/usr/bin/swtpm socket --tpm2 --terminate \
|
||||
--tpmstate "dir=$RUN/tpm" \
|
||||
--ctrl "type=unixio,path=$RUN/swtpm.sock" \
|
||||
--log level=0 &
|
||||
for _ in $(seq 1 50); do [[ -S "$RUN/swtpm.sock" ]] && break; sleep 0.1; done
|
||||
[[ -S "$RUN/swtpm.sock" ]] || die "the software TPM did not start"
|
||||
TPM_ARGS=(
|
||||
-chardev "socket,id=chrtpm,path=$RUN/swtpm.sock"
|
||||
-tpmdev emulator,id=tpm0,chardev=chrtpm
|
||||
-device tpm-tis,tpmdev=tpm0
|
||||
)
|
||||
fi
|
||||
|
||||
args=(
|
||||
-name "owuivm-$MODE"
|
||||
-no-user-config
|
||||
-nodefaults
|
||||
-machine q35,accel=kvm,smm=on,vmport=off
|
||||
-cpu "$CPU"
|
||||
-smp "$CPUS"
|
||||
-m "$MEM_MB"
|
||||
-rtc "base=$RTC_BASE,driftfix=slew"
|
||||
-global kvm-pit.lost_tick_policy=delay
|
||||
|
||||
# Shared memory is a hard requirement of vhost-user-fs; it has to be
|
||||
# the machine's only memory backend, so it is set unconditionally
|
||||
# rather than only when a share is attached - a guest that boots with a
|
||||
# different memory topology depending on whether a project was passed
|
||||
# is a guest that behaves differently in testing than in use.
|
||||
-object "memory-backend-memfd,id=mem0,size=${MEM_MB}M,share=on"
|
||||
-numa node,memdev=mem0
|
||||
|
||||
-drive "if=pflash,format=raw,unit=0,readonly=on,file=$OVMF_CODE"
|
||||
-drive "if=pflash,format=raw,unit=1,file=$VARS"
|
||||
|
||||
# cache=none is O_DIRECT: this VM's disk I/O does not go through the
|
||||
# host's page cache at all. That is not a throughput tweak, it is what
|
||||
# keeps the cgroup accounting honest - see MemoryMax in the unit. The
|
||||
# guest's RAM is unreclaimable shmem, so any page cache charged to the
|
||||
# same cgroup is in a fight it cannot win, and the loser was the
|
||||
# installer media being read from disk over and over.
|
||||
# Verified O_DIRECT works on both filesystems in play here: ext4 for
|
||||
# the base images, and ZFS 2.4.1 for the session overlays.
|
||||
-drive "file=$DISK,if=none,id=disk0,format=qcow2,discard=unmap,cache=none,aio=native"
|
||||
-device virtio-blk-pci,drive=disk0,bootindex=0
|
||||
|
||||
# THE LINE THE WHOLE ISOLATION ARGUMENT RESTS ON.
|
||||
# user-mode networking: no tap, no bridge, no CAP_NET_ADMIN, and no
|
||||
# layer-2 presence anywhere on this host. Every guest packet leaves as
|
||||
# an ordinary socket owned by this process's uid, which is what makes
|
||||
# both IPAddressDeny= and `meta skuid owuivm` able to see it at all.
|
||||
# ipv6=off because the host has no global v6 and enumerating v6 ranges
|
||||
# nobody has yet is how a filter silently stops matching.
|
||||
-netdev user,id=net0,ipv6=off
|
||||
-device virtio-net-pci,netdev=net0,romfile=
|
||||
|
||||
# virtio-vga, not the plain VGA device: a desktop on the emulated VGA
|
||||
# MMIO framebuffer repaints through a slow path for every update, and
|
||||
# with llvmpipe doing the rendering anyway that is the difference
|
||||
# between usable and painful.
|
||||
#
|
||||
# Still 2D. NOT virtio-gpu-gl/virgl: QMP screendump reads the console
|
||||
# surface, and a device rendering through a GL context does not give it
|
||||
# one - the agent's whole GUI mode depends on that surface existing.
|
||||
# virtio-vga also keeps a VGA compatibility mode, which is what the
|
||||
# Windows installer uses before its drivers are loaded.
|
||||
-device virtio-vga,max_outputs=1
|
||||
-display none
|
||||
|
||||
# Absolute pointing, so a click at (x,y) lands at (x,y) whatever the
|
||||
# guest's pointer acceleration is doing.
|
||||
-device qemu-xhci,id=xhci
|
||||
-device usb-tablet,bus=xhci.0
|
||||
|
||||
# A ROOT PORT THAT IS USUALLY EMPTY, AND WHY IT IS ALWAYS THERE.
|
||||
#
|
||||
# q35's root complex is not hot-pluggable, so a machine resumed from a
|
||||
# saved state has nowhere to put the project share - and the share
|
||||
# cannot be in the saved state, because virtiofsd refuses migration
|
||||
# ("vhost-user backend lacks VHOST_USER_PROTOCOL_F_LOG_SHMFD").
|
||||
#
|
||||
# So the share lives behind this port on EVERY path: plugged in at
|
||||
# startup on a cold boot, hot-plugged into the same address on a warm
|
||||
# one. Same bus, same address, one device as far as the guest is
|
||||
# concerned - a drive letter that moves between sessions would be worse
|
||||
# than a slow boot.
|
||||
#
|
||||
# It is present even when no share is attached, because a saved state
|
||||
# and the machine that loads it must have exactly the same devices.
|
||||
-device pcie-root-port,id=vfsport,bus=pcie.0,addr=0x10,chassis=1,slot=1
|
||||
|
||||
# Two virtio-serial ports, no network involved in either: the standard
|
||||
# guest-agent channel, and a second one for the Windows broker that
|
||||
# runs in the interactive admin session (qemu-ga there is LocalSystem,
|
||||
# which is not the "normal elevated PowerShell" this is meant to give).
|
||||
-device virtio-serial-pci,id=vser0
|
||||
-chardev "socket,id=chga,path=$RUN/ga.sock,server=on,wait=off"
|
||||
-device virtserialport,chardev=chga,bus=vser0.0,name=org.qemu.guest_agent.0
|
||||
-chardev "socket,id=chbr,path=$RUN/broker.sock,server=on,wait=off"
|
||||
-device virtserialport,chardev=chbr,bus=vser0.0,name=com.owuitools.broker.0
|
||||
|
||||
# Control plane. 0600 inside a 0700 directory, because QMP is a
|
||||
# capability and not a debug port: it carries human-monitor-command,
|
||||
# migrate exec: and a screendump that writes a caller-chosen filename.
|
||||
# vmd never forwards a command to it, only typed verbs built from
|
||||
# constants.
|
||||
-qmp "unix:$RUN/qmp.sock,server=on,wait=off"
|
||||
)
|
||||
|
||||
# Secure boot is not just the firmware file: without this the variable
|
||||
# store is writable from inside the guest and the whole thing is theatre.
|
||||
if [[ "$name" == "windows" ]]; then
|
||||
args+=( -global driver=cfi.pflash01,property=secure,value=on )
|
||||
args+=( "${TPM_ARGS[@]}" )
|
||||
fi
|
||||
|
||||
if [[ -n "$SHARE" ]]; then
|
||||
args+=(
|
||||
-chardev "socket,id=chfs,path=$RUN/vfs.sock"
|
||||
-device vhost-user-fs-pci,chardev=chfs,tag=project,bus=vfsport,addr=0x0
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ -n "$INCOMING" ]]; then
|
||||
# `file:` rather than `exec:`. Measured: `exec:` fails with "Unable to
|
||||
# write to command: Broken pipe", because this unit's syscall filter
|
||||
# and NoNewPrivileges stop qemu spawning the helper - which is the
|
||||
# sandbox doing its job. `file:` opens a file and needs nobody.
|
||||
args+=( -incoming "file:$INCOMING" )
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "maint" ]]; then
|
||||
# THE CONSOLE IS A UNIX SOCKET, NOT A TCP PORT, AND THAT IS NOT STYLE.
|
||||
#
|
||||
# systemd's IPAddressDeny= filters INGRESS as well as egress, checking
|
||||
# the SOURCE address on the way in. This unit denies 127.0.0.0/8 -
|
||||
# which is what stops the guest reaching the host's own loopback
|
||||
# through SLIRP's 10.0.2.2 alias - and the source of a connection to a
|
||||
# loopback VNC port is, of course, 127.0.0.1. So the same rule that
|
||||
# protects the host silently dropped the operator's own console: the
|
||||
# port listened, `query-vnc` reported it enabled, and every client sat
|
||||
# there until it timed out. Measured both ways: with the deny list
|
||||
# cleared, the server greets with RFB 003.008 immediately.
|
||||
#
|
||||
# A unix socket is not IP, so the filter does not apply to it, and the
|
||||
# guest's egress policy stays exactly as strict as a session's. The
|
||||
# desktop tool relays it to a loopback port for the viewer, from a
|
||||
# process outside this cgroup.
|
||||
args+=( -vnc "unix:/run/owuivm-maint/vnc.sock" )
|
||||
if [[ -n "$ISO" ]]; then
|
||||
# The DISK is tried first and the media second, which is the opposite
|
||||
# of the obvious arrangement and is deliberate.
|
||||
#
|
||||
# UEFI walks its boot entries in order, so a blank or half-installed
|
||||
# disk has nothing to boot and falls through to the installer by
|
||||
# itself, while a finished install boots itself and ignores the media
|
||||
# still sitting in the drive. That makes an interrupted install
|
||||
# resumable without anyone having to decide whether it finished -
|
||||
# which is exactly the judgement that went wrong before: a partial
|
||||
# install had written 3.45 GB, was declared complete on size alone,
|
||||
# lost its media, and came up in an EFI shell with no way back.
|
||||
args+=(
|
||||
-drive "file=$ISO,if=none,id=cd0,media=cdrom,readonly=on,cache=none,aio=native"
|
||||
-device ide-cd,drive=cd0,bus=ide.0,bootindex=1
|
||||
)
|
||||
# A SECOND disc, for Windows only: its setup cannot see a virtio-blk
|
||||
# disk and will report that there are no drives to install to, which
|
||||
# looks like a broken VM rather than a missing driver. Point setup's
|
||||
# "Load driver" at this disc, amd64\w11.
|
||||
virtio=/var/lib/owuivm/iso/virtio-win.iso
|
||||
if [[ "$name" == "windows" && -f "$virtio" ]]; then
|
||||
args+=(
|
||||
-drive "file=$virtio,if=none,id=cd1,media=cdrom,readonly=on,cache=none,aio=native"
|
||||
-device ide-cd,drive=cd1,bus=ide.1
|
||||
)
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exec /usr/bin/qemu-system-x86_64 "${args[@]}"
|
||||
Reference in New Issue
Block a user