Skip to content

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.scr on first boot.
  • QEMU Guest Agent is used for validation only, not for RouterOS command execution.

Validated lab status

Validated against the live lab on Friday, July 17, 2026:

  • 4004 was rebuilt from the official CHR 7.21.5 image and kept as a never-booted source template.
  • 4010 was cloned successfully from 4004.
  • the clean clone reported only ether1 and ether2
  • the first-boot bootstrap mechanism worked when injected into rw/autorun.scr
  • the earlier attempt against rw/store/autorun.scr did 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 4004 exists and is still a template.
  • Do not boot 4004 directly. Validate only through clones.
  • Confirm GEILWAN exists.
  • Confirm GEILLAN exists and is VLAN-aware.
  • Confirm storage local-zfs is active.
  • Confirm VMID 4010 is 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:

scripts/Deploy-HQ-CHR01.sh
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 4010 must include:
  • agent: enabled=1
  • boot: order=virtio0
  • name: HQ-CHR01
  • net0: virtio=...,bridge=GEILWAN,firewall=0
  • net1: virtio=...,bridge=GEILLAN,firewall=0
  • virtio0: ...size=512M
  • virtio1: ...size=1G
  • qm guest cmd 4010 get-osinfo reports RouterOS 7.21.5 and CHR.
  • qm guest cmd 4010 network-get-interfaces shows only ether1 and ether2 on a clean clone.
  • qm guest cmd 4010 get-host-name may still show MikroTik immediately 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 1 if the wrong bridges, disks, or first-boot bootstrap values were injected.
  • Do not boot or modify 4004 directly as part of rollback.