430 lines
18 KiB
Bash
430 lines
18 KiB
Bash
# shellcheck shell=sh
|
|
#
|
|
# Die goldene Vorlage: ein Debian-13-Cloud-Abbild, in das Docker, Compose und
|
|
# die Nextcloud-Aufstellung eingebaut werden, konvertiert zur Proxmox-Vorlage
|
|
# auf VMID 9000.
|
|
#
|
|
# ---------------------------------------------------------------------------
|
|
# Die drei Fallen aus Block A
|
|
# ---------------------------------------------------------------------------
|
|
#
|
|
# Sie sind nicht verhandelbar, und sie werden hier alle drei GEPRÜFT statt
|
|
# angenommen — jede von ihnen hat einmal einen bezahlten Auftrag gekostet:
|
|
#
|
|
# 1. `user: www-data` am `app`-Dienst der Compose-Datei. Ohne sie schlagen ALLE
|
|
# `occ`-Aufrufe fehl. Steht in `assets/docker-compose.yml`; hier wird
|
|
# nachgesehen, dass sie es auch ins Abbild geschafft hat.
|
|
# 2. Die Root-Partition ist die LETZTE auf der Platte und liegt NICHT auf LVM.
|
|
# `GrowGuestFilesystem` erkennt LVM absichtlich nicht und schriebe sonst ein
|
|
# Kontingent über ein Dateisystem, das nie gewachsen ist.
|
|
# 3. `qemu-guest-agent` MUSS im Abbild sein — `WaitForGuestAgent` hängt daran,
|
|
# und ohne ihn wartet jede Bereitstellung bis zum Zeitablauf.
|
|
#
|
|
# ---------------------------------------------------------------------------
|
|
# Warum das Cloud-Abbild und nicht der Installer
|
|
# ---------------------------------------------------------------------------
|
|
#
|
|
# Debians Cloud-Abbild bringt genau die Eigenschaften mit, die Falle 2 verlangt:
|
|
# eine einzige Root-Partition, als letzte auf der Platte, ohne LVM. Und es hat
|
|
# cloud-init, worüber die Pipeline beim Klonen die `.env` in den Gast schreibt.
|
|
# Beides selbst zu bauen hieße, es beim ersten Kunden zu prüfen.
|
|
|
|
CLUPILOT_TEMPLATE_VMID="${CLUPILOT_TEMPLATE_VMID:-9000}"
|
|
CLUPILOT_TEMPLATE_NAME='clupilot-nextcloud-vorlage'
|
|
CLUPILOT_CLOUD_IMAGE_BASE="${CLUPILOT_CLOUD_IMAGE_BASE:-https://cloud.debian.org/images/cloud/trixie/latest}"
|
|
CLUPILOT_CLOUD_IMAGE='debian-13-genericcloud-amd64.qcow2'
|
|
|
|
# Wieviel unter dem Arbeitsverzeichnis frei sein muss, in MB. Abbild,
|
|
# Arbeitskopie und die importierte Platte liegen gleichzeitig da, dazu die
|
|
# Appliance von virt-customize — ein Vielfaches der Abbildgröße.
|
|
CLUPILOT_TEMPLATE_MIN_FREE_MB="${CLUPILOT_TEMPLATE_MIN_FREE_MB:-12000}"
|
|
|
|
# Wohin die Vorlagenplatte kommt. `local-zfs` ist das, was der PVE-Installer auf
|
|
# ZFS anlegt; `local-lvm` wäre es bei ext4. Zur Laufzeit gesucht statt geraten.
|
|
#
|
|
# Gefragt wird EINMAL nach der Menge der Ablagen, die Platten aufnehmen, und die
|
|
# Wunschkandidaten werden darin gesucht. Vorher fragte die Schleife
|
|
# `pvesm status --storage X --content images` nach dem Rückgabewert ab — der
|
|
# sagt aber nicht zuverlässig „diese Ablage nimmt Platten", und `local` hätte so
|
|
# durchgehen können, obwohl ein Import dorthin erst nach dem Kopieren scheitert.
|
|
#
|
|
# `$3 == "active"`: eine abgemeldete oder tote Netzablage steht weiterhin in der
|
|
# Liste und wäre die schlechteste denkbare Wahl.
|
|
detect_vm_storage() {
|
|
_capable="$(pvesm status --content images 2>/dev/null | awk 'NR > 1 && $3 == "active" { print $1 }')"
|
|
|
|
if [ -z "$_capable" ]; then
|
|
return 0
|
|
fi
|
|
|
|
for _candidate in local-zfs local-lvm local; do
|
|
if printf '%s\n' "$_capable" | grep -qx "$_candidate"; then
|
|
printf '%s' "$_candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
printf '%s' "$(printf '%s\n' "$_capable" | head -1)"
|
|
}
|
|
|
|
# Eine Zeile je Ablage aus /etc/pve/storage.cfg: "<name> <typ> <inhalt> <pfad>".
|
|
# Leere Felder als "-", damit die Spaltennummern stehen bleiben.
|
|
#
|
|
# Der Pfad ist überschreibbar, damit die Auswertung dieser Datei ohne einen
|
|
# Proxmox-Host durchgespielt werden kann — sie ist das Stück neuer Logik mit dem
|
|
# größten Anteil an awk und dem kleinsten an „sieht man sofort".
|
|
CLUPILOT_STORAGE_CFG="${CLUPILOT_STORAGE_CFG:-/etc/pve/storage.cfg}"
|
|
|
|
storage_table() {
|
|
awk '
|
|
function flush() {
|
|
if (name != "") {
|
|
printf "%s %s %s %s\n", name, type,
|
|
(content == "" ? "-" : content), (path == "" ? "-" : path)
|
|
}
|
|
}
|
|
/^[a-z]+:[[:space:]]/ {
|
|
flush()
|
|
type = substr($1, 1, length($1) - 1); name = $2; content = ""; path = ""
|
|
next
|
|
}
|
|
/^[[:space:]]+content[[:space:]]/ { content = $2; next }
|
|
/^[[:space:]]+path[[:space:]]/ { path = $2; next }
|
|
END { flush() }
|
|
' "$CLUPILOT_STORAGE_CFG" 2>/dev/null
|
|
}
|
|
|
|
# Sorgt dafür, dass überhaupt eine Ablage Platten annimmt — und greift NUR ein,
|
|
# wenn keine es tut.
|
|
#
|
|
# Ein per Debian aufgesetztes Proxmox hat nur `local`, eine Verzeichnis-Ablage,
|
|
# deren Inhaltsliste ab Werk keine Platten enthält. `detect_vm_storage` findet
|
|
# dann nichts und der Bau stürbe vor der ersten Zeile. Schlimmer ist der leise
|
|
# Teil: `RegisterCapacity` zählt später die Ablagen, die Platten aufnehmen, und
|
|
# meldete Kapazität 0 — ein Host, der in der Liste fertig aussieht und auf den
|
|
# nie ein Kunde gelegt werden kann.
|
|
#
|
|
# Zwei Dinge passieren hier bewusst NICHT:
|
|
#
|
|
# - Eine feste Inhaltsliste schreiben. Was jemand auf diesem Host ergänzt hat,
|
|
# wäre weg. Die vorhandene wird gelesen und `images` ANGEHÄNGT.
|
|
# - In /etc/pve/storage.cfg schreiben. Das ist pmxcfs; ein von Hand
|
|
# geschriebener Eintrag umgeht Prüfung und Neuladen. Gelesen wird die Datei,
|
|
# geschrieben wird über `pvesm set`.
|
|
#
|
|
# Ein Host mit local-lvm oder local-zfs kommt hier nie an: die nehmen Platten
|
|
# längst, `detect_vm_storage` liefert sie, und an `local` wird nicht gerührt.
|
|
ensure_image_storage() {
|
|
if [ -n "$(detect_vm_storage)" ]; then
|
|
return 0
|
|
fi
|
|
|
|
# `local` zuerst, sonst die erste Ablage einer Art, die Platten überhaupt
|
|
# tragen kann. Netzablagen (nfs/cifs) stehen bewusst nicht in der Liste:
|
|
# deren Inhaltsliste zu ändern ist eine Entscheidung über fremden Speicher.
|
|
_name="$(storage_table | awk '
|
|
$2 ~ /^(dir|btrfs|zfspool|lvmthin|lvm)$/ {
|
|
if ($1 == "local") { print $1; found = 1; exit }
|
|
if (first == "") first = $1
|
|
}
|
|
END { if (!found && first != "") print first }
|
|
')"
|
|
|
|
if [ -z "$_name" ]; then
|
|
log 'Keine Ablage auf diesem Host nimmt Platten auf, und keine, der es beizubringen wäre.'
|
|
return 1
|
|
fi
|
|
|
|
_content="$(storage_table | awk -v n="$_name" '$1 == n { print $3 }')"
|
|
if [ "$_content" = '-' ] || [ -z "$_content" ]; then
|
|
_new='images'
|
|
else
|
|
_new="${_content},images"
|
|
fi
|
|
|
|
log "Ablage ${_name} nimmt keine Platten auf; Inhaltsliste wird erweitert: ${_new}"
|
|
|
|
if ! pvesm set "$_name" --content "$_new" >> "${CLUPILOT_WORK_DIR}/pvesm.log" 2>&1; then
|
|
log "pvesm set ${_name} --content ${_new} fehlgeschlagen"
|
|
return 1
|
|
fi
|
|
|
|
# Nachgesehen, nicht aus dem Rückgabewert geschlossen.
|
|
if [ -z "$(detect_vm_storage)" ]; then
|
|
log "Ablage ${_name} nimmt auch nach der Erweiterung keine Platten auf"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Freier Platz auf jedem übergebenen Pfad, bevor irgendetwas geladen wird.
|
|
#
|
|
# Ein df vorab mit klarer Absage ist billiger als ein Abbruch nach zwanzig
|
|
# Minuten mitten im virt-customize. Pfade statt einer Ablagengröße, weil `df`
|
|
# in derselben Einheit antwortet, egal wen man fragt — die Spalten von
|
|
# `pvesm status` tun das nicht.
|
|
#
|
|
# Block-Ablagen (lvmthin, zfspool) haben keinen Pfad und werden nicht geprüft;
|
|
# dort scheitert der Import laut und sofort statt nach dem Kopieren.
|
|
check_build_space() {
|
|
for _path in "$@"; do
|
|
[ -z "$_path" ] && continue
|
|
[ "$_path" = '-' ] && continue
|
|
[ -d "$_path" ] || continue
|
|
|
|
_free="$(df -Pm "$_path" 2>/dev/null | awk 'NR == 2 { print $4 }')"
|
|
|
|
if [ -z "$_free" ]; then
|
|
log "Freier Platz unter ${_path} nicht feststellbar"
|
|
return 1
|
|
fi
|
|
|
|
if [ "$_free" -lt "$CLUPILOT_TEMPLATE_MIN_FREE_MB" ]; then
|
|
log "Zu wenig Platz unter ${_path}: ${_free} MB frei, ${CLUPILOT_TEMPLATE_MIN_FREE_MB} MB nötig."
|
|
return 1
|
|
fi
|
|
|
|
log "Platz unter ${_path}: ${_free} MB frei"
|
|
done
|
|
|
|
return 0
|
|
}
|
|
|
|
# Lädt das Cloud-Abbild und prüft es gegen Debians SHA512SUMS.
|
|
#
|
|
# Geladen wird nach `.part`, umbenannt erst nach geprüfter Summe. Vorher schrieb
|
|
# die Funktion direkt ans Ziel, und ein abgebrochener Lauf hinterließ eine
|
|
# abgeschnittene Datei — die der nächste Lauf wegen `[ ! -f "$_image" ]`
|
|
# ungeprüft weiterbenutzt hätte. Unter dem Zielnamen liegt damit nur, was die
|
|
# Prüfsumme bestanden hat.
|
|
fetch_cloud_image() {
|
|
_target="$1"
|
|
_part="${_target}.part"
|
|
|
|
rm -f "$_part"
|
|
|
|
if ! http_download "${CLUPILOT_CLOUD_IMAGE_BASE}/${CLUPILOT_CLOUD_IMAGE}" "$_part"; then
|
|
log "Cloud-Abbild nicht ladbar: ${CLUPILOT_CLOUD_IMAGE_BASE}/${CLUPILOT_CLOUD_IMAGE}"
|
|
rm -f "$_part"
|
|
return 1
|
|
fi
|
|
|
|
_sums="$(http_get "${CLUPILOT_CLOUD_IMAGE_BASE}/SHA512SUMS" || true)"
|
|
if [ -z "$_sums" ]; then
|
|
log 'SHA512SUMS von cloud.debian.org nicht abrufbar'
|
|
rm -f "$_part"
|
|
return 1
|
|
fi
|
|
|
|
_expected="$(printf '%s\n' "$_sums" | awk -v n="$CLUPILOT_CLOUD_IMAGE" '$2 == n || $2 == "*" n { print $1; exit }')"
|
|
if [ -z "$_expected" ]; then
|
|
log "keine Prüfsumme für ${CLUPILOT_CLOUD_IMAGE}"
|
|
rm -f "$_part"
|
|
return 1
|
|
fi
|
|
|
|
_actual="$(sha512sum "$_part" | awk '{ print $1 }')"
|
|
if [ "$_actual" != "$_expected" ]; then
|
|
log 'Prüfsumme des Cloud-Abbilds stimmt nicht'
|
|
rm -f "$_part"
|
|
return 1
|
|
fi
|
|
|
|
mv "$_part" "$_target" || { log 'Abbild ließ sich nicht an seinen Platz schieben'; return 1; }
|
|
|
|
log 'Cloud-Abbild geladen, Prüfsumme stimmt'
|
|
return 0
|
|
}
|
|
|
|
# FALLE 2, geprüft statt angenommen.
|
|
#
|
|
# Zwei Aussagen: kein LVM, und die Root-Partition ist die letzte auf der Platte.
|
|
# `virt-filesystems` beantwortet beide, ohne das Abbild einzuhängen.
|
|
verify_image_layout() {
|
|
_image="$1"
|
|
|
|
_fs="$(virt-filesystems --long --parts --blkdevs -h -a "$_image" 2>/dev/null || true)"
|
|
if [ -z "$_fs" ]; then
|
|
log 'virt-filesystems liefert nichts — Abbildaufbau nicht prüfbar'
|
|
return 1
|
|
fi
|
|
|
|
if printf '%s' "$_fs" | grep -qiE '(lvm|/dev/[a-z0-9_-]+/[a-z0-9_-]+)'; then
|
|
log 'Das Abbild benutzt LVM. GrowGuestFilesystem erkennt LVM absichtlich nicht und schriebe ein Kontingent über ein Dateisystem, das nie gewachsen ist.'
|
|
return 1
|
|
fi
|
|
|
|
# Die Wurzel ist die Partition mit der höchsten Nummer, wenn sie die letzte
|
|
# ist. Anders gefragt: gibt es hinter der Root-Partition noch eine?
|
|
_last_part="$(printf '%s\n' "$_fs" | awk '/^\/dev\/[a-z]+[0-9]+/ { print $1 }' | sort -V | tail -1)"
|
|
_root_part="$(virt-filesystems --long -a "$_image" 2>/dev/null | awk '$0 !~ /swap/ && /ext4|xfs|btrfs/ { print $1; }' | sort -V | tail -1)"
|
|
|
|
if [ -n "$_last_part" ] && [ -n "$_root_part" ] && [ "$_last_part" != "$_root_part" ]; then
|
|
log "Die Root-Partition (${_root_part}) ist nicht die letzte auf der Platte (${_last_part}). GrowGuestFilesystem wächst nur die letzte."
|
|
return 1
|
|
fi
|
|
|
|
log "Abbildaufbau in Ordnung: kein LVM, Wurzel auf ${_root_part:-(unbestimmt)} als letzte Partition"
|
|
return 0
|
|
}
|
|
|
|
# Baut alles in das Abbild ein, was die Vorlage ausmacht.
|
|
#
|
|
# `virt-customize` statt „VM starten und hineinreden": es braucht kein Netz im
|
|
# Gast, keine Anmeldung und keinen laufenden Rechner, und es ist wiederholbar.
|
|
customise_cloud_image() {
|
|
_image="$1"
|
|
_compose="$2"
|
|
|
|
# `qemu-guest-agent`: ohne ihn hängt jede Bereitstellung an
|
|
# WaitForGuestAgent bis zum Zeitablauf — Falle 3.
|
|
#
|
|
# `cloud-guest-utils` bringt `growpart` — Falle 4. `GrowGuestFilesystem`
|
|
# ruft es auf, und installiert hat es nie jemand: es lief bisher, weil
|
|
# Debians Cloud-Abbild es zufällig mitbringt. Fiele es dort heraus, bekäme
|
|
# jeder Kunde stillschweigend ein Kontingent über ein Dateisystem, das nie
|
|
# gewachsen ist — bemerkt erst, wenn eine Platte voll ist, die laut Rechnung
|
|
# dreimal so groß sein sollte. Hier ausdrücklich installiert statt
|
|
# vorausgesetzt, und unten nachgewiesen statt geglaubt.
|
|
_packages='qemu-guest-agent,cloud-guest-utils,ca-certificates,curl,gnupg'
|
|
|
|
if ! virt-customize -a "$_image" \
|
|
--install "$_packages" \
|
|
--run-command 'systemctl enable qemu-guest-agent' \
|
|
>> "${CLUPILOT_WORK_DIR}/virt-customize.log" 2>&1
|
|
then
|
|
log 'virt-customize: Grundpakete ließen sich nicht einbauen'
|
|
return 1
|
|
fi
|
|
|
|
# Docker aus der Quelle von Docker selbst, nicht aus Debian: `docker.io` hat
|
|
# kein Compose-Plugin, und `docker-compose` in Debian ist die alte
|
|
# Python-Fassung, die diese Compose-Datei nicht liest.
|
|
if ! virt-customize -a "$_image" \
|
|
--run-command 'install -m 0755 -d /etc/apt/keyrings' \
|
|
--run-command 'curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc' \
|
|
--run-command 'chmod a+r /etc/apt/keyrings/docker.asc' \
|
|
--run-command 'echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian trixie stable" > /etc/apt/sources.list.d/docker.list' \
|
|
--run-command 'apt-get update' \
|
|
--install 'docker-ce,docker-ce-cli,containerd.io,docker-buildx-plugin,docker-compose-plugin' \
|
|
--run-command 'systemctl enable docker' \
|
|
>> "${CLUPILOT_WORK_DIR}/virt-customize.log" 2>&1
|
|
then
|
|
log 'virt-customize: Docker ließ sich nicht einbauen'
|
|
return 1
|
|
fi
|
|
|
|
if ! virt-customize -a "$_image" \
|
|
--mkdir /opt/nextcloud \
|
|
--copy-in "${_compose}:/opt/nextcloud" \
|
|
--run-command 'systemctl enable docker.service containerd.service' \
|
|
>> "${CLUPILOT_WORK_DIR}/virt-customize.log" 2>&1
|
|
then
|
|
log 'virt-customize: die Compose-Datei kam nicht ins Abbild'
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Prüft im fertigen Abbild nach, dass die drei Fallen wirklich zu sind.
|
|
#
|
|
# Nach dem Einbauen, nicht davor: `virt-customize` meldet auch dann Erfolg, wenn
|
|
# apt ein Paket stillschweigend nicht installiert hat.
|
|
verify_image_contents() {
|
|
_image="$1"
|
|
|
|
if ! virt-ls -a "$_image" /usr/bin 2>/dev/null | grep -q '^qemu-ga$'; then
|
|
log 'qemu-guest-agent ist NICHT im Abbild (Falle 3) — jede Bereitstellung hinge an WaitForGuestAgent'
|
|
return 1
|
|
fi
|
|
|
|
_compose_in_image="$(virt-cat -a "$_image" /opt/nextcloud/docker-compose.yml 2>/dev/null || true)"
|
|
if [ -z "$_compose_in_image" ]; then
|
|
log '/opt/nextcloud/docker-compose.yml fehlt im Abbild'
|
|
return 1
|
|
fi
|
|
|
|
if ! printf '%s' "$_compose_in_image" | grep -qE '^[[:space:]]*user:[[:space:]]*www-data[[:space:]]*$'; then
|
|
log 'Die Compose-Datei im Abbild hat kein `user: www-data` (Falle 1) — alle occ-Aufrufe schlügen fehl'
|
|
return 1
|
|
fi
|
|
|
|
if ! virt-ls -a "$_image" /usr/libexec/docker/cli-plugins 2>/dev/null | grep -q 'docker-compose'; then
|
|
if ! virt-ls -a "$_image" /usr/lib/docker/cli-plugins 2>/dev/null | grep -q 'docker-compose'; then
|
|
log 'Das Compose-Plugin ist nicht im Abbild'
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# FALLE 4. Siehe die Paketliste oben: das Kontingent, das der Kunde bezahlt
|
|
# hat, wird über ein Dateisystem geschrieben, das growpart vorher vergrößert
|
|
# haben muss. Fehlt es, tut die Vergrößerung still nichts.
|
|
if ! virt-ls -a "$_image" /usr/bin 2>/dev/null | grep -q '^growpart$'; then
|
|
log 'growpart ist NICHT im Abbild (Falle 4) — GrowGuestFilesystem liefe ins Leere und jedes gekaufte Kontingent läge über einem Dateisystem, das nie gewachsen ist'
|
|
return 1
|
|
fi
|
|
|
|
log 'Abbildinhalt in Ordnung: qemu-guest-agent, growpart, Compose-Plugin, user: www-data'
|
|
return 0
|
|
}
|
|
|
|
# Legt die VM an, hängt die Platte ein und macht eine Vorlage daraus.
|
|
create_proxmox_template() {
|
|
_image="$1"
|
|
_storage="$2"
|
|
_vmid="$CLUPILOT_TEMPLATE_VMID"
|
|
|
|
if qm status "$_vmid" >/dev/null 2>&1; then
|
|
log "VMID ${_vmid} existiert bereits — wird ersetzt"
|
|
qm destroy "$_vmid" --purge >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
qm create "$_vmid" \
|
|
--name "$CLUPILOT_TEMPLATE_NAME" \
|
|
--memory 4096 \
|
|
--cores 2 \
|
|
--net0 "virtio,bridge=vmbr0" \
|
|
--scsihw virtio-scsi-single \
|
|
--ostype l26 \
|
|
--agent enabled=1 \
|
|
--serial0 socket \
|
|
--vga serial0 \
|
|
>> "${CLUPILOT_WORK_DIR}/qm.log" 2>&1 || { log 'qm create fehlgeschlagen'; return 1; }
|
|
|
|
# Platte importieren UND einhängen, in einem Zug.
|
|
#
|
|
# Hier stand `qm importdisk` und danach ein `qm set --scsi0
|
|
# ${_storage}:vm-${_vmid}-disk-0`. Dieser Datenträgername gilt nur bei
|
|
# BLOCK-Ablagen (local-lvm, local-zfs). Auf einer Verzeichnis-Ablage — genau
|
|
# der, die ensure_image_storage gerade erst möglich macht, und die einzige
|
|
# auf einem per Debian aufgesetzten Proxmox — heißt er
|
|
# `local:9000/vm-9000-disk-0.qcow2`. Das Skript legte die Platte also an und
|
|
# hängte sie unter einem Namen ein, den es dort nicht gibt.
|
|
#
|
|
# `--import-from` überlässt die Benennung Proxmox, das als einziges weiß,
|
|
# wie sie auf dieser Ablage lautet.
|
|
qm set "$_vmid" --scsi0 "${_storage}:0,import-from=${_image},discard=on,ssd=1" \
|
|
>> "${CLUPILOT_WORK_DIR}/qm.log" 2>&1 \
|
|
|| { log 'Platte ließ sich nicht importieren'; return 1; }
|
|
|
|
# cloud-init: darüber schreibt die Pipeline beim Klonen die .env in den Gast.
|
|
qm set "$_vmid" --ide2 "${_storage}:cloudinit" --boot order=scsi0 \
|
|
>> "${CLUPILOT_WORK_DIR}/qm.log" 2>&1 || { log 'cloud-init-Laufwerk ließ sich nicht anlegen'; return 1; }
|
|
|
|
qm template "$_vmid" >> "${CLUPILOT_WORK_DIR}/qm.log" 2>&1 \
|
|
|| { log 'qm template fehlgeschlagen'; return 1; }
|
|
|
|
return 0
|
|
}
|
|
|
|
# Ist es wirklich eine Vorlage?
|
|
#
|
|
# `VerifyVmTemplate` prüft heute nur die EXISTENZ von 9000, nicht das Merkmal —
|
|
# der Handoff hält das als eigenen Fund fest. Eine VM, die zufällig 9000 heißt,
|
|
# bestünde diese Prüfung. `template: 1` besteht nur eine Vorlage.
|
|
template_is_really_a_template() {
|
|
qm config "$CLUPILOT_TEMPLATE_VMID" 2>/dev/null | grep -qE '^template:[[:space:]]*1[[:space:]]*$'
|
|
}
|