feat(templates): Template-Bau für VMs mit eingebautem Guest-Agent
Cloud-Image laden (download-url mit Prüfsumme, ab PVE 8.4 per API, davor per SSH), VM mit q35/UEFI/virtio-scsi-single anlegen, Disk per import-from importieren, Guest-Agent mit einem einmaligen Vorbereitungsstart einbauen und als Template markieren. Auf pve-demo (PVE 9.2.2) gebaut: Template 9000 aus Debian 13 in 5 min 26 s, Snippet und cicustom danach entfernt. Der Weg ohne SSH über eine Bau-Adresse ist als #021 ausgegliedert. Templates-Seite mit Liste (Proxmox ist maßgeblich, Tag proxdeploy-template) und Formular; Abfrageschleifen erscheinen im Detail-Log nur noch bei Fehlern. Closes #007 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { EINGABE_KLASSE, Feld } from "@/components/formular";
|
||||
import { Knopf } from "@/components/knopf";
|
||||
import { Status } from "@/components/status";
|
||||
import { apiAufruf, ApiFehler } from "@/lib/api-client";
|
||||
|
||||
type Knoten = { name: string; storages: string[]; bridges: string[] };
|
||||
|
||||
export function TemplateBauen({
|
||||
server,
|
||||
distributionen,
|
||||
knoten,
|
||||
faehigkeit,
|
||||
}: {
|
||||
server: string;
|
||||
distributionen: Array<{ id: string; name: string }>;
|
||||
knoten: Knoten[];
|
||||
faehigkeit: { weg: "api" | "ssh" | "nicht_moeglich"; grund: string };
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [werte, setWerte] = useState({
|
||||
distribution: distributionen.find((d) => d.id === "debian-13")?.id ?? distributionen[0]?.id ?? "",
|
||||
knoten: knoten[0]?.name ?? "",
|
||||
storage: knoten[0]?.storages[0] ?? "",
|
||||
bridge: knoten[0]?.bridges.includes("vmbr0") ? "vmbr0" : (knoten[0]?.bridges[0] ?? ""),
|
||||
});
|
||||
const [fehler, setFehler] = useState<{ text: string; hinweis?: string } | null>(null);
|
||||
const [sendet, setSendet] = useState(false);
|
||||
const aktuell = knoten.find((k) => k.name === werte.knoten);
|
||||
|
||||
async function bauen(ereignis: React.FormEvent) {
|
||||
ereignis.preventDefault();
|
||||
setSendet(true);
|
||||
setFehler(null);
|
||||
try {
|
||||
const { kurzId } = await apiAufruf<{ kurzId: string }>(
|
||||
"POST",
|
||||
`/api/v1/server/${server}/templates`,
|
||||
werte,
|
||||
);
|
||||
router.push(`/auftraege/${kurzId}`);
|
||||
} catch (ursache) {
|
||||
setFehler(
|
||||
ursache instanceof ApiFehler
|
||||
? { text: ursache.message, hinweis: ursache.hinweis }
|
||||
: { text: "Unerwarteter Fehler." },
|
||||
);
|
||||
setSendet(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={bauen} className="flex flex-col gap-4">
|
||||
<Status art={faehigkeit.weg === "api" ? "erledigt" : faehigkeit.weg === "ssh" ? "laeuft" : "fehler"}>
|
||||
{faehigkeit.weg === "api" ? "Per API" : faehigkeit.weg === "ssh" ? "Per SSH" : "Nicht möglich"} –{" "}
|
||||
{faehigkeit.grund}
|
||||
</Status>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Feld id="distribution" label="Distribution">
|
||||
<select
|
||||
id="distribution"
|
||||
className={EINGABE_KLASSE}
|
||||
value={werte.distribution}
|
||||
onChange={(e) => setWerte({ ...werte, distribution: e.target.value })}
|
||||
>
|
||||
{distributionen.map((d) => (
|
||||
<option key={d.id} value={d.id}>
|
||||
{d.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Feld>
|
||||
<Feld id="knoten" label="Node">
|
||||
<select
|
||||
id="knoten"
|
||||
className={EINGABE_KLASSE}
|
||||
value={werte.knoten}
|
||||
onChange={(e) => {
|
||||
const k = knoten.find((x) => x.name === e.target.value);
|
||||
setWerte({
|
||||
...werte,
|
||||
knoten: e.target.value,
|
||||
storage: k?.storages[0] ?? "",
|
||||
bridge: k?.bridges[0] ?? "",
|
||||
});
|
||||
}}
|
||||
>
|
||||
{knoten.map((k) => (
|
||||
<option key={k.name}>{k.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</Feld>
|
||||
<Feld id="storage" label="Storage">
|
||||
<select
|
||||
id="storage"
|
||||
className={EINGABE_KLASSE}
|
||||
value={werte.storage}
|
||||
onChange={(e) => setWerte({ ...werte, storage: e.target.value })}
|
||||
>
|
||||
{aktuell?.storages.map((s) => (
|
||||
<option key={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
</Feld>
|
||||
<Feld id="bridge" label="Bridge (Vorbereitungsstart)">
|
||||
<select
|
||||
id="bridge"
|
||||
className={EINGABE_KLASSE}
|
||||
value={werte.bridge}
|
||||
onChange={(e) => setWerte({ ...werte, bridge: e.target.value })}
|
||||
>
|
||||
{aktuell?.bridges.map((b) => (
|
||||
<option key={b}>{b}</option>
|
||||
))}
|
||||
</select>
|
||||
</Feld>
|
||||
</div>
|
||||
<p className="text-muted text-sm">
|
||||
Der Vorbereitungsstart braucht DHCP und Internetzugang auf der Bridge, um qemu-guest-agent zu
|
||||
installieren.
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Knopf type="submit" disabled={sendet || faehigkeit.weg === "nicht_moeglich" || !werte.distribution}>
|
||||
{sendet ? "Startet …" : "Template bauen"}
|
||||
</Knopf>
|
||||
<div aria-live="polite" className="text-sm">
|
||||
{fehler ? (
|
||||
<div>
|
||||
<Status art="fehler">{fehler.text}</Status>
|
||||
{fehler.hinweis ? <p className="text-muted">{fehler.hinweis}</p> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user