Skip to content

This is the multi-page printable view of this section. .

Return to the regular view of this page.

Barn documentation

Start Barn with two commands, then look up operations, configuration, and CLI contracts as needed.

Barn turns one Pigsty-compatible Inventory into fixed-IP QEMU virtual machines. It manages one deployment per Unix user; state lives under ~/.barn, so lifecycle and SSH commands work from any directory.

Choose the shortest path for your task:

  • Start — boot the first lab with two commands, then operate and troubleshoot it.
  • Reference — exact Inventory fields, commands, flags, output, and exit codes.
  • About — design, native validation, limits, and release gates.

Start with the Quick Start to prepare the Barn 0.9.0 release candidate from source. Once the CLI is ready, run barn up to start the lab, then barn ssh to connect.

Important

0.9.0 is the first Barn release and is not yet published. It uses the new commands, environment variables and fresh state, without compatibility or migration for earlier development builds. See release and validation status.

1 - Start

Start Barn with up, connect with ssh, and read the other guides only when needed.

With Barn installed, new users can start a test lab with the Quick Start:

barn up
barn ssh

When no inventory or deployment exists, interactive up creates the default inventory. It can prepare missing host dependencies and networking. Repeat barn up to retry unfinished guest setup without restarting healthy VMs. For unattended setup, run barn setup --yes before barn up.

Package availability is recorded on Status; developers and source reviewers can use Build from Source.

Everything else is separated by task:

  1. Daily Operations — status, access, start/stop, changes, scale-in, and destroy.
  2. Troubleshooting — diagnostics and common fixes.
  3. Image Repositories — choose images, use mirrors, import, and prune the cache.
  4. Build from Source — developer builds, checks, and local PATH setup.
  5. Uninstall and Clean Up — remove the deployment, images, networking, and state.
  6. Automation and Guest Scripts — unattended setup, JSON acceptance, repeatable guest commands, and the Pigsty handoff.
  7. Storage and Access — disk retention, file transfer, SSH tunnels, and Linux directory sharing.
  8. macOS Virtual Machines — unreleased barn mac: macOS 27 guests on Apple Silicon with desktop, SSH, shared folders, and clipboard.

1.1 - Quick Start

Install Barn 0.9.0, start an Ubuntu lab with up, connect with ssh, and scale from the same inventory.

Install

This tutorial targets the Barn 0.9.0 release candidate. For now, build from source. The package and Homebrew commands below apply after 0.9.0 is published and the formula is updated; these links do not establish publication. See Status.

Barn has no compatibility layer for earlier development builds. It uses barn.yml, BARN_*, and fresh ~/.barn state, with no old command aliases or state migration. Stop internal old environments, preserve needed data, and create a fresh Barn installation. Renaming an old state directory is unsupported.

After publication, the user-scoped installer supports macOS and Linux on arm64 and amd64, verifies the archive checksum, and needs no sudo to install:

curl -fLO https://github.com/pgsty/barn/releases/download/v0.9.0/install.sh
chmod +x install.sh
BARN_VERSION=0.9.0 ./install.sh
export PATH="$HOME/.local/bin:$PATH"
barn version

The default installation directory is ~/.local/bin; add the same PATH line to your shell configuration. A release build should report 0.9.0. GitHub excludes prereleases from /releases/latest, so specify BARN_VERSION=0.9.0.

See Download and PATH problems if downloads fail.

Other installation methods

Once the release assets and Homebrew formula are available, choose one method. The Linux examples use amd64 packages; use the corresponding linux_arm64 asset on ARM64 Linux.

Homebrew
brew install pgsty/infra/barn
barn version
Debian / Ubuntu
barn_release=https://github.com/pgsty/barn/releases/download/v0.9.0
curl -fLO "$barn_release/barn_0.9.0_linux_amd64.deb"
sudo apt install ./barn_0.9.0_linux_amd64.deb
barn version
RHEL / Fedora
barn_release=https://github.com/pgsty/barn/releases/download/v0.9.0
curl -fLO "$barn_release/barn_0.9.0_linux_amd64.rpm"
sudo dnf install ./barn_0.9.0_linux_amd64.rpm
barn version

The host requirements below apply to Linux guests. macOS guests use the independent barn mac command.

Host requirements

Host Native acceleration Minimum QEMU
macOS arm64 / amd64 HVF 8.2.1
Linux amd64 / arm64 KVM 6.2

The host also needs qemu-img, OpenSSH, and firmware for the selected guest. Interactive up can prepare missing dependencies through Homebrew on macOS or apt/dnf on supported Linux distributions, and install the fixed-IP network. Host package and network changes may require sudo; run Barn itself as your normal user. Linux needs usable KVM and NetworkManager or systemd-networkd. The dated native validation covers macOS arm64 and Ubuntu amd64; other build platforms have narrower evidence. See Status.

Boot the first lab

For a first deployment, open a terminal in an empty directory:

mkdir -p ~/barn-lab && cd ~/barn-lab
barn up
barn ssh

Use exit to return from the guest to your host terminal before running more Barn commands.

When no inventory or applied deployment exists, interactive up creates barn.yml with one meta node. It prepares missing host dependencies and networking, downloads and verifies the image, starts QEMU, and waits for management SSH. Host changes are displayed; sudo may ask for your password. To review the full host plan before applying it, use barn setup --dry-run.

Note

A new directory is not a new lab. State lives in $BARN_HOME (default ~/.barn). If a deployment already exists and no inventory is found, up continues that deployment. Use barn status to inspect it first.

The default template resolves to:

Setting Default
Node / fixed IP meta / 10.10.10.10
Guest image Ubuntu 24.04, u24:stable, native host architecture
Login dba, with SSH key authentication
CPU / memory 2 vCPUs / 4 GiB per node
Root / data disk 64 GiB root + 128 GiB at /data, not persistent

Disk sizes are virtual capacities; qcow2 files grow as data is written. A four-node lab uses 8 vCPUs and 16 GiB of guest memory, in addition to host resources. Use barn plan to inspect totals before starting.

A fresh, unedited built-in template on the default subnet may be moved to an available private /24 when setup finds a subnet conflict. An existing template is backed up as barn.yml.before-network-change. Check the resulting barn.yml and barn status for actual addresses; explicit -f files, edited templates, and existing deployments keep their selected subnet.

A healthy first start ends with a result such as:

  ✓  1 node ready
connect:   barn ssh meta

barn ssh selects the control node, meta in this template. You can also name it or run a command directly:

barn ssh meta
barn exec meta -- hostname
barn st

st is the alias of status; its running state describes the VM process, not a fresh guest-readiness check.

Continue interrupted setup

Repeat barn up to continue interrupted work, retry unfinished guest setup, or update older guest helpers. Healthy running VMs keep their process and root disk. A guest with usable management SSH can finish with limitations, such as a read-only share or unavailable private networking. Review those messages; automation should inspect nodes[].warnings and nodes[].repairs in barn up --json, as these limitations still return exit 0.

Warning

Data disks are disposable test storage. up can reset an unrecognized or confirmed damaged filesystem, including a persistent disk, and reports discarded data. persistent retains disks across destroy/recreate; it does not protect corrupt contents during recovery. See Data disks.

--no-wait skips guest readiness, recovery, and metadata refresh; a later barn up completes them. Image downloads support retries and resumption. Use barn up --mirror to prefer the official China repository; see Image Repositories for image selection and fallback behavior.

Choose an inventory before booting

This is an alternative to the automatic first run above. In a fresh lab directory, generate and inspect the configuration before starting:

barn init
barn validate
barn plan
barn up

For the Catalog images used here, init, validate, and plan do not require QEMU or host-network setup. Planning a registered local-* image does require qemu-img to validate its cached bytes. The default meta inventory is:

all:
  vars:
    admin_ip: 10.10.10.10
  children:
    nodes:
      hosts:
        10.10.10.10: { nodename: meta }

There are four built-in templates:

Template Nodes Default addresses
meta 1 10.10.10.10
dual 2 10.10.10.10–10.10.10.11
trio 3 10.10.10.10–10.10.10.12
full 4 10.10.10.10–10.10.10.13

For example, barn init full writes four nodes; barn init full -c 10.20.30.0/24 selects another subnet. Existing files are preserved unless --force is explicit. Set vm_cpu, vm_mem, vm_image, and other fields before the first up; see Configuration.

Prepare the host explicitly

setup prepares dependencies and networking without starting VMs:

barn setup --dry-run
barn setup

Unlike the preparation performed by up, standalone setup asks for confirmation before applying a mutating plan. It reuses the discovered inventory, or generates meta if no file exists. The optional /etc/hosts helper is installed only when barn hosts install --yes needs it; ordinary startup and barn ssh do not need that integration.

Downloads honor HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and NO_PROXY, including lowercase forms. For unattended first setup in an empty directory:

barn setup --yes
barn up --json

setup --yes can generate the inventory itself; a separate init is needed only when you want to edit it first. Automation still needs credentials for any required sudo operation; --yes does not supply them. The automation guide shows how to retain command results and check guest limitations before proceeding.

Use an existing Pigsty inventory

barn validate -f pigsty.yml
barn plan -f pigsty.yml
barn up -f pigsty.yml

Barn reads the documented VM, naming, and login fields and preserves other Pigsty settings. This starts the virtual machines; installing PostgreSQL or other Pigsty services is a separate Pigsty operation. The built-in templates describe VM topology and do not include a complete Pigsty service configuration. See Automation and Guest Scripts for the handoff, and Storage and Access for file transfer and service connections.

Scale and operate

To expand the default one-node lab, preserve the existing settings and add three hosts to barn.yml:

all:
  vars:
    admin_ip: 10.10.10.10
  children:
    nodes:
      hosts:
        10.10.10.10: { nodename: meta }
        10.10.10.11: { nodename: node-1 }
        10.10.10.12: { nodename: node-2 }
        10.10.10.13: { nodename: node-3 }

This example assumes the default subnet; if setup chose another, use that subnet for every address, including admin_ip. Do not overwrite a customized inventory with init --force to expand it.

barn plan
barn up
barn st

With only these additions, the plan lists three nodes to create. up creates them, keeps a running meta process, and refreshes guest hosts and control-node SSH entries. A healthy result is 4 nodes ready. The embedded 0.9.0 Catalog resolves u24:stable to [email protected]; a manually updated Catalog may resolve another version, which appears in plan and status.

Changing CPU, memory, or other consumed VM fields requires an explicit barn recreate <node>. Removing a YAML entry never deletes its VM. Stop and resume the lab without recreating disks:

barn stop
barn start

When finished, destroy the deployment:

barn destroy

On a terminal, type destroy to confirm. Root and non-persistent data disks are deleted; cached images, keys, declared persistent disks, and host networking remain. See Uninstall and Clean Up for complete disposal, or Daily Operations for restart, logs, explicit changes, and scale-in.

Fresh installation for 0.9.0

Barn 0.9.0 is the first release under the new name. Stop earlier internal labs, preserve needed data, and create a fresh Barn lab. There is no in-place upgrade, old command alias, or state migration. Build from source before publication; the installation commands at the top apply after release. barn update refreshes the image Catalog, not the Barn executable.

1.2 - Daily Operations

The normal lifecycle for the one deployment: inspect, access, scale, change, stop, and destroy.

This guide describes the Barn 0.9.0 release candidate. See Status for the publication and validation boundary.

Inspect and access

barn status
barn ssh meta
barn exec node-1 -- hostname
barn logs meta --source serial

Applied state is under ~/.barn by default (BARN_HOME overrides it); these commands work from any directory. Changing the working directory does not create a separate deployment. Status shows images and resources; --verbose adds architecture, accelerator, SSH ports, and PID. TCG is marked in ordinary output, and a degraded node does not hide its peers. barn up rebuilds the default SSH aliases from the complete applied deployment after the selected VMs are started, so a scoped up never drops unselected peers and plain ssh meta just works; barn ssh-config --install rewrites it by hand if you ever need to. plan, up, reload, and recreate prefer -f, then a discovered Inventory, then the applied spec when no file exists. validate always needs a file.

Repeat up to retry unfinished guest setup and refresh older guest helpers without restarting healthy VMs. Optional limitations appear in the result; JSON/YAML expose nodes[].warnings and nodes[].repairs. Unusable test data filesystems may be reset, including persistent disks; see Data disks.

Stop and start

barn stop
barn start
barn restart node-1
barn reload -f barn.yml       # read/check config, stop, then converge

start powers on stopped VMs and re-checks readiness of running ones. Both start and restart use applied state and refresh SSH aliases, including any reassigned automatic ports. reload reads the Inventory and checks drift and startup dependencies before stopping selected nodes and following the full up path.

Starting commands also refresh Barn hosts and control-node SSH entries in running guests. --no-wait skips readiness, guest recovery, and this refresh; run up later to finish them.

Change the deployment

barn plan
barn up                         # create/start selected nodes and install SSH aliases
barn recreate node-1            # applies a changed VM definition

recreate and destroy ask you to type the confirmation word on a terminal; --force skips that prompt and is required without a terminal.

plan can inspect Catalog-backed images before host setup and shows images, total resources, change reasons, and disk effects. Planning an imported local-* image also validates its cache and requires qemu-img. CPU/memory changes still require recreate: root and ephemeral data disks are replaced, while persistent disks are kept. A selected recreate blocked by unselected peer changes refuses before deletion and names the nodes that need attention.

Inventory changes appear in these fields:

Field Meaning Action
create desired node has no state barn up
recreate VM definition changed barn recreate <node>
missing stateful node left the file restore it, or destroy it explicitly

Deleting YAML never deletes a VM. Unconsumed Pigsty changes produce action:none; native naming and node-admin fields are consumed even though they do not begin with vm_. Successful recreate refreshes the complete SSH fragment as well.

Concurrent commands (0.9 candidate)

Deployment mutations wait behind another Barn operation for up to ten minutes, bounded by the command’s own deadline. The waiting message identifies the command, PID, and start time. A lock timeout returns exit 4, JSON error: conflict, and reason: deployment_busy; retry after the holder finishes. The lock is released automatically when the holding process exits. Do not delete a lock file to interrupt a live operation.

status, ssh, exec, ssh-config, and the deployment-state read for hosts do not queue behind this lock. They use the published state; status adds a note when another command owns the deployment and does not reconcile its transitions. A VM still starting may therefore be unavailable to SSH.

See Troubleshooting for interrupted operations and Automation for scriptable results.

Destroy

barn destroy node-3
barn destroy
barn destroy --delete-persistent
barn destroy --purge
barn purge                         # discard everything without confirmation

--delete-persistent and --purge are valid only for whole-deployment destroy, not with node selectors. --purge removes persistent disks, keys, and deployment state; images remain cached. Node destroy refreshes the SSH fragment for remaining peers, while whole destroy removes the default Barn SSH integration. Host network removal is separate and refuses while a VM is attached.

barn purge is the concise disposable-lab path. It is equivalent to destroy --force --purge for an existing deployment, accepts no node selectors, and is idempotent when no deployment exists. It keeps the image cache and host network, and it does not bypass process, ownership, or path-integrity checks.

0.9 candidate: plain destroy succeeds when no deployment exists. If deployment state is gone but owned persistent disks remain, use purge; destroy --delete-persistent or destroy --purge points to that command. The old rm alias has been removed; spell out purge.

barn network uninstall --yes

See Image Repositories for image selection, mirrors, and cache pruning, or Uninstall and Clean Up to remove host state.

1.3 - Troubleshooting

Short, safe runbooks for setup, networking, images, drift, interrupted state, and SSH.

This page describes the Barn 0.9.0 release candidate. Check barn version before applying version-specific guidance.

Start with diagnostics (status may reconcile interrupted runtime state):

barn doctor --json
barn network status --json
barn status --json

Download and PATH problems

The installer uses GitHub Release assets; --mirror selects the Barn image repository and does not redirect installer downloads. If your network needs a proxy, set HTTPS_PROXY or ALL_PROXY in the terminal to your existing proxy’s address. A macOS system proxy setting alone does not configure these environment variables for command-line tools.

The user-scoped installer defaults to ~/.local/bin. If barn is missing or reports an older version after installation, check which executable is selected:

export PATH="$HOME/.local/bin:$PATH"
command -v barn
barn version

For Homebrew or native packages, use that channel’s executable instead. Keep the CLI and its packaged barn-hosts-helper from the same release together.

No inventory found

Interactive up can create the first default inventory when no deployment exists. For an explicit configuration, run plan, up, or validate beside barn.yml/pigsty.yml, pass -f /path/to/file, or run barn init to write one. Once state exists, plan, up, reload, and recreate can fall back to its applied spec. Status, start, stop, SSH, and destroy always use applied state. If status reports no deployment state found, the selected BARN_HOME has no applied deployment; it may be fresh or previously purged.

Setup needs sudo

The line before the prompt names the exact host mutation. Barn attaches an interactive terminal directly to sudo when the privileged step begins. --yes accepts the setup plan; it does not bypass sudo authentication. Automation needs an existing credential or a suitable NOPASSWD policy. Use barn setup --dry-run to inspect the plan first.

