#!/usr/bin/env bash # proxmox/build-vm.sh — Maakt de doel-VM aan op een Proxmox-node. # # Optioneel script: sla dit over als je de VM handmatig aanmaakt of een # andere hypervisor gebruikt. De overige scripts (01–05) werken op elke # Linux-VM met SSH-toegang. # # Wat het doet: # - VM aanmaken (Debian 13 cloud image, qcow2) # - Systeemdisk + aparte datadisk (LVM, ext4, /mnt/ncdata) # - Netplan vervangen door /etc/network/interfaces # - Loopback VIP (publiek IP) via systemd oneshot (indien VM_PUBLIC_IP gezet) # - OPNSense: gateway + static route voor het publieke VIP (indien VM_PUBLIC_IP gezet) # # Vereisten: # - SSH-alias voor PROXMOX_HOST en (indien VIP) fw01 werken # - Debian 13 genericcloud qcow2 beschikbaar op de PVE-node (zie CLOUD_IMAGE) # - config.sh ingevuld set -euo pipefail source "$(dirname "$0")/../config.sh" log() { echo "[$(date '+%H:%M:%S')] $*"; } SCRATCHDIR=$(mktemp -d) trap 'rm -rf "$SCRATCHDIR"' EXIT # ── STAP 1: VM aanmaken ─────────────────────────────────────────────────────── log "VM ${PROXMOX_VMID} (${VM_NAME}) aanmaken op ${PROXMOX_HOST}..." ssh "${PROXMOX_HOST}" " qm create ${PROXMOX_VMID} \ --name ${VM_NAME} \ --memory ${VM_MEMORY} \ --cores ${VM_CORES} \ --sockets 1 \ --cpu host \ --net0 virtio,bridge=${VM_BRIDGE}$([ -n '${VM_VLAN}' ] && echo ',tag=${VM_VLAN}' || true) \ --scsihw virtio-scsi-single \ --ostype l26 \ --serial0 socket \ --vga serial0 \ --agent enabled=1 \ --onboot 1 " log "Systeemdisk importeren als qcow2..." ssh "${PROXMOX_HOST}" \ "qm importdisk ${PROXMOX_VMID} ${CLOUD_IMAGE} local --format qcow2 2>&1 | tail -1" PUBKEY_PVE=$(ssh "${PROXMOX_HOST}" \ "cat /root/.ssh/id_ed25519.pub 2>/dev/null || cat /root/.ssh/id_rsa.pub") PUBKEY_WS=$(cat ~/.ssh/id_ed25519.pub 2>/dev/null || cat ~/.ssh/id_rsa.pub) log "Disks koppelen + cloud-init configureren..." ssh "${PROXMOX_HOST}" " qm set ${PROXMOX_VMID} \ --scsi0 local:${PROXMOX_VMID}/vm-${PROXMOX_VMID}-disk-0.qcow2,discard=on,ssd=1,iothread=1 \ --scsi1 local:${VM_DATA_SIZE},format=qcow2,discard=on,iothread=1 \ --boot order=scsi0 \ --ide2 local:cloudinit \ --ciuser root \ --sshkeys <(printf '%s\n%s\n' '${PUBKEY_PVE}' '${PUBKEY_WS}') \ --ipconfig0 ip=${VM_PRIVATE_IP}/${VM_PRIVATE_PREFIX},gw=${VM_PRIVATE_GW} \ --nameserver ${VM_DNS} \ --searchdomain ${VM_SEARCH_DOMAIN} qm resize ${PROXMOX_VMID} scsi0 ${VM_DISK_SIZE}G " # ── STAP 2: VM starten en wachten op SSH ───────────────────────────────────── log "VM starten..." ssh "${PROXMOX_HOST}" "qm start ${PROXMOX_VMID}" log "Wachten op SSH (${VM_PRIVATE_IP})..." for i in $(seq 1 60); do ssh "${PROXMOX_HOST}" \ "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=4 \ root@${VM_PRIVATE_IP} hostname 2>/dev/null" && break || true sleep 5 done # ── STAP 3: Netplan → ifupdown ─────────────────────────────────────────────── log "Netplan vervangen door ifupdown..." ssh "${PROXMOX_HOST}" "ssh root@${VM_PRIVATE_IP} bash -s" << EOF set -e apt-get install -y ifupdown 2>&1 | tail -1 cat > /etc/network/interfaces << 'IFEOF' source /etc/network/interfaces.d/* auto lo iface lo inet loopback auto ens18 iface ens18 inet static address ${VM_PRIVATE_IP}/${VM_PRIVATE_PREFIX} gateway ${VM_PRIVATE_GW} dns-nameservers ${VM_DNS} 1.1.1.1 dns-search ${VM_SEARCH_DOMAIN} IFEOF systemctl disable systemd-networkd systemd-networkd-wait-online 2>/dev/null || true mkdir -p /etc/cloud/cloud.cfg.d echo "network: {config: disabled}" > /etc/cloud/cloud.cfg.d/99-disable-network.cfg apt-get purge -y netplan.io 2>&1 | tail -1 rm -rf /etc/netplan /usr/share/netplan EOF # ── STAP 4: Loopback VIP (optioneel) ───────────────────────────────────────── if [[ -n "${VM_PUBLIC_IP}" ]]; then log "Loopback VIP ${VM_PUBLIC_IP} instellen..." ssh "${PROXMOX_HOST}" "ssh root@${VM_PRIVATE_IP} bash -s" << EOF set -e cat > /etc/systemd/system/loopback-vip.service << 'VIPEOF' [Unit] Description=Loopback VIP After=network.target [Service] Type=oneshot ExecStart=/sbin/ip addr add ${VM_PUBLIC_IP}/32 dev lo label lo:vip ExecStop=/sbin/ip addr del ${VM_PUBLIC_IP}/32 dev lo label lo:vip RemainAfterExit=yes [Install] WantedBy=multi-user.target VIPEOF systemctl enable loopback-vip.service EOF fi # ── STAP 5: Datadisk — LVM + ext4 ─────────────────────────────────────────── log "Datadisk inrichten (LVM vg_data/ncdata, ext4, /mnt/ncdata)..." ssh "${PROXMOX_HOST}" "ssh root@${VM_PRIVATE_IP} bash -s" << 'EOF' set -e apt-get install -y lvm2 2>&1 | tail -1 DISK=/dev/sdb [ -b "$DISK" ] || { echo "FOUT: $DISK niet gevonden"; exit 1; } [ -z "$(lsblk -dn -o FSTYPE $DISK 2>/dev/null)" ] || { echo "FOUT: $DISK is niet leeg"; exit 1; } pvcreate "$DISK" vgcreate vg_data "$DISK" lvcreate -n ncdata -l 100%FREE vg_data mkfs.ext4 -L ncdata /dev/vg_data/ncdata mkdir -p /mnt/ncdata echo "/dev/vg_data/ncdata /mnt/ncdata ext4 defaults,noatime 0 2" >> /etc/fstab mount -a df -h /mnt/ncdata EOF # ── STAP 6: Basis-pakketten + reboot ───────────────────────────────────────── log "Basis-pakketten installeren..." ssh "${PROXMOX_HOST}" "ssh root@${VM_PRIVATE_IP} bash -s" << 'EOF' set -e apt-get update -qq apt-get install -y qemu-guest-agent rsync curl ca-certificates gnupg htop 2>&1 | tail -1 systemctl enable --now qemu-guest-agent EOF log "Reboot..." ssh "${PROXMOX_HOST}" "ssh root@${VM_PRIVATE_IP} reboot" || true sleep 10 log "Wachten op herstart..." for i in $(seq 1 60); do ssh "${PROXMOX_HOST}" \ "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=4 \ root@${VM_PRIVATE_IP} hostname 2>/dev/null" && break || true sleep 5 done # ── STAP 7: OPNSense gateway + static route (optioneel) ────────────────────── if [[ -n "${VM_PUBLIC_IP}" ]]; then log "OPNSense: gateway + static route voor ${VM_PUBLIC_IP}..." GW_NAME="GW_$(echo ${VM_NAME} | tr '[:lower:]' '[:upper:]')" cat > "${SCRATCHDIR}/add_route.py" << PYEOF import xml.etree.ElementTree as ET, uuid, sys tree = ET.parse('/conf/config.xml') root = tree.getroot() name, gw_ip, pub_ip, iface = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] gws = root.find('OPNsense/Gateways') for old in gws.findall('gateway_item'): if old.findtext('name') == name: gws.remove(old) gw = ET.SubElement(gws, 'gateway_item') gw.set('uuid', str(uuid.uuid4())) for tag, val in [ ('disabled','0'),('name',name),('descr',name), ('interface',iface),('ipprotocol','inet'),('gateway',gw_ip), ('defaultgw','0'),('fargw','0'),('monitor_disable','1'), ('force_down','0'),('nosync','0'),('priority','255'),('weight','1') ]: ET.SubElement(gw, tag).text = val sr = root.find('staticroutes') for old in sr.findall('route'): if old.findtext('network') == pub_ip: sr.remove(old) route = ET.SubElement(sr, 'route') route.set('uuid', str(uuid.uuid4())) for tag, val in [('network',pub_ip),('gateway',name),('descr',name),('enabled','1')]: ET.SubElement(route, tag).text = val tree.write('/conf/config.xml') print('done') PYEOF # fw01 root shell is csh — altijd wrappen in sh -c, anders verminkt csh # fd-redirects (2>/dev/null) en kan een bogus 0.0.0.0/1 route aanmaken. scp "${SCRATCHDIR}/add_route.py" fw01:/tmp/add_route.py ssh fw01 "sh -c 'python3 /tmp/add_route.py \ ${GW_NAME} ${VM_PRIVATE_IP} ${VM_PUBLIC_IP}/32 opt5 \ && rm /tmp/add_route.py'" ssh fw01 "sh -c 'configctl filter reload'" log "✓ OPNSense route actief." fi log "✓ VM klaar: ${VM_PRIVATE_IP}$([ -n '${VM_PUBLIC_IP}' ] && echo " / VIP ${VM_PUBLIC_IP}" || true)" log " Pas TARGET_SSH aan in config.sh en ga verder met 01-transfer.sh"