summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--frontends/Rexfile56
-rw-r--r--packages/Makefile96
-rw-r--r--packages/buildvm/.gitignore5
-rwxr-xr-xpackages/buildvm/install-expect.exp167
-rw-r--r--packages/buildvm/install.conf18
-rwxr-xr-xpackages/buildvm/provision.sh61
-rwxr-xr-xpackages/buildvm/setup.sh41
-rwxr-xr-xpackages/buildvm/start.sh51
-rwxr-xr-xpackages/buildvm/stop.sh34
-rwxr-xr-xpackages/scripts/pkg-dtail-openbsd.sh80
10 files changed, 570 insertions, 39 deletions
diff --git a/frontends/Rexfile b/frontends/Rexfile
index add480e..610083c 100644
--- a/frontends/Rexfile
+++ b/frontends/Rexfile
@@ -462,44 +462,40 @@ task 'nsd_failover',
run 'rm /tmp/root.cron';
};
+# Install or update dtail from the custom package repository.
+# The package includes binaries, config, rc script, and key cache helper.
+desc 'Install DTail package';
+task 'dtail_install',
+ group => 'frontends',
+ sub {
+ # Remove any previously manually deployed binaries not managed by pkg
+ my $pkg_check = run 'pkg_info dtail 2>/dev/null';
+ if ( $? != 0 ) {
+ for my $bin (qw(dserver dcat dgrep dmap dtail dtailhealth)) {
+ if ( is_file("/usr/local/bin/$bin") ) {
+ Rex::Logger::info("Removing manually installed $bin binary...");
+ run "rm -f /usr/local/bin/$bin";
+ }
+ }
+ }
+
+ # Install or update from custom repo (packages signed with signify)
+ say run 'PKG_PATH="https://pkgrepo.f3s.buetow.org/openbsd/7.8/packages/amd64/" pkg_add -u dtail || PKG_PATH="https://pkgrepo.f3s.buetow.org/openbsd/7.8/packages/amd64/" pkg_add dtail';
+ };
+
+# Set up the dserver service user, daily key cache cron, and start dserver.
+# Binaries, config, rc script, and key cache script come from the dtail package.
desc 'Setup DTail';
task 'dtail',
group => 'frontends',
sub {
- my $restart = FALSE;
+ run_task 'dtail_install';
run 'adduser -class nologin -group _dserver -batch _dserver', unless => 'id _dserver';
run 'usermod -d /var/run/dserver _dserver';
- file '/etc/rc.d/dserver',
- content => template('./etc/rc.d/dserver.tpl'),
- owner => 'root',
- group => 'wheel',
- mode => '755',
- on_change => sub { $restart = TRUE };
-
- file '/etc/dserver',
- ensure => 'directory',
- owner => 'root',
- group => 'wheel',
- mode => '755';
-
- file '/etc/dserver/dtail.json',
- content => template('./etc/dserver/dtail.json.tpl'),
- owner => 'root',
- group => 'wheel',
- mode => '755',
- on_change => sub { $restart = TRUE };
-
- file '/usr/local/bin/dserver-update-key-cache.sh',
- content => template('./scripts/dserver-update-key-cache.sh.tpl'),
- owner => 'root',
- group => 'wheel',
- mode => '500';
-
append_if_no_such_line '/etc/daily.local', '/usr/local/bin/dserver-update-key-cache.sh';
- service 'dserver' => 'restart' if $restart;
service 'dserver', ensure => 'started';
};
@@ -764,9 +760,7 @@ task 'commons',
# run_task 'gorum';
run_task 'foostats';
-
- # Requires installing the binaries first!
- #run_task 'dtail';
+ run_task 'dtail';
};
1;
diff --git a/packages/Makefile b/packages/Makefile
index 2afcdd8..4933872 100644
--- a/packages/Makefile
+++ b/packages/Makefile
@@ -1,16 +1,23 @@
# Build and upload custom packages to the f3s package repository.
#
-# Usage:
-# make pkg NAME=gogios SRC=~/git/gogios # both FreeBSD + OpenBSD
-# make pkg-freebsd NAME=gogios SRC=~/git/gogios # FreeBSD only
-# make pkg-openbsd NAME=gogios SRC=~/git/gogios # OpenBSD only
+# Single-binary Go packages (pure Go, cross-compiled):
+# make pkg NAME=gogios SRC=/home/paul/git/gogios # both OSes
+# make pkg-freebsd NAME=gogios SRC=/home/paul/git/gogios # FreeBSD only
+# make pkg-openbsd NAME=gogios SRC=/home/paul/git/gogios # OpenBSD only
#
-# Required variables:
+# Multi-binary / CGo packages (built natively on OpenBSD build VM):
+# make dtail-openbsd # DTail for OpenBSD
+#
+# Build VM management:
+# make buildvm-start # boot the OpenBSD build VM
+# make buildvm-stop # shut it down
+#
+# Required variables (single-binary):
# NAME — package name (e.g. gogios)
# SRC — path to the Go project root (must have cmd/$(NAME)/main.go
# and internal/version.go with a Version constant)
#
-# Optional variables:
+# Optional variables (single-binary):
# COMMENT — one-line package description
# DESC — longer description (for OpenBSD desc file)
# MAINTAINER — maintainer email
@@ -20,7 +27,7 @@
SHELL := /bin/bash
.ONESHELL:
-# SSH targets
+# SSH targets for production hosts
FREEBSD_HOST := f0.lan.buetow.org
FREEBSD_SSH := ssh -p 22
FREEBSD_SCP := scp -P 22
@@ -28,6 +35,12 @@ OPENBSD_HOST := rex@fishfinger.buetow.org
OPENBSD_SSH := ssh
OPENBSD_SCP := scp
+# Local OpenBSD build VM (QEMU/KVM) for native compilation
+BUILDVM_SSH := ssh -o StrictHostKeyChecking=no -p 2222
+BUILDVM_SCP := scp -o StrictHostKeyChecking=no -P 2222
+BUILDVM_HOST := pbuild@localhost
+BUILDVM_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))buildvm
+
# NFS-backed PV on f0
PV_BASE := /data/nfs/k3svolumes/pkgrepo
FREEBSD_REPO := freebsd/FreeBSD:15:amd64/latest
@@ -46,7 +59,31 @@ SCRIPTS := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))scripts
# Extract version from internal/version.go, stripping the "v" prefix
VERSION = $(shell grep 'Version' $(SRC)/internal/version.go | sed 's/.*"\(.*\)"/\1/' | tr -d v)
-.PHONY: pkg pkg-freebsd pkg-openbsd check-vars clean
+# DTail settings
+DTAIL_SRC := /home/paul/git/dtail
+DTAIL_VERSION = $(shell grep 'Version string' $(DTAIL_SRC)/internal/version/version.go | sed 's/.*"\(.*\)"/\1/')
+DTAIL_BINARIES := dserver dcat dgrep dmap dtail dtailhealth
+CONF_FRONTENDS := $(abspath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))../frontends)
+
+.PHONY: pkg pkg-freebsd pkg-openbsd dtail-openbsd check-vars clean
+.PHONY: buildvm-start buildvm-stop buildvm-ensure
+
+# --- Build VM management ---
+
+buildvm-start:
+ @$(BUILDVM_DIR)/start.sh
+
+buildvm-stop:
+ @$(BUILDVM_DIR)/stop.sh
+
+# Ensure the build VM is running before native builds
+buildvm-ensure:
+ @if ! $(BUILDVM_SSH) $(BUILDVM_HOST) true 2>/dev/null; then \
+ echo "Build VM not running, starting..."; \
+ $(BUILDVM_DIR)/start.sh; \
+ fi
+
+# --- Single-binary Go packages (cross-compiled on Linux) ---
check-vars:
ifndef NAME
@@ -98,5 +135,48 @@ pkg-openbsd: /tmp/$(NAME)-openbsd
# Build and upload for both OSes
pkg: pkg-freebsd pkg-openbsd
+# --- DTail (multi-binary, native build on OpenBSD VM) ---
+
+# Sync dtail source to build VM and compile natively.
+# Native build handles CGo dependencies (e.g. DataDog/zstd).
+/tmp/dtail-binaries/.built: buildvm-ensure
+ @echo "Building DTail $(DTAIL_VERSION) natively on OpenBSD build VM..."
+ @mkdir -p /tmp/dtail-binaries
+ @# Sync source to VM via git archive (excludes build artifacts, saves space)
+ $(BUILDVM_SSH) $(BUILDVM_HOST) "rm -rf /tmp/dtail-src"
+ cd $(DTAIL_SRC) && git archive HEAD | $(BUILDVM_SSH) $(BUILDVM_HOST) "mkdir -p /tmp/dtail-src && tar -C /tmp/dtail-src -xf -"
+ @# Build all binaries natively on OpenBSD
+ $(BUILDVM_SSH) $(BUILDVM_HOST) "cd /tmp/dtail-src && for bin in $(DTAIL_BINARIES); do \
+ echo \" Building \$$bin...\"; \
+ go build -o /tmp/\$$bin ./cmd/\$$bin/main.go || exit 1; \
+ done"
+ @# Retrieve built binaries
+ @for bin in $(DTAIL_BINARIES); do \
+ $(BUILDVM_SCP) $(BUILDVM_HOST):/tmp/$$bin /tmp/dtail-binaries/$$bin; \
+ done
+ $(BUILDVM_SSH) $(BUILDVM_HOST) "rm -rf /tmp/dtail-src /tmp/dserver /tmp/dcat /tmp/dgrep /tmp/dmap /tmp/dtail /tmp/dtailhealth"
+ @# Bundle config files alongside binaries
+ cp $(CONF_FRONTENDS)/etc/dserver/dtail.json.tpl /tmp/dtail-binaries/dtail.json
+ cp $(CONF_FRONTENDS)/scripts/dserver-update-key-cache.sh.tpl /tmp/dtail-binaries/dserver-update-key-cache.sh
+ cp $(CONF_FRONTENDS)/etc/rc.d/dserver.tpl /tmp/dtail-binaries/dserver.rc
+ @touch $@
+
+# Package, sign, and upload the OpenBSD dtail package.
+# Binaries are built on the local build VM; packaging and signing happen on fishfinger.
+dtail-openbsd: /tmp/dtail-binaries/.built
+ @echo "Packaging dtail $(DTAIL_VERSION) for OpenBSD..."
+ $(OPENBSD_SCP) -r /tmp/dtail-binaries $(OPENBSD_HOST):/tmp/dtail-binaries
+ $(OPENBSD_SCP) $(SCRIPTS)/pkg-dtail-openbsd.sh $(OPENBSD_HOST):/tmp/pkg-dtail-openbsd.sh
+ $(OPENBSD_SSH) $(OPENBSD_HOST) "/bin/sh /tmp/pkg-dtail-openbsd.sh '$(DTAIL_VERSION)'"
+ @echo "Copying signed package to PV via f0..."
+ $(OPENBSD_SCP) $(OPENBSD_HOST):/tmp/dtail-pkg/out/dtail-$(DTAIL_VERSION).tgz /tmp/dtail-$(DTAIL_VERSION).tgz
+ $(FREEBSD_SCP) /tmp/dtail-$(DTAIL_VERSION).tgz $(FREEBSD_HOST):/tmp/dtail-$(DTAIL_VERSION).tgz
+ $(FREEBSD_SSH) $(FREEBSD_HOST) "doas cp /tmp/dtail-$(DTAIL_VERSION).tgz $(PV_BASE)/$(OPENBSD_REPO)/ && rm /tmp/dtail-$(DTAIL_VERSION).tgz"
+ @# Clean up remote and local temp files
+ $(OPENBSD_SSH) $(OPENBSD_HOST) "doas rm -rf /tmp/dtail-pkg /tmp/dtail-binaries /tmp/pkg-dtail-openbsd.sh"
+ rm -rf /tmp/dtail-binaries /tmp/dtail-$(DTAIL_VERSION).tgz
+ @echo "OpenBSD package dtail-$(DTAIL_VERSION) uploaded to repo"
+
clean:
rm -f /tmp/$(NAME)-freebsd /tmp/$(NAME)-openbsd /tmp/$(NAME)-*.tgz
+ rm -rf /tmp/dtail-binaries /tmp/dtail-*.tgz
diff --git a/packages/buildvm/.gitignore b/packages/buildvm/.gitignore
new file mode 100644
index 0000000..d036f27
--- /dev/null
+++ b/packages/buildvm/.gitignore
@@ -0,0 +1,5 @@
+*.qcow2
+*.iso
+*.pid
+custom-pkg.sec
+custom-pkg.pub
diff --git a/packages/buildvm/install-expect.exp b/packages/buildvm/install-expect.exp
new file mode 100755
index 0000000..e3f0c16
--- /dev/null
+++ b/packages/buildvm/install-expect.exp
@@ -0,0 +1,167 @@
+#!/usr/bin/expect -f
+# Automated OpenBSD installer via serial console.
+# Called by setup.sh — arguments: disk iso ram cpus ssh_port
+
+set timeout 600
+log_user 1
+
+set disk [lindex $argv 0]
+set iso [lindex $argv 1]
+set ram [lindex $argv 2]
+set cpus [lindex $argv 3]
+set sshport [lindex $argv 4]
+
+spawn qemu-system-x86_64 \
+ -machine accel=kvm \
+ -cpu host \
+ -m $ram \
+ -smp $cpus \
+ -drive file=$disk,format=qcow2,if=virtio \
+ -cdrom $iso \
+ -boot d \
+ -netdev user,id=net0,hostfwd=tcp::${sshport}-:22 \
+ -device virtio-net-pci,netdev=net0 \
+ -nographic
+
+# Redirect console to serial at boot prompt
+expect "boot>"
+send "set tty com0\r"
+expect "boot>"
+send "\r"
+
+# Installer menu
+expect "(I)nstall"
+send "i\r"
+
+# Terminal type — accept default vt220
+expect "Terminal type"
+send "\r"
+
+expect "System hostname"
+send "buildvm\r"
+
+# Network — accept default vio0
+expect "Network interface to configure"
+send "\r"
+
+# IPv4 — default is autoconf
+expect "IPv4 address"
+send "\r"
+
+expect "IPv6 address"
+send "none\r"
+
+# Done configuring interfaces
+expect "Network interface to configure"
+send "done\r"
+
+# DNS domain — installer may skip this prompt entirely.
+# Either way, wait for the password prompt.
+expect -re "will not echo.*$"
+sleep 2
+send "build123\r"
+
+expect -re "again.*$"
+sleep 2
+send "build123\r"
+
+expect "Start sshd"
+send "\r"
+
+expect "Do you expect to run the X"
+send "no\r"
+
+# Console redirect prompt (because we set tty com0)
+expect "Change the default console"
+send "yes\r"
+
+# Speed — accept default
+expect "Which speed"
+send "\r"
+
+# User setup — wait for full prompt to appear before sending
+expect -re "etup a user.*\\]"
+sleep 1
+send "pbuild\r"
+
+expect "Full name"
+send "\r"
+
+# User password — same serial console timing as root password
+expect -re "will not echo.*$"
+sleep 2
+send "build123\r"
+
+expect -re "again.*$"
+sleep 2
+send "build123\r"
+
+expect "Allow root ssh login"
+send "no\r"
+
+expect "timezone"
+send "UTC\r"
+
+expect "root disk"
+send "\r"
+
+# Disk encryption — decline (OpenBSD 7.8+)
+expect "Encrypt the root disk"
+send "\r"
+
+# Whole disk
+expect "Use (W)hole"
+send "w\r"
+
+# GPT or auto layout
+expect {
+ "Use (G)PT" { send "\r"; exp_continue }
+ "(A)uto layout" { send "a\r" }
+}
+
+expect "Location of sets"
+send "cd0\r"
+
+expect "Pathname to the sets"
+send "\r"
+
+expect "Set name"
+send -- "-game*\r"
+
+expect "Set name"
+send -- "-x*\r"
+
+expect "Set name"
+send "\r"
+
+expect "without verification"
+send "yes\r"
+
+# After sets install, installer asks if we want more sets — accept "done" default
+expect -timeout 600 "Location of sets"
+send "\r"
+
+# Wait for finalization
+expect -timeout 120 "CONGRATULATIONS"
+
+# Time may or may not appear wrong
+expect -re "Time appears wrong|Exit to"
+if {[string match "*Time*" $expect_out(0,string)]} {
+ send "\r"
+ expect "Exit to"
+}
+
+# Drop to installer shell instead of rebooting (CD is still attached,
+# reboot would boot the installer again). The installed system is at /mnt.
+send "s\r"
+expect "#"
+
+# Configure doas and wheel group on the installed system
+send "chroot /mnt usermod -G wheel pbuild\r"
+expect "#"
+send "echo 'permit nopass pbuild' > /mnt/etc/doas.conf\r"
+expect "#"
+
+# Shut down cleanly
+send "halt -p\r"
+expect -timeout 60 eof
diff --git a/packages/buildvm/install.conf b/packages/buildvm/install.conf
new file mode 100644
index 0000000..7a509e6
--- /dev/null
+++ b/packages/buildvm/install.conf
@@ -0,0 +1,18 @@
+System hostname = buildvm
+Which network interface do you wish to configure = vio0
+IPv4 address for vio0 = autoconf
+IPv6 address for vio0 = none
+Which network interface do you wish to configure = done
+DNS domain name = local
+Password for root = build123
+Setup a user = pbuild
+Password for user = build123
+Allow root ssh login = no
+What timezone are you in = UTC
+Which disk is the root disk = sd0
+Encrypt the root disk = no
+Use (W)hole disk MBR, whole disk GPT, (O)penBSD area or (E)dit = whole
+Use (A)uto layout, (E)dit auto layout, or create (C)ustom layout = a
+Location of sets = cd0
+Set name(s) = -game*.tgz -x*.tgz
+Continue without verification = yes
diff --git a/packages/buildvm/provision.sh b/packages/buildvm/provision.sh
new file mode 100755
index 0000000..0d95856
--- /dev/null
+++ b/packages/buildvm/provision.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+# Provision the OpenBSD build VM after a fresh install.
+# Run once from the host after setup.sh completes.
+#
+# Installs Go, gmake, git, sets up SSH key access,
+# doas for the build user, and signify keys for package signing.
+# Uses sshpass for initial password-based SSH (before key is installed).
+
+set -e
+
+VMDIR="$(cd "$(dirname "$0")" && pwd)"
+SSH_PORT=2222
+SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT"
+
+# First boot the VM
+echo "Starting build VM..."
+"$VMDIR/start.sh"
+
+# Set up SSH key — use sshpass if available, fall back to manual prompt
+echo "Setting up SSH key access..."
+if command -v sshpass &>/dev/null; then
+ sshpass -p build123 ssh-copy-id $SSH_OPTS pbuild@localhost 2>/dev/null
+else
+ echo "sshpass not found. Enter the build user password (build123) when prompted:"
+ ssh-copy-id $SSH_OPTS pbuild@localhost
+fi
+
+SSH="ssh $SSH_OPTS pbuild@localhost"
+SCP="scp $SSH_OPTS"
+
+# Configure doas for passwordless access (may already be set by setup.sh)
+echo "Configuring doas..."
+$SSH "echo 'permit nopass pbuild' | doas tee /etc/doas.conf > /dev/null"
+
+# Install build tools
+echo "Installing Go, git, gmake..."
+$SSH "doas pkg_add go git gmake"
+
+# Copy signify keys for package signing (if available locally)
+if [ -f "$VMDIR/custom-pkg.sec" ] && [ -f "$VMDIR/custom-pkg.pub" ]; then
+ echo "Installing signify keys..."
+ $SCP "$VMDIR/custom-pkg.sec" "$VMDIR/custom-pkg.pub" pbuild@localhost:/tmp/
+ $SSH "doas cp /tmp/custom-pkg.sec /tmp/custom-pkg.pub /etc/signify/ && \
+ doas chmod 600 /etc/signify/custom-pkg.sec && \
+ doas chmod 644 /etc/signify/custom-pkg.pub && \
+ rm /tmp/custom-pkg.sec /tmp/custom-pkg.pub"
+ echo "Signify keys installed."
+else
+ echo ""
+ echo "WARNING: Signify keys not found at $VMDIR/custom-pkg.{sec,pub}"
+ echo "Copy them from fishfinger before building signed packages:"
+ echo " scp rex@fishfinger.buetow.org:/etc/signify/custom-pkg.sec $VMDIR/"
+ echo " scp rex@fishfinger.buetow.org:/etc/signify/custom-pkg.pub $VMDIR/"
+ echo "Then re-run: $0"
+fi
+
+echo ""
+echo "Verifying..."
+$SSH "go version && uname -a"
+echo ""
+echo "Build VM provisioned. Ready for: make dtail-openbsd"
diff --git a/packages/buildvm/setup.sh b/packages/buildvm/setup.sh
new file mode 100755
index 0000000..49312bd
--- /dev/null
+++ b/packages/buildvm/setup.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+# Create and configure an OpenBSD QEMU/KVM VM for native package builds.
+#
+# Fully automated via expect driving the serial console installer.
+# The expect script is in install-expect.exp (separate file avoids
+# bash/expect quoting issues with password prompts).
+#
+# Prerequisites: qemu-system-x86_64, expect, KVM (/dev/kvm)
+
+set -e
+
+VMDIR="$(cd "$(dirname "$0")" && pwd)"
+DISK="$VMDIR/openbsd-build.qcow2"
+OBSD_VERSION="7.8"
+ISO="$VMDIR/install${OBSD_VERSION//./}.iso"
+SSH_PORT=2222
+RAM=1024
+CPUS=2
+
+if [ -f "$DISK" ]; then
+ echo "Disk $DISK already exists. Delete it first to reinstall."
+ exit 1
+fi
+
+# Download install ISO if not cached
+if [ ! -f "$ISO" ]; then
+ echo "Downloading OpenBSD $OBSD_VERSION install ISO..."
+ curl -L -o "$ISO" "https://cdn.openbsd.org/pub/OpenBSD/$OBSD_VERSION/amd64/install${OBSD_VERSION//./}.iso"
+fi
+
+echo "Creating ${DISK}..."
+qemu-img create -f qcow2 "$DISK" 4G
+
+echo ""
+echo "Starting automated OpenBSD install (takes ~5 minutes)..."
+echo ""
+
+expect "$VMDIR/install-expect.exp" "$DISK" "$ISO" "$RAM" "$CPUS" "$SSH_PORT"
+
+echo ""
+echo "OpenBSD install complete. Now run: $VMDIR/provision.sh"
diff --git a/packages/buildvm/start.sh b/packages/buildvm/start.sh
new file mode 100755
index 0000000..2cc3e54
--- /dev/null
+++ b/packages/buildvm/start.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+# Start the OpenBSD build VM in the background.
+# SSH available at localhost:2222 after boot (~15s).
+
+set -e
+
+VMDIR="$(cd "$(dirname "$0")" && pwd)"
+DISK="$VMDIR/openbsd-build.qcow2"
+PIDFILE="$VMDIR/qemu.pid"
+SSH_PORT=2222
+RAM=1024
+CPUS=2
+
+if [ ! -f "$DISK" ]; then
+ echo "Error: $DISK not found. Run setup.sh first."
+ exit 1
+fi
+
+if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
+ echo "Build VM already running (PID $(cat "$PIDFILE"))"
+ exit 0
+fi
+
+# Use -display none + -serial null for headless background operation.
+# -nographic cannot be combined with -daemonize.
+echo "Starting OpenBSD build VM..."
+qemu-system-x86_64 \
+ -machine accel=kvm \
+ -cpu host \
+ -m "$RAM" \
+ -smp "$CPUS" \
+ -drive file="$DISK",format=qcow2,if=virtio \
+ -netdev user,id=net0,hostfwd=tcp::${SSH_PORT}-:22 \
+ -device virtio-net-pci,netdev=net0 \
+ -display none \
+ -serial null \
+ -daemonize \
+ -pidfile "$PIDFILE"
+
+echo "VM started (PID $(cat "$PIDFILE")), waiting for SSH..."
+
+# Wait for SSH to become available
+for i in $(seq 1 30); do
+ if ssh -q -o ConnectTimeout=2 -o StrictHostKeyChecking=no -p "$SSH_PORT" pbuild@localhost true 2>/dev/null; then
+ echo "SSH ready at localhost:$SSH_PORT"
+ exit 0
+ fi
+ sleep 2
+done
+
+echo "Warning: SSH not responding after 60s. VM may still be booting."
diff --git a/packages/buildvm/stop.sh b/packages/buildvm/stop.sh
new file mode 100755
index 0000000..923ddaa
--- /dev/null
+++ b/packages/buildvm/stop.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+# Shut down the OpenBSD build VM gracefully.
+
+set -e
+
+VMDIR="$(cd "$(dirname "$0")" && pwd)"
+PIDFILE="$VMDIR/qemu.pid"
+SSH_PORT=2222
+
+if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
+ echo "Build VM is not running."
+ rm -f "$PIDFILE"
+ exit 0
+fi
+
+echo "Shutting down build VM..."
+# Graceful shutdown via SSH, fall back to SIGTERM
+ssh -q -o ConnectTimeout=5 -p "$SSH_PORT" pbuild@localhost "doas halt -p" 2>/dev/null || true
+
+# Wait for QEMU process to exit
+PID=$(cat "$PIDFILE")
+for i in $(seq 1 15); do
+ if ! kill -0 "$PID" 2>/dev/null; then
+ echo "VM stopped."
+ rm -f "$PIDFILE"
+ exit 0
+ fi
+ sleep 1
+done
+
+# Force kill if still running
+echo "Force-killing QEMU (PID $PID)..."
+kill "$PID" 2>/dev/null || true
+rm -f "$PIDFILE"
diff --git a/packages/scripts/pkg-dtail-openbsd.sh b/packages/scripts/pkg-dtail-openbsd.sh
new file mode 100755
index 0000000..1206927
--- /dev/null
+++ b/packages/scripts/pkg-dtail-openbsd.sh
@@ -0,0 +1,80 @@
+#!/bin/sh
+# Build and sign an OpenBSD dtail package from pre-compiled binaries and config files.
+# Run on an OpenBSD host (e.g. fishfinger). Called by the Makefile via SSH.
+# The signed .tgz is left in /tmp/dtail-pkg/out/ for the Makefile to retrieve.
+#
+# Arguments:
+# $1 — version (e.g. 4.3.2)
+
+set -e
+
+VERSION="$1"
+NAME="dtail"
+COMMENT="Distributed log tail and grep tool"
+DESC="DTail is a distributed DevOps tool for tailing, grepping, catting, and
+mapping across many remote machines at once via SSH."
+
+WORKDIR="/tmp/${NAME}-pkg"
+doas rm -rf "$WORKDIR"
+mkdir -p "$WORKDIR/stage/usr/local/bin"
+mkdir -p "$WORKDIR/stage/etc/dserver"
+mkdir -p "$WORKDIR/stage/etc/rc.d"
+mkdir -p "$WORKDIR/out"
+
+# Place the pre-compiled binaries
+for bin in dserver dcat dgrep dmap dtail dtailhealth; do
+ cp "/tmp/dtail-binaries/${bin}" "$WORKDIR/stage/usr/local/bin/${bin}"
+ chmod 755 "$WORKDIR/stage/usr/local/bin/${bin}"
+done
+
+# Place the key cache helper script
+cp "/tmp/dtail-binaries/dserver-update-key-cache.sh" \
+ "$WORKDIR/stage/usr/local/bin/dserver-update-key-cache.sh"
+chmod 500 "$WORKDIR/stage/usr/local/bin/dserver-update-key-cache.sh"
+
+# Place the config file
+cp "/tmp/dtail-binaries/dtail.json" "$WORKDIR/stage/etc/dserver/dtail.json"
+chmod 644 "$WORKDIR/stage/etc/dserver/dtail.json"
+
+# Place the rc script
+cp "/tmp/dtail-binaries/dserver.rc" "$WORKDIR/stage/etc/rc.d/dserver"
+chmod 755 "$WORKDIR/stage/etc/rc.d/dserver"
+
+# Packing list — all files with absolute paths
+cat > "$WORKDIR/plist" <<'PLIST'
+usr/local/bin/dserver
+usr/local/bin/dcat
+usr/local/bin/dgrep
+usr/local/bin/dmap
+usr/local/bin/dtail
+usr/local/bin/dtailhealth
+usr/local/bin/dserver-update-key-cache.sh
+etc/dserver/dtail.json
+etc/rc.d/dserver
+PLIST
+
+# Description file
+printf '%s\n' "$DESC" > "$WORKDIR/desc"
+
+# Build the package
+doas pkg_create \
+ -D COMMENT="$COMMENT" \
+ -d "$WORKDIR/desc" \
+ -f "$WORKDIR/plist" \
+ -B "$WORKDIR/stage" \
+ -p / \
+ "$WORKDIR/out/${NAME}-${VERSION}.tgz"
+
+# Sign with signify if the key exists
+if [ -f /etc/signify/custom-pkg.sec ]; then
+ mkdir -p "$WORKDIR/signed"
+ doas pkg_sign -s signify2 -s /etc/signify/custom-pkg.sec \
+ -o "$WORKDIR/signed" "$WORKDIR/out/${NAME}-${VERSION}.tgz"
+ mv "$WORKDIR/signed/${NAME}-${VERSION}.tgz" "$WORKDIR/out/${NAME}-${VERSION}.tgz"
+ rm -rf "$WORKDIR/signed"
+ echo "Package signed with signify"
+else
+ echo "Warning: /etc/signify/custom-pkg.sec not found, package is unsigned"
+fi
+
+echo "OpenBSD package ${NAME}-${VERSION} built in $WORKDIR/out/"