On macOS, setup prepares the pinned socket_vmnet source before requesting administrator authentication. A download failure therefore does not require a password. 0.9 candidate: the setup plan spells out sudo use and the socket_vmnet source; if an automatically selected subnet changes after the first confirmation, setup asks again unless --yes was supplied.

Native acceleration or compatibility runtime is unavailable

Native paths require HVF on macOS or KVM on Linux. TCG is selected only for an explicit foreign vm_arch or a built-in image/host compatibility rule; an arbitrary native failure never falls back. Homebrew QEMU contains both system emulators. Linux setup installs only the native family, so a foreign Guest also requires its matching qemu-system-* binary and firmware.

plan resolves Catalog-backed images and their intended runtime without QEMU installed; imported local-* images still need qemu-img and a valid cache. up and recreate check the selected emulator and firmware before changing VM resources. Performance results from TCG are not meaningful.

Network is partial or invalid

An intact but inactive Barn network can be restored by interactive up. For partial or invalid installations, do not delete host files by hand; review the owned cleanup plan:

barn network status --json --verbose
barn network uninstall --json

Without --yes, JSON output only plans removal. The network plan may still need sudo to read protected ownership state. Apply the reviewed plan with barn network uninstall --yes. 0.9 candidate: ordinary terminal output asks [y/N] and can apply removal immediately after confirmation. A failed Linux bridge smoke test rolls the install back automatically; an explicit automatic rollback failed message means manual inspection is required.

On macOS, a missing or restrictive root-owned /var/log/barn-vmnet is repairable. Read the finding from network status: the expected directory is root:wheel 0755. barn setup repairs a recognized installation; follow the exact diagnostic command if repairing manually. A symlink, wrong owner, or group/world-writable directory is not repaired automatically. Do not diagnose a route conflict from the bridge name alone.

Linux bridge helper fails

id
stat -c '%U:%G %a %n' /usr/lib/qemu/qemu-bridge-helper
dpkg-statoverride --list /usr/lib/qemu/qemu-bridge-helper

Debian/Ubuntu uses root:<caller-accessible-group> 4750; the caller does not have to belong to kvm when /dev/kvm access comes from a desktop ACL.

Plan reports recreate or missing

recreate means the node’s definition changed: review it with barn plan, then run barn recreate <node>. On a terminal the command asks you to type recreate; without a terminal it requires --force. missing is only a report: restore the host entry or run barn destroy <node>.

A node did not become ready

Management SSH and guest instance identity are required for readiness. If a node cannot be created, started, or reached, a node-level partial result reports the node and stage and exits 5. A command-wide failure, such as a missing host capability or an inventory conflict, uses its own exit class. Read its logs:

barn logs <node>                  # serial console
barn logs <node> --source qemu    # QEMU diagnostics
barn logs --source events        # deployment/setup events, even before the first VM
barn status

Data disks, shares, hostnames, guest hosts, control-node SSH, and private-network setup run independently. Failure of one does not prevent management SSH or the other stages. A usable guest returns 0 with specific limitations; JSON/YAML expose them as nodes[].warnings. Internet access is not a readiness requirement.

Limitation Next action
Data disk unavailable Correct a missing device, probe, tool, busy mount, or I/O problem, then run up
Shared directory is read-only Correct host permissions, then run up to retry writes
Guest hosts or control-node SSH incomplete Run up to refresh the managed files
Private interface unavailable Check barn network status, then run up; management SSH can still work

Repeat up after fixing the underlying issue. It retries unfinished stages, upgrades old guest helpers in place, and skips healthy work without restarting running VMs. Unrecognized or confirmed damaged test data filesystems are reset automatically, including persistent disks; the result reports discarded data. Failed probes, busy mounts, and I/O failures do not trigger formatting. See Data disks.

A repeated up can also clean recognized leftovers from interrupted preparation. --rollback removes failed prepare artifacts in the same run and lists them in rolled_back. --no-wait returns once QEMU is running and skips readiness, guest recovery, and metadata refresh; a later up completes them.

SSH fails

Startup restores a missing deployment public key from the intact original private key. If the private key is missing, restore that same key from backup; Barn refuses to generate a replacement identity for existing VMs. This host-side recovery is separate from a missing guest key in a control node. up now checks that installed guest key as well: a missing copy is a control-ssh limitation, not a management SSH failure. Restoring the original guest key and running up clears the limitation. Automatic private-key reinjection into existing guests is not supported.

Check barn status, barn ssh-config, and the serial log. Barn’s own SSH uses a loopback management port; direct Ansible traffic uses the fixed IP. If another process occupies a stopped VM’s automatically allocated management port, the next start selects a free port and refreshes its SSH aliases. Running VM ports stay unchanged. SSH host-key trust is scoped to the VM instance UUID, so recreating a VM does not require deleting unrelated known-host entries. A changed key for the same instance still fails verification.

doctor excludes fixed IPs reserved by the applied deployment from its generic eligibility scan; up and start still reject a new or stopped node address that already accepts SSH.

0.9 candidate: a symlinked or hard-linked ~/.ssh/config is not rewritten. Barn publishes its fragment and shows the Include line to add through your dotfile manager. If ssh meta fails while barn ssh meta works, check that include before changing guest keys. Near-miss node names in ssh/exec are rejected with a suggestion when they contain a digit or - and match the typo heuristic; use -- when explicitly separating a node selector from its remote command.

Catalog or image verification fails

The current binary embeds active and standby Catalog public keys. Unknown signers, version rollback/equivocation, artifact size/SHA mismatch, and unsafe qcow2 structure are distinct integrity failures. Use a correctly signed repository or barn image import --sha256 ...; do not copy bytes directly into ~/.barn/images.

A command was killed

First check whether another Barn command is still running. In the 0.9 candidate, status reads published state without waiting and reports a note while another command holds the deployment lock. Wait for that command to finish before treating its in-progress state as an interruption.

When no operation holds the lock, run barn status. A provably live or dead runtime is reconciled using its recorded identity; an ambiguous process remains blocked. Never kill an unknown PID based only on a state file.

The 0.9 candidate additionally handles these recovery cases:

Interrupted operation Recovery
Host reboot or recycled QEMU PID status recognizes a provably unrelated PID and marks the old VM stopped; use start
stop while QEMU kept running status restores running state; repeat stop if shutdown is still intended
First up failed during preparation Correct the inventory and repeat up -f /path/to/barn.yml; only journaled unfinished artifacts are rolled back
destroy stopped midway Repeat the same explicit destroy scope; interrupted transitions and previously retained persistent disks can be resumed

If recovery still fails, retain the state and logs; do not delete node directories or rewrite PIDs to imitate a successful recovery.

If a recorded QEMU process still exists but its QMP socket is absent, preserve the evidence and inspect serial/QEMU logs before using stop to converge it. Do not delete runtime sockets or state files by hand.

0.9 candidate: the generic error envelope uses a stable class in error, with optional reason, next, and external-program details in command. Some commands return their own diagnostic reports. Read the cause and proposed next step; do not parse human text as an API. See Automation for exit codes and result handling. Event and QEMU logs use readable records; --verbose adds QEMU arguments when those are needed.

For a bug report include the exact command and exit code, barn version, the three JSON reports above, host OS/architecture, and QEMU version.

1.4 - Automation and Guest Scripts

Prepare an unattended lab, check JSON results, and run repeatable scripts inside selected guests.

These examples target the Barn 0.9.0 release candidate. Run host commands as the Unix user who owns the deployment. Use the same inventory and BARN_HOME on every invocation; a new working directory does not create an independent lab.

Prepare a predictable lab

For a first lab, create and review the inventory before starting automation:

mkdir -p ~/barn-lab
cd ~/barn-lab
barn init dual
# Edit barn.yml before proceeding.
barn version
barn validate -f barn.yml
barn plan -f barn.yml
barn setup -f barn.yml --dry-run

Keep the selected inventory in version control. Set vm_image explicitly; use a version such as vm_image: [email protected] when new nodes must use the same base after a Catalog update. Explicit -f also prevents first-run setup from automatically moving an untouched default template to another subnet.

After reviewing the host plan, prepare the machine once:

barn setup -f barn.yml --yes
barn up -f barn.yml --json > up.json

--yes accepts the setup plan; it does not grant sudo credentials. An unattended runner must already have the required host dependencies, network, and privilege policy. Noninteractive up does not perform the interactive first-run host preparation. up has no --yes flag.

For a custom repository, use the same --repo value for setup and up, and explicitly activate its Catalog with barn update --repo URL before planning. See Image Repositories.

Check more than the exit code

Barn writes structured results to stdout and diagnostics to stderr. Preserve both outputs and the command’s exit code; a later shell command must not overwrite the status you intend to inspect. This Bash example also uses jq:

