Skip to content

Instantly share code, notes, and snippets.

@mark-kubacki
Last active September 23, 2021 10:34
Show Gist options
  • Save mark-kubacki/18d34b52a61906cc24f94ec39c8d34ff to your computer and use it in GitHub Desktop.
Save mark-kubacki/18d34b52a61906cc24f94ec39c8d34ff to your computer and use it in GitHub Desktop.
Windows in Docker
# Image meant to host Windows in QEMU, hence we install QEMU
# and everything that is needed to get the networking for it working.
FROM ubuntu:latest
RUN apt-get -q update \
&& apt-get -y install \
kvm qemu-kvm bridge-utils psmisc \
&& apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN update-alternatives --install /usr/bin/qemu qemu /usr/bin/qemu-system-x86_64-spice 10
EXPOSE 3389 5900
# 3389 for Windows RDP
# 5900 for QEMU's SPICE
VOLUME /var/cache/media /var/vm
COPY kvm-bootstrap.sh /sbin/kvm-bootstrap.sh
ENTRYPOINT ["/sbin/kvm-bootstrap.sh"]
#!/bin/bash
# This file encapsulates the rather elaborate networking setup within Docker.
# Furthermore, absent any Windows VM file, starts the Windows installation from a provided ISO file.
set -eo pipefail
if [[ ! -z ${debug+x} ]]; then
set -x
fi
# Again, this is run within the Docker container.
# Usually runtimes will replace /dev with a standardized set of devices.
# That’s why we need to extend them:
if [[ ! -c "/dev/kvm" ]]; then
rm /dev/kvm || true
set +e
read -r NODNUM _ < <(grep '\<kvm\>' /proc/misc)
mknod /dev/kvm c 10 "${NODNUM}"
set -e
else
if ! dd if=/dev/kvm count=0 2>/dev/null; then
>&2 printf "Cannot open /dev/kvm - please run this in a privileged context.\n"
# see: /usr/include/sysexits.h: EX_OSFILE
exit 72
fi
fi
if [[ ! -z "${BRIDGE_IF+x}" ]]; then
printf "allow ${BRIDGE_IF}" >/etc/qemu/bridge.conf
# Make sure we have the tun device node
if [[ ! -c "/dev/net/tun" ]]; then
rm /dev/net/tun || true
mkdir -p /dev/net
set +e
read -r NODNUM _ < <(grep '\<tun\>' /proc/misc)
mknod /dev/net/tun c 10 "${NODNUM}"
set -e
fi
fi
# End of network plumbing.
# Start of setting up, or setting up and starting the actual VM.
# This script accepts some arguments (the first one must be "windows"; else this runs any supplied KVM),
# which are referred to by shortcuts such as:
# windows <spice address> <spice port> <spice password> <MAC> <nic-device> <name> <memory in MB> <boot ISO>
# example:
# windows ${PUBLIC_IPV4} 5900 geheim 52:54:00:xx:xx:xx macvtap0 "windows-1" $((16 * 8 * 1024)) Windows-10-threshold-2-take-1.iso
if (( $# >= 8 )) && [[ "$1" == "windows" ]]; then
# No prior Windows installation? Create a fresh image:
if [[ ! -e /var/vm/disks/"$7".img ]]; then
mkdir -p /var/vm/disks
chmod 0700 /var/vm/disks
qemu-img create -f qcow2 /var/vm/disks/"$7".img 80G
fi
read MAJOR MINOR < <(cat /sys/devices/virtual/net/$6/tap*/dev | tr ':' ' ')
mknod /dev/tap-vm c ${MAJOR} ${MINOR}
# -device virtio-balloon,id=balloon0,bus=pci.0,addr=0x7 \ Windows 10 won't start with this
if [[ -z ${ncores+x} ]]; then
: ${ncores:="4"}
if (( $(nproc) > 16 )); then
# MCC or HCC cpu(s)
if (( $(nproc) > 20)); then
let ncores="$[ $(nproc --ignore 4)/2 ]"
else
let ncores="$(nproc --ignore 2)"
fi
fi
fi
drives=()
drives+=("-drive" "file=/var/vm/disks/$7.img,if=virtio,index=0,media=disk")
if [[ -s /var/cache/media/virtio-win.iso ]]; then
drives+=("-drive" "file=/var/cache/media/virtio-win.iso,index=3,media=cdrom,readonly")
else
>&2 printf "Virtio drivers not found in: %s\n" "/var/cache/media/virtio-win.iso"
>&2 printf "Probably okay if this is no Windows, else download them from here:\n"
>&2 printf " https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso\n"
fi
# 9th argument is expected to be an ISO file to install a fresh Windows from.
if (( $# >= 9 )); then
if [[ -s "/var/cache/media/$9" ]]; then
drives+=("-drive" "file=/var/cache/media/$9,index=2,media=cdrom,readonly")
drives+=("-boot" "once=d")
else
>&2 printf "Ignored, because it is no file: %s\n" "$9"
fi
fi
spice=()
spice+=("-vga" "qxl")
spice+=("-spice" "addr=$2,port=$3,password=$4")
spice+=("-chardev" "spicevmc,id=vdagent,name=vdagent")
spice+=("-device" "virtserialport,chardev=vdagent,name=com.redhat.spice.0")
exec /usr/bin/qemu -enable-kvm -nographic -rtc base=utc \
-monitor unix:/run/kvm/"$7".monitor,server,nowait \
-cpu host -m $8 -smp ${ncores},sockets=1 -k de -usbdevice tablet \
-device virtio-serial \
${spice[*]} \
-net nic,model=virtio,macaddr=$5 -net tap,fd=3 3<>/dev/tap-vm \
${drives[*]} \
-name "$7"
else
exec /usr/bin/qemu -enable-kvm "$@"
fi
# ExecStart is the important part. It shows how to run the Docker container this all is for.
[Unit]
Description=KVM with Windows using macvtap
Wants=network-online.target docker.service
Requires=docker.service
After=network-online.target
[Service]
Restart=on-abort
RestartSec=30s
SuccessExitStatus=2
EnvironmentFile=/etc/environment
Environment=PATH=/opt/sbin:/opt/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=VM_NAME=windows-1
Environment=VM_MAC=52:54:00:12:34:56
Environment=VM_TAP=macvtap0
Environment=VM_PASSWORD=geheim
ExecStartPre=/bin/sh -c "[ -d /run/kvm ] || mkdir /run/kvm && chmod 0700 /run/kvm"
ExecStartPre=/bin/sh -c "/bin/ip link add link ext0 name ${VM_TAP} type macvtap && /bin/ip link set ${VM_TAP} address ${VM_MAC} up"
ExecStart=/bin/docker run -t --rm --privileged \
--net host \
--cgroup-parent machine.slice \
-v /var/cache/media:/var/cache/media \
-v /var/vm:/var/vm \
-v /run/kvm:/run/kvm \
wmark/docker-kvm \
windows ${PUBLIC_IPV4} 5900 "${VM_PASSWORD}" ${VM_MAC} ${VM_TAP} "${VM_NAME}" 8192 Windows10.iso
ExecReload=/bin/sh -c "echo system_reset | nc -U /run/kvm/${VM_NAME}.monitor"
ExecStop=/bin/sh -c "echo system_powerdown | nc -U /run/kvm/${VM_NAME}.monitor || rm /run/kvm/${VM_NAME}.monitor"
ExecStopPost=/bin/sh -c "/bin/ip link set dev ${VM_TAP} down && /bin/ip link del ${VM_TAP}"
[Install]
WantedBy=multi-user.target
@mark-kubacki
Copy link
Author

mark-kubacki commented Sep 23, 2021

QEMU/KVM using macvtap, in Docker

  • Request a »virtual MAC« for the guest, if you run this in a datacenter.
    If this is in your LAN, pick a unique MAC address (a random one will do). It’s for Windows.
  • Get the Remote Viewer here: http://www.spice-space.org/download.html
  • (optional) Prepare a Windows (or Linux, or BSD) ISO, to install Windows from.

Example: Windows 10

Prepare the host:

mkdir -p /var/vm /run/kvm
chmod 0700 /run/kvm

# /var/vm will contain your VMs. This command can file, and that’s no blocker. It disables CoW of the VM files for a better I/O performance:
chattr -R +C /var/vm

mkdir /var/cache/media
cd /var/cache/media
# Download, for example, a Windows10.iso or place your prepaered ISO in this folder.

apt-get -y install netcat-openbsd

Install the systemd unit file which takes care of starting, resetting, and stopping the KVM:

cp -a systemd-examples/windows-macvtap.service /etc/systemd/system/kvm-windows-1.service
systemctl daemon-reload

systemctl edit --full kvm-windows-1.service
# More than a single VM? change port 5900 to something else.
# Customize all "VM_*" values.
# 'ext0' on my host might be 'eth0' on yours - change that in the file accordingly.
# Replace ${PUBLIC_IPV4} by 127.0.0.1 or your host's IP address for the remote viewer endpoint.

And finally, start the KVM

systemctl start kvm-windows-1.service
systemctl enable kvm-windows-1.service

…  and point the Remote Viewer to spice://<host ip>:5900. That’s it!

Hints for your custom ISO to install Windows

You will need to install Windows (or Linux, or BSD) if the virtual HDD is empty.

Connect to the VM, the virtio drivers for Windows—which you need to install for QEMU—will be available in the seconds virtual DVD drive.
Have the Windows installer load NetKVM first, even though it is no storage driver;
then viostor. For Windows 10 both are in subfolder 2k12R2/amd64.

Once the system is ready you can install all remaining drivers by right-clicking on the corresponding INF file.
Don't forget the guest agent!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment