VM Image Creation¶
ADARE creates QEMU/KVM virtual machines with automated OS installation. The
adare vm create command handles ISO download, unattended install, disk
provisioning, and ADARE agent setup in a single step.
This guide covers when and how to create custom VM images for your research, from quick-start commands through advanced profile customization to registering the finished VM as an ADARE environment.
When and Why to Create Custom VMs¶
The pre-built ADARE images cover common configurations, but custom VMs are needed when:
Specific OS versions – your research targets a particular build number, service pack, or point release not available as a pre-built image.
Specific software versions – you need a known version of an application (e.g., Firefox 102 ESR, Office 2019) pre-installed for consistent baselines.
Custom configurations – your experiment requires non-default OS settings, group policies, locale, or timezone configurations.
Non-supported distributions – you are working with a Linux distribution or Windows variant that ADARE does not ship a profile for.
Forensic baselines – you need a clean, minimal OS install with nothing extra installed so that every artifact in the diff is attributable to your experiment.
Quick Start¶
Linux (fully automated)¶
# Ubuntu 24.04 -- downloads ISO, installs unattended, pre-installs ADARE agent
adare vm create ubuntu2404
# Ubuntu 22.04 with custom name and larger disk
adare vm create ubuntu2204 --name my-ubuntu --disk-size 100G --ram 8192
# Bare install -- OS only, no guest tools or Python
adare vm create ubuntu2404 --setup bare
# Base install -- guest tools only, no Python environment
adare vm create ubuntu2404 --setup base
Windows (user-supplied ISO)¶
# Windows 11 -- requires a Windows ISO from Microsoft
adare vm create windows11 --iso /path/to/Win11.iso
# Windows 10
adare vm create windows10 --iso /path/to/Win10.iso
# Windows 11 ARM64 on Apple Silicon
adare vm create windows11arm64 --iso /path/to/Win11_ARM64.iso
# Or use --arch to override any profile's architecture
adare vm create windows11 --arch aarch64 --iso /path/to/Win11_ARM64.iso
Manual ISO install¶
For OSes without a built-in unattended template, use a custom profile with
install_mode: manual and pass your ISO:
adare vm create my-custom-os --iso /path/to/installer.iso
The VM boots the live ISO in a window and waits for you to click through the graphical installer. (Manual mode drives QEMU directly and writes no seed file.)
GUI-automated installation (record once, replay deterministically)¶
Some distributions ship the Calamares GUI installer — most notably
Kubuntu 24.04+, plus Mint, Pop!_OS and elementary — which has no
answer-file mechanism (no Subiquity/preseed/kickstart/AutoYaST). Instead of
clicking through it by hand, install_mode: gui-auto drives the installer’s
screen directly:
adare vm create kubuntu2404 --iso kubuntu-24.04-desktop-amd64.iso
How it works — record once, then replay.
First run (record, needs a vLLM endpoint). A vision-LLM agent runs a
perceive → decide → actloop: it screenshots the installer, the model decides the next action (click / type / key / scroll / wait / done), and the action is executed over QEMU’s host-side QMP engine (no guest agent). As it acts it records a reusable ADARE playbook — per click it saves an image crop (animage:target the CV engine can re-find) plus the model’s natural-language description. Output: the installedqcow2, a baked environment YAML, the generatedgui_<distro>.play.yaml(+ crop images), and a screenshot-illustratedinstall_report.md.Later runs (replay, no LLM). The generated playbook replays through ADARE’s ordinary CV/OCR engine (
ActionExecutor+MCPTargetResolver) — deterministic and LLM-free. If a step’s target no longer matches (e.g. a new point release moved a button), replay falls back to the vision model to re-locate it, clicks it, and re-crops the target so the playbook stays current (self-heal).
The generated .play.yaml is a first-class ADARE playbook: hand-editable,
shareable, and usable in experiments. A validated playbook can be shipped in
the package templates so most users never trigger a record run.
When to use which route. Prefer gui-auto when you want a faithful
native Calamares install automated end-to-end. Alternatives: build an Ubuntu
Server autoinstall + kubuntu-desktop (robust, but not a native Calamares
install), or install once by hand and capture a disk image (faithful, but
semi-manual and needs BYO hosting).
vLLM setup (record / self-heal only). Serve a grounding-capable vision model (Qwen2-VL / UI-TARS / Molmo-class) over an OpenAI-compatible endpoint and point ADARE at it:
export ADARE_VLLM_BASE_URL=http://localhost:8000/v1
export ADARE_VLLM_MODEL=Qwen/Qwen2-VL-7B-Instruct
# If your model returns 0..1000 normalized coordinates instead of pixels:
export ADARE_VLLM_COORD_SPACE=normalized_1000
Pure replay needs no endpoint. Budgets bound the record run
(ADARE_GUI_AGENT_MAX_STEPS, ADARE_GUI_AGENT_STALL_LIMIT,
ADARE_GUI_AGENT_WALL_CLOCK_SECONDS).
Using Ollama Cloud. The client is a plain OpenAI-compatible caller, so an Ollama Cloud model works with no code change — point it at the cloud endpoint and use a grounding model:
export ADARE_VLLM_BASE_URL=https://ollama.com/v1
export ADARE_VLLM_API_KEY=<key from ollama.com/settings/keys>
export ADARE_VLLM_MODEL=gemma4:31b
export ADARE_VLLM_COORD_SPACE=normalized_1000 # coord convention varies by model
Ollama Cloud retired the qwen3-vl GUI/computer-use line in 2026-06;
gemma4:31b is the current cloud vision model. It is a general multimodal
model (not coordinate-tuned), so keep --ground on — LocateAnything localizes
each click from the model’s textual target description. Verify everything up
front with the preflight, which pings the endpoint and auto-detects the
coordinate convention:
adare vm gui-doctor
Driving an existing environment / authoring experiments. The same agent can
drive an already-installed environment to author reusable automation. Bring the
environment up once, then hand the agent a goal (add --out to record a
playbook):
adare dev start -e <environment> # boots the VM + CV server, keeps them alive
adare dev agent --goal "open the Files app and navigate to Documents" \
--out experiments/files.play.yaml # drives the VM; records a playbook
The recorded .play.yaml is an ordinary ADARE playbook. Replay it
deterministically (no LLM) via adare dev playbook <session> -f files.play.yaml
or the normal engine with adare experiment run <exp> -e <env> --gui-mode host.
A raw drive (no --out) needs only the VLM; recording a playbook whose steps
use image/text targets also relies on the CV server (already up in a dev session).
Optional: precise icon grounding (LocateAnything). By default each recorded click stores a fixed ~220×90 crop around the model’s click point. If you run the standalone LocateAnything grounding sidecar and point the agent at it, each click is grounded to the element’s true bounding box and the recorded image target is the tight icon crop instead — better centred on the element (the recorded playbook still replays deterministically via the CV matcher, no model needed):
# sidecar wraps the locate-anything-cli binary; no VLM deps enter the adare package
LA_CLI_BIN=/path/to/locate-anything-cli LA_MODEL=/path/to/model.gguf \
python3 scripts/locate_anything_sidecar.py --port 13111
export ADARE_LOCATE_URL=http://127.0.0.1:13111 # enables grounding for `dev agent`
A tight crop is more precise but less distinctive to the CV matcher: a small, generic glyph (e.g. a bare document icon) can collide with near-duplicate UI (a full trash bin). Grounding is best-effort — a miss falls back to the fixed crop, so a run never breaks when the sidecar is down.
Goal / acceptance spec. The record-run input is a high-level goal, not a
per-screen script, in gui_<distro>.yaml (bundled, or overridden in
~/.adare/vm-templates/):
goal: >
Install Kubuntu to the whole disk (erase it). Create user adare /
password adare. Accept sensible defaults and reboot when finished.
hints:
- "Choose 'Erase disk' — not 'Install alongside'."
acceptance:
min_disk_bytes: 5000000000
visual:
- "a Kubuntu/KDE SDDM login or desktop for user adare is shown"
The stem is derived from the profile name with trailing digits stripped
(kubuntu2404 → kubuntu), so all versions of a distribution share one
goal file and one cached gui_<stem>.play.yaml by default. When two versions
ship different installers — Kubuntu 20.04/22.04 run ubiquity, 24.04 runs
Calamares — pin the version by setting template: to the bare stem in the
profile (the loader prepends gui_), as kubuntu2404.yml does with
template: kubuntu2404 → gui_kubuntu2404.yaml +
gui_kubuntu2404.play.yaml. Without it, a Calamares recording would be
replayed against ubiquity.
The acceptance block is the single place “what success looks like” lives:
after the installed disk reboots, ADARE runs acceptance checks (a visual
check via the model plus structural checks — domain running, disk grew) and
fails the build (non-zero exit) if they do not pass.
Limitations & safety. The record run is non-deterministic and needs a
capable grounding model; step / stall / wall-clock budgets bound it. Disk
partitioning during install is destructive but sandboxed — the blast radius is
only the throwaway VM disk. Replay is deterministic; self-heal recovers from
minor drift but a heavily redesigned installer may need --relearn.
Scripted GUI installation (gui-script) — deterministic, no model¶
install_mode: gui-script replays a hand-calibrated QMP playbook against the
installer’s GUI. It is the no-model sibling of gui-auto: no vision model,
no CV server, no ADARE_VLLM_* configuration, and the same playbook
produces the same disk on any host with QEMU.
adare vm create ubuntu1804 # ISO auto-downloads (iso_url is baked in)
adare vm create ubuntu2004
|
|
|
|---|---|---|
Targets a screen by |
image/text matching (CV), model on miss |
fixed pixel coordinates |
Needs a vision model |
yes to record or self-heal |
never |
Needs the CV server |
yes |
no |
Waits by |
CV target appearing |
|
Recovers from UI drift |
yes (self-heal, re-crop) |
no — coords must be recalibrated |
Use it to |
record a route for a new release |
replay a route that is already proven |
How the waiting works. The primitive that makes replay robust is
wait_stable (adare.hypervisor.qemu.vm_creator.qmp_replay): successive
screendump frames are diffed as raw bytes until the screen stops changing
for a settle period. There is no template matching, no OCR and no model, so a
step never waits on a fixed sleep that is too short on a slow host or wastefully
long on a fast one. During the copy phase the progress bar keeps the frame
changing, so the wait only returns once the installer is genuinely idle on its
“Installation Complete” dialog. A min: floor skips the static early-boot
plymouth screens, which are otherwise perfectly stable before any UI exists.
Two constraints, both learned by breaking them. The creator asserts the first and the playbooks depend on the second:
``-vga qxl``, never ``-vga std``. The std adapter’s tablet applies a 2x coordinate scaling, so absolute clicks land at double the intended offset and the right half of the screen is simply unreachable.
Click buttons with ``tap``, not the keyboard. ubiquity does not reliably hold keyboard focus at live-session start, and its timezone screen traps focus in the city-entry field. A mouse click both focuses the window and activates the button.
Playbooks. They live beside the other templates as
qmpinstall_<stem>.yaml (override in ~/.adare/vm-templates/) and are
resolved by the same stem lookup gui-auto uses, so template: ubuntu1804
keeps per-version isolation — 18.04’s and 20.04’s ubiquity differ visually and
must not share a recording. Steps are key / type / tap / wait /
wait_stable / shot:
vm:
ram_mb: 4096
cpus: 4
disk_size: "60G"
vga: qxl
firmware: bios
frame: # the frame the tap coords were calibrated at
width: 1024
height: 768
install:
- {action: wait_stable, settle: 6, timeout: 360, min: 120, shot: welcome}
- {action: tap, coords: [763, 434, 1024, 768], note: "click 'Install Ubuntu'"}
- {action: type, text: "adare"}
- {action: key, keys: [tab], repeat: 2}
reboot_from_disk: true
verify:
- {action: wait_stable, settle: 8, timeout: 240, min: 45, shot: login}
Every tap carries the frame its coordinates belong to, and the creator
rejects a playbook whose coords disagree with the declared frame or fall
outside it. A coordinate recorded at another resolution otherwise mis-clicks
silently, which is far more expensive to diagnose after a 40-minute install. A
screenshot is written per step (<vm>_gui-script/) and stays on by default —
it is the only useful debugging aid when a click lands wrong.
Calibrating a new playbook. scripts/gui-install/ is the authoring tool
and shares this playbook format. Boot the installer with --keep-running and
poke at it interactively to read coordinates off the screen:
cd scripts/gui-install
./gui_install.py playbooks/ubuntu2004-desktop.yaml \
--iso ubuntu-20.04.6-desktop-amd64.iso --vm-dir /vms --keep-running
./qmp_drive.py --sock /tmp/gui-install/qmp.sock shot /tmp/now.png
./qmp_drive.py --sock /tmp/gui-install/qmp.sock tap 924 566 1024 768
Once the coordinates are proven, copy the playbook to
adare/adare/hypervisor/qemu/vm_creator/templates/qmpinstall_<stem>.yaml and
flip the profile to install_mode: gui-script.
Limitations. Coordinate-fragile by construction: a different guest
resolution silently mis-clicks, and a redesigned installer needs recalibration
(there is no self-heal). Use gui-auto to record a route for a release nobody
has coordinates for, and gui-script to replay one that is proven.
Warning
The 18.04 / 20.04 playbooks are x86_64 desktop installs and have not been replayed by us on this Apple Silicon host — x86_64 guests cannot be built there (no TCG fallback). See Replicating on x86_64.
Options¶
Option |
Description |
|---|---|
|
Path to OS installer ISO (required for Windows and manual profiles) |
|
VM name (auto-generated as |
|
Disk image size, e.g. |
|
RAM in megabytes (default from OS profile) |
|
CPU core count (default: half of host cores, clamped 2–8) |
|
Setup level: |
|
Deprecated alias for |
|
Boot the VM after automated install for manual customization |
|
Overwrite an existing disk image with the same name |
|
Directory for the disk image (default: |
|
Override CPU architecture: |
|
Environment file name (defaults to VM name) |
|
Emit a declarative recipe environment (built on load) instead of a baked disk. Default: recipe for Windows, baked for Linux. See Recipe Environments. |
|
GUI-auto only: record a fresh playbook with the vision agent even if a cached one exists. |
|
GUI-auto only: discard the cached playbook and re-record from scratch. |
|
GUI-auto only: show the VM window while the agent drives the installer (and leave it up on failure for inspection). |
|
GUI-auto only: explicit goal/spec template name (default: |
Interrupting a build¶
Ctrl-C and kill <pid> both abort a running install cleanly: the QEMU child
is terminated (escalating to SIGKILL after 30 s) before adare exits, and
the partial disk is cleaned up.
This matters because the installer VM is started with Popen and nothing in the
OS ties its lifetime to ours. Before SIGTERM was handled, kill-ing a build
— which is what scripted cleanup usually does — left a full installer VM running
with ppid=1, still appending to its -serial install log. Two such orphans
once wrote to the same log for ~15 hours, interleaving output from different
installs and making the failure they were supposed to document unreadable.
Only SIGKILL on the adare process itself can still orphan the VM; there is
no in-process defence against that. If it happens, find the stray process with
pgrep -f qemu-system and check for ppid=1:
ps -o pid,ppid,etime,command -p $(pgrep -f qemu-system-aarch64 | tr '\n' ',')
Two QEMUs sharing one -serial file: path is the signature — kill them, and
discard the disk and any log they both wrote to, since neither is a coherent
record of a single install.
Setup Levels¶
Each level is cumulative – it includes everything from the levels below it.
Level |
What it adds |
Use case |
|---|---|---|
|
OS + user + basic config (autologin, UAC/sleep disabled) |
Forensic baselines, custom provisioning |
|
Guest tools (see below) |
VMs managed externally without Python |
|
Python environment (Miniforge3 + |
Standard ADARE experiments |
|
Not implemented – the CLI rejects |
Documented placeholder only |
What ``full`` actually bakes:
Linux and Windows x86_64 – Miniforge3 plus a
pyadareconda environmentWindows ARM64 – plain CPython 3.11 (no conda; there is no Miniforge build for Windows on ARM64), which is by design
``gui-auto`` and ``manual`` installs – no Python environment at all; the level therefore has little effect for those profiles
Guest tools per platform:
Linux –
qemu-guest-agentWindows x86_64 –
virtio-win-guest-tools.exe(VirtIO drivers, QGA, SPICE) + firewall rule for port 18765Windows ARM64 (UTM) – UTM guest tools + firewall rule for port 18765
Conda vs. system Python is never a create-time choice. The guest’s Python stack is
auto-detected at run time (a baked conda env is used if present, otherwise the system
interpreter, with a create-the-env recovery path). Likewise the adarevm/adarelib
wheels are not installed during vm create – they install themselves on the first
experiment or dev-session start.
Recipe Environments¶
An ADARE environment can be defined in one of two ways:
Baked disk (Linux default) –
adare vm createbuilds aqcow2disk now and the environment references that frozen artifact. Integrity is anchored on the disk’s SHA256 (Vm.hash). This is the historical model and is unchanged.Recipe (Windows default) – the environment is defined declaratively by its build inputs: an OS profile, a user-supplied ISO plus its expected
iso_sha256, an optional unattended-install template, and build params. The disk is built on load and cached. Integrity is anchored on the inputs (“same inputs -> forensically equivalent system”), because OS installs are never bit-reproducible.
Why recipes for Windows¶
Windows evaluation editions and activation expire. A baked Windows disk cannot
be refreshed without becoming a different artifact. With a recipe you simply
drop in a fresh ISO and rebuild – the environment definition (profile + params)
is unchanged, only the ISO and its iso_sha256 move forward. Because the ISO
is part of the integrity identity, a new ISO is always a new environment,
never a silent in-place refresh, so historical results stay reproducible.
Recipe environment file¶
adare vm create windows11 --iso /path/to/Win11.iso emits a recipe
environment instead of building immediately:
vm_type: recipe
hypervisor: qemu
recipe:
profile: windows11 # resolves via the OS profile catalog
iso: /path/to/Win11.iso # user-supplied installer ISO
iso_sha256: "abc123..." # expected SHA256 of the ISO (hard-checked)
params:
setup_level: 2 # 0=bare, 1=base, 2=full
disk_size: 80G
ram_mb: 16384
# Build-time provisioning: run ONCE while the disk is built (see below).
# provision:
# - {name: my-tool, command: "msiexec /i ... /qn", timeout_minutes: 30}
# Per-RUN steps, unchanged: these execute inside every experiment run. Use
# them only for work that must be redone each run -- installing software here
# writes Prefetch/registry/MFT entries into the artifact set under
# measurement, which is a forensic contamination. For software installs, use
# `recipe.provision` above instead.
# postsetupinstallations:
# - {name: my-tool, command: "...", description: "..."}
Build lifecycle¶
On adare environment load the recipe flow:
Enforces the ISO-source contract for a consumer (see Consumer-supplied (BYO) ISOs – Windows only below), resolves the ISO to a local file, and verifies
hash(iso) == iso_sha256– hard-fails on mismatch.Expands and validates
recipe.provision, and checks free disk space. Every cheap failure happens here, before any multi-hour install.Computes two hashes:
base_hash–iso_sha256+ the install template + the params/profile/post-install identity, withoutprovision.recipe_hash– the same, withprovision.
If a VM with that
recipe_hashalready exists (and its cached disk is present), reuses it – no rebuild (build once, cache).Otherwise builds in two stages:
Stage 1 – base disk. The normal creator machinery installs the OS into
~/.adare/qemu/cache/recipe-bases/{profile}-recipebase-{base_hash}.qcow2. Already there? Cache hit, and the OS install is skipped entirely.Stage 2 – provisioning. Only when
provisionis non-empty: boot a throwaway overlay of the base, run the steps through the guest agent, shut the guest down cleanly, and flatten the overlay into~/.adare/state/vms/{profile}-{recipe_hash}.qcow2.
Registers the result with
build_source='recipe'plus the recipe hash, ISO hash, and profile name for provenance.
With no provision steps base_hash == recipe_hash, Stage 2 is skipped, and the
result is identical to the pre-provisioning behaviour – including the disk’s name
and location.
Because the built disk is still hashed into Vm.hash, it remains
tamper-checked exactly like a baked disk. A recipe environment gains one
recovery advantage a baked disk lacks: if the cached disk goes missing, integrity
verification rebuilds it from the recipe instead of hard-failing.
Build-time provisioning¶
recipe.provision installs software once, while the disk is being built,
through the QEMU guest agent. This is not a convenience: for forensic work it is
the only correct place to do it. Installing an application writes Prefetch entries,
registry keys and MFT records – precisely the artifacts an experiment measures –
so an install that ran inside every run would contaminate its own results.
Contrast the two fields:
|
|
|
|---|---|---|
Runs |
Once, at disk build time |
Inside every experiment run |
Result |
Baked into the cached disk |
Re-applied to a fresh overlay each run |
Use for |
Installing the software under test |
Per-run setup that must not persist |
Transport |
QEMU guest agent ( |
The ADARE guest agent, at run time |
Both are folded into the recipe hash, so changing either yields a new environment identity.
Schema¶
Each provision entry is either a single command or a repeated group:
provision:
# Single command.
- name: boot-hardening
description: Survive a hard power-off without Startup Repair
shell: cmd # powershell | cmd | bash | auto (default)
command: bcdedit /set {default} recoveryenabled No
# Repeated group: every step replays once per for_each item, with
# {{ item }} substituted through name, description, command, verify, cwd
# and log_files.
- name: autopsy
description: Autopsy {{ item }}
for_each: ["4.4.0", "4.4.1"]
steps:
- name: autopsy-{{ item }}-download
command: curl.exe -L -f -o "C:\Windows\Temp\a-{{ item }}.msi" https://...
timeout_minutes: 20
- name: autopsy-{{ item }}-install
command: msiexec /i "C:\Windows\Temp\a-{{ item }}.msi" /qn /norestart
allow_exit_codes: [0, 3010] # 3010 = success, reboot required
verify: 'if (-not (Test-Path "C:\Program Files\Autopsy-{{ item }}")) { exit 1 }'
log_files: ['C:\Windows\Temp\a-{{ item }}-msi.log']
timeout_minutes: 45
reboot: false
Fields on a command: name (required, must be unique after expansion),
command (required), description, cwd, shell,
allow_exit_codes (default [0]), verify, log_files,
timeout_minutes (default 30), reboot. A group adds for_each and
steps; exactly one of command / steps per entry.
Three details worth knowing:
Success is the exit code, never stderr. A successful PowerShell command routinely writes CLIXML progress records to stderr.
``verify`` is how a step proves it worked. An installer reporting exit 0 is not evidence the files exist; a non-zero
verifyfails the build.``{{ item }}`` is strict. An unknown variable (
{{ version }}for{{ item }}) is a hard error, not an empty string – otherwise a typo yields a plausible-but-wrong disk.
shell matters more than it looks. cmd exists because PowerShell parses
{default} in bcdedit /set {default} ... as a script block, so such a
command has to reach cmd.exe verbatim.
Requires setup_level 1 (base) or higher: the QEMU guest agent is what
provisioning talks to, and level 0 (bare) does not install it.
Downloads inside a provision command are not digest-pinned¶
There is no sha256: field on a provision command, and this is a settled design
decision rather than a missing feature. iso_sha256 is the integrity boundary of
a recipe: the ISO is the one input a consumer supplies from their own copy, so
its digest is what makes two independent builds of the same recipe comparable.
A URL fetched by a provision command (curl.exe -L -f -o ... https://...) points
at an asset the upstream project controls. Pinning it would move the trust anchor
from “the OS install is reproducible” to “upstream never re-cuts a tag” – a
property ADARE cannot enforce – and would turn every legitimate upstream
re-release into an apparent tampering event, fixable only by editing the recipe,
which changes recipe_hash and forces a rebuild.
The accepted consequence: if an upstream release is retagged, the same recipe hash
can describe a different disk. What limits the damage is already in the model –
the recipe records the exact version list, verify asserts each install’s own
target exists, and log_files are pulled on failure, so a substituted installer
surfaces as a changed artifact set rather than a silent pass. Reproducing a
specific historical build therefore depends on upstream tag stability; the
published disk hash, not the recipe, is the durable record of what was built.
When a step fails¶
Provisioning aborts on the first failure and no environment is registered. A recipe hash must describe a disk where every step succeeded; a partially-provisioned disk that occupied that hash’s cache slot would be a disk whose contents do not match its identity.
On failure ADARE pulls each log_files entry from the guest to
~/.adare/qemu/build-logs/ and names the failing step, its exit code and its
output. The base disk is cached, so retrying is cheap:
# Reuse the cached OS install, re-run provisioning from scratch.
adare env load myenv.yml --reprovision
# Rebuild both stages.
adare env load myenv.yml --force
# Keep the failed overlay for post-mortem.
ADARE_KEEP_FAILED_PROVISION=1 adare env load myenv.yml --reprovision
Step-level resume is deliberately not offered: a half-installed MSI is not a clean
resume point, and pretending otherwise produces disks whose contents do not match
their hash. If one for_each item is at fault, bisect the list.
A guest that will not shut down cleanly is also a hard failure. Flattening a dirty NTFS volume produces a disk that boots into Startup Repair on the consumer’s machine – and the recipe hash would vouch for it.
Every command’s name, interpreter, full text, exit code, wall time, stdout and
stderr are recorded in ~/.adare/qemu/build-logs/provision-<hash>.log. That
file is the complete account of what was done to the disk, and is worth attaching
to a published artifact.
One builder per base disk¶
The rename is atomic for readers. It gives no exclusion between writers, and
the writers all aim at the same hash-derived name on purpose — so two concurrent
environment load calls over the same base used to start two QEMU processes
against one disk, one serial log and one UEFI variable store. Observed for real:
five concurrent installs on a single inode, each one’s --force unlinking the
disk the others were still installing into.
Stage 1 therefore holds an exclusive flock on
~/.adare/qemu/cache/recipe-bases/{profile}-recipebase-{hash}.lock — the cache
key, not the process, is the unit of exclusion. What that means in practice:
The second build waits and prints
Another ADARE process is already building this base disk, then reuses the finished base as a cache hit. It does not install a second copy: the cache check is re-run after the lock is acquired, precisely so a queue of waiters collapses into one install plus (N-1) hits.A cache hit takes no lock at all — that path only reads.
--forcequeues like any other builder but never accepts the re-check’s cache hit, because it means “this base is not to be trusted”; taking a base that another build had just published would turn the flag into a silent no-op.The lock is advisory and released by the kernel when the holder dies, so a killed build cannot wedge the cache. The lock file itself is empty and is never deleted: the exclusion belongs to the inode, so unlinking the file would silently end it.
Each attempt installs under its own
.partial-{pid}-{token}name, which also gives it its own_install.log(QEMU’s serial console), its own NVRAM and its own QMP socket — all three are derived from the disk name, and all three used to collide. The successful attempt’s log is published as{profile}-recipebase-{hash}_install.log; a failed attempt’s log is kept for post-mortem while its multi-GB partial disk is reclaimed.
This does not make concurrent unrelated builds serial — the lock is per base hash, so two different bases still build at the same time.
Consumer-supplied (BYO) ISOs – Windows only¶
A recipe is only shareable if the consumer can obtain its ISO. For Linux that is
easy: the ISO is freely redistributable, so recipe.iso is a published
http(s) URL and publishing requires one. For Windows it is not: Microsoft
installer media is licensed, not redistributable, and hosting it yourself is not an
option.
So a Windows recipe may instead name the file the consumer must already own:
recipe:
profile: windows11arm64
iso_name: Win11_25H2_English_Arm64_v2.iso
iso_sha256: 638aa2c8...adf0
iso_notes: |
Download from https://www.microsoft.com/software-download/windows11 --
"Windows 11 Arm64", English (International). Requires a valid licence.
Exactly one of iso / iso_name. iso_sha256 is required in both forms
and is the actual integrity boundary – the Windows-only restriction on BYO is a
quality rule about redistributability, not a security control.
iso_notes is plain text (never markdown or HTML) and is shown to a consumer who
does not have the file. Omit it and ADARE falls back to the OS profile’s own
iso_notes, so a consumer always gets a download pointer.
Supplying the ISO as a consumer¶
environment load searches these locations by filename, first existing file
wins:
--iso PATH– a file, or a directory to look inside.$ADARE_ISO_DIR/<iso_name>~/.adare/isos/<iso_name>the environment file’s own directory
~/.adare/qemu/cache/<iso_name>
cp Win11_25H2_English_Arm64_v2.iso ~/.adare/isos/
adare env load win11-autopsy-solr4.yml
# or, without moving anything:
adare env load win11-autopsy-solr4.yml --iso ~/Downloads/
ADARE never guesses: a single unrelated *.iso sitting in the search path is
not used, because silently building from the wrong ISO is the one failure that must
never be silent. If the file is absent the error names the required filename, its
sha256, the profile, the download pointer, and every path searched.
Converting an existing recipe¶
adare vm create can emit the BYO form directly with --byo-iso, and an
existing recipe with a local ISO path is converted in place:
adare vm create windows11arm64 --iso ~/ISO/Win11.iso --byo-iso
adare env recipe-byo win11-autopsy-solr4
The recipe hash does not change: how a consumer obtained the ISO is not a build input, so any disk already built stays a cache hit. Only the descriptor changes.
Choosing the mode¶
Use --recipe / --no-recipe to override the platform default:
# Linux, opt into a recipe environment (needs an ISO)
adare vm create ubuntu2404 --iso /path/to/ubuntu.iso --recipe
# Windows, force a baked disk instead of a recipe
adare vm create windows11 --iso /path/to/Win11.iso --no-recipe
Profile System¶
ADARE ships profiles for Ubuntu 20.04–26.04, Kubuntu 20.04–24.04 (both architectures), Debian, Kali, Fedora, RHEL rebuilds, openSUSE, Windows 10, Windows 11 and Windows 11 ARM64. You can add custom profiles for other distributions.
Listing profiles¶
adare os-profile list
Showing profile details¶
adare os-profile show ubuntu2404
Adding a custom profile¶
Create a YAML file (e.g. my-distro.yml):
name: my-distro
display_name: My Distro 1.0
platform: linux # 'linux' or 'windows'
distribution: ubuntu # distribution family
version: '1.0'
architecture: x86_64 # 'x86_64' or 'aarch64'
install_mode: auto # 'auto', 'manual', 'gui-auto', or 'gui-script'
# Optional -- omit for manual installs or when using --iso
iso_url: https://example.com/my-distro.iso
iso_sha256: abcdef...
iso_filename: my-distro.iso
# Kernel paths inside ISO (required for automated Linux installs)
kernel_path_in_iso: /casper/vmlinuz
initrd_path_in_iso: /casper/initrd
# Defaults
default_disk_size: 60G
default_ram_mb: 8192
default_cpus: 4
# UEFI / TPM
requires_uefi: false
requires_tpm: false
# Custom Jinja2 template (see Custom Templates below)
template: my_autoinstall.yaml
# Extra apt packages to install
extra_packages:
- htop
- vim
Then add it:
adare os-profile add my-distro.yml
Removing a custom profile¶
adare os-profile remove my-distro
YAML field reference¶
Field |
Required |
Description |
|---|---|---|
|
Yes |
Unique identifier used on the command line |
|
Yes |
|
|
Yes |
Distribution family ( |
|
Yes |
Version string |
|
No |
Human-readable name (defaults to |
|
No |
|
|
No |
|
|
No |
Jinja2 template filename for unattended install (empty = default lookup) |
|
No |
Direct download URL for the ISO |
|
No |
Expected SHA-256 hash of the ISO |
|
No |
Cache filename for the downloaded ISO |
|
No |
Path to vmlinuz inside ISO (Linux auto installs) |
|
No |
Path to initrd inside ISO (Linux auto installs) |
|
No |
Default disk size (e.g. |
|
No |
Default RAM in MB (default: 4096) |
|
No |
Default CPU count (0 = auto-detect) |
|
No |
Whether the OS needs UEFI firmware |
|
No |
Whether the OS needs a TPM device |
|
No |
List of additional packages to install |
|
No |
Installer family, i.e. how the rendered answer file reaches the installer:
|
|
No |
Kernel command line passed to QEMU’s |
|
No |
Volume label of the seed ISO attached as the second drive; the installer
auto-detects the answer file by it. |
|
No |
How the answer file reaches the guest. |
Installer families¶
|
Rendered file |
How the installer finds it |
|---|---|---|
|
|
cloud-init NoCloud auto-detects the |
|
|
Same NoCloud mechanism. |
|
|
debian-installer auto-loads |
|
|
Ubuntu / Kubuntu desktop ISOs. ubiquity has no labelled-drive
auto-detect: casper’s network-preseed script fetches the file from the
installer: ubiquity
seed_transport: http
kernel_cmdline: >-
automatic-ubiquity noprompt boot=casper console={console} ---
The labelled seed drive is still attached, because that is the
configuration the 18.04 install was validated against. It means the guest
has a second block device, so pin |
|
|
Anaconda reads it from the |
|
|
AutoYaST auto-loads it from |
|
(none) |
No answer file is written; the installer is driven by the vision agent
( |
Desktop guests on aarch64¶
Desktop ISO availability on arm64 is uneven, and it is what bounds the options:
Ubuntu publishes an arm64 desktop ISO from 24.04.3 onwards (
ubuntu-24.04.4-desktop-arm64.iso). Earlier LTS releases (22.04, 20.04) do not — those trees carry only server, ppc64el and s390x images.Kubuntu publishes
desktop-amd64exclusively, for every release. There is no arm64 Kubuntu desktop ISO to install.Fedora published no aarch64 Workstation Live image before release 42.
The rule ADARE follows:
x86_64 — install the real desktop ISO where one exists (Ubuntu Desktop, Kubuntu) through the
ubiquityfamily, or Calamares throughgui-autofor Kubuntu 24.04+.aarch64 — install the live-server ISO of the matching version through
subiquityand pull in the desktop metapackage (ubuntu-desktop-minimal/kubuntu-desktop). This is the route that produced the existing 24.04 ARM64 environments, and the only route available for Kubuntu and for Ubuntu 22.04 / 20.04 on arm64.
The two are not byte-identical installs, so the divergence is stated in the
profile’s display_name — e.g. “Kubuntu 22.04 (KDE Plasma on Ubuntu 22.04
Server base, ARM64)” — and therefore in the environment metadata of every
experiment run against it.
Warning
The x86_64 profiles ubuntu2004, kubuntu2004 and kubuntu2204
ship untested. They cannot be built on an Apple Silicon host: the guest
architecture is guarded for non-aarch64 guests and QEMU is invoked as
qemu-system-<host arch> with accel=hvf, and ADARE deliberately has no
TCG fallback. A green adare os-profile list row is not a verification —
treat these as recipes for replication on Intel/AMD hosts.
Paper-replication profiles¶
The profiles behind the case studies, and the ISO each one expects:
Profile |
Installer |
ISO to pass with |
|---|---|---|
|
subiquity |
|
|
subiquity |
|
|
subiquity |
|
|
subiquity |
|
|
subiquity |
|
|
subiquity |
|
|
kickstart |
|
|
kickstart |
|
|
gui-script |
auto-downloads |
|
gui-script |
auto-downloads |
|
ubiquity |
auto-downloads |
|
gui (Calamares) |
|
|
kickstart |
auto-downloads |
Both Fedora profiles install from Everything-netinst rather than the
Workstation Live ISO: Fedora 41 has no aarch64 live image at all, so netinst is
the only route that keeps 41 and 42 method-identical, and directly kernel-booting
live media would additionally need a per-release root=live:CDLABEL=.... Both
releases are EOL, so their metalink no longer resolves to a mirror — the profiles
pin inst.repo at dl.fedoraproject.org/pub/archive/..., which also serves
stage2. The shared kickstart template’s %post repoints dnf at the same
archive if the installed system’s metalink fails, then installs the ADARE extras
— which also covers live media, where Anaconda ignores %packages entirely.
Note
Ubuntu 20.04 (focal) needs two extra autoinstall keys, both in
autoinstall_ubuntu_focal.yaml / autoinstall_kubuntu_focal.yaml:
No ``packages:`` block. Focal’s curtin leaves
devptsunmounted inside/target, so apt’s pty logging fails with “Can not write log (Is /dev/pts mounted?) - posix_openpt (19: No such device)” and subiquity’s in-target package step exits 100 — after which subiquity drops to a rescue shell and the unattended install hangs until ADARE’s timeout. Settingapt: conf: 'Dpkg::Use-Pty "false";'does not help: curtin writes that to/etc/apt/apt.conf.d/94curtin-configand deletes it again before the package step runs. Focal therefore installs everything fromlate-commandswithapt-get install -y -o Dpkg::Use-Pty=false, the one place the setting survives. Jammy (22.04) hits the same bug, but only sometimes — builds from the identical template and ISO have completed with noposix_openptline at all, so a green build is not evidence of immunity. The jammy templates therefore take the focal route as well and install their package set fromlate-commands; noble (24.04) keepspackages:, and its rendered answer file is deliberately left unchanged.shutdown: poweroffworks on focal — a 20.04.**5** ISO carries subiquity snap rev 3704 (2022-era), not the 2020 original, because point releases refresh the installer.sizing-policy: allis accepted but ignored by it: the root LV still lands at half the PV (measured 28.5 GiB of 57 GiB on a 60 GB disk), which is ample for a desktop guest.
Every new template also declares error-commands that dump the curtin/apt
logs to /dev/console (captured into <vm>_install.log). Without it a
failed in-target package install reaches the host as nothing more than
returned non-zero exit status 100. Those commands deliberately do not
power the guest off: ADARE infers install success from QEMU exiting, so a
poweroff-on-error would present a half-installed disk as a finished build.
A <disk>_install.log scan for installer failure markers runs after every
Linux build for the same reason — QEMU exits 0 on SIGTERM and on a closed
QEMU window too.
Note
Ubuntu 22.04 / 24.04 are served by two different built-in templates, split
by architecture through their supports: blocks:
autoinstall_ubuntu_lts.yaml—ubuntu2204/ubuntu2404(x86_64). Installs Miniforge and creates thepyadareconda environment at setup level 2.autoinstall_ubuntu_lts_noconda.yaml—ubuntu2204arm64/ubuntu2404arm64. No conda: it clears pip’s PEP-668 guard so the harness can install theadarevmwheels into the system Python, and bakes in the standaloneuvbinary thatadare vm testneeds. This is the variant the paper’s aarch64 images are built from.
The two cannot both claim the same OS name — within one template directory that is a discovery error. Adding an aarch64 profile to the conda variant therefore means removing it from the no-conda one, and vice versa.
Warning
Fedora guests run with SELinux in permissive mode. ADARE drives Linux
guests through the QEMU guest agent, and Fedora confines that agent to the
virt_qemu_ga_t domain — confined so tightly that it cannot stat
/usr/bin/sudo. Probing a stock enforcing install through the agent gives
ls -l /usr/bin/sudo → Permission denied, command -v sudo → not found,
systemctl → Access denied, so every ADARE setup step fails even though
every package is installed. kickstart_fedora_workstation.yaml therefore
sets selinux --permissive.
Permissive, not disabled: files are still labelled and AVCs are still audited, so SELinux contexts and audit entries keep showing up in forensic diffs — only the denials stop. It remains a deviation from a stock Fedora install and should be stated wherever these environments are reported. Debian/Ubuntu guests are unaffected (no SELinux confinement of the agent).
kickstart_fedora_kde.yaml now carries the same line, and it is confirmed to
have taken effect: getenforce reports Permissive in-guest on both
fedora-41-kde and fedora-42-kde. kickstart_rhel_workstation.yaml
still does not have it.
Warning
Fedora KDE guests run firewalld, and the kickstart does not stop them.
services --disabled=firewalld does not take effect on the KDE spin:
measured on both built images, systemctl is-enabled firewalld reports
enabled and is-active reports active, with the public zone bound
to the external interface. The mechanism is not established, so do not assume
that line works — verify in-guest if you depend on it.
ADARE does not depend on it. The only inbound port a run needs is the adarevm WebSocket port, which is opened at run time in firewalld’s in-memory state only (see Guest Agent). Everything else is outbound (PyPI, the SLIRP SMB share) or not network at all (QGA rides a virtio-serial channel).
The line is kept rather than made to work, because a stock-as-shipped firewall is the better forensic baseline: the guest’s on-disk configuration stays untouched, so artifact diffs stay attributable to the experiment rather than to the image build.
Install timeout. One unattended install is allowed
ADARE_VM_INSTALL_TIMEOUT_MINUTES minutes (default 150) before it is
treated as hung and the half-written disk is discarded. It is a hang detector,
not a budget — a healthy install that runs long must not be killed. Measured on
an M-series host: an Ubuntu live-server autoinstall lands well inside an hour,
while a Fedora Workstation netinst needs longer (≈1900 RPMs fetched from the
archive, then their scriptlets, before %post even starts) and a
kubuntu-desktop build pulls the whole Plasma set over the network. Raise it
for a slow link:
ADARE_VM_INSTALL_TIMEOUT_MINUTES=240 adare vm create fedora42arm64 --iso ...
Register an environment under a name of your choosing with --env-name, which
is how the paper’s hyphenated environment names are produced:
adare vm create fedora42arm64 \
--iso ~/.adare/isos/Fedora-Everything-netinst-aarch64-42-1.1.iso \
--env-name fedora-42
Replicating on x86_64¶
Important
The aarch64 disks are not portable to x86_64. A qcow2 built by the
*arm64 profiles contains an ARM64 kernel and userland; there is no
conversion. Replicating on an x86 host means rebuilding from the x86
profiles, which produces a different (though method-identical) artefact.
Host requirements. Linux with KVM. ADARE selects the accelerator by host
OS: hvf on Darwin, otherwise kvm
(adare.hypervisor.qemu.vm_creator.qmp_utils, config/__init__.py).
Windows hosts are not covered — WHPX is never selected, so a Windows host
falls through to kvm and QEMU fails to start. Use Linux, or WSL2 with nested
virtualisation.
Each x86 profile bakes its iso_url + iso_sha256, so no --iso is
needed: the ISO is downloaded into the cache and hash-checked, and a mismatch
re-downloads rather than installing a wrong image. One command per environment:
# Ubuntu desktop, deterministic GUI replay (no vision model)
adare vm create ubuntu1804 --env-name ubuntu-1804
adare vm create ubuntu2004 --env-name ubuntu-2004
# Kubuntu desktop via ubiquity + an HTTP-served preseed
adare vm create kubuntu2004 --env-name kubuntu-2004
adare vm create kubuntu2204 --env-name kubuntu-2204
# Kubuntu 24.04 runs Calamares -> needs the vision agent (see gui-auto)
adare vm create kubuntu2404 --iso kubuntu-24.04.3-desktop-amd64.iso \
--env-name kubuntu-2404
# Fedora, Everything-netinst pinned to the archive mirror
adare vm create fedora41 --env-name fedora-41
adare vm create fedora42 --env-name fedora-42
# Ubuntu server-based x86 profiles (same method as the arm64 ones)
adare vm create ubuntu2204 --env-name ubuntu-2204
adare vm create ubuntu2404 --env-name ubuntu-2404
Then load and verify each one, sequentially — a concurrent build starves the booting guest badly enough to time out the guest agent:
adare env load ~/.adare/state/environments/<name>_*.yml -f
adare env verify <name> # installs the agent and performs a GUI click
Warning
The x86 profiles are unverified by us. This work was done on an Apple
Silicon host, which cannot build x86_64 guests at all (no TCG fallback), so
every x86 profile above is shipped on the strength of its template and — for
ubuntu1804 / ubuntu2004 — a playbook validated on someone else’s x86
host. Treat the first run on x86 as a bring-up, not a regression test.
Vendor URLs for EOL images also rot: focal has already moved to
old-releases, and Kubuntu 20.04.5 has already been pruned (only .6
remains). The baked SHA-256 turns that rot into a loud failure rather than a
silently wrong image, but a 404 still needs the URL refreshed.
The two warnings above apply to x86 exactly as they do to aarch64: Fedora
guests run with SELinux permissive, and Ubuntu 20.04 (focal) needs the
``-o Dpkg::Use-Pty=false`` workaround for its unmounted /dev/pts.
Note
Images built before the interface-matching fix boot slowly and need the
run-time network repair. Ubuntu/Kubuntu images whose autoinstall baked a
named interface (enp0s1) into netplan cannot configure their NIC at run
time, because ADARE gives it a different PCI address (enp0s31). Symptoms,
as measured on the eight paper-replication environments:
every boot pays the full 120 s
systemd-networkd-wait-onlinetimeout, soVM is readylands at 127-164 s instead of ~16 s;Kubuntu / Ubuntu 22.04 / 20.04 end up with no address at all, the
//10.0.2.4/qemumount fails, and file transfer degrades to QGA;Ubuntu 24.04 is rescued by NetworkManager one second after the timeout, so it passes but still pays the 120 s;
Fedora is unaffected (NetworkManager manages any unnamed device).
ADARE repairs this after boot (see Guest network repair), so these
images verify green as they are. Rebuilding them picks up the
match: {name: "e*"} seed and removes the 120 s penalty as well.
Custom Templates¶
ADARE uses Jinja2 templates for unattended installation configs:
Linux: autoinstall YAML (Ubuntu cloud-init)
Windows: Autounattend XML
Template search order¶
User template directory:
~/.adare/vm-templates/Built-in templates (shipped with ADARE)
A file in the user directory with the same name as a built-in template takes precedence, allowing you to override defaults without modifying ADARE source.
Available template variables¶
Linux (autoinstall YAML):
hostname– sanitized VM name (RFC 1123)password_hash– SHA-512 crypt hash for theadareuserminiforge_arch–x86_64oraarch64(for Miniforge download URL)setup_level– integer (0=bare, 1=base, 2=full); use{% if setup_level >= 1 %}conditionalsos_name– OS profile name being installed (e.g.ubuntu2204arm64)os_version– release string from the profile (e.g.22.04)distribution–ubuntu,fedora,debian, …architecture–x86_64oraarch64
The last four mirror the OS-profile fields and exist because one template
routinely serves several releases – autoinstall_ubuntu_lts.yaml covers
22.04 and 24.04 – and release-specific installer bugs then have to be worked
around inside it. Branch on os_version, never on the presence of some other
variable:
{%- set apt_pty_broken = os_version == '22.04' -%}
{%- if not apt_pty_broken %}
packages:
- ubuntu-desktop-minimal
{%- endif %}
Note the {%- / -%} whitespace control: without it, a conditional wrapped
around a block leaves stray blank lines in the rendered answer file for every
release, including the ones the branch was not meant to affect. #cloud-config
in particular must stay the first line.
Windows (Autounattend XML):
setup_level– integer (0=bare, 1=base, 2=full); use{% if setup_level >= 1 %}conditionalsproc_arch–amd64orarm64(forprocessorArchitectureattributes)driver_arch–amd64orARM64(for virtio-win driver paths)miniforge_arch–x86_64oraarch64(for Miniforge download URL)
Template metadata¶
Each Linux autoinstall template carries a self-describing metadata block as a
Jinja comment at the top of the file. The block declares which OS profiles the
template covers, so dropping a template into ~/.adare/vm-templates/ is
enough to register it – no Python edits required.
{# adare-template
schema: 1
id: my-ubuntu
description: Custom Ubuntu 24.04 with extra tooling
maintainer: yourname
revision: 2026-05-06
supports:
- ubuntu2404
- ubuntu2404arm64
#}
#cloud-config
autoinstall:
...
Field reference:
schema(required, integer) – Metadata schema version. Currently1. Loading a template with an unknown schema raises an explicit error.id(required, string) – Stable identifier surfaced inadare os-profile show. Independent of filename.description(string) – Human-readable summary.maintainer(string) – Name or handle responsible for the template.revision(string) – Free-form revision marker (date, version, etc.).supports(list of strings) – OS profile names this template applies to. Order does not matter. Within a single template directory, two templates cannot claim the same OS name; across directories, user templates override built-ins.
Because the block is a Jinja {# ... #} comment, it is stripped at render
time and never reaches cloud-init. The line #cloud-config remains the first
line of the rendered output.
Writing a custom template¶
Copy an existing template from the built-in directory as a starting point:
cp $(python -c "import adare.hypervisor.qemu.vm_creator.autoinstall as a; print(a.TEMPLATES_DIR)")/autoinstall_ubuntu_lts.yaml \ ~/.adare/vm-templates/my_autoinstall.yaml
Edit the template using the Jinja2 variables listed above.
Create a profile YAML with the
templatefield pointing to your file:name: my-ubuntu platform: linux distribution: ubuntu version: '24.04' template: my_autoinstall.yaml kernel_path_in_iso: /casper/vmlinuz initrd_path_in_iso: /casper/initrd
Add the profile and create the VM:
adare os-profile add my-ubuntu.yml adare vm create my-ubuntu
Interactive Mode¶
The --interactive flag adds a second phase after automated installation:
the finished VM boots from its disk so you can install additional software or
configure settings that are not covered by the unattended template.
adare vm create ubuntu2404 --interactive
What happens:
The automated install runs as usual (unattended, ISO + autoinstall).
After the install completes, QEMU boots the VM from the finished disk image.
A native display window opens (Cocoa on macOS, GTK on Linux).
You install software, tweak settings, etc.
When done, shut down from within the VM or press Enter in the terminal to send an ACPI shutdown.
When to use it:
You need software that is not available via the unattended template (e.g. commercial tools, GUI applications requiring manual license activation)
You want to verify the install before committing to experiment runs
You need to configure settings that require a running desktop session
Note
--interactive is ignored for install_mode: manual profiles since
those already provide a full interactive QEMU session during install.
Legacy: Manual VirtualBox Setup¶
The following instructions apply to the older VirtualBox-based workflow.
For new VMs, the QEMU-based adare vm create command above is recommended.
General Guideline¶
There are two options for creating your own ADARE-compatible VM:
Start from scratch: Create a new VM with a minimal OS installation.
Modify an existing VM: Take one of the provided ADARE VMs and customize it.
Once configured, export the VM as an .ova or .ovf file in OVF 1.0 format.
VirtualBox Configuration Requirements¶
Disable all that can create popups (update notifications, automatic updates, error reporting, etc.)
Disable screen lock and sleep mode
Windows Setup¶
User Account Configuration¶
Create a user account with:
Username:
adarePassword:
adare
Enable autologin so the VM boots directly into the
adareuser’s desktop.
Additional Configuration¶
Disable User Account Control (UAC):
Go to Control Panel > User Accounts > User Accounts > Change User Account Control settings
Set the slider to the bottom (Never notify)
Enable Developer Mode:
In Windows Settings, search for “For Developers”
Toggle the switch to enable Developer Mode
Configure Windows Defender Firewall:
Press
Win + R, typewf.msc, and press EnterIn the left panel, right-click Windows Defender Firewall with Advanced Security > Properties
For each profile tab (Domain, Private, Public), set Inbound connections to Allow
Click Apply and OK
Perform a clean shutdown to apply all security changes.
Required Software:
VirtualBox Guest Additions
Python 3.9+ (added to PATH)
uv (Python package manager)
Linux Setup¶
Create user
adare/adarewith autologin (X11 session, not Wayland)Enable passwordless sudo:
adare ALL=(ALL) NOPASSWD:ALLInstall Miniforge3, VirtualBox Guest Additions
Disable auto-updates:
sudo systemctl disable unattended-upgrades
Installing ADARE Guest Agent¶
The ADARE guest agent (adarevm + adarelib) is normally installed
automatically during experiment runs. Manual pre-installation saves 10-30
seconds per experiment. See the package wheels in the shared folder
(/adare/app/wheels/ on Linux, Z:\wheels on Windows with VirtualBox).
Registering as an Environment¶
After creating a VM, register it as an ADARE environment so experiments can target it. Create an environment YAML file describing the VM:
name: win11-custom
vm: "~/.adare/state/vms/windows11-20260408.qcow2"
os:
os: "Windows"
platform: "windows"
distribution: "Pro"
version: "11"
architecture: "x64"
description: "Windows 11 with Firefox 102 ESR for browser artifact research"
tags: ["windows", "browser-forensics"]
For a Linux VM:
name: ubuntu2404-custom
vm: "~/.adare/state/vms/ubuntu2404-20260408.qcow2"
os:
os: "Ubuntu"
platform: "linux"
distribution: "ubuntu"
version: "24.04"
architecture: "x64"
description: "Ubuntu 24.04 minimal for filesystem artifact research"
tags: ["linux", "filesystem"]
Load the environment into ADARE:
adare environment load my-win11-env.yml
Verify it is available:
adare environment list
The environment name (e.g., win11-custom) can now be used in experiment
metadata and with the -e flag when running experiments:
adare experiment run my-experiment -e win11-custom
Tip
If you used the --env-name option during adare vm create, an
environment file was already generated automatically. Check with
adare environment list before creating one manually.
See Environments for full details on environment configuration.
See Also¶
Environments – environment configuration and management
Tutorial – basic ADARE workflow tutorial