P01-T01 - HQ-CHR01 Clean Deployment
Objective
Deploy HQ-CHR01 from the low-VMID CHR template and inject the first-boot RouterOS bootstrap offline before the clone is started.
This reflects the validated lab reality on Friday, July 17, 2026:
- Proxmox handles the clone lifecycle.
- RouterOS consumes
rw/autorun.scron first boot. QEMU Guest Agentis used for validation only, not for RouterOS command execution.
Validated lab status
Validated against the live lab on Friday, July 17, 2026:
4004was rebuilt from the officialCHR 7.21.5image and kept as a never-booted source template.4010was cloned successfully from4004.- the clean clone reported only
ether1andether2 - the first-boot bootstrap mechanism worked when injected into
rw/autorun.scr - the earlier attempt against
rw/store/autorun.scrdid not execute and is not an approved path
Inputs
| Key | Value |
|---|---|
| Node | H1 |
| Source template | 4004 / TPL-CHR-7.21.5 |
| VMID | 4010 |
| Name | HQ-CHR01 |
| RouterOS version | 7.21.5 Long-term |
| CPU | 2 vCPU |
| Memory | 2048 MB |
| System disk | 512 MB |
| Container disk | 1 GB |
| Storage | local-zfs |
net0 |
VirtIO -> GEILWAN |
net1 |
VirtIO -> GEILLAN |
| Guest agent | Enabled |
| Startup | Disabled initially |
| Console | Serial |
net1 must remain untagged in Proxmox because it carries the full VLAN trunk into RouterOS.
Target State
| Property | Value |
|---|---|
| Proxmox WAN bridge | GEILWAN |
| Proxmox LAN bridge | GEILLAN |
| First-boot bootstrap path | rw/autorun.scr inside the clone disk |
| First runtime hostname before bootstrap settles | MikroTik |
| First runtime timezone before bootstrap settles | UTC |
| CHR post-build validation channels | qm terminal 4010 and qm guest cmd 4010 ... |
Prechecks
- Run on Proxmox node
H1. - Confirm
4004exists and is still a template. - Do not boot
4004directly. Validate only through clones. - Confirm
GEILWANexists. - Confirm
GEILLANexists and is VLAN-aware. - Confirm storage
local-zfsis active. - Confirm
VMID 4010is unused before creation.
Execution
This block creates the runtime VM in Proxmox and prepares the guest-side bootstrap before the first boot. It is intentionally split this way because CHR configuration is not driven through QGA.
Run the deployment script on H1:
Show full CHR deployment script
#!/usr/bin/env bash
set -Eeuo pipefail
VMID="${VMID:-4010}"
SOURCE_TEMPLATE_VMID="${SOURCE_TEMPLATE_VMID:-4004}"
VM_NAME="${VM_NAME:-HQ-CHR01}"
WAN_BRIDGE="${WAN_BRIDGE:-GEILWAN}"
LAN_BRIDGE="${LAN_BRIDGE:-GEILLAN}"
WAN_IP_CIDR="${WAN_IP_CIDR:-172.31.255.2/30}"
WAN_GATEWAY="${WAN_GATEWAY:-172.31.255.1}"
TIMEZONE_NAME="${TIMEZONE_NAME:-America/Santo_Domingo}"
AUTORUN_IDENTITY_NAME="${AUTORUN_IDENTITY_NAME:-$VM_NAME}"
STARTUP_ORDER="${STARTUP_ORDER:-10}"
STARTUP_UP_DELAY="${STARTUP_UP_DELAY:-30}"
STARTUP_DOWN_DELAY="${STARTUP_DOWN_DELAY:-60}"
KEEP_AUTORUN_TEST_FILE="${KEEP_AUTORUN_TEST_FILE:-0}"
SKIP_BOOT_VALIDATION="${SKIP_BOOT_VALIDATION:-0}"
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
}
bridge_exists() {
ip link show "$1" >/dev/null 2>&1
}
cleanup_mount() {
mountpoint -q /mnt/gntech-chr-rw && umount /mnt/gntech-chr-rw || true
}
mount_rw_partition() {
local disk_path zvol_dev
disk_path="$(qm config "$VMID" | awk -F': ' '/^virtio0:/ {print $2; exit}' | cut -d',' -f1)"
[[ -n "$disk_path" ]] || fail "Could not resolve virtio0 path for VM $VMID"
zvol_dev="$(readlink -f "/dev/zvol/$disk_path")"
[[ -b "$zvol_dev" ]] || fail "Resolved zvol device is not a block device: $zvol_dev"
[[ -b "${zvol_dev}p2" ]] || fail "RouterOS RW partition not found: ${zvol_dev}p2"
mkdir -p /mnt/gntech-chr-rw
mount -o rw "${zvol_dev}p2" /mnt/gntech-chr-rw
}
write_autorun() {
local marker_line=""
if [[ "$KEEP_AUTORUN_TEST_FILE" == "1" ]]; then
marker_line='/file print file=bootstrap-validation'
fi
cat > /mnt/gntech-chr-rw/rw/autorun.scr <<EOF
/system identity set name=${AUTORUN_IDENTITY_NAME}
/system clock set time-zone-name=${TIMEZONE_NAME}
/ip dhcp-client remove [find where interface=ether1]
/ip address add address=${WAN_IP_CIDR} interface=ether1 comment=WAN
/ip route add dst-address=0.0.0.0/0 gateway=${WAN_GATEWAY} comment=Default
${marker_line}
EOF
sync
}
validate_boot() {
log "Waiting for QEMU Guest Agent on VM ${VMID}"
local ready=0
for _ in $(seq 1 30); do
if qm guest cmd "$VMID" ping >/dev/null 2>&1; then
ready=1
break
fi
sleep 2
done
[[ "$ready" == "1" ]] || fail "QEMU Guest Agent did not respond within 60 seconds on VM ${VMID}"
log "Collecting first-boot guest state"
qm guest cmd "$VMID" get-osinfo
qm guest cmd "$VMID" get-host-name
qm guest cmd "$VMID" network-get-interfaces
}
trap cleanup_mount EXIT
require_command qm
require_command ip
require_command mount
require_command umount
require_command readlink
qm status "$SOURCE_TEMPLATE_VMID" >/dev/null 2>&1 || fail "Source template VMID ${SOURCE_TEMPLATE_VMID} not found"
qm config "$SOURCE_TEMPLATE_VMID" | grep -q '^template: 1' || fail "Source VMID ${SOURCE_TEMPLATE_VMID} is not a template"
qm status "$VMID" >/dev/null 2>&1 && fail "Target VMID ${VMID} already exists"
bridge_exists "$WAN_BRIDGE" || fail "WAN bridge ${WAN_BRIDGE} does not exist"
bridge_exists "$LAN_BRIDGE" || fail "LAN bridge ${LAN_BRIDGE} does not exist"
log "Cloning template ${SOURCE_TEMPLATE_VMID} to ${VMID}"
qm clone "$SOURCE_TEMPLATE_VMID" "$VMID" --name "$VM_NAME" \
--description "Enterprise headquarters MikroTik CHR router and firewall. Clean validation deploy from TPL-CHR-7.21.5 (${SOURCE_TEMPLATE_VMID})."
log "Applying Proxmox-side runtime settings"
qm set "$VMID" \
--net0 "virtio,bridge=${WAN_BRIDGE},firewall=0" \
--net1 "virtio,bridge=${LAN_BRIDGE},firewall=0" \
--onboot 0 \
--startup "order=${STARTUP_ORDER},up=${STARTUP_UP_DELAY},down=${STARTUP_DOWN_DELAY}" \
--tags "enterprise;hq;router;firewall;mikrotik"
log "Injecting first-boot RouterOS bootstrap into rw/autorun.scr"
mount_rw_partition
write_autorun
cleanup_mount
log "Starting VM ${VMID}"
qm start "$VMID"
if [[ "$SKIP_BOOT_VALIDATION" != "1" ]]; then
validate_boot
fi
log "Deployment completed for VM ${VMID}"
The sysadmin can override deployment variables through environment values before running the script.
Full reference:
Configurable inputs in the script:
| Variable | Purpose | Default |
|---|---|---|
VMID |
target runtime VMID | 4010 |
SOURCE_TEMPLATE_VMID |
source template VMID | 4004 |
VM_NAME |
runtime router name in Proxmox | HQ-CHR01 |
WAN_BRIDGE |
Proxmox WAN bridge | GEILWAN |
LAN_BRIDGE |
Proxmox LAN bridge | GEILLAN |
WAN_IP_CIDR |
first-boot static WAN address | 172.31.255.2/30 |
WAN_GATEWAY |
first-boot default gateway | 172.31.255.1 |
TIMEZONE_NAME |
RouterOS time zone | America/Santo_Domingo |
AUTORUN_IDENTITY_NAME |
RouterOS hostname set by bootstrap | HQ-CHR01 |
KEEP_AUTORUN_TEST_FILE |
create a validation marker file | 0 |
SKIP_BOOT_VALIDATION |
skip immediate QGA checks |
0 |
Validation
This validation block confirms the Proxmox clone is correct before deeper RouterOS work starts.
qm config 4010must include:agent: enabled=1boot: order=virtio0name: HQ-CHR01net0: virtio=...,bridge=GEILWAN,firewall=0net1: virtio=...,bridge=GEILLAN,firewall=0virtio0: ...size=512Mvirtio1: ...size=1Gqm guest cmd 4010 get-osinforeportsRouterOS 7.21.5andCHR.qm guest cmd 4010 network-get-interfacesshows onlyether1andether2on a clean clone.qm guest cmd 4010 get-host-namemay still showMikroTikimmediately after the first boot. In the validated lab, the bootstrap identity change was visible by the next boot cycle.
Evidence
- Output of
qm config 4010 - Output of
qm guest cmd 4010 get-osinfo - Output of
qm guest cmd 4010 network-get-interfaces - Output of
qm guest cmd 4010 get-host-name
Rollback
- Destroy the runtime clone with
qm destroy 4010 --purge 1if the wrong bridges, disks, or first-boot bootstrap values were injected. - Do not boot or modify
4004directly as part of rollback.