Skip to content
/ osrun Public

Short-lived containerized Windows instances

Notifications You must be signed in to change notification settings

lg/osrun

Repository files navigation

osrun

A docker container to run Windows commands and processes. On first run it will generate a Windows 11 ISO and run a VM to install it and create a system snapshot. On subsequent runs it will use the existing cached VM image to run the command passed in. noVNC is available on port 8000 (when forwarded via Docker) to view the VM's display.

Usage

docker run -it --rm --device=/dev/kvm -v $(pwd)/cache:/cache \
  ghcr.io/lg/osrun 'dir "C:\Program Files"'
 Volume in drive C is Windows 11
 Volume Serial Number is F48D-7158

 Directory of C:\Program Files

08/06/2023  09:24 PM    <DIR>          .
05/06/2022  10:42 PM    <DIR>          Common Files
05/07/2022  12:38 AM    <DIR>          Internet Explorer
...
Usage: osrun [flags] '<command>'
Short-lived containerized Windows instances

  -h --help: Display this help
  -v --verbose: Verbose mode (default: false)

Install
  -k --keep: Keep the installation ISOs after successful provisioning (default: false)

Run
  -p --pause: Do not close the VM after the command finishes (default: false)
  -n --new-snapshot <name>: Generate a new snapshot after the command finishes
  -s --use-snapshot <name>: Restore from the specified snapshot (default: provisioned)

Building and running locally

# Build the container
docker build -t osrun .

# Install Windows and run a command
docker run -it --rm --device=/dev/kvm -v $(pwd)/cache:/cache osrun 'dir "C:\Program Files"'

Protips

  • Don't forget to mount the cache directory in Docker and passthrough kvm
  • Use single quotes around the run command to avoid shell expansion. There is no need for double backslashes in Windows paths. Ex: osrun 'dir "C:\Program Files"'.
  • Take advantage of noVNC, --verbose and --pause to debug installation/execution
  • You can inspect the container state using docker exec -it <container-id> ash.
  • You can enter the QEMU Monitor using docker exec -it <container-id> socat tcp:127.0.0.1:55556 readline or just socat tcp:127.0.0.1:55556 readline locally if you forwarded the port.

Details

This container uses QEMU to run a Windows 11 VM. Windows 11 is built with the file list from UUP dump and files are downloaded directly from Microsoft's Windows Update servers. The UUP dump script generates a Windows ISO into which we then add an autounattend.xml script to start the installation automation. To keep the resultant VM small and fast we remove a lot of the default Windows components and services including Windows Defender, Windows Update, Edge, most default apps, and also disable things like paging, sleep and hibernation, plus the hard drive is compressed and trimmed to be <10GB. This process is done by the files in the win11-init directory. This image and VM state is then snapshotted when the system is stable and is saved to a cache directory so that subsequent runs start quickly. On a reasonably modern machine the installation process takes about 20 minutes end-to-end and runs take about 3-4 seconds for simple commands like dir.

flowchart LR
  subgraph Z["Installer preparation"]
    A["Windows Setup file-list assembled"]
    -->
    A1["UUPDump script generates Setup ISO"]
    -->
    A2["autounattend.xml injected into ISO"]
    --"win11-clean.iso artifact prepared"-->
    A3["QEMU mounts ISO and starts VM"]
  end

  subgraph ZA["Installation process"]
    B-1["noVNC started on port 8000 for debugging"]
    -->
    B["Setup runs autounattend.xml"]
    --Artifact saved: /cache/win11-step0-HASH.qcow2-->
    B1["boot-0.ps1 as NT AUTHORITY/SYSTEM"]
    --Artifact saved: /cache/win11-step1-HASH.qcow2-->
    B2["boot-1.ps1 as Administrator"]
    --Artifact saved: /cache/win11-step2-HASH.qcow2-->
    B3["QEMU Agent waits for boot"]
    --'provisioned' snapshot saved to /cache/win11.qcow2\nAll other artifacts removed-->
    B4["Ready to run"]
  end

  subgraph ZB["Running"]
    C["Command written to /tmp/qemu-status/run.cmd"]
    -->
    C0["noVNC started on port 8000 for debugging"]
    -->
    C1["QEMU snapshot restored"]
    --/tmp/qemu-status mounted as \\10.0.2.4\QEMU in Windows-->
    C2["Clock set to proper time using QEMU Agent"]
    --inotify triggered on /tmp/qemu-status/done-->
    C3["Command executed using QEMU Agent"]
    --/tmp/qemu-status/out.txt tailed for output \nuntil inotify triggered on /tmp/qemu-status/done-->
    C4["New snapshot optionally created via QEMU Monitor"]
    -->
    C5["Exit code returned"]
  end

  Z-->ZA
  ZA-->ZB
Loading

Communication between the QEMU VM and the Docker container is done via the QEMU Agent and a QEMU-started Samba server (available in the host container in /tmp/qemu-status or in the VM in \\10.0.2.4\qemu). During installation and execution, multiple debugging services are started (you'll need to forward these ports using Docker if you want to use them outside the container):

  • a noVNC HTTP server is started on port 8000 to view the VM's display,
  • the raw QEMU-run VNC server is also available on port 5950 (not compatible with Apple Screen Sharing) if you don't prefer noVNC,
  • the QEMU Monitor (ie. command-line interface) is available on port 55556 (supported commands are here), and
  • the QEMU Guest Agent is available on port 44444 (its JSON protocol is here).
flowchart LR
  subgraph "Host machine"
    subgraph "Docker Container"
      subgraph "QEMU VM"
        A[["\\10.0.2.4\qemu"]]
        D["virtio display"]
        I["QEMU Agent"]
        A<--When installing---O[["\\10.0.2.4\qemu\status.txt"]]
        A<--When running---P[["\\10.0.2.4\run.cmd<br/>\\10.0.2.4\qemu\out.txt<br/>\\10.0.2.4\qemu\done"]]
      end

      A<-->B[["/tmp/qemu-status"]]
      D-->E["QEMU VNC server"]
      G["QEMU Monitor"]
      K[["/cache"]]
      L[["/win11-init/*.ps1"]]-.Mounted on Install.->A
      E-.Port 5950.->R["HTTP server w/ websockets proxy"]
    end


    R-.Port 8000.->Q["noVNC HTTP frontend"]
    E-.Port 5950.->F["VNC client"]
    G-.Port 55556.->H["QEMU Monitor client"]
    I-.Port 44444.->J["QEMU Agent client"]
    K--Docker Volume-->M[["Directory"]]
  end
Loading

The --new-snapshot and --use-snapshot flags

While the final image will always have a provisioned snapshot, you can create new snapshots from the VM end-state of your command using the --new-snapshot <name> flag. You can then use this snapshot for subsequent runs using the --use-snapshot <name> flag. This is useful if you need to change the VM's configuration or install a tool on top of the base provisioned snapshot. Behind the scenes this uses the savevm and loadvm commands on the QEMU Monitor protocol which snapshots memory and disk state into the main qcow2 image.

As an example (in order):

  1. osrun --new-snapshot greeted 'mkdir C:\hello'

    flowchart LR
      A["'provisioned' snapshot restored"]
      -->B["mkdir executed"]
      -->C["New 'greeted' snapshot created"]
    
    Loading
  2. osrun --use-snapshot greeted 'dir C:\'

    flowchart LR
      A["'greeted' snapshot restored"]
      -->B["dir executed, will display the 'hello' directory"]
    
    Loading
  3. osrun 'dir C:\'

    flowchart LR
      A["'provisioned' snapshot restored"]
      -->B["dir executed, will not display the 'hello' directory"]
    
    Loading