Initial commit: Nextcloud bare/Hansson → AIO migration toolkit
Scripts 01-05 voor sequentiële migratie: transfer, staging, upgrade-keten, AIO-install en DB-import. PostgreSQL + MySQL/MariaDB bronnen ondersteund. Proxmox/OPNSense VM-builder optioneel in proxmox/. README met 12 gotchas. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
+210
@@ -0,0 +1,210 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user