if barn up -f barn.yml --json > up.json 2> up.stderr; then
  jq -e '
    (.nodes | type == "array" and length > 0) and
    all(.nodes[];
      .state == "running" and .ready == true and
      ((.warnings // []) | length == 0) and
      ((.repairs // []) | length == 0)) and
    ((.warnings // []) | length == 0)
  ' up.json
else
  barn_exit=$?
  cat up.stderr >&2
  cat up.json
  exit "$barn_exit"
fi

The jq check deliberately asks for every returned node to be ready without limitations or reported repairs. A usable guest with a failed optional disk, share, or peer-SSH step can return exit 0 and list nodes[].warnings. nodes[].repairs can report a filesystem reset that discarded test data. Choose the acceptance policy your workload needs instead of silently ignoring these fields. Top-level warnings can describe optional SSH integration or metadata-refresh failures. A failed jq -e returns nonzero to the caller.

Do not use --no-wait when the next step requires guest readiness. A status --json snapshot reports VM state and cached warnings; it is not a new guest readiness test. Run up to complete setup, then run an application check inside the guest when your workflow depends on a service.

Failures and version boundaries

Use CLI exit codes together with the payload for the command you ran. Partial operations can retain successful nodes; inspect nodes and/or failures when present before retrying.

The 0.9 candidate moves some failures to different classes. For example, a missing first inventory is usage/2 and an unknown image is usage/2; recreate_required and nodes_removed are reason values under conflict/4. It also uses bounded lock waiting and deployment_busy on timeout.

Not every failing command returns the generic error/message envelope: doctor, network status, provision, and SSH execution can return their own reports. ssh/exec pass through the remote exit status, including 255 from OpenSSH. In structured execution output, inspect success, exit_code, stdout, and stderr; do not interpret a remote status as a Barn class.

Run one command or a script

Use an explicit node and -- to separate it from the remote command:

barn exec meta -- hostname
barn exec node-1 -- sh -c 'id; df -h /data'
barn exec meta --json -- uname -a > uname.json

For multiple guests, save a local Bash script as check-lab.sh:

#!/usr/bin/env bash
set -euo pipefail
hostname
id
findmnt /data
test -d /data

Then run it on selected nodes:

barn provision --script ./check-lab.sh meta node-1
barn provision --script ./check-lab.sh --parallel 2 --timeout 5m --json > provision.json

With no selectors, provision targets all committed nodes; they must be running. It does not create or start VMs. The local script must be a nonempty, regular, non-symlink file of at most 4 MiB. Barn streams one verified snapshot to guest Bash, records its SHA-256, and does not save the script as a guest file. The script need not be executable on the host.

Execution is serial by default, with --parallel from 1 to 4. --timeout defaults to one hour, applies to the whole operation, and cannot exceed 24 hours. --sudo runs through guest sudo -n, so it cannot prompt for a password. Results contain results[], per-node stdout/stderr and exit codes, and successful/failed counts. A partial run can leave successful changes in place. Write scripts so that running them again is safe; Barn does not roll back guest commands or automatically rerun them on the next up.

A provision run with successful and failed targets exits 5. With one failing target, a positive remote status other than 255 is passed through. Other all-failed runs exit 1; inspect results[].exit_code for the guest/SSH details.

Use the lab with Pigsty

Barn and Pigsty can read the same pigsty.yml, but barn init dual only creates VM topology. It does not configure a PostgreSQL cluster. Start with the service inventory appropriate to your Pigsty checkout and review it:

barn validate -f pigsty.yml
barn plan -f pigsty.yml
barn up -f pigsty.yml
barn ssh

Barn prepares the guest administrator and control-node SSH access. Deploy services through Pigsty after checking the inventory and guest connectivity. The validation record distinguishes Ansible connectivity checks from a complete Pigsty installation. For host-side file transfer or port tunneling, see Storage and Access.

1.5 - Storage, Files, and Service Access

Configure test data disks, understand retention, transfer files, and reach guest services through OpenSSH.

This guide uses the Barn 0.9.0 release candidate interface. Start with the Quick Start before running the guest-side checks. The examples use node meta and the default subnet; retain your actual names and addresses when adapting an existing inventory.

Choose disks before creating the VM

Every node has a 64 GiB root disk and, by default, one 128 GiB non-persistent data disk at /data. vm_disk sets the root disk size in GiB. vm_disks replaces the entire data-disk list; vm_disks: [] disables extra data disks.

For a new single-node lab, save this as storage.yml:

all:
  vars:
    admin_ip: 10.10.10.10
    vm_image: [email protected]
  children:
    nodes:
      hosts:
        10.10.10.10:
          nodename: meta
          vm_cpu: 2
          vm_mem: 4096
          vm_disk: 64
          vm_disks:
            - {path: /data, size: 64, fs: auto, persistent: true}
            - {path: /scratch, size: 32, fs: ext4, persistent: false}

Then review and create it:

barn validate -f storage.yml
barn plan -f storage.yml
barn up -f storage.yml
barn exec meta -- findmnt /data
barn exec meta -- findmnt /scratch
barn exec meta -- df -h / /data /scratch

size: 64 means 64 GiB; size: 64GiB is also valid for a data disk. These are virtual capacities, not immediately allocated host space. Monitor host free space as the qcow2 files grow. fs: auto prefers XFS if the guest provides mkfs.xfs, otherwise ext4. A successful VM process start alone does not prove that either data disk mounted; check the guest result and warnings.

This is a first-creation example. If the same node already exists with another definition, up reports drift. Review plan and back up needed data before explicitly using recreate; that replaces the root and non-persistent disks.

What survives each operation

Operation Root and non-persistent disks Persistent disks
stop then start, or restart retained retained
Healthy repeated up retained retained
Compatible recreate replaced retained and reattached
Ordinary destroy deleted retained
Whole-deployment destroy --delete-persistent deleted deleted, including retained disks
Whole-deployment purge deleted deleted, including retained disks

Persistence is based on disk identity and a compatible specification. Keep the node, mount path, size, and filesystem definition consistent when reusing a retained disk. It is not an automatic resize, rename, filesystem conversion, backup, or snapshot facility. Barn rejects incompatible retained disks; do not edit state files to force attachment.

A persistent disk is still disposable test storage. During guest recovery, up may reset an unrecognized or confirmed damaged filesystem and report the discarded contents. Persistence only controls VM destruction/recreation. Missing devices, failed probes, busy mounts, and I/O failures do not authorize formatting. Copy valuable data elsewhere before testing failure recovery.

A freshly formatted data filesystem is owned by root. Use guest sudo for this write check, or deliberately prepare permissions for your application. To demonstrate normal restart retention on the created lab:

barn exec meta -- sudo -n sh -c \
  'printf "retention check\n" > /data/barn-retention.txt'
barn stop meta
barn start meta
barn exec meta -- cat /data/barn-retention.txt

This checks a VM stop/start, not persistence after a physical-host reboot. See Status for the native validation boundary.

Copy files with the managed SSH connection

Generate a standalone OpenSSH configuration from the running deployment:

barn ssh-config > barn-ssh.conf
ssh -F ./barn-ssh.conf barn-meta hostname
scp -F ./barn-ssh.conf ./storage.yml barn-meta:/tmp/storage.yml
scp -F ./barn-ssh.conf barn-meta:/data/barn-retention.txt ./barn-retention.txt

The generated fragment selects the current loopback SSH port, deployment key, and instance host-key identity. Regenerate it after recreation or a management port change. barn ssh-config --install is optional when you want these aliases available in your normal SSH configuration; -F works without that integration. The exported file references your deployment key; it does not embed or export the private key.

Reach a service inside the guest

From the host, a service listening on the guest’s fixed IP can be reached on that IP if its guest firewall and service configuration allow it. For example, a PostgreSQL server on 10.10.10.10:5432 is a separate service you must install; Barn does not install PostgreSQL merely by booting a VM.

To reach a service listening only on the guest’s loopback address, use OpenSSH with the generated configuration:

ssh -F ./barn-ssh.conf -N \
  -L 127.0.0.1:15432:127.0.0.1:5432 barn-meta

Keep that host terminal open, then connect a local client to 127.0.0.1:15432. The guest service must already be listening on port 5432. Ctrl-C closes the tunnel. Choose another local port if 15432 is occupied. The explicit loopback bind keeps this example local to your host.

The inventory has no vm_ports or vm_forwards setting; an unknown vm_* key is rejected. Use the fixed-IP network or OpenSSH forwarding. Management SSH uses a separate loopback connection and can remain available while the fixed-IP network has a reported limitation.

Host directory sharing on Linux

For a Linux host, a read-only share can be added before the first up:

vm_shares:
  - host: /srv/barn-project
    guest: /workspace
    readonly: true

Put this under the intended host or all.vars. Replace the host path with an existing real directory owned by your Barn user and accessible to that user. Read-only shares also require this ownership. Host paths must be absolute, cannot pass through symlinks, and cannot overlap the Barn data root. An explicit read-only share is a useful starting point for source files. For writable shares, guest-user permissions also matter; Barn can fall back to read-only and report a limitation without changing host ownership.

Do not add vm_shares to a macOS lab using the currently documented runtime. The tested macOS/QEMU path cannot reopen the secure directory descriptor and the affected node cannot start. Use SSH file transfer there. Restoring a missing host directory or mount lets you retry up; Barn never creates an empty replacement source. Changing an existing node’s share definition requires explicit recreation. See Configuration for the full disk and share constraints.

1.6 - macOS Virtual Machines

Run macOS 27 virtual machines on Apple Silicon with barn mac — create, connect, share files and the clipboard, and clean up.
Important

Barn 0.9.0 release candidate; unreleased. This page describes the renamed barn mac, which uses fresh Barn state and provides no development-state migration. Earlier native records are retained in Status and do not establish the same acceptance for the renamed build. Check the barn mac --help of the binary you run.

barn mac creates and runs macOS virtual machines on an Apple Silicon Mac. Each machine is a clean, disposable macOS with an administrator account, passwordless sudo, pinned SSH keys and a fixed address — for testing, building and reproducing macOS-specific behavior. It uses Apple’s Virtualization framework directly and needs no administrator access.

Mac machines are separate from the Linux lab. They never read barn.yml, never join a Pigsty inventory, and keep their files under $BARN_HOME/mac (default ~/.barn/mac). Linux destroy and purge leave them alone.

Requirements

  • An Apple Silicon Mac running macOS 27 or later, with a user logged in to its desktop. Guests also run macOS 27.
  • About 65 GiB free for the first machine: the 25 GiB restore image from Apple (kept until you prune it), the 27 GiB installed base, and room to start. Each machine then grows with its own changes up to its disk capacity, 100 GiB by default.
  • Xcode 27 to build the Mac component from source, until a release includes it.
  • No sudo. Every machine, its network and its desktop run as your user.

Apple allows two macOS virtual machines running at a time on one Mac, including those of other tools and macOS installation itself. You can create more machines and start any two.

Build the Mac component

From a Barn source checkout that contains barn mac:

make mac-build
export PATH="$PWD/bin/mac:$PATH"
barn mac doctor

bin/mac holds the CLI, Barn Mac.app (the native component that runs the machines and their desktops) and the guide; keep them together. The build is signed ad hoc for local use. doctor checks macOS, the component, and free disk space:

CHECK           RESULT  DETAIL
component       ok      /path/to/barn/bin/mac/Barn Mac.app/Contents/MacOS/barn-mac-runner
virtualization  ok      macOS 27.0.0 on Apple Silicon; virtualization supported
disk            ok      549.8 GiB free
data            ok      no Mac machines yet; barn mac up creates the first

Create your first machine

barn mac up

On a Mac without a prepared macOS, up first shows what it needs and asks:

→ macOS 27.0 (26A428) is not prepared on this Mac yet.
download:  24.8 GiB from Apple (updates.cdn-apple.com)
then:      install macOS once into a reusable base (about 20 minutes)
free:      551.2 GiB
Download macOS from Apple now? [Y/n]

After you confirm, Barn:

  1. Downloads the restore image from Apple only and verifies it against Apple’s published SHA-256. An interrupted download resumes where it stopped.
  2. Installs macOS once into an unbooted base. Installation uses one of the two macOS VM slots while it runs.
  3. Creates mac1 as a copy-on-write clone of the base, boots it, creates your account, and waits until SSH and sudo work.
  ✓  mac1 created and ready · macOS 27.0 (26A428) · [email protected]
shell:     barn mac ssh mac1
desktop:   barn mac open mac1

Every later machine reuses the base and is ready in tens of seconds; a machine created from a prepared base on the validation host was ready in 22 seconds.

If you already have Apple’s restore image, pass it instead of downloading. On the same APFS volume Barn clones it without copying; elsewhere it verifies and uses the file where it is:

barn mac up --ipsw ~/Downloads/UniversalMac_27.0_26A428_Restore.ipsw

Without a terminal, for example in a script, up needs --yes to download: it refuses rather than silently fetching 25 GiB. barn mac setup prepares the base ahead of time without creating a machine.

Work in the machine

Shell and commands

barn mac ssh                                  # interactive shell
barn mac exec -- sw_vers                      # one command
barn mac ssh -- 'id; sudo -n true && echo sudo works'
ProductName:		macOS
ProductVersion:		27.0
BuildVersion:		26A428

The account has your macOS user name (choose another with --user when creating the machine) and passwordless sudo. ssh passes a command line to the guest shell like plain ssh; exec keeps argument boundaries. Both return the guest’s exit status, and --json records stdout, stderr and the exit code:

barn --json mac exec -- sh -c 'echo out; exit 3'
{
  "command": "exec",
  "node": "mac1",
  "host": "10.10.20.10",
  "arguments": ["sh", "-c", "echo out; exit 3"],
  "success": false,
  "exit_code": 3,
  "stdout": "out\n"
}

The JSON above is shortened; the Mac reference lists every field.

Desktop

barn mac open

The desktop opens in a native window sized to your screen. Resizing the window changes the guest resolution, and View → Enter Full Screen works as usual. Closing the window keeps the machine running; open brings it back, and starts a stopped machine first. Keyboard shortcuts go to the guest while its window is focused, so the host commands live in the menu bar:

Menu Action
Machine → Share Clipboard turn clipboard sharing on or off for this session
Machine → Restart… restart macOS in the guest
Machine → Shut Down… shut down normally, like barn mac stop
Window → Keep Running in Background hide the window; the machine keeps running
Barn Mac → Quit Barn Mac… choose to keep the machine running or shut it down

The login password, needed for the lock screen and administrator prompts in the desktop, is random per machine. Copy it without printing it:

barn mac password --copy

Clipboard

Plain text follows your focus. What you copied on the Mac is available in the guest when you click into its window, and what you copy in the guest comes back when you switch to another app. It travels over the machine’s own SSH connection; nothing is installed in the guest. Items that password managers mark as concealed never leave the Mac. Images and files are not shared.

Turn it off for a machine with barn mac configure mac1 --clipboard off; the setting applies from the machine’s next start.

Shared folders

Share Mac folders when creating a machine. The guest mounts them under /Volumes/My Shared Files/<name>:

barn mac up dev --share ~/src --share docs=~/Documents:ro
barn mac exec dev -- ls "/Volumes/My Shared Files"

The name defaults to the folder’s last path component; :ro makes a share read-only. A share must be an existing directory, not a symlink; Barn never creates or deletes shared folders. To change shares later, stop the machine and use configure:

barn mac stop dev
barn mac configure dev --share data=/Volumes/Work/data --unshare docs
barn mac start dev

macOS guests can show stale file contents for a short while after the Mac changes a shared file. Use SSH or exec when you need an immediately consistent view.

SSH from other tools

When the first machine becomes ready, Barn adds one marked Include to ~/.ssh/config, so ssh mac1, scp, rsync, and editors with Remote-SSH reach every machine by name, with its own key and pinned host key:

ssh mac1 'uptime'
rsync -a ./project/ mac1:project/
barn mac ssh-config              # print the entries
barn mac ssh-config --remove     # remove only what Barn added

Lifecycle commands keep the entries current. A ~/.ssh/config managed by a dotfile tool through a link is never edited; Barn prints the Include line to add instead.

Several machines

Give each machine a name. Creation options apply only to a new machine:

barn mac up dev --cpu 8 --memory 16G
barn mac ls
NAME  STATE    ADDRESS      SSH    USER   OS          CPU  MEMORY  DISK                 SHARED
dev   running  10.10.21.10  ready  alice  macOS 27.0    8  16 GiB  504.0 MiB / 100 GiB
mac1  running  10.10.20.10  ready  alice  macOS 27.0    4   8 GiB  4.7 GiB / 100 GiB    src
limit:     2 of 2 macOS VMs are running; stop one before starting another

Names use lowercase letters, digits and inner hyphens and start with a letter. A command without a name acts on the only machine, or on mac1, and asks you to choose when that is ambiguous. DISK shows the space the machine uses now and its capacity. Capacity belongs to the base: a --disk other than the prepared base’s installs another base first, which needs the restore image again.

Each machine has its own private network: mac1 gets 10.10.20.10, later machines the next free /24, avoiding your LAN, VPNs and the Linux lab. Machines reach the internet and the Mac, but not each other. With two machines running, a third is refused before anything is created, naming a machine to stop:

barn mac up build --user ci
error: dev and mac1 are running; macOS allows 2 macOS virtual machines at a time
next: barn mac stop mac1

Everyday lifecycle

barn mac stop dev               # shut down through macOS
barn mac start dev              # boot and wait for SSH
barn mac restart dev            # stop, then start, applying changes
barn mac stop --all             # every machine

stop shuts down normally. A machine still running after two minutes is powered off, and the result says so. stop --force powers off at once, like holding a power button; unsaved work in the guest is lost. start --recovery boots macOS Recovery and shows its desktop.

up never reconfigures an existing machine. If you pass an option that differs, it refuses and names the command to use:

error: dev already exists, so --cpu would not apply; its configuration and data were preserved
next: barn mac configure dev --cpu 4

configure changes CPUs, memory, shared folders and the network while the machine is stopped, and clipboard sharing at any time. Changes apply at the next start:

barn mac stop dev
barn mac configure dev --cpu 6 --memory 12G --subnet auto
barn mac start dev

recreate replaces a machine with a fresh macOS from the base, keeping its name, account, resources, shared folders and address. destroy deletes machines. Both describe what they delete and ask you to type the command name; --force confirms without a terminal.

barn mac recreate dev
barn mac destroy dev build
Operation Guest disk and apps Settings, address, account
stop/start, restart, repeated up kept kept
configure kept changed as requested
recreate replaced with a fresh macOS kept; new password and SSH keys
destroy deleted deleted

The shared base is never changed by any of these, and is kept when machines are destroyed.

macOS versions and disk space

barn mac image ls
KIND  OS          BUILD   STATE  ON DISK   CAPACITY  USED BY
base  macOS 27.0  26A428  ready  26.7 GiB  100 GiB   mac1,default

Updates are explicit. barn mac image update asks Apple for the newest macOS 27, downloads it after you confirm, and makes it the base for new machines. Existing machines keep their macOS until you run barn mac recreate NAME --update. up and start never change a machine’s macOS.

image prune lists bases that no machine uses and that are not the default, and deletes them with --yes; --installers adds downloaded restore images. APFS clones share blocks, so ON DISK and machine disk figures are not exclusive usage and should not be added up.

barn mac image prune --installers         # review
barn mac image prune --installers --yes   # delete

Troubleshooting

Start with barn mac doctor; it checks the host, the component, the base and every machine, and prints a next: command for each failure. barn mac logs [name] shows the machine’s runtime log: startup, network, shutdown and Apple Virtualization errors.

Symptom What to do
network … overlaps route … on start A VPN or another tool now uses that subnet. Run barn mac configure NAME --subnet auto.
macOS allows 2 macOS virtual machines at a time Stop one of the named machines, or quit another tool’s macOS VM.
ssh mac1 from a third-party client says “No route to host” macOS Local Network privacy blocks that app from private networks. Allow it in System Settings → Privacy & Security → Local Network, or use /usr/bin/ssh. barn mac ssh and exec always use Apple’s tools and are not affected.
the Barn Mac component is not installed or speaks protocol … Keep barn and Barn Mac.app from the same build together; rebuild with make mac-build.
Starting fails from an SSH session to the Mac Run barn mac in a terminal of the Mac’s desktop session: machines need the logged-in user’s graphical session.

Apple Account sign-in inside a virtual machine is unreliable, and USB devices, snapshots and suspending a machine are not supported.

Clean up

barn mac destroy --force mac1 dev           # delete machines
barn mac image prune --installers --yes     # delete unused images

Destroying the last machine also removes its entries from ~/.ssh/config. The default base stays for new machines; to remove every Mac file including it, destroy all machines and then delete $BARN_HOME/mac (default ~/.barn/mac). Outside that directory Barn writes only its ~/.ssh/config entries, the desktop window positions in ~/Library/Preferences/io.pgsty.barn.mac-runner.plist, and a short runtime directory under /tmp. Nothing needs sudo.

1.7 - Image Repositories

Choose guest images, use a mirror, import a local qcow2, and prune the cache.

Normal use needs no image command first: barn up resolves u24:stable for the native host architecture and pulls the resulting immutable version. Barn uses the Catalog embedded in the installed build until you run barn update, which fetches, verifies, and activates the repository’s current Catalog. Nothing refreshes it automatically; image sync explicitly activates an exact URL or file for recovery.

Choose an image

Inspect the available aliases:

barn image list
barn image info u24
barn image info u24:stable

Built-in families are el7, el8, el9, el10, d12, d13, u22, u24, and u26. A bare name selects stable; name:channel selects a channel. An exact name@version key wins; a shorter numeric selector chooses the newest matching version on dot-component boundaries:

all:
  vars:
    vm_image: el9
    vm_version: "9.7"

Here 9.7 selects the newest 9.7.x build; 9 selects the newest 9.x release. Use vm_image: el9:stable and remove vm_version when the repository’s movable stable channel is the intended policy. A separate vm_version cannot be combined with :channel or @version in vm_image.

Run barn plan after editing the inventory. Changing an existing node’s image request requires an explicit barn recreate <node>; up reports the definition drift instead of rebuilding it. Updating the Catalog alone does not change existing nodes or their stored base-image identity. Newly created or explicitly recreated nodes resolve the selector against the active Catalog.

For a reproducible lab, pin the complete version shown by image info, rather than a movable channel or a numeric prefix:

all:
  vars:
    vm_image: [email protected]

Quote numeric vm_version values in YAML so their original text is preserved.

Warning

Built-in versions are supported except deprecated compatibility images: EOL el7, EL9 9.3/9.6, and EL10 10.0.

Use a mirror

Released builds use https://repo.pigsty.io/barn by default. Select the official China repository for one command with long-only --mirror, or name a custom root with --repo:

barn image pull u24 --mirror
barn up --mirror
barn update --repo https://mirror.example/barn
barn image pull u24 --repo https://mirror.example/barn
barn up --repo https://mirror.example/barn

Or set the default repository for the current shell:

export BARN_REPO=https://mirror.example/barn
barn update
barn up

Selection precedence is --repo, --mirror, BARN_REPO, then the global default. --mirror resolves to https://repo.pigsty.cc/barn and both official roots retain canonical signed-Catalog trust. BARN_REPO may also be an absolute local directory. Explicit local and HTTPS repositories may use an unsigned Catalog; HTTP repositories require a Catalog signed by a trusted key. Artifact size, SHA-256, and qcow2 structure are always verified.

Image downloads retry transient failures and resume interrupted transfers. Since 0.7.0, the two official repositories can fall back to one another if the selected endpoint cannot supply an image; the same Catalog size and digest must still match. Custom repositories remain exclusive. Catalog upstream URLs are provenance, never an alternate download source.

This fallback concerns image artifacts. barn update fetches the selected repository’s Catalog; image sync reads the exact URL or file you supply. Neither command upgrades the Barn executable. The active Catalog is scoped to the selected repository. A new --repo uses the embedded Catalog until you activate that root’s Catalog; changing the download source alone does not make custom aliases appear.

Build a static repository

A repository is an ordinary directory that can be copied with rsync or served by a static HTTP server:

barn/
├── repo.yaml
├── catalog.json
├── catalog.json.minisig       # required for official and HTTP repositories
└── images/
    └── d13-1-arm64.qcow2

repo.yaml is the only human-maintained source. For the single arm64 image shown above, a minimal complete source is:

schema: 1
revision: 1
defaults: { image: d13, channel: stable, arch: native, boot: uefi }
images:
  d13:
    channels: { stable: "1" }
    versions:
      "1":
        status: testing
        variants:
          arm64:
            source_user: debian

Place your independently verified, cloud-init-capable image at /srv/barn/images/d13-1-arm64.qcow2. Use amd64 in both the filename and variant for an x86 guest, and set source_user to the image’s source identity. The repository root must be an absolute, non-symlink directory that is not writable by group or others. Generate catalog.json locally:

barn repo scan /srv/barn
barn repo build /srv/barn
barn repo verify /srv/barn

Scan is read-only. Build never changes repo.yaml or image bytes; it performs a full qemu-img check and materializes file names, SHA-256, artifact size, and virtual size. build and verify require local qemu-img; scan does not. Build on a machine with QEMU, then publish immutable QCOW files first and catalog.json with its matching signature last. The local/HTTPS example may remain unsigned; plain HTTP and official repositories require a trusted signature. Increase revision whenever Catalog contents change.

Activate and inspect this local repository before creating VMs:

barn update --repo /srv/barn
barn image info d13 --arch arm64 --repo /srv/barn
barn image pull d13 --arch arm64 --repo /srv/barn

Use the same --repo /srv/barn for plan, up, and recreate, or export BARN_REPO=/srv/barn. In the Inventory select vm_image: d13@1 and vm_arch: arm64; importing a Catalog does not rewrite Inventory defaults. barn image reset --repo /srv/barn restores the embedded Catalog for that root while preserving its anti-rollback history.

Import and prune

For a single custom image on the host’s native architecture, import it with an independently obtained digest. --sha256 is optional in the CLI but is recommended when you have a trusted digest:

barn image import --name local-mybase --boot uefi \
  --source-user ubuntu --sha256 <digest> /path/to/base.qcow2

Custom aliases must begin with local-; --name, --boot, and --source-user are required together. Import checks the qcow2 and copies it into Barn’s cache without preparing its guest software. The image must already support Barn’s cloud-init bootstrap. Named imports record the host architecture, so use a static repository for foreign-architecture images. Select the alias with vm_image: local-mybase, then run barn plan.

Prune protects every image in the selected active Catalog, every applied node image, and every registered local alias. It therefore does not empty the cache merely because all VMs were destroyed. Inspect candidates before deletion:

barn image prune --dry-run
barn image prune --yes

See Images for signatures, rollback protection, cache layout, architecture, and TCG rules. See the Image Pipeline for preparing image candidates and the separate checks required before publication.

1.8 - Build from Source

Build Barn for development and review, then run the complete source gate.

Barn 0.9.0 is an unreleased candidate. Use this page to build and check a checkout containing the rename. Installation commands for the eventual release are in the Quick Start.

Choose the source

Use a checkout containing the Barn changes. Once that source is pushed to the public repository, it can also be cloned with:

git clone https://github.com/pgsty/barn.git
cd barn

Do not assume a v0.9.0 tag exists before publication. Check the source identity and working-tree changes first; go.mod must name github.com/pgsty/barn and the command source must live in cmd/barn:

git log -1 --oneline
git status --short

Build

The reviewed candidate tree pins Go 1.27.1 in go.mod and packaging/toolchain.env. You also need Git, Make, Bash, and standard build tools. QEMU and privileged network setup are needed to run VMs, not to compile the CLI. From the selected checkout:

make build
export PATH="$PWD/bin:$PATH"
barn version

make build writes the matching barn and barn-hosts-helper binaries under the Git-ignored bin/ directory. Do not mix the two binaries across commits or releases. Development builds report dev by default; their commit field identifies a clean source revision or says uncommitted for a dirty checkout. The version string alone does not prove candidate behavior.

Keep the inventory in a separate lab directory. This does not isolate Barn state: if you already have a deployment, inspect it before running up:

mkdir -p ~/barn-lab && cd ~/barn-lab
barn setup
barn up

Complete checks

The complete checks also need Python 3, jq, a working C toolchain for race tests, and the pinned quality tools. Install the versions used by the reviewed candidate’s CI (check CONTRIBUTING.md and packaging/toolchain.env again when using another revision):

go install honnef.co/go/tools/cmd/[email protected]
go install golang.org/x/tools/cmd/[email protected]
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
go install golang.org/x/vuln/cmd/[email protected]

Ensure the Go tools installation directory (GOBIN, or $(go env GOPATH)/bin when unset) is in PATH. Before submitting a source change, run:

make check

This gate includes module verification, shell syntax, maintenance ownership, unit and race tests, Vet, Staticcheck, four-target dead-code checks, errcheck, vulnerability checks, cross-builds, image-pipeline and installer tests, and dependency-license verification. CI separately checks formatting, whitespace, pinned tool versions, and GoReleaser configuration. Changes to packaging also need a verified packaging snapshot.

A passing source gate is not package publication or a native VM lifecycle replay; make image-pipeline-native-test is a separate native image gate. That gate requires the explicit image inputs documented at the top of tests/image-pipeline-native-test.sh; it does not download a test image.

See Engineering for release tooling, dependency licenses, and validation boundaries.

1.9 - Uninstall and Clean Up

Safely remove the Barn deployment, integrations, images, host network, and default state directory.

This page deletes VMs and local data. Inspect the current state and stop if any Barn VM must remain:

barn st

1. Remove the deployment

Delete nodes, persistent disks, keys, and deployment state:

barn purge

This whole-deployment command asks for no confirmation. The image cache and host network remain. Use the granular, confirmed barn destroy command instead when preserving persistent disks or removing selected nodes.

2. Remove optional integrations

Whole-deployment destroy already removes the default barn SSH integration. If you installed a custom fragment name or /etc/hosts entries:

barn ssh-config --remove --name lab
barn hosts uninstall --json
barn hosts uninstall --yes

Without --yes, the --json command only shows the marker-owned plan. Apply the --yes command after checking its target. In Barn 0.9.0, ordinary terminal output asks [y/N] and applies removal after confirmation. Barn reads the hosts plan without sudo; applying the change still needs privilege.

3. Remove cached images

barn image prune --dry-run
barn image prune --yes

Prune removes unreferenced cached images and stale staging files. It protects images referenced by deployment state, the active Catalog, and registered local aliases, so it is not a complete cache wipe. The optional final state-directory cleanup below removes the remaining cache too.

4. Uninstall host networking

barn network uninstall --json
barn network uninstall --yes

The first JSON command only shows the owned removal plan, although sudo may be needed to read protected network state. Uninstall refuses while any VM remains attached. The network is shared across users; removing your deployment does not establish that another user’s VMs have stopped.

5. Clean up a source setup

Network uninstall preserves the independently useful hosts helper. Only after confirming Barn is no longer needed, remove these exact paths:

sudo rm -f -- /opt/barn/libexec/barn-hosts-helper
sudo rmdir /opt/barn/libexec /opt/barn

With the default state directory, and only after every earlier step succeeds, remove the remaining state:

(
  set -eu
  test -z "${BARN_HOME:-}"
  barn_state_root="$(cd "$HOME" && pwd -P)/.barn"
  test ! -L "$barn_state_root"
  if test -d "$barn_state_root"; then
    printf 'removing exact state root: %s\n' "$barn_state_root"
    find "$barn_state_root" -depth -delete
  fi
)

This snippet stops if BARN_HOME is set or the default path is a symlink. Review a custom state directory separately; never substitute $HOME, /, a workspace root, or an unverified path. After deletion, avoid running lifecycle commands just to check that the directory is gone: they may recreate lock directories.

QEMU may be shared by other tools, so keep it by default. On macOS, remove it only when nothing else needs it:

brew uninstall qemu

Verify network removal and the default state directory:

barn network status --json
test ! -e "$HOME/.barn" && echo 'no Barn state'

An uninstalled network is expected to report an absent/not-ready finding; inspect the result rather than requiring a zero exit code. Do not use bridge100 disappearing as proof: macOS chooses its bridge name and may use other vmnet bridges for unrelated software.

Remove an Archive, Homebrew, DEB, or RPM binary through its installation channel. The bin/ directory from a source build is only a checkout artifact, separate from the host state above.

2 - Reference

Exact contracts for the Pigsty-compatible Inventory and Barn command line.

This reference describes the Barn 0.9.0 release candidate. Barn uses only the new names and fresh Barn state; it has no compatibility or migration layer for earlier development builds. Check barn version before scripting against these contracts. See the release status.

  • Configuration — discovery, accepted variables, defaults, disks, shares, naming, and drift.
  • CLI — commands, important flags, output modes, and exit codes.
  • Mac Commands — the unreleased barn mac: commands, JSON results, and failure reasons.
  • Images — signed catalogs, aliases, cache layout, pulls, imports, and pruning.
  • Image Pipeline — candidate validation and offline normalization.

Barn exposes no supported Go library API. Packages under internal/ are implementation details.

2.1 - Configuration

The Pigsty-compatible Inventory fields Barn reads, their defaults, and node-level drift behavior.

This reference describes the Barn 0.9.0 release candidate. Barn uses only the new names and fresh Barn state; it has no compatibility or migration layer for earlier development builds. Check barn version before scripting against these contracts. See the release status.

Discovery

Configuration lookup order is explicit -f, then barn.yml, barn.yaml, pigsty.yml, and pigsty.yaml in the current directory. Every name uses the same Pigsty-compatible YAML Inventory format.

For plan, up, reload, and recreate, absence of a file falls back to the applied spec when a deployment exists. validate has no fallback. A configuration must be a regular non-symlink file no larger than 4 MiB. The first existing discovery candidate wins; if it is invalid, Barn reports the error instead of trying the next filename. Renaming a file or moving directories does not create another deployment; applied state lives in BARN_HOME.

A complete inventory

all:
  vars:
    admin_ip: 10.10.10.10
    vm_image: u24
    vm_cpu: 2
    vm_mem: 4GiB
    vm_disk: 64
  children:
    lab:
      hosts:
        10.10.10.10: { nodename: meta }
        10.10.10.11:
          nodename: worker
          vm_mem: 8GiB
          vm_disks:
            - { path: /data, size: 128, fs: auto, persistent: true }

This creates two managed definitions; the control node is meta. Save it as barn.yml, then inspect it without starting VMs:

barn validate -f barn.yml
barn --json validate -f barn.yml
barn plan -f barn.yml

The JSON validation result includes valid, source, spec_hash, and resolved. In the 0.9 candidate, validate also resolves catalog image references and accepts --repo; an unreadable catalog adds a warning instead of a false success claim about images, and local-* image-byte checks remain the responsibility of up. Validation does not prove host resources, share access, networking, image bytes, or guest readiness. It never starts VMs or downloads images.

What Barn reads

Barn reads host IPs, nodename, admin_ip, pg_cluster, pg_seq, node_admin_username, node_admin_uid, and the documented vm_* variables. admin_ip is read from all.vars; it selects the control node, or the first managed host is used. All nodes must resolve the same login username. For the default dba user, an explicit node_admin_uid must be 88. For a custom user, node_admin_uid is validated as an integer but does not set the guest UID; Barn does not expose a general UID customization contract.

Everything else is opaque and cannot create drift. This means unconsumed fields such as pg_role, pg_version, repo_*, and node_packages, not all possible pg_* or node_* names.

Inside this namespace validation is strict: unknown vm_* names, wrong types, Jinja expressions, invalid addresses, and conflicting sibling-group values are errors. Inheritance is all.vars → deeper children.<group>.vars → host variables. Host values replace an entire list such as vm_disks; lists are not appended. Different values inherited from groups at the same depth must be resolved with a host-level override. YAML anchors and merge keys are supported; explicit keys win, and the first mapping in a merge sequence wins. Duplicate mapping keys and multiple YAML documents are errors.

Host keys must be IPv4 addresses even for vm_skip: true entries. Skipped hosts do not consume the 20-node budget or determine the managed subnet, but the inventory must still contain at least one managed host. Skipping an already applied node marks it as removed; it does not destroy that VM.

The 0.9 candidate improves errors with the offending rule, value, and line where available, and suggests a nearby vm_* spelling. Those diagnostics do not add new inventory variables.

VM variables

Variable Default Meaning
vm_skip false do not virtualize this real/external host
vm_image u24 image family, channel reference, or image@version selector
vm_version unset newest numeric version matching this prefix, such as 9 or 9.7
vm_arch native deployment-wide Guest architecture: native, amd64, or arm64
vm_cpu 2 vCPU count
vm_mem 4096 MiB integer, or a size such as 8GiB
vm_disk 64 root disk: GiB integer or an explicit size such as 64GiB
vm_disks [{path: /data}] extra disks (one 128 GiB non-persistent disk at /data by default)
vm_alias [] guest /etc/hosts, SSH-config, and optional host aliases
vm_shares [] QEMU 9p host-directory shares

An empty host entry is a complete VM. A deployment contains 1–20 managed hosts; vm_cpu accepts 1–256 and memory must be at least 512 MiB. Bare integer memory is MiB; bare integer disk sizes are GiB. Explicit size strings accept positive integers plus B, KiB, MiB, GiB, TiB, KB, MB, GB, or TB (case-sensitive). 8GiB is valid; 8G, 1.5GiB, and the quoted unitless string "8192" are not. Root/data disk sizes must be positive; up also checks the root disk against the selected base image’s virtual size.

Omitting vm_image selects Ubuntu 24.04. To use Debian 13, set vm_image: d13 under all.vars. Inspect barn plan before applying changes to an existing Barn inventory.

vm_version keeps short version intent separate from the image family:

vm_image: el9
vm_version: 9.7

An exact Catalog version wins first. Otherwise Barn matches only on a dot component boundary and selects the numerically newest match: 9.7 resolves to the newest 9.7.* build, while 9 resolves to the newest 9.x release. Numeric components are compared as integers, so 9.10 sorts after 9.9. Do not combine vm_version with a vm_image that already contains :channel or @version.

vm_arch is stricter than ordinary per-host VM fields: when present it must resolve to one value on every managed host, so define it once in all.vars. Changing it is a deployment-envelope change and requires whole-deployment recreation. Linux setup installs only the native emulator; a foreign architecture also needs its matching qemu-system-* binary and firmware.

Data disks

vm_disks:
  - path: /data
    size: 128
    fs: auto
    persistent: false

path is the disk identity and mount point. fs is auto (the default), xfs, or ext4. A blank auto disk is formatted XFS when the guest has mkfs.xfs and ext4 otherwise, which matches what the Vagrant flow did. Explicit xfs and ext4 never fall back. Healthy existing filesystems are reused. persistent: true keeps the disk across an ordinary destroy; vm_disks: [] means no extra disk. size defaults to 128 GiB for each entry; integer sizes are GiB and explicit size strings are accepted.

Use clean absolute mount paths such as /data or /data/pg. The derived disk identity trims leading/trailing / and replaces inner / separators with - and must match [a-z][a-z0-9-]{0,31}; identities and mount paths must be unique within the node. /, system paths such as /etc, /usr, /root, and /var/lib/barn, and their overlapping parents/children are refused. Changing a persistent disk’s identity or declaration can require explicit migration; persistent does not mean every new definition can automatically reuse the old disk.

Data disks are disposable test storage. up resets unrecognized or confirmed damaged filesystems to the configured type and reports that old data was discarded. This includes persistent disks: persistence controls destroy/recreate, not retention of corrupt contents. Probe errors, missing devices, busy mounts, and backend I/O failures do not authorize formatting. Root disks and host shares are outside this recovery path.

Shares

macOS limitation: Barn’s guarded directory sharing is not supported on macOS; a node with vm_shares cannot start. The candidate also warns during validate and plan. Omit shares in new macOS labs; full macOS sharing support remains pending. Changing an existing node’s shares requires recreate, which replaces the root disk. Preserve needed data before considering that operation. Barn does not fall back to unchecked host paths.

vm_shares:
  - host: /absolute/owned/source
    guest: /src
    readonly: true

readonly defaults to true; each node accepts at most eight shares. Host and guest paths must be clean absolute paths; ~ and relative paths are not expanded. Host directories must already exist, be caller-owned, have no symlink path components, and not overlap BARN_HOME. On Linux, prefer the real path (realpath /path/to/source). The 0.9 candidate includes that replacement path in a symlink diagnostic.

Within one node, host sources and guest targets must not overlap. Across nodes, overlapping host sources are allowed only when all are read-only. Guest targets must not overlap data-disk mounts, reserved system paths, or the login user’s .ssh directory. Shares are for trusted development files, not PostgreSQL data. If a requested writable share cannot support guest writes, Barn tries read-only access and reports the limitation. Correct permissions and repeat up to retry; Barn does not recursively change the ownership of host files.

A missing source fails only its node during up or start. Other selected nodes continue. Restore the original directory or its host mount and retry that node; Barn never creates an empty replacement. Restart, reload and recreate validate sources before stopping existing nodes.

Names and addresses

Node name order: nodename, then <pg_cluster>-<pg_seq>, then node-<last-octet>. Names must be unique, 1–63 lowercase letters/digits/hyphens, and may not begin or end with -. An explicit non-empty nodename takes precedence, so unrelated pg_cluster or pg_seq values need not derive a name. vm_alias is a list of lowercase DNS-style names; aliases must not duplicate a node name or any other alias in the deployment.

All managed hosts must be in one RFC1918 /24: .1 is the host, .2–.8 are reserved, and nodes use .9–.254.

Inside the guest the fixed-IP interface is the one carrying the inventory address (ip -br addr); its name is not a Barn contract.

Drift

Barn hashes each resolved node. Added hosts are created by up; selected existing stopped nodes are started; running peers keep their processes while unfinished guest setup is retried. Changed VM definitions require per-node recreate; removed hosts are reported but never destroyed. Deployment architecture, user, or subnet changes require whole-deployment recreation. Image selectors are resolved to exact image identities by plan/up; review the plan after a catalog update, even if the inventory text is unchanged. Changing a field used to derive a node name appears as a missing old node plus a new node, so prefer stable explicit nodename values.

2.2 - CLI

Barn commands, important flags, structured output, and exit codes.

This reference describes the Barn 0.9.0 release candidate. Barn uses only the new names and fresh Barn state; it has no compatibility or migration layer for earlier development builds. Check barn version before scripting against these contracts. See the release status.

barn [--json|--yaml] [-v|--verbose] <command> [flags] [node...]

The installed binary is the authoritative reference for its own version. Every visible command includes its operational boundary and copyable examples:

barn --help
barn setup --help
barn image pull --help

Bare barn prints a short welcome with next commands, exits 0, and exposes actions[] in JSON/YAML. A bare namespace such as barn image still exits 2 (human help in text mode, a structured usage error in JSON/YAML). Explicit --help always renders human help and exits 0. Use barn --version or barn version to inspect the build identity.

Commands

Area Commands
Prepare setup, init, validate, doctor
Lifecycle plan, up, start, stop, restart, reload, recreate, status, destroy, purge
Access ssh, exec, logs, provision, ssh-config, hosts install/uninstall
Images update, image list/info/pull/import/sync/prune/reset, repo scan/build/verify
Host network network status/install/uninstall
macOS guests (unreleased) mac …; see Mac Commands
Misc version, completion

No command refreshes the Catalog implicitly. update fetches the configured repository’s Catalog, verifies it, and activates it; image sync is the explicit recovery path for an exact URL or file. Ordinary commands use the active local Catalog. Neither operation updates the Barn executable.

Frequently used commands have scoped aliases:

Command Aliases Command Aliases
setup s validate v
plan pl recreate rc
status st destroy de
ssh-config sc image images, im
doctor dt network n, net
exec / logs ex / l version ver

up, ssh, init, start, stop, restart, reload, provision, hosts, and completion have no aliases. Use the explicit barn purge spelling; rm is not a command alias. Inside a namespace, hosts and network use i/u for install/uninstall; network status uses st; and image maps list=ls, info=in, pull=p, prune=pr, sync=sy, and import=i. image reset keeps reset-manifest as a compatibility alias.

Barn manages one deployment in the selected BARN_HOME (default ~/.barn), independently of the inventory directory. Commands using applied state work from any directory. Configuration selection is command-scoped; -f is deliberately not a global flag:

Commands Desired-state source
setup [template] explicit -f, otherwise discovery, otherwise generate meta; template and -f are exclusive
init [template] generate a new inventory; read no desired state; --force explicitly replaces the output
validate explicit -f, then discovery; never applied state
plan, up, reload, recreate explicit -f, then discovery, then the applied resolved specification
other lifecycle/access commands no desired-state inventory; they use applied state

When neither an inventory nor an applied deployment exists, interactive up can generate the default inventory. It can also prepare missing host dependencies and restore an intact inactive Barn network. This implicit preparation accepts the setup plan; sudo may still request credentials. Use setup --dry-run to review the host plan first. Scripts should run setup --yes explicitly; init is optional when no inventory exists.

Important flags

Flag Meaning
--json, --yaml machine-readable stdout; progress remains on stderr; intentionally no shorthand
-v, --verbose bounded diagnostics on stderr
-c, --cidr select the RFC1918 /24 for generated init/setup templates or host-network inspection/install
-f, --file select an Inventory for commands that read desired state
-r, --repo select a repository where exposed; overrides --mirror and BARN_REPO; validate gains this flag in the 0.9 candidate
--mirror use the China official repository for setup, Catalog, and image-resolving lifecycle commands
-m, --mode select host or shared where the command exposes the macOS network mode
-d, --dry-run show a setup/image plan without changing state
-y, --yes apply a displayed host/setup/image plan
--force (init, destroy, recreate) overwrite generated output or skip the typed confirmation; long-only because -f selects the Inventory
-n, --no-wait return once QEMU is running, without readiness or guest recovery checks
--rollback (up, reload) remove the prepare artifacts of nodes that failed to prepare in this run
--delete-persistent during whole destroy, also delete retained data disks; invalid with node selectors
--purge whole-deployment disposal: delete disks, keys, and deployment state; keep images

Flags belong to commands; the following table lists the less obvious scopes:

Command Local controls
init --output/-o (default ./barn.yml; - prints), --cidr/-c, --force
plan --file/-f, --repo/-r; no --mirror or --dry-run
start, restart --no-wait/-n; no --file or repository selection
provision required --script/-s; --sudo uses guest sudo -n; --parallel/-p is 1–4 (default 1); --timeout/-t is positive, at most 24h (default 1h)
ssh-config --install/-i or --remove (exclusive); --name defaults to barn; removal accepts no nodes and requires no deployment
logs --source/-s serial|qemu|events (default serial); --follow/-f; events accept no node
image info, image pull optional image selector, --arch/-a amd64|arm64, --repo/-r; only pull accepts --mirror
image import --sha256/-s; --name local-* also requires --boot/-b bios|uefi and --source-user/-u
image prune --dry-run/-d or --yes/-y (exclusive); --repo/-r
image sync URL or path, --repo/-r, and explicit --allow-downgrade
network install --cidr/-c (default 10.10.10.0/24), --mode/-m (default host), --yes/-y; macOS-only --archive/-a, --interface-id/-i
network status optional --cidr/-c; no --file
network uninstall, hosts install/uninstall --yes/-y

setup --dry-run and setup --yes are mutually exclusive. --cidr rebases a generated template; it does not rewrite an explicitly selected inventory. validate --repo is available in the 0.9 candidate and has no --mirror; use --repo https://repo.pigsty.cc/barn when checking that catalog.

In the 0.9 candidate, network and hosts install/uninstall show a plan and ask on a terminal (install defaults to yes, uninstall to no); without a terminal they only show the plan unless --yes is supplied. For a fresh macOS network, use setup; the candidate’s network install directs you there before asking for sudo. --yes accepts Barn’s plan but cannot supply a sudo password.

Rare, selection, or safety-widening controls such as --mirror, --force, --rollback, --remove, --allow-downgrade, --sudo, --delete-persistent, and --purge are long-only. On commands that read an Inventory, -f always selects a file; logs -f retains the conventional --follow. -n always means --no-wait, and -d always means a dry run.

With a deployment, barn purge performs the same disposal as barn destroy --force --purge, without confirmation. It accepts no nodes or Inventory, removes the complete deployment plus persistent disks, keys, state, and the default SSH fragment, and keeps images and the host network. With no deployment it succeeds without changing the image cache, and can remove provably owned retained disks. Missing state never authorizes deletion of residual node artifacts whose identity cannot be proven. In the 0.9 candidate, plain whole-deployment destroy without a deployment also succeeds; destroy --force --delete-persistent and destroy --force --purge without state instead fail with a hint to use purge.

Structured failures (0.9 candidate)

The following unified failure contract describes the Barn 0.9.0 release candidate.

Ordinary failures print error: <message> on stderr, the failing program’s last stderr lines when an external tool failed, and a next: line when there is one clear action. SSH child exit failures are silent because the child has its own output. If a failing command supplies no richer typed result, structured mode writes a generic failure object before returning the exit code: error (the class below), message, and where they apply a stable reason, next, operation_id, and command (name, argv, exit_status, signal, timed_out, stderr) for a failed external program. Existing typed failure results are never followed by a second JSON/YAML document. The closed generic error classes are listed below. recreate_required and nodes_removed are now reason values under error: "conflict", and the old resource_conflict class is now resource.

A nonzero exit does not guarantee that stdout has this generic envelope. doctor, network, provision, lifecycle operations, and remote commands can return their own report schemas. SSH child exits are internally classified as remote_exit, but their public result has fields such as success, exit_code, stdout, and stderr; its optional error is not the generic class contract. Always preserve the process exit code and interpret the payload for that command.

Lifecycle results

plan is read-only and returns success even when its action is recreate or blocked-removal; automation must inspect the action and create, recreate, start, missing, and blocked fields. For catalog images, plans read local configuration and catalog data without requiring QEMU or host networking. A registered local-* image also undergoes cache validation and requires qemu-img. Plans download no images. They show exact images, total resources, change reasons, and disk effects. The 0.9 candidate also lists each data disk, including the implicit 128 GiB /data. up checks host capabilities and address availability before applying changes. up creates missing nodes, starts stopped ones, re-checks readiness of running ones, and rewrites the SSH client configuration Barn installed from the complete applied deployment. recreate performs the same full refresh; node destroy removes stale entries, and whole destroy removes that configuration. start powers on stopped nodes and re-checks readiness of running ones; start and restart also refresh SSH aliases. Destructive drift returns a conflict that names the next commands: barn plan, then barn recreate <node> or barn destroy <node>. On a terminal those commands ask you to type the confirmation word; --force is for scripts. If VM lifecycle succeeds but the SSH client configuration cannot be written, the command reports a warning and remains successful; barn ssh still works. Structured output carries integration warnings in warnings[]. The 0.9 candidate leaves symlinked or hard-linked ~/.ssh/config untouched, publishes its fragment, and reports the Include line to add manually.

Guest management SSH is the readiness boundary. Optional setup failures are reported in nodes[].warnings; completed recovery actions, including data resets, are in nodes[].repairs. A usable guest with these limitations exits 0. Repeat up to retry unfinished stages without restarting running VMs. Inspect the warning fields when automation requires every configured feature.

A lifecycle batch with isolated node failures can exit 5 even when every selected node failed; it reports N of M node(s) failed: <node> (<stage>: <error>); .... Common stages include prepare, start, readiness, bootstrap, guest-setup, stop, and status; readiness or bootstrap failures add run \barn logs ` for the guest console. Structured output carries failures[]withnode, stage, and error(and optionalreasonin the **0.9 candidate**), plusrolled_backwhen–rollback` removed the prepare artifacts of nodes that never committed. See A node did not become ready.

status shows node, state, IP, exact image, and CPU/memory. Use --verbose for SSH ports, architecture, accelerator, and PID. TCG is marked in ordinary text as well. One degraded node does not hide its peers; status exits 5 and retains per-node errors and failures[]. Running means the VM process is running; status does not claim to have checked guest readiness.

Starting commands also refresh Barn hosts and control-node SSH entries in running guests. Stopped guests catch up when started. --no-wait skips guest readiness, guest recovery, and that refresh; a later up completes them. Selected recreate refuses remaining peer drift before stopping or deleting disks; select the required nodes together as directed.

The control guest’s Barn-managed SSH entries accept replacement host keys without recording them in known_hosts, so recreated lab nodes remain reachable. User-added SSH entries are preserved.

Recovery

up and start isolate missing host-share failures by node; up also continues existing stopped peers when a new node fails to prepare. Partial results keep exit code 5 and preserve successful nodes. Retry hints retain the inventory, repository and applicable flags; a start retry remains start.

Setup and its lifecycle retry share one operation_id. Even before deployment state exists, barn logs --source events --json can read the bounded phase trace after failed setup. Setup traces omit command arguments and authentication data; retain the command output for its detailed cause. setup --dry-run writes no trace. Successful destroy --delete-persistent and purge summaries describe the final deletion/retention result; purge leaves the image cache and host network. Owned persistent disks left by a previously removed node no longer block destroying the remaining nodes. Ordinary destroy retains those disks; explicit persistent deletion or purge is still required to remove them.

On macOS, fresh network setup finishes Homebrew discovery/installation or the pinned-archive download before requesting administrator authentication. Homebrew can invalidate an earlier sudo credential; the new order avoids that failure without widening the privileged operation. Failed downloads do not prompt.

Interrupted operations (0.9 candidate)

The candidate recognizes a recorded QEMU PID reused by an unrelated process as a stopped node. An interrupted stop whose VM still runs is reconciled to running; other unfinished transitions name the command that can finish them. destroy settles interrupted transitions itself. A failed first up can be retried after editing the inventory because its uncommitted artifacts are rolled back from the journal.

Logs and environment

logs defaults to the guest serial console; --source qemu reads QEMU diagnostics, and --source events reads the deployment-wide bounded event log. With --follow, text output streams bytes and JSON emits NDJSON records (YAML emits a document stream). The 0.9 candidate renders ordinary event/QEMU log reads as readable records, showing a QEMU argv only with --verbose.

Environment variable Purpose
BARN_HOME absolute private state directory; default ~/.barn; not a symlink or broad directory such as your home
BARN_REPO repository default; overridden by --mirror, then --repo where exposed
BARN_OUTPUT text, json, or yaml; presentation flags override it
BARN_VERBOSE boolean diagnostic default; presentation flags override it
BARN_VMNET_ARCHIVE absolute path to the pinned socket_vmnet archive for macOS setup; digest checks still apply
NO_COLOR non-empty disables color

SSH passthrough and completion

barn ssh [node] [--] [command ...] opens a session or runs an optional command. barn exec [node] [--] <command ...> requires a command and passes through its exit status. Presentation flags before -- belong to Barn; ssh arguments after -- are joined with spaces and interpreted by the remote shell, like plain SSH. exec preserves argument boundaries; explicitly use sh -c when you need shell expansion or pipelines. A single command string retains the shell shorthand. Before --, only zero or one known node is accepted. For convenience, omitting -- uses a known first argument as the node, or runs all arguments as a command on the default node with a warning. In the 0.9 candidate, a first argument containing a digit or - is checked for a near-miss node name: at most one edit for words of four characters or fewer, or two edits for longer words. Such a typo is refused; ordinary commands such as ls, df, and wc still run. Use an explicit -- in scripts.

Load barn completion bash|zsh|fish|powershell for command and scoped-flag completion. It also provides command aliases, templates, image aliases, closed flag choices, and best-effort node names from the desired or applied specification. In the 0.9 candidate, -f completion filters for YAML files.

Exit codes

This table gives the Barn 0.9.0 release candidate exit-code contract. Missing inventory and unknown images are usage errors (2); setup and network failures use runtime (1) or capability (3), according to their cause.

Code error Meaning
0 success, including usable guests with optional limitations
1 runtime the operation ran and failed (a tool, download, or guest failed)
2 usage the command line or inventory is wrong
3 capability the host lacks a tool, the Barn network, or a privilege
4 conflict the deployment’s state forbids it, or another barn command holds it
5 partial node-level batch failures; inspect failures[]; any successful peers are retained
6 resource a host address, port, subnet, or disk is taken
7 integrity a verified digest, signature, identity, or ownership did not match
130 cancelled interrupted (SIGINT/SIGTERM) or confirmation declined

In the 0.9 candidate, a modifying command that finds another Barn command holding the deployment waits up to 10 minutes and names it; timeout is exit 4 with reason: "deployment_busy". status, ssh, exec, ssh-config, and hosts do not wait. status reports the concurrent operation in note while showing recorded state; this is not a guarantee of a completed deployment.

ssh and exec pass through the SSH child exit code unchanged, including 255. That value may indicate an SSH connection failure or a remote command returning 255; text, JSON, and process exit status agree.

2.3 - Mac Commands

barn mac commands and flags, machine rules, JSON results, failure reasons, networks, and files.
Important

Barn 0.9.0 release candidate; unreleased. This page describes the renamed barn mac, which uses fresh Barn state and provides no development-state migration. Earlier native records are retained in Status and do not establish the same acceptance for the renamed build. Check the barn mac --help of the binary you run.

barn [--json|--yaml] [-v|--verbose] mac <command> [flags] [name...]

barn mac requires Apple Silicon and macOS 27 or later, and runs as the logged-in user; it refuses to run as root. On other hosts the command is hidden, and commands that need the Mac component fail with mac_host_unsupported. Bare barn mac is barn mac ls.

Commands

Command Purpose
ls every machine with state, address, SSH, macOS, resources and shares; aliases list, status, st
up [name] create the machine if needed, start it, wait for SSH and sudo
start [name...] start existing machines
stop [name...] shut down normally; power off after two minutes
restart [name] stop, then start, applying configuration changes
open [name] show the desktop, starting the machine first if needed
ssh [name] interactive shell, or, after --, a command line for the guest shell
exec [name] -- cmd run a command, keeping argument boundaries
configure name change a machine’s settings
recreate name replace the machine with a fresh macOS, keeping its settings
destroy name... delete machines
password [name] show or copy the login password
ssh-config print, install or remove the OpenSSH entries
logs [name] recent runtime log
setup prepare the macOS base without creating a machine
image ls restore images and bases with the machines using them; alias list
image update prepare Apple’s newest macOS 27 as the default base
image prune list, and with --yes delete, unused images
doctor check the host, the component, the base and every machine

Flags by command

Command Flags
up --cpu --memory --disk --user --share --clipboard --subnet --ipsw -y/--yes -n/--no-wait --open
start --all -n/--no-wait --open --recovery
stop --all --force
restart -n/--no-wait --open
configure --cpu --memory --share --unshare --clipboard --subnet
recreate --update --force -n/--no-wait
destroy --force
password -c/--copy
ssh-config -i/--install --remove
logs -n/--lines (default 100, at most 10000)
setup --ipsw --disk -y/--yes
image update --ipsw -y/--yes
image prune --installers -y/--yes

A command without a machine name acts on the only machine, or on mac1; with several machines and none named mac1, it asks for a name. start and stop accept several names, or --all. destroy and recreate describe what they delete and ask for the command name to be typed; --force confirms without a terminal.

Flag values

Flag Value
--cpu virtual CPUs, at least 2 and at most the Mac’s logical CPUs; default 4
--memory 16G, 16GiB, 16GB or bytes; at least 4 GiB and at most installed memory; default 8 GiB
--disk capacity of the base, at least 32 GiB; default the prepared base’s, 100 GiB. Another capacity installs another base
--user administrator account; default your macOS user name, or barn when that is not a valid account name
--share [name=]path[:ro|:rw], repeatable, at most 8; the name defaults to the last path component; ~/ expands to your home
--clipboard on or off; default on
--subnet a canonical private /24 such as 10.10.30.0/24, or auto for the first free one

up refuses creation options that differ from an existing machine instead of ignoring them, and its next: line names the fix. CPUs, memory, shares, the subnet and the clipboard change with configure. The account and disk capacity are fixed for a machine’s lifetime, and recreate keeps them: other values need another machine. A different macOS comes from image update, then recreate --update.

Machines

  • Names: 1–32 lowercase letters, digits and inner hyphens, starting with a letter: mac1, dev, build-2.
  • Running limit: two macOS virtual machines per Mac, counting other tools and macOS installation. Barn never stops a machine to make room.
  • Account: an administrator with passwordless sudo, SSH key login, desktop automatic login and Remote Login. SSH password login is disabled. The login password is random and stored in the machine’s password file.
  • Guest names: the computer name is the machine name; the local host name is barn-<name>, so the guest answers as barn-<name>.local.
  • Shares: one VirtioFS device mounted by macOS under /Volumes/My Shared Files/<name>. Shares must be existing directories, not symlinks, and change only while the machine is stopped.
  • Clipboard: plain text, synchronized over the machine’s SSH connection when its window gains or loses focus; at most 1 MiB; items marked concealed are never sent.
  • Stop: normal shutdown through macOS in the guest; if the machine is still running after two minutes it is powered off and the result carries "forced": true. --force powers off at once.

Networks

Each machine’s network is created by its own runner process when the machine starts and disappears when it stops. No daemon and no root is involved.

Item Value
Subnet first free private /24 from 10.10.20.0/24 to 10.10.59.0/24, avoiding host routes and other machines; --subnet selects one
Gateway .1, the Mac
Guest address .10, by DHCP reservation for the machine’s MAC address
Reachability the Mac and the internet through NAT; not other machines, not the LAN

start refuses a subnet that a host route now overlaps, such as a VPN, with reason mac_subnet_in_use. SSH host keys are pinned to the machine instance, not its address, so configure --subnet keeps trust.

macOS Local Network privacy stops third-party programs from connecting to these networks unless their app is allowed under Privacy & Security → Local Network; the error is “No route to host”. Barn itself connects through Apple’s /usr/bin/nc and /usr/bin/ssh, which are exempt.

JSON output

Every command honors --json and --yaml. Progress goes to stderr.

ls

{
  "schema_version": 2,
  "root": "/Users/alice/.barn/mac",
  "prepared": true,
  "base": {"version": "27.0", "build": "26A428", "base_id": "26A428-e16af589f4b705ca397f5b21"},
  "machines": [
    {
      "name": "mac1",
      "kind": "macos",
      "state": "running",
      "ready": true,
      "ssh": "ready",
      "address": "10.10.20.10",
      "address_stable": true,
      "ssh_host": "10.10.20.10",
      "ssh_port": 22,
      "user": "alice",
      "image": {"version": "27.0", "build": "26A428", "base_id": "26A428-e16af589f4b705ca397f5b21"},
      "cpus": 4,
      "memory_bytes": 8589934592,
      "disk": {"capacity_bytes": 107374182400, "allocated_bytes": 1202647040},
      "network": {"subnet": "10.10.20.0/24", "gateway": "10.10.20.1", "address": "10.10.20.10"},
      "shares": [{"name": "src", "host": "/Users/alice/src", "guest": "/Volumes/My Shared Files/src", "readonly": false}],
      "clipboard": true,
      "pid": 2545,
      "instance_id": "b1482ffc-2da7-45d9-ace5-c5f3193a9172",
      "warnings": []
    }
  ],
  "running": 1,
  "limit": 2
}
Field Values
state prepared (never booted to readiness), starting, running, stopping, stopped, unknown
ready true only when this check reached the guest over SSH with sudo
ssh ready, pending (first boot in progress), unavailable, offline, unchecked
observed present when the guest reports another macOS build than its base
window_visible present while the desktop window is shown
error, warnings the last recorded failure, and SSH problems found by this check

Lifecycle results

up, restart, open, recreate and configure return one result; start, stop and destroy return {"machines": [...]}, one result per machine, even for a single name.

{"name": "mac1", "action": "created", "state": "running", "ready": true,
 "address": "10.10.20.10", "user": "alice", "version": "27.0", "build": "26A428"}
Field Values
action created, started, running (already running), restarted, recreated, opened, configured, stopped, powered_off, already_stopped, destroyed, absent
forced true when a normal stop had to power the machine off
window true when the desktop was shown
warnings non-fatal follow-ups, such as a change that applies at the next start

exec --json returns the same object as Linux barn exec --json: node is the machine name, and exit_code, stdout and stderr come from the guest. An interactive ssh has no JSON form.

Failures

Exit codes are the Barn CLI’s. A remote command’s own exit status passes through ssh and exec unchanged. JSON failures carry a stable reason and a next command:

Reason Exit Meaning and next step
mac_host_unsupported 3 not Apple Silicon, or older than macOS 27
mac_runner_missing 3 the Mac component is not installed next to the CLI
mac_runner_protocol 3 CLI and component come from different builds; install them together
mac_root 2 run as your normal login user, not with sudo
mac_download_consent 2 downloading macOS needs --yes without a terminal, or use --ipsw
mac_machine_absent 4 no machine by that name; barn mac up NAME
mac_not_initialized 4 the machine has not finished its first boot; barn mac up NAME
mac_not_running 4 ssh/exec need a running machine; barn mac start NAME
mac_running 4 a change needs the machine stopped; barn mac stop NAME
mac_configuration_conflict 4 up options differ from the existing machine; the named configure or recreate
ssh_config_linked 4 ~/.ssh/config is a link; add the printed Include line yourself
mac_vm_limit 6 two macOS VMs already run; stop the named one
mac_subnet_in_use 6 a host route now overlaps the machine’s network; configure NAME --subnet auto
disk_full 6 not enough free space to download or install macOS
mac_machine_damaged 7 a machine that booted before lost disk or identity files; its directory is kept
mac_readiness_interrupted 130 a stop interrupted a first-boot SSH wait

Files

$BARN_HOME/mac/
  config.json                        installation identity and default base
  images/ipsw/<build>.ipsw(.json)    Apple restore images; .partial while downloading
  images/base/<id>/                  read-only, unbooted macOS bases
  slots/<name>/state.json            the machine record (schema 2)
  slots/<name>/disk.asif             copy-on-write disk layer over the base
  slots/<name>/machine-id.bin        Apple machine identifier
  slots/<name>/auxiliary-storage.bin boot storage
  slots/<name>/id_ed25519(.pub)      the machine's SSH key pair
  slots/<name>/known_hosts           the pinned host key, under barn-mac-<instance>
  slots/<name>/password              login password, mode 0600
  slots/<name>/runner.log            runtime log (barn mac logs)

Runtime sockets live in /tmp/barn-mac-<uid>-<hash>/. ssh-config writes ~/.ssh/barn-mac_config and one # barn-mac:include block in ~/.ssh/config, independent of the Linux # barn:include block. The desktop window positions are saved in ~/Library/Preferences/io.pgsty.barn.mac-runner.plist.

Inside the guest, Barn writes ~/.ssh/authorized_keys, /private/etc/sudoers.d/80-barn, /etc/ssh/sshd_config.d/000-barn.conf, the computer and local host names, and disables sleep with pmset.

2.4 - Images

Signed catalogs, built-in aliases, repository selection, local cache verification, imports, and pruning.

Barn uses a materialized static-file Catalog plus immutable qcow2 artifacts. Official and HTTP Catalogs are signed; explicitly selected local and HTTPS repositories may be unsigned. A Catalog update does not require a new Barn binary, but the binary decides which signing keys and image safety rules are trusted.

Warning

EL7, EL9 9.3/9.6, and EL10 10.0 are deprecated compatibility images. All other built-in versions are supported.

Aliases and pull order

Barn 0.9.0 embeds Catalog 2026092001: 9 families and 37 artifacts, retaining all 27 artifacts from the previous Catalog. el7 is amd64-only; every other family has amd64 and arm64 artifacts. EL9 includes 9.3, 9.6, 9.7, and 9.8; EL10 includes 10.0, 10.1, and 10.2. u24:stable (Ubuntu 24.04) on the native architecture is the default request.

The September stable versions below include both amd64 and arm64. This is the embedded Catalog snapshot, not a live repository listing. Run barn update then barn image list to inspect the currently selected repository. Dated public endpoint checks and guest point-release observations are recorded in Status.

Family Embedded stable Distribution series
d12 20260909.2596.1 Debian 12
d13 20260914.2601.1 Debian 13
u22 20260913.0.0 Ubuntu 22.04 LTS
u24 20260911.0.0 Ubuntu 24.04 LTS
u26 20260918.0.0 Ubuntu 26.04 LTS

Debian retains offline-installed XFS tools and the generated en_US.UTF-8 locale, with C.UTF-8 still the default. Ubuntu retains Canonical’s original image bytes; cloud-init configures accounts and networking at startup. A Catalog refresh changes newly resolved stable requests. Existing VMs and explicitly pinned versions continue using their original base images.

Alias Distribution Architectures Boot Status
el7 CentOS Linux 7.9 / 2211 amd64 BIOS deprecated
el8 Rocky Linux 8.10 amd64, arm64 UEFI supported
el9 Rocky Linux 9.7 / 9.8 amd64, arm64 UEFI supported
el9 Rocky Linux 9.3 / 9.6 amd64, arm64 UEFI deprecated
el10 Rocky Linux 10.1 / 10.2 amd64, arm64 UEFI supported
el10 Rocky Linux 10.0 amd64, arm64 UEFI deprecated
d12, d13 Debian amd64, arm64 UEFI supported
u22, u24, u26 Ubuntu amd64, arm64 UEFI supported
barn image list
barn image info d13
barn image info d13:stable
barn image info [email protected]
barn image pull [email protected]
barn image pull d13 --arch arm64
barn update

Catalog status values are advisory rather than an activation switch: supported has passed the declared support gate; testing is available for explicit test/risk acceptance but is not supported; deprecated is retained only for EOL compatibility; and unknown has no support classification. Non-supported entries remain runnable and print a warning.

For a pull, Barn:

  1. reads the selected repository’s active local Catalog once for the complete command: the Catalog embedded in this build, or the one last activated for that repository by barn update or image sync;
  2. resolves image[:channel] or image@version-prefix, defaulting to u24:stable with the official Catalog; standalone image pull defaults to the native architecture and accepts --arch, while lifecycle resolution honors vm_arch;
  3. reuses a local file only after size, SHA-256, and qcow2 checks pass;
  4. otherwise downloads the exact Catalog-named artifact, with retries and resumption; the two official repositories can fall back to one another, while custom repositories remain exclusive. All accepted bytes must match the Catalog. An immutable upstream URL is provenance, not a fallback.

Released builds use https://repo.pigsty.io/barn by default. Long-only --mirror selects https://repo.pigsty.cc/barn; precedence is --repo, --mirror, BARN_REPO, then the global default. Both official roots retain canonical signed-Catalog trust. Repository selection determines both the local Catalog slot and the source of downloads. Keep selecting the same custom repository even when its image bytes are cached. Barn never refreshes the Catalog on its own; ordinary image resolution can work offline with an active local Catalog and verified cache. Run barn update to fetch, verify, and activate the selected repository’s current Catalog. Catalog updates use that selected source; a failed update is an error. An image download fails when none of its permitted sources supplies verified bytes. Changing --repo alone does not fetch or activate that repository’s Catalog; run barn update --repo <root> before using its custom aliases.

Verified writable cache files are made read-only again. A damaged, unreferenced cache file is preserved with a .corrupt-<timestamp> suffix before replacement; a base image still referenced by a VM is kept in place and reported as an error.

Runtime policy

Matching architectures use native HVF/KVM except one catalogued incompatibility: the stock EL8 arm64 64K-granule kernel cannot run through Apple HVF, so Apple Silicon uses visible same-architecture TCG automatically. Explicit foreign vm_arch also uses TCG. amd64-on-arm64 uses a single translation thread to preserve x86 memory ordering. TCG results are not performance evidence.

EL7 is deliberately limited to native Linux/amd64. Linux setup installs only the native QEMU family; foreign architectures require the matching system emulator and UEFI firmware before up or recreate can proceed. For Catalog images, plan resolves the intended image and runtime without requiring those tools. Named local-* imports are byte-checked during resolution and still need qemu-img.

barn image pull d13 --mirror
barn image pull d13 --repo https://mirror.example/barn
BARN_REPO=/absolute/local/repository barn up

Unsigned repositories must be local paths or HTTPS. HTTP repositories require a Catalog signed by a trusted key. Immutable upstream artifact URLs must be HTTPS.

Trust and verification

Current ordinary builds embed both production public verification keys. The private signing keys are external to the source repository. Catalog activation rejects unknown keys, malformed content, equivocation, and revisions below the repository-scoped high-water mark unless the operator explicitly allows a downgrade.

Every accepted image must be a size- and SHA-256-matched plain qcow2 with no backing file, external data file, encryption, or unknown incompatible feature. Verified base images become read-only; node root disks are overlays and never modify the base.

barn update
barn image sync --repo https://repo.example/barn \
  https://repo.example/barn/catalog.json
barn image sync --repo /absolute/repo --allow-downgrade /absolute/repo/catalog.json
barn image reset

image reset restores the embedded Catalog but keeps the anti-rollback high-water mark; reset-manifest remains as a compatibility alias.

barn update checks the repository now and activates a newer Catalog. Barn never refreshes the Catalog on its own; the Catalog embedded in each release is used until you update. image sync is the recovery path for an exact URL or file, including a downgrade.

For repository-scoped recovery, pass the same root explicitly:

barn image sync --repo /srv/barn --allow-downgrade /srv/barn/catalog.json
barn image reset --repo /srv/barn

--repo selects the independent active-Catalog and high-water slot. The source argument does not change this selection. image sync and image reset accept --repo, but not --mirror; when --repo is omitted they use BARN_REPO or the compiled default. For an unsigned custom Catalog, the exact source must be the selected root’s catalog.json.

Static repository format

The published root is deliberately small:

barn/
├── repo.yaml
├── catalog.json
├── catalog.json.minisig       # required for official and HTTP repositories
└── images/
    └── <image>-<version>-<arch>.qcow2

repo.yaml stores author intent: defaults, aliases, channels, exact versions, architectures, boot mode, status, and optional provenance-only upstream URLs. source_user records the image’s declared source login identity, for example rocky in an upstream image or dba after Barn’s official normalization. The pipeline takes the upstream account separately when sanitizing a candidate. Catalog/import metadata does not replace the deployment SSH user (dba by default) or itself normalize the image. The file contains no generated checksum or size fields. catalog.json uses the same logical tree but materializes each variant’s file, SHA-256, artifact size, and virtual size. repo.yaml is schema: 1; the generated catalog.json is the schema-3 Catalog that Barn embeds and signs.

schema: 1
revision: 1
defaults: { image: u24, channel: stable, arch: native, boot: uefi }
images:
  u24:
    aliases: [ubuntu24, noble, ubuntu]
    channels: { stable: "1" }
    versions:
      "1":
        status: unknown
        variants:
          amd64: {}
          arm64: {}

With no explicit file, the two expected artifacts are images/u24-1-amd64.qcow2 and images/u24-1-arm64.qcow2. A variant may use a safe basename override for an existing custom file.

Channels and numeric prefixes are movable selectors. An exact key wins; otherwise a prefix matches on dot-component boundaries and chooses the numerically newest version ([email protected] selects the newest 9.7 build, while el9@9 selects the newest 9.x release). The immutable artifact identity remains (image, exact version, arch):

d13:stable + native
  -> [email protected] + arm64
  -> images/d13-20260914.2601.1-arm64.qcow2

barn repo scan is read-only. build performs strict YAML validation, full qcow2 inspection/checking, and atomic Catalog replacement without changing repo.yaml or QCOW bytes. verify requires the generated Catalog bytes to match a fresh materialization exactly. build and verify require local qemu-img; scan does not. Build on a QEMU host, then publish immutable artifacts first and the Catalog plus its matching signature last. Update a signed Catalog/signature pair together where possible; an inconsistent pair fails verification. Increase revision when changing Catalog contents.

Local layout and imports

Images live under BARN_HOME/images (default ~/.barn/images): family directories contain downloaded artifacts, manifests/ stores the active Catalog with an independent high-water entry per repository, and local/ plus local-images.json hold imports.

barn image import --sha256 <digest> /path/to/base.qcow2
barn image import --name local-mybase --boot uefi \
  --source-user ubuntu --sha256 <digest> /path/to/base.qcow2

The expected --sha256 is optional in the CLI; supplying an independently obtained trusted digest adds an explicit authenticity check to the mandatory qcow2 inspection. Import copies and verifies the file; it does not clean credentials, install cloud-init, detect the guest CPU architecture, or prove that it boots.

Named local aliases must begin with local-, so a future signed Catalog cannot shadow them. --name, --boot, and --source-user must be supplied together. A named import records the importing host’s native architecture; there is no image import --arch option. Use a static repository with explicit variants for foreign-architecture artifacts. Aliases are immutable: choose a new name for different bytes or metadata. Use vm_image: local-mybase in an inventory to select a named import; unnamed imports only populate the cache.

Pruning

barn image prune --dry-run
barn image prune --yes

Bare prune and --dry-run only report candidates; --yes deletes them. Prune protects the union of all artifacts in the selected active Catalog, applied node image digests, and registered local aliases. Therefore a cached Catalog image or named import is retained even when no VM uses it. Unprotected images and recognized stale staging files are candidates; unsafe or damaged files cause an error. Use the same --repo when inspecting a custom Catalog’s cache policy. Images remain cached after destroy, destroy --purge, and purge.

The compiled schema-3 Catalog can be exported byte-for-byte with go run ./tools/catalogexport /absolute/new/catalog.json. A public Catalog at the embedded version must use those exact bytes; same-version different bytes are rejected as equivocation. Release signing and image Catalog signing remain separate trust domains.

2.5 - Image Pipeline

Validate or offline-normalize an explicit qcow2 candidate without downloading, uploading, or signing it.

The low-level packaging/image-pipeline/build.sh accepts one already-downloaded immutable qcow2 and an independently obtained SHA-256. It never downloads, uploads, touches Barn runtime/network state, reads signing keys, or marks an image supported.

Run these commands from the Barn source checkout. Python 3 and qemu-img are required; offline additionally needs working virt-customize and virt-cat tools from libguestfs. These are build-host dependencies, separate from the dependencies that barn setup installs for running VMs.

Modes

  • validate: copy/re-hash, force qcow2 inspection, validate the single backing chain, run qemu-img check, and emit an explicitly unpublishable evidence bundle. Guest credentials are not changed.
  • offline: additionally use libguestfs virt-customize --no-network and virt-cat on the staged copy. It rejects unrelated UID/GID 88 occupants, normalizes the locked dba/admin identity, disables password/root SSH, removes keys/history/host identity/cloud-init cache, restores targeted SELinux labels, and reads back a deterministic marker.

Official candidate matrix

build-official.py wraps the same offline boundary for a fixed eight-target matrix: Debian 12/13 and Rocky Linux 8/9, each on amd64 and arm64. Every upstream qcow2, RPM/DEB input, release name, digest, and source epoch is pinned in official-v1.json.

./packaging/image-pipeline/build-official.py --list

./packaging/image-pipeline/build-official.py \
  --source-cache /absolute/source-cache \
  --package-cache /absolute/package-cache \
  --output /absolute/existing-output-root \
  --target d13/arm64 --fetch

Without --fetch, every locked input must already exist in the two canonical cache directories. With it, the wrapper downloads only the pinned HTTPS URLs and rejects any digest mismatch before invoking offline normalization. Debian 12/13 install the locked XFS userspace closure; Rocky Linux 8 installs the locked python36 and python3-pip RPMs, and Rocky Linux 9 needs no extra package input. SELinux label restoration belongs to normalization, not an additional package set.

Debian also generates en_US.UTF-8 while retaining C.UTF-8 as the default. Both the guest normalization script and host-side marker validation check these postconditions so a base-image refresh cannot lose the earlier customization. Ubuntu uses dated, unmodified official images outside this offline matrix.

Each result remains an unsigned testing candidate. To build the complete matrix, omit --target; repeat it to select several targets. --list shows the exact releases pinned by this checkout. The matrix currently contains Debian 20260909.2596.1/20260914.2601.1 and Rocky Linux 8.10.20240528.1/9.8.20260525.1.

Assembly takes parent directories containing the named bundles, not the individual bundle directories. If all eight builds were written below one output root, assemble them with:

./packaging/image-pipeline/build-official.py \
  --assemble-from /absolute/existing-output-root \
  --output /absolute/new-candidate-repository

Repeat --assemble-from if builds are split across roots. Assembly requires exactly one bundle for each of the eight targets, creates a new static repository, and runs barn repo build plus verify using barn on PATH (or --barn /absolute/path/to/barn). Unlike build mode’s existing output root, the assembly destination must not exist. Its channels are candidate, not stable: use d13:candidate, for example. This does not perform native smoke, signing, upload, or Catalog publication.

Validate one downloaded image

SOURCE_DATE_EPOCH=1787486400

./packaging/image-pipeline/build.sh \
  --mode validate \
  --source /absolute/source.qcow2 \
  --expected-sha256 <digest> \
  --output /absolute/new/evidence-directory \
  --name u24 --release 20260801.0.0 --arch amd64 \
  --source-user ubuntu --boot uefi \
  --source-uri https://immutable.example/source.qcow2 \
  --artifact-url 'https://images.example/u24/{sha256}.qcow2' \
  --license NOASSERTION \
  --source-date-epoch "$SOURCE_DATE_EPOCH" \
  --manifest-version 2026082903

Source/output paths must be absolute; source is canonical, regular, non-symlinked, stable while copied, and at most 16 GiB. Output must not exist. The builder uses an exclusive adjacent lock, mode-0700 staging, and one final rename. Failure removes only its guarded staging directory.

Every successful bundle contains the read-only qcow2, recipe, SLSA provenance, SPDX boundary SBOM, manifest-candidate.json (testing), validation evidence, and checksums. The candidate manifest is a pipeline evidence format; repository assembly produces the runtime schema-3 catalog.json. The SPDX file describes the declared input/build boundary and is not a complete package inventory of the guest filesystem. Signing is deliberately outside this pipeline. Validate mode is byte-reproducible for fixed inputs/tools; offline mutation must be built twice and compared before release evidence is accepted.

A release still needs runtime smoke on each declared host/guest path, explicit review of support status and provenance, a new Catalog revision, production signing, and public artifact verification. Build success alone does not authorize a supported status or prove that a candidate is publicly available.

3 - About Barn

The product model, implementation boundaries, native evidence, current limits, and release gates.
  • Design explains why Barn has one Inventory, one deployment, and one fixed-IP network.
  • Status separates implemented behavior, native validation, and remaining release gates.
  • Engineering defines source, generated-output, package, image-pipeline, and evidence boundaries.

3.1 - Design

Barn’s one-deployment architecture, networking, state, and safety boundaries.

One useful abstraction

Barn boots one Pigsty Inventory as one local QEMU deployment. It deliberately has no project marker, project registry, lease model, provider layer, or second configuration format.

State lives under BARN_HOME (default ~/.barn) for one Unix user. The product assumes one active Pigsty deployment per computer; this is not a root-enforced cross-user singleton.

Node-level convergence

Barn extracts only the documented VM and Pigsty-native fields, computes per-node hashes, and keeps applied state plus process identity. Additions are incremental. Changes require an explicit per-node recreate. up also starts selected existing stopped nodes; already-running peers keep their processes while unfinished guest setup and managed hosts/SSH entries are refreshed. Unrecognized or confirmed damaged test data filesystems may be reset, including persistent disks; see Data disks. Absence never authorizes deletion.

Runtime selection

Guest architecture is deployment-wide desired state. Omitted/native follows the host; explicit amd64 or arm64 selects that Catalog artifact exactly. Native HVF/KVM remains the default. A foreign architecture or one catalogued image/host incompatibility selects a fixed TCG profile; there is no user accelerator argument and no arbitrary failure fallback.

The effective architecture and accelerator are persisted in each QEMU invocation and exposed by status. Before destructive recreate, Barn proves the selected QEMU binary and version, network backend, image bytes, boot mode, and firmware. A later binary changing runtime policy cannot mix new nodes with old invocations: runtime drift requires whole-deployment recreation.

Two NICs, one fixed subnet

The management NIC supplies DHCP, DNS, egress, and loopback SSH. The fixed-IP NIC supplies host/peer/Ansible traffic. macOS uses socket_vmnet. Linux follows active NetworkManager; otherwise it uses systemd-networkd and connects through the distribution bridge helper. Inactive networkd is started only after an activation-safety scan proves existing units cannot claim a real host link.

On Debian, the helper is temporarily and reversibly scoped to a group the caller actually belongs to. A real unprivileged QEMU bridge smoke must pass before setup accepts the network; failure rolls the install back automatically.

Storage and configuration have different lifetimes

The Inventory records desired VM definitions. Applied state records what was created, including the exact base-image identity and runtime invocation. Changing a Catalog channel does not rewrite an existing root disk.

Verified base images are shared read-only; each VM writes to its own root overlay. Data disks have a separate preservation contract: normal destroy retains persistent disks, while explicit disk deletion or purge removes them. Cache pruning has another boundary and also protects the active Catalog and registered local aliases. See Storage and access and Images.

Safety boundary

QEMU and all guest artifacts run as the caller. Root is limited to host package installation, network setup, and the optional hosts publisher. Destruction requires matching ownership, containment, node identity, QMP/process identity, and an allowlist of artifacts. Ambiguity stops the operation.

3.2 - Status

Barn 0.9.0 rename candidate, publication boundaries, historical validation, and known limits.

Barn 0.9.0 is the first release candidate under the new name and is not yet published. Source checks, local builds, packages, CI, releases, and the public site are verified separately. Renamed source alone does not establish public delivery. Installation instructions are in the Quick Start.

Documentation baseline

Object Current identity How to use it
Application and current docs Barn 0.9.0 release candidate Build a checkout containing the rename; package commands apply after publication.
CLI and configuration barn, barn.yml, BARN_* No old-name command aliases or environment fallbacks.
State and host resources ~/.barn, Barn networking and helpers Create fresh Barn state; there is no development-state migration.
Image repository /barn at the official endpoints Catalog signatures remain required; cloud migration and public access need independent verification.

Stop old internal environments and preserve needed data before creating a fresh Barn installation. Do not simply rename an old state directory. The final release commit, artifact digests, Homebrew and public endpoints will be recorded after verification. Historical 0.2–0.8 records keep the Farrow name and do not establish Barn 0.9.0 publication or acceptance.

macOS guests

barn mac runs macOS 27 guests on Apple Silicon; see the guide and reference. Its component is Barn Mac.app, with signing identifier io.pgsty.barn.mac-runner. It uses independent $BARN_HOME/mac state and has no command to migrate earlier development data.

After the rename on 2026-09-29, local CLI/hosts-helper tests, native Bridge/image/exit-prompt/menu tests, the runner build, ad-hoc signature checks and probe passed. These checks started no VM and did not repeat the complete Mac native lifecycle. Earlier native results retain their original identity below.

Remaining release checks

  • The full source, archives, DEB/RPM, installer and cross-platform checks at the final Barn commit;
  • Fresh host setup and Linux/macOS VM lifecycle and cleanup under the new names;
  • Mac Developer ID signing, notarization and publication;
  • The live GitHub repository, Homebrew, both image repositories and barn.pgsty.com.

The records below describe pre-rename checkpoints. Their old commands, paths, versions and links are historical evidence only.

Farrow Mac native record: 2026-09-29

The pre-rename Farrow development tree was validated on 2026-09-29 on an Apple Silicon Mac running macOS 27.0 (26A428), with an ad-hoc signed development build. No macOS image was downloaded for the run: every test home used an APFS clone of a base prepared on 2026-09-26 from Apple’s pinned 27.0 restore image.

  • Source gates: complete make check, native component tests, and the Mac bundle build with extracted-archive checksum, signature, and probe checks passed.
  • Automated live acceptance: all 16 phases passed in 244.9 s: creation from the base, a named machine with a shared folder, independent identities and sshd policy, disk isolation, per-machine networks isolated from each other, DNS and public HTTPS, exit codes and argument boundaries, bidirectional shared-folder writes, normal stop/start, configure, refusal of a third running VM, recreate rotating identities, desktop and two-way clipboard, repeated up leaving the base unchanged, and destroy.
  • Manual checks: creation to SSH-ready in 22 s from a prepared base; normal stop 6.5 s; start to SSH-ready 6–12 s; forced stop 0.7 s; macOS Recovery boot; a subnet change keeping the pinned host key; ssh mac1 through the installed OpenSSH entries.

Still open for macOS guests: Developer ID signing, notarization, and a published release; downloading and installing macOS through the current CLI (a first up without a prepared base, or image update); the desktop’s Restart… and Shut Down… menu actions; other Apple Silicon models and macOS 27 updates; and physical-host reboot.

Pre-rename Linux validation summary

Host or artifact Path Last verified Result
Two Linux amd64 hosts (m0, m3) KVM, seven operating systems per host 2026-09-21 (0.8.0) clean initialization, first SSH, final candidate upgrade, repeated up, stop/start, disk identity, and Ansible configuration reads passed
Two macOS arm64 hosts (m1, m5) HVF, seven operating systems per host 2026-09-21 (0.8.0) the same lifecycle checks plus seven-node Ansible ping passed
Catalog 2026092001 9 families, 37 artifacts; September Debian/Ubuntu refresh 2026-09-21 embedded/local/LAN/public Catalog bytes match; ten new image objects reachable at both public endpoints with expected sizes; selected images passed the four native pro runs
Ubuntu 26.04 amd64 KVM, QEMU 10.2.1, Ubuntu 24.04 guest 2026-09-16 (0.7.0 recovery) damaged disk resets, failed probes, busy mounts, retained-disk recreation, share recovery, and repeated healthy up passed
macOS arm64 HVF, Ubuntu 24.04.4 guests 2026-09-05 (0.6.0 lifecycle changes) create, scale-out, peer SSH, stop/start, reload, recreate, partial status, and scale-in passed
macOS 26.6.2 arm64 HVF, QEMU 11.1, socket_vmnet 2026-09-01 (v0.2.0) selected create/SSH/stop/start, incremental cached create, whole status, whole destroy passed
macOS 26.6.2 arm64 HVF, QEMU 11.1, socket_vmnet 2026-08-27 one node and additive four nodes passed
Ubuntu 26.04 amd64 (mx) KVM, QEMU 10.2.1, NetworkManager 2026-09-01 (v0.2.0) audited an existing four-node deployment as live and reached its control guest
Ubuntu 26.04 amd64 (mx) KVM, QEMU 10.2.1, NetworkManager 2026-08-27 setup, one node, additive four nodes, and uninstall passed
macOS arm64 HVF host, TCG compatibility rule, Rocky Linux 8.10 arm64 2026-08-28 boot, stop/start, readiness in 44.2 s passed
Published Catalog 2026090501 9 families, 27 qcow2 artifacts 2026-09-05 artifact verification, embedded/public byte equality, and signed update through both official endpoints passed
Published Catalog 2026082903 9 families, 27 signed qcow2 artifacts 2026-08-29 full SHA-256 sweep and clean-client d13:stable pull passed

The 0.8 run covered Rocky Linux 9.8/10.2, Debian 12.15/13.7, and Ubuntu 22.04.5/24.04.5/26.04.1 on each host: 28 successful guest instances in total. The temporary acceptance VMs were subsequently cleaned up. Both public Catalogs have SHA-256 23e8dbf6c19bd192d56c6d71eb30901f17945b3487e427a43abe108463780306. Isolated farrow update runs at both endpoints verified the signatures and activated revision 2026092001. The public image check used HEAD/content lengths for ten new objects at each endpoint, not a fresh download/hash of every public artifact.

Coverage left open at the earlier checkpoint

  • EL9 hosts with NetworkManager and firewalld, and a current systemd-networkd replay;
  • physical-host reboot persistence;
  • macOS amd64 and Linux arm64 native runs, beyond their build/package checks;
  • EL7 through the current native Linux/amd64 lifecycle;
  • macOS directory sharing: the tested QEMU cannot reopen Farrow’s securely held directory descriptor;
  • a complete current Pigsty configure → farrow up → install.yml run;
  • a native replay of the corrected fresh Homebrew socket_vmnet authentication path;
  • clean-host setup and VM creation through the public installer, Homebrew, or published DEB/RPM packages.

Current built-in versions are supported, except EOL EL7 and the retained compatibility versions EL9 9.3/9.6 and EL10 10.0, which are deprecated. Active and standby Catalog public keys are embedded. Private-key custody and rotation, together with release custody, must be formalized before 1.0.

Farrow verification history

Each entry belongs to the exact checkpoint exercised that day. Later source or documentation edits do not inherit native proof without another replay.

Farrow 0.8.0 — 2026-09-21

Release tag v0.8.0 points to 320a32afa8f6fca02592215aa0d5607ca4e852b2. The source CI, independent packaging snapshot, and tag workflow passed. The tag workflow produced 20 assets, including 19 checksummed payloads. The publication commit changes only README and release notes relative to the tested runtime below; a new local build at the tag passed the archive/package checks.

The release became public on 2026-09-21. All 20 assets were downloaded anonymously through the host’s configured proxy, without GitHub credentials. Every request returned HTTP 200 and the full-body SHA-256 matched both the API digest and inspected draft; all 19 checksummed payloads matched the manifest. Public installers succeeded in isolated user directories on macOS arm64 and Linux amd64. Both reported 0.8.0 / 320a32a and installed exactly the CLI/helper bytes from their verified public archives. Linux downloads used a temporary SSH loopback forward to the existing proxy, removed afterward. Default installations and VM/network state stayed unchanged. These checks verify binary installation; fresh-host setup and VM creation through the public installer remain untested.

The Homebrew tap now selects 0.8.0 for all four targets with the public archive digests. Local syntax, updater tests, consistency/style/platform checks, strict online audit, and native arm64 brew fetch passed. Its macOS and Linux CI jobs also passed metadata, updater, style, platform, and audit checks. There was no new brew install, upgrade, relink, or brew test in this release verification.

The clean initialization baseline was 6d7870e26cb2f4082a00c188f746783fc027687e. On each of four hosts, the run removed the owned old lab and network, started from an empty Farrow user state/cache, and created the seven-system pro inventory using one LAN repository. Linux used the actual DEB; macOS used a complete checksum-verified user installation of the archive’s paired CLI/helper.

The runtime candidate 1c054a027420b5410c6f6feb344e27e001ae1af1 added the Homebrew authentication-order fix and passed complete local make check and release archive/package verification. It was installed in place on all four hosts, then passed healthy up, two rounds of guest SSH, stop/start, and Ansible configuration reads. Both Macs also passed Ansible ping on every node. VM UUIDs, image identities, and root/data disk paths and inodes were retained; healthy up also kept the running processes. All runs checked real data-disk access, Debian locales/XFS, and control-node SSH to the other guests.

This is a clean 6d baseline followed by a final 1c in-place lifecycle replay, not a second clean initialization at 1c. The new Homebrew ordering has a regression that fails before the fix and passes after it; the native clean runs used the pinned backend archive, so they do not exercise a new Homebrew formula install. No physical host was rebooted, and no full Pigsty installation or macOS shared folder was included. The release notes include the measured lifecycle samples and image versions.

Farrow 0.7.0 release — 2026-09-16

Release commit 9c6d4896d93733d1cb60a7e5d8591e9a06659c9d passed the complete source CI and independent packaging snapshot before tagging. The tag workflow repeated the gates and published a draft with 20 assets. After inspection, the release was made public; all 20 anonymous downloads returned HTTP 200 and matched the inspected bytes, including all 19 checksummed payloads. Public installers on macOS arm64 and Ubuntu amd64 installed the exact archive binaries. Download tests used the host’s configured proxy; m3 reached it through a temporary loopback tunnel. The pgsty/infra/farrow formula was refreshed to v0.7.0; a local Homebrew upgrade and brew test passed, while a clean-host formula install remains open.

The Ubuntu amd64/KVM recovery matrix covered corrupt ext4/XFS filesystems, retained disks, failed probes, busy mounts, read-only shares, and repeated healthy up without process replacement. A public 0.6.0 bootstrap failed its egress probe; 0.7.0 resumed that same VM in 3.3 seconds. Probe failure left existing disk data and UUID intact. The old 0.6.0 CLI still completed status, stop, start, and destroy after upgrade, including with a cached optional warning. A fresh VM from the final 0.7.0 archive booted without warnings and ignored the old VM’s cache.

The publicly installed Linux binary also reset a deliberately damaged disposable ext4 disk in 3.2 seconds, reported discarded data, and retained the running VM’s process. Stop, start, and purge then passed.

That interrupted 0.6.0 bootstrap had already deleted its staged control-node SSH key. Management access recovered, but missing peer SSH remained an explicit control-ssh limitation; see the upgrade notes. This release adds no guest images: Catalog 2026090501 remains unchanged. There was no new macOS HVF, host reboot, or complete Pigsty replay.

Farrow 0.6.0 release — 2026-09-05

The lifecycle and UX changes passed two adversarial Claude Code Fable 5.1 reviews at xhigh effort; release metadata and the CI test fixture received follow-up approvals. Release commit 057774e3a13477782a2ae07bd71127d03c0f1ae7 passed the complete Go 1.27.1 source CI. The latest packaging changes passed the independent snapshot and package checks at 13d9d70. The tag workflow repeated source checks and verified four platform archives, four Linux packages, eight SPDX documents, the installer, Homebrew formula, release metadata, and all 19 checksummed payloads before creating the 20-asset release. The release is public. All asset digests match the checksum manifest. An isolated macOS arm64 installation upgraded from public 0.5.0 to public 0.6.0; both installed executables match the verified release archive. The released binary also passed init and a fresh U24 plan.

An isolated macOS arm64/HVF U24 lab passed initial boot, expansion without restarting the control node, control-to-peer SSH, stop/start, normal reload, selected recreation, scale-in, and guest-name refresh. Invalid-image reload and conflicting selected recreation stopped before disrupting the existing VMs. Partial status kept the healthy peer visible, and SSH exit 255 passed through. Effective OpenSSH configuration tests covered the final guest SSH scope correction. The test lab was removed after validation.

Published Catalog 2026090501 defaults to u24:stable and incorporates the Debian/Rocky image updates already published in Catalog 2026090302. All 27 artifacts passed repository byte verification. Both official endpoints now serve the exact embedded catalog and its production signature; isolated clients completed farrow update against each endpoint. This application release builds no new guest images. Linux-host VM lifecycle, host reboot, and a complete Pigsty installation were not replayed for 0.6.0.

Farrow 0.5.0 release — 2026-09-03

Exact commit fc85b65ff6a24b0933b56ae1179be9ada2ba91b1 passed both main-branch workflows before tagging: the complete Go 1.27.1 source gate and the independent GoReleaser snapshot/package path. The exact tag workflow then repeated the source/toolchain checks, built and verified four platform archives, four native Linux packages, eight SPDX documents, the Homebrew formula, installer, release.json, and the 19-entry checksum manifest before creating the 20-asset pre-release.

0.5.0 adds no-confirmation whole-deployment purge/rm, makes the global repo.pigsty.io default and China --mirror explicit, removes hidden Catalog upstream fallback, and adds the digest-pinned eight-target Debian/Rocky official image candidate builder. The builder results remain unsigned testing candidates; no image Catalog or native VM lifecycle result is promoted by this application release.

Farrow 0.4.0 release — 2026-09-02

The exact tag commit passed make check and the release workflow’s archive, DEB/RPM, SBOM, checksum, installer, Homebrew-formula, and package-parity gates. up now runs setup itself on an unprepared terminal host, vm_disks[].fs defaults to auto, readiness failures carry the guest’s last error line, and every command shares one output style. The first-run path was not replayed on a fresh host; no new native VM replay is claimed here.

Farrow 0.3.0 release — 2026-09-02

The exact tag commit passed make check and the release workflow’s archive, DEB/RPM, SBOM, checksum, installer, Homebrew-formula, and package-parity gates. Catalog refresh is now explicit (farrow update for the configured repository, image sync for an exact source), and guest readiness failures carry per-node stages and next-step log commands. No new native VM replay is claimed here.

Farrow 0.2.0 release — 2026-09-01

Source commit 59d1b62aebb3d044a317e4006cc8a0bf56f4feaf is tagged v0.2.0. Its source CI and independently dispatched packaging workflow passed the exact commit. The stable local release path also built and verified all four platform archives, amd64/arm64 DEB and RPM packages, eight SPDX documents, paired helper digests, archive/package parity, Homebrew formula, installer, release metadata, and 19 checksummed final assets.

The macOS arm64/HVF replay ran with MonoProxy’s covering 10.0.0.0/8 exclusion present. Selected u24-1 create/SSH/stop/start, incremental cached el9-1 create, whole status with five absent desired peers, both SSH connections, and whole destroy/SSH-fragment cleanup passed. On Ubuntu 26.04 amd64/KVM, the Linux binary audited an existing four-node Farrow deployment as live and reached its control guest without mutating that host.

The compiled default image repository remains the signed COS-backed https://repo.pigsty.cc/farrow. The independently checked https://repo.pgsty.com/farrow source endpoint serves identical Catalog, authoring metadata, checksums, and image bytes with a read-only Nginx worker.

Schema-3 Catalog closure — 2026-08-29

Catalog revision 2026082903 was the source and development-repository checkpoint on that date: 9 families and 27 architecture-specific artifacts. The embedded Catalog and published catalog.json have the same SHA-256 571b1ff9c7d4d42355df3392ea62a339471c2d01d868669a7625fac8b93f245d; the published repo.yaml also matches the source-controlled authoring file. Fresh HTTP and HTTPS clients accepted the detached signature from production key 4686B39A40F9B562.

All 27 published qcow2 files (19 GiB total) passed a full SHA-256 sweep against the Catalog. A clean temporary Farrow home then downloaded the complete 409.3 MiB Darwin/arm64 default d13:stable artifact, rehashed it, and accepted its qcow2 structure and virtual size. These are publication-integrity and client-path checks, not a replacement for the native lifecycle matrix; no existing VM was recreated for this Catalog check.

0.1.0 candidates — 2026-08-28 and 2026-08-29

Two isolated v0.1.0 candidates passed the stable local release path before 0.2.0 superseded them. Two facts from those runs still stand on their own: the Darwin/arm64 binary repaired two live nodes whose QMP sockets had been removed externally by stopping and starting only those nodes, both reaching readiness in 13.7 seconds while the two untouched peers kept their boot IDs; and a full macOS factory reset exposed a source-test dependency on an installed qemu-img, so catalog-only image list could not run on a blank host. The store is resolved lazily only when local qcow2 bytes need validation, regression tests explicitly remove QEMU from PATH, and make check passes with QEMU, Farrow, and network state absent.

EL7/EL8 compatibility — 2026-08-28

Commit 7c666c7 restored EL7/EL8 after two independent adversarial reviews. The first review blocked on destructive runtime preflight ordering and signed-Catalog baseline migration; both were fixed, regression tested, and the second review returned PASS with no required fixes.

At that checkpoint, Catalog 2026082801 was signed and active on the development repository: 9 families, 17 image artifacts, and 19 SHA-verified repository payloads including the two socket_vmnet archives. A clean client accepted the public signature and exact embedded digest.

An isolated macOS arm64 lifecycle replay booted Rocky Linux 8.10 arm64 with the built-in TCG compatibility rule, passed stop/start and readiness in 44.2 seconds, and verified NetworkManager, fixed IP/no-route/no-DNS, dba UID/GID 88, and the generation/spec marker. EL7 bytes, qcow2 structure, BIOS layout, and 4K XFS root are verified; the native Linux/amd64 Farrow lifecycle replay remains open.

Native replay — 2026-08-27

Both hosts in the summary table passed fixed IP, SSH readiness, default CPU/memory/root/data disk, cloud-init, stop/start, cross-directory commands, unchanged control-node boot ID during scale-out, control-to-peer SSH, ignored unconsumed Pigsty changes, absence-never-destroys, and explicit destroy.

Linux additionally proved valid NOPASSWD automation, caller-accessible Debian helper policy, unprivileged bridge smoke, refusal to uninstall with four tap members, and exact restoration after destroy.

Interactive host-network and hosts commands invoke sudo themselves; an external sudo -v is optional. Darwin cleanup can reconstruct an uninstall-only ownership plan from byte-identical interface evidence, the exact launchd plist, and installed binary digests when network.json is missing.

On 2026-08-28 the post-calibration tree passed unit, race, vet, staticcheck, govulncheck, all four cross-builds, the simulated image-pipeline boundary, license verification, and GoReleaser configuration validation. An isolated local GoReleaser snapshot also built and verified all four archives, both DEB/RPM architectures, SPDX documents, checksums, dependencies, modes, and archive/package parity. Nothing was published, and those results do not extend the native matrix.

3.3 - Engineering

Source layout, build and test gates, image normalization, release outputs, and evidence policy.

This page describes the Barn 0.9.0 release candidate. Build commands use your current checkout; record its commit and uncommitted changes. Source builds and local checks do not establish a published release.

Repository boundary

The Barn source repository contains code, tests, build/package definitions, legal notices, README.md, CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, and bilingual application release notes. This site provides user, design, operator, and release documentation. Runtime behavior and command flags must be checked against the matching source and binary; an unpublished source change is not evidence that a public package has the same behavior.

Review transcripts, scratch inventories, generated binaries, and release output trees are not production source inputs.

Generated output is disposable:

  • bin/ — development builds;
  • dist/ and .goreleaser-* — release/snapshot staging;
  • root barn, barn-hosts-helper, and catalogsign binaries;
  • Hugo public/ and resources/.

Build and source gates

make check

make check runs module and shell checks, maintenance ownership, unit/race tests, Vet, Staticcheck, dead-code and errcheck checks, vulnerability scanning, four-target cross-builds, installer/image-pipeline tests, and license checks. The Makefile defines the exact list. CI additionally checks the pinned toolchain, Go formatting, whitespace, and GoReleaser configuration. Installation of the quality tools is covered in Build from Source.

Packaging changes have a separate snapshot gate:

make release-check
make release-snapshot SNAPSHOT_DIST=.goreleaser-review

Install the versions in packaging/toolchain.env, including GoReleaser, nFPM, and Syft. The snapshot target also needs the archive/package inspection tools used by the verification scripts. Choose a new output directory directly under the checkout; existing output is refused. A snapshot is local and does not upload a release.

A source gate is not native VM evidence. macOS HVF, Linux KVM/networking, package consumption, release publication, and public website rendering remain separate checks. make image-pipeline-native-test runs the separate native image-pipeline gate with QEMU/libguestfs and explicit image inputs; the required BARN_IMAGE_PIPELINE_NATIVE_* variables are documented in tests/image-pipeline-native-test.sh. It never downloads a test image.

Release and package contract

Release tooling under packaging/, .goreleaser.yaml, and .github/workflows is source, even though its generated directories are not. Archives and Linux packages contain the matching CLI and hosts-helper binaries, LICENSE, the source README, and exact upstream license bytes reconstructed from modules pinned by go.mod. Archives place the two binaries under bin/ and the license texts under licenses/. Linux packages install /usr/bin/barn, /opt/barn/libexec/barn-hosts-helper, and documentation under /usr/share/doc/barn/.

BUILD_INFO.json is included in Linux packages and the older development archive format. Formal GoReleaser archives carry build identity in the binary, with release metadata alongside the published assets; do not assume every archive contains that file. Generated dependency license files are staged at build time. Detailed user documentation stays on this site.

Application releases are built in GitHub Actions, with checksums.txt, release metadata, and SPDX SBOM assets. The current workflow does not produce a separate application-release signature or provenance/attestation bundle. Catalog Minisign signatures authenticate image catalogs and are a separate trust mechanism.

Commit, tag, archive/package verification, CI, draft upload, public release, and anonymous consumption are separate evidence. The tag workflow creates a draft; it does not publish it. Pre-1.0 versions are GitHub prereleases and the installer requires an explicit BARN_VERSION.

make release-local VERSION=<version> builds and verifies without publishing. It requires a clean checkout at the matching v<version> tag, an origin remote, pinned tools, and unused staging/output directories. Use the snapshot path for reviewing an untagged candidate.

Image normalization

The low-level packaging/image-pipeline/build.sh accepts an explicit local qcow2 source and never downloads or uploads. It copies and hashes the source, forces qcow2 parsing, rejects backing/external/encrypted/unknown features, runs qemu-img check, and can perform a no-network offline Guest mutation in an explicit QEMU sandbox. UID/GID 88 collisions are rejected rather than rewritten ambiguously.

build-official.py adds a fixed digest-pinned wrapper for Debian 12/13 and Rocky Linux 8/9 on amd64/arm64. It may fetch only the locked source and offline package inputs, then emits unsigned testing candidates and can assemble a separate candidate repository. Native smoke, repeat-build comparison, production signing, upload, and Catalog activation remain later gates.

Catalog bytes are exported with:

go run ./tools/catalogexport /absolute/new/catalog.json

The exporter is atomic and refuses an existing output path. make catalog-sign and make catalog-verify use the catalog Minisign key pair; production private keys stay outside source and CI. Application checksums do not replace catalog signatures.

Evidence policy

Historical M0–M4 notes were useful during implementation but are not product documentation. Their durable conclusions are condensed into Design and Status. A later source edit inherits no native proof; every status claim names its date, host, path, and remaining gates.