14 Commits (589489a9fcad1463ffbe50570ad8796f20551fa7)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
589489a9fc |
Release v1.3.92 — sechs Kacheln statt dreier ungleicher Kästen
tests / pest (push) Failing after 10m15s
Details
tests / assets (push) Successful in 28s
Details
tests / release (push) Has been skipped
Details
Nach der Vorlage des Besitzers. Zustand, Speicher und die gemeinsame Kurve standen nebeneinander: zwei davon halb leer, die dritte überfüllt, und die gemeinsame Kurve brauchte eine Legende, um zu sagen, welche Linie welche ist. Eine Reihe je Kachel löst alle drei Beschwerden auf einmal. Die Beschriftung der Kachel benennt die Reihe, also braucht es keine Legende mehr. Die Höhen sind durch das Raster gleich statt zufällig. Und der leere Platz ist mit Messwerten gefüllt, die es ohnehin schon gab: Proxmox' Aufzeichnung liefert Netzdurchsatz in beide Richtungen mit, ungefragt. Sechs Kacheln: CPU-Auslastung, RAM-Auslastung, Speicher (als Ring), Eingehend, Ausgehend, Zustand. Gebaut mit x-ui.metric, x-ui.spark und x-ui.ring — die gab es alle schon, und der Kopfkommentar von x-ui.metric sagt selbst "exactly as the approved template draws it". Nichts daneben neu gebaut. Die öffentliche IP steht jetzt auf der Seite. Sie stand vorher NUR klein unter der Überschrift — die Adresse, unter der der Host wirklich erreichbar ist, war in der Detailseite nirgends ein Feld. Sie führt jetzt die Ausstattungs-Tafel an, und die Reserve-Eingabe ist mit dorthin gezogen: die Kacheln zeigen, was gemessen wurde, die Tafel, was eingestellt ist. Ein Eingabefeld zwischen Messwerten sähe aus, als ließe sich eine Messung ändern. Alle vier Verlaufslinien tragen denselben Ton. Die Regel steht im Bauteil selbst — "muted where the figure is observed, accent where it can be acted on" —, und hier ist keine Zahl anzufassen. Vier verschiedene Töne nebeneinander behaupten einen Unterschied, den es nicht gibt. Zwei Funde aus der Prüfung -------------------------- - x-ui.spark warf fehlende Messwerte per array_filter heraus und verband die Nachbarn. Zwei Fehler auf einmal: die Linie behauptete eine Messung, die es nicht gab, und alles danach rutschte nach links — eine Stunde mit zwei Lücken zeichnete sich als achtundfünfzig Minuten. Die x-Lage kommt jetzt aus dem Platz in der URSPRÜNGLICHEN Reihe, und zusammenhängende Messwerte werden als eigene Züge gezeichnet. Eine saubere Reihe ergibt genau einen Zug und dasselbe Bild wie vorher, was alle bisherigen Aufrufer liefern. - Der Zwischenspeicher überlebt einen Deploy. Ein Eintrag aus v1.3.91 kennt netin/netout nicht, und die Host-Seite wäre 55 Sekunden lang an einem fehlenden Schlüssel gestorben — genau in der Minute, in der jemand nachsieht, ob das Update durch ist. Der Schlüssel heißt jetzt host-load:v2:<id> und wandert mit der Form mit. Und einer, den kein Prüfer gemeldet hat: beim Zerlegen in Züge stand im Flächenpfad ein `L` unmittelbar vor einem `M`. Gültig gelesen, nicht gezeichnet — die Füllung verschwand still. Aufgefallen ist es beim Ansehen der Seite, nicht durch eine Meldung; jetzt prüft ein Test, dass jeder Flächenpfad mit M anfängt, mit Z endet und keinen Befehl direkt hinter einem anderen trägt. x-ui.chart behält seinen update-on-Weg, obwohl diese Seite ihn nicht mehr benutzt: er ist eine geprüfte Fähigkeit des gemeinsamen Bauteils, und der darunterliegende Fix (Instanz aus dem reaktiven Alpine-Objekt) gilt für jeden Chart. Geprüft: 2291 Tests grün, Pint sauber, Codex ohne Befund. Im Browser mit eingespielten Messwerten: sechs Kacheln, Lücke als echte Aussparung in Linie UND Fläche, Leerzustand zeigt "—" statt einer Null, null Konsolenfehler über einen vollen Poll-Zyklus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
665a0c4e08 |
Release v1.3.91 — die Host-Seite zeigt Last statt Ausstattung
tests / pest (push) Failing after 9m29s
Details
tests / assets (push) Successful in 20s
Details
tests / release (push) Has been skipped
Details
Die Karte "Rechenleistung" zeigte keine Leistung. 12 Kerne und 63 GB sind die
Ausstattung des Blechs und ändern sich nie — sie standen aber im selben
Kartenraster wie "Zustand" und "Speicher", die beide leben. Wer die Seite
öffnete, um zu sehen, wie es dem Host geht, las dort eine Zahl, die das nie
sagen konnte.
An ihrer Stelle steht jetzt die Last: CPU und RAM als Stundenkurve, beide in
Prozent auf EINER Achse, dazu die aktuellen Werte als beschriftete Zahlen.
Die Geschichte kommt aus Proxmox' eigener Aufzeichnung
(/nodes/{node}/rrddata), nicht aus einem eigenen Sampler. Ein Sampler hieße
neue Tabelle, minütlicher Job, Aufräum-Job und ~1440 Zeilen je Host und Tag —
um weniger genau nachzubauen, was ohnehin auf der Platte liegt. Die RRD ist ab
der ersten Sekunde gefüllt, auch für die Stunde vor dem ersten Hinsehen, und
kann nicht von dem abweichen, was Proxmox' eigene Oberfläche zeigt.
Eine Lücke bleibt eine Lücke: ein Punkt ohne Messwert wird null, nie 0, und
spanGaps steht auf false. Dieselbe Regel wie in instance_metrics. Antwortet der
Host gar nicht, sagt die Tafel das in einem Satz, statt eine ruhige Stunde zu
zeichnen — auf dem Testhost live bestätigt.
Der Fehler, den das ans Licht gebracht hat
------------------------------------------
x-ui.chart steht überall unter wire:ignore, sonst zerstört Livewire das Canvas.
Ein Poll erreicht den Chart also nie. Dafür bekam das Bauteil ein optionales
update-on: es hört auf ein Fenster-Ereignis und tauscht die Daten IM
bestehenden Chart.js-Objekt.
Das lief nicht. Die Zahlen neben der Kurve wanderten, die Kurve nicht, und
chart.update() starb still im Legenden-Plugin:
TypeError: Cannot set properties of undefined (setting 'fullSize')
Grund: die Chart.js-Instanz lag als Eigenschaft im Alpine-Objekt und wurde
damit reaktiv umhüllt. Chart.js' Plugin-Innenleben überlebt das Proxy nicht.
Gemessen statt geschlossen: dieselbe Instanz wirft über das Proxy und läuft
über Alpine.raw(). Sie liegt jetzt in der Closure.
Aufgefallen ist es nie, weil bis zum ersten Live-Chart kein einziger Chart in
diesem Repo je update() gerufen hat — konstruieren und Erstzeichnen gehen durch
die Hülle noch. tests/Feature/ChartLiveUpdateTest.php hält die Regel fest,
damit der nächste Live-Chart nicht denselben Nachmittag kostet.
Der Rest
--------
- Version lesbar: "Proxmox VE 9.2.6" statt pve-manager/9.2.6/7f8d…, mitten in
der Bau-Kennung abgeschnitten. Die Kennung steht klein darunter. Eine
unerwartete Form wird unverändert durchgereicht statt verschluckt.
- Vier Kleinkarten (Mgmt-IP, Node, Version, Instanzen) sind eine
Ausstattungs-Tafel geworden. Die Instanzen-Anzahl steht in der Überschrift
der Liste, die sie ohnehin zeigt.
- Der Übernahme-Fortschritt klappt zu, sobald sie durch ist. Fünfzehn
abgehakte Schritte sind auf einem laufenden Host kein Dauerinhalt —
aufklappbar über <details>, ohne JavaScript.
- PlanVersion::requiredTemplateVmids() ersetzt die dritte Kopie derselben
Fensterlogik.
- BuildVmTemplate sagt nicht mehr "this takes 10–20 minutes". Das war eine
Schätzung vor dem ersten Lauf; gemessen waren es unter zwei. Eine Konsole,
die falsch vorhersagt, erzieht dazu, sie zu ignorieren.
Geprüft: 2281 Tests grün, Pint sauber, Codex ohne Befund. Die Farbwahl gegen
den Validator gerechnet (ΔE 28,3 protan / 39,2 normal; der Akzent liegt unter
3:1 gegen die Fläche, deshalb tragen beide Reihen sichtbare Beschriftung). Im
Browser: null Konsolenfehler über einen vollen Poll-Zyklus, und die Kurve
wandert auf dem echten Poll ohne Neuladen — mit eingespielten Messwerten
belegt, weil die Testhosts in TEST-NET liegen und nie antworten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
db125781ed |
Release v1.3.88 — die Vorlage baut sich selbst
tests / pest (push) Failing after 9m17s
Details
tests / assets (push) Successful in 22s
Details
tests / release (push) Has been skipped
Details
Der letzte Handgriff in der Host-Übernahme fällt weg. VerifyVmTemplate meldete
bisher nur, dass eine Vorlage fehlt, weil niemand entschieden hatte, was in die
goldene Vorlage gehört. Entschieden ist es längst und steht in
deploy/bootstrap/lib/template.sh — der neue Schritt BuildVmTemplate lädt genau
diese Datei auf den Host und führt sie dort aus, statt ihre Prüfungen ein
zweites Mal in PHP zu haben.
Er läuft abgekoppelt und wird abgefragt: Abbild laden und drei
virt-customize-Läufe brauchen zehn bis zwanzig Minuten, ein einzelner
SSH-Aufruf liefe gegen den Befehlszeitablauf von 2000 s. "Läuft noch" heißt
dabei, dass der Prozess lebt (kill -0 gegen die hinterlegte PID) — in der
Statusdatei steht "running" auch dann noch, wenn niemand mehr da ist, der sie
ändert.
Fünf Fehler, die dabei aufgefallen sind und Geld gekostet hätten:
- qm importdisk hängte die Platte unter ${storage}:vm-9000-disk-0 ein. Der Name
gilt nur bei Block-Ablagen; auf einer Verzeichnis-Ablage heißt sie
local:9000/vm-9000-disk-0.qcow2 — also genau auf dem per Debian aufgesetzten
Proxmox, um das es hier geht. Jetzt qm set --import-from, und Proxmox
benennt selbst.
- growpart war nie installiert. GrowGuestFilesystem ruft es auf, und es lief
bisher, weil Debians Cloud-Abbild es zufällig mitbringt. Fiele es heraus,
läge jedes gekaufte Kontingent über einem Dateisystem, das nie gewachsen ist.
Jetzt ausdrücklich eingebaut und als vierte Falle nachgewiesen.
- local nimmt ab Werk keine Platten an. Ohne das stirbt nicht nur der Bau,
RegisterCapacity meldet danach Kapazität 0: ein Host, der fertig aussieht und
nie einen Kunden tragen kann. ensure_image_storage greift nur ein, wenn keine
Ablage Platten annimmt, hängt images an die vorhandene Liste an statt sie zu
ersetzen, und schreibt über pvesm set statt in die pmxcfs-Datei.
- Ein abgebrochener Download blieb unter dem Zielnamen liegen und wäre beim
nächsten Lauf ungeprüft weiterbenutzt worden. Jetzt .part, umbenannt erst
nach geprüfter Summe.
- VerifyVmTemplate und VmTemplateCheck fragten nur, ob VMID 9000 existiert. Ein
abgebrochener Bau hinterlässt eine gewöhnliche VM mit dieser Nummer, und
beide sagten dazu "passt" — der Fehler kam beim ersten bezahlten Klon zurück.
Jetzt template: 1.
isTemplate() stellt zwei Anfragen, weil die falsche Antwort hier etwas
zerstört: false heißt "Vorlage fehlt", und der Bau fängt mit qm destroy --purge
an. Proxmox beantwortet die Konfiguration einer nicht vorhandenen VM mit 500 —
demselben Code wie einen Knoten in Not. Die VM-Liste klärt deshalb die
Abwesenheit, alles darunter wirft und landet im Wiederholungs-Zweig.
Aufgeben beendet erst die Prozessgruppe, dann räumt es auf, und gebaut wird nur
die Fehlliste: create_proxmox_template räumt eine VMID weg, bevor es sie
anlegt, also hätte "alles Verlangte" eine gesunde zweite Vorlage auf dem Weg
zerstört.
Geprüft: 2267 Tests grün, Pint sauber, sh -n über alle drei Shell-Dateien, die
storage.cfg-Auswertung gegen eine echte Beispieldatei durchgespielt, und jeder
Befehl, den der Schritt absetzt, geht durch sh -n — keine andere Prüfung führt
diese Shell je aus. Drei Codex-Runden (R15), alle Befunde behoben.
Nicht geprüft: nichts davon lief je gegen echte Hardware. Die erste Übernahme
auf einem Proxmox-Host ist die Abnahme.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
b9fb401823 |
Release v1.3.85 — Handshake-Prüfung hing an einem Programm, das es nicht gibt
tests / pest (push) Failing after 8m54s
Details
tests / assets (push) Successful in 22s
Details
tests / release (push) Has been skipped
Details
Die Übernahme brach in "WireGuard einrichten" ab: fünf Wiederholungen, "WireGuard handshake not up yet". Geprüft wurde mit `ping -c1 -W2 <hub-ip>`. Das fragte drei Dinge auf einmal und nannte nur eines: ob der Handshake steht, ob ICMP durchkommt — und ob es `ping` auf der Maschine überhaupt GIBT. Auf Hetzners `Debian-trixie-latest-amd64-base` gibt es das nicht. `iputils-ping` ist im Image nicht dabei, und PrepareBaseSystem installiert `curl gnupg ifupdown2 chrony`. Der Schritt scheiterte damit über einem Tunnel, der stehen konnte. Auf dem alten Proxmox-Image war ping dabei, deshalb lief es dort durch — dieselbe Pipeline, anderes Grundsystem. Gefragt wird jetzt WireGuard selbst: `date +%s; wg show wg0 latest-handshakes`. Die Uhr des HOSTS kommt in derselben Antwort mit, weil `latest-handshakes` eine absolute Zeit ausgibt und ein Vergleich gegen UNSERE Uhr eine Zeitverschiebung zwischen zwei Maschinen als Tunnelzustand läse. `date` ist in coreutils und überall da. Codex, zwei Runden: - P1: `> 0` hiesse "hat jemals". WireGuard behält den Zeitstempel unbegrenzt, also meldete ein Wiederholungslauf über einem toten Tunnel "steht", und die Schritte danach wählten die Tunneladresse für SSH. Jetzt muss der Handshake frisch sein (180 s) und vom KONFIGURIERTEN Hub kommen — ein fremder Peer auf wg0 ist kein Beweis dafür, dass wir erreichbar sind. - P1: Der neue Host-Versatz (.100) liess die Vergabe in einem Subnetz kleiner als /26 "erschöpft" melden, obwohl unten alles frei war. Der Versatz ist eine Bevorzugung, keine Bedingung: zweiter Durchgang von vorn. Ausserdem, wie gewünscht: Hosts bekommen ihre Tunneladresse ab .100 (CLUPILOT_WG_HOST_OFFSET), Personen zählen weiter von unten. Fortlaufend vergeben landete der erste Host zwischen zwei Notebooks, und wer eine Adresse in einem Protokoll las, konnte nicht sagen, ob dahinter ein Mensch oder eine Maschine steht. 2243 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
200df30b9d |
Hetzner-Cloud-DNS, Bereitschaftsseite aus sich heraus behebbar
Die alte Hetzner-DNS-API ist abgeschaltet (301 auf die Weboberflaeche).
Jede Bereitstellung starb in ConfigureDnsAndTls, nachdem der Kunde bezahlt
hatte. HttpHetznerDnsClient und DnsTokenCheck sprechen jetzt die Cloud-API:
Bearer statt Auth-API-Token, RRSets ueber {name}/{typ} statt Record-IDs, der
Zonen-Lookup entfaellt. Gegen das Live-Konto gemessen, nicht geraten.
Drei Fallen, die am Konto gemessen wurden: TXT-Werte muessen in
Anfuehrungszeichen (sonst 422, was als read_only gemeldet worden waere), ein
Name mit Zonensuffix wird STILL angenommen (201), und der Fake gab andere IDs
aus als der echte Client -- zwoelf Tests waren gruen ueber einem Abbau, der im
Betrieb geworfen haette.
Bereitschaftspunkte, die nur eine Shell beheben konnte, sind jetzt bedienbar:
SSH-Schluesselpaar erzeugen (Ed25519 ueber phpseclib, privater Teil direkt in
den Tresor), Stripe-Katalog abgleichen (Warteschlange, Trockenlauf, Ausgabe
wortgleich), Mailzustellung als Schalter statt MAIL_MAILER, Neustart der
Arbeiterprozesse. Stripe hat jetzt alle drei Werte in der Konsole: Secret Key,
Signatur-Secret (Tresor, je Modus getrennt) und Publishable Key (Klartext).
Codex-Review (R15), zwei P1 behoben: der Signaturschluessel faellt nicht mehr
vom Live- in den Testplatz, und eine Record-ID aus der alten API macht einen
Host nicht mehr unloeschbar -- was adressierbar ist, wandelt eine Migration um,
der Rest wird beim Loeschen laut uebergangen statt geworfen.
2109 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
889b401faf |
Fix nine defects in the provisioning pipelines
The capacity park could not survive its own first poll. ReserveResources
polls every 120 s and declared a maxDuration of 60, and RunRunner measures
a step's budget from a started_at it deliberately does not reset on a poll
— so every re-entry was ruled timed out before the body ran, a timeout
consumes an attempt, and five of them failed a paid order in about six
minutes. The fourteen-day promise, the console's capacity queue and the
"go and buy a server" workflow were unreachable code. maxDuration is now
the whole park plus one poll interval, and a test drives a park through
the runner rather than calling execute() directly, which is why this was
invisible.
Every guest occ call ran as root. `docker compose exec` defaults to root
and the Nextcloud image's console.php exits 1 unless the caller owns
config/config.php, so all five call sites were failing on every instance.
There is now one builder, App\Support\NextcloudOcc, and a test refuses a
second: nothing in app/ may spell the invocation out by hand. deploy/
update.sh had learned this for our own container and nobody carried it
across.
RunAcceptanceChecks was terminal on the first no. certReachable() answers
false for a connect timeout as readily as for a missing certificate, so
one bad second ended a finished, certified Nextcloud as a failed order
with the instance released. The probes retry now and the run still fails
for good with the probe's own reason once the budget is spent; the two
facts a retry cannot change stay terminal.
The in-flight guard asked whether anything was running, not whether it
would do the work. App\Provisioning\WorkInFlight asks the second question,
reading the step lists the runner executes: a domain proven during a
restart is routed, and a storage pack booked during an address run is
delivered instead of charged monthly and forgotten.
The address pipeline ran its steps in the wrong order for the direction it
exists for. Nextcloud before the router in `address` and `plan-change`, as
in `customer`, because a run that got the certificate and then failed left
the customer's own domain serving an untrusted-domain error under a valid
certificate while the portal called it live. Safe in both directions; the
reasoning is written above the pipeline.
A stale guest_ip could never be corrected. ConfigureDnsAndTls compared the
hostnames but never the backend, and nothing re-read the address after the
build. It now records and compares `routed_backend`, and ConfigureNetwork
joins the `restart` pipeline — a cold boot is what moves a DHCP lease — and
asks for the address to be re-applied when the guest has actually moved.
Also: HetznerDnsClient::upsertRecord read only the first page of a zone
that pages at 100, so past a hundred records it created a second A record
for the same name and the cloud was up about half the time; the console's
retry left a live customer's order in `provisioning` for ever for any
maintenance pipeline; and a revived run's first pass burned an attempt on a
timeout that had already happened, because the clause written for it used
`??=`.
Seven test sites wrote config('provisioning.plans.*'), a key that has not
existed since the catalogue moved into the database. Every write was a
no-op, so each test proved something other than its name: the missing
template, the two snapshot regressions and the grandfathered-price scenario
are now actually constructed, ValidateOrder's reasons are asserted rather
than only its type, and the end-to-end run checks that the quota step
delivered something.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
|
2b1989f53d | Move host DNS names off the public zone into vpn-dns's internal hostsdir | |
|
|
69043f9ffc |
fix(dns): deleting a record that is already gone is success
A lost response, or a retry after a later step failed, made every further attempt a 404 — and the host impossible to purge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
71ca0e1394 |
feat(admin): VPN access management with live peer state
The console can now create, block and remove VPN accesses, and shows who is actually connected — traffic counters, source endpoint and last handshake. Design notes: - The web container has no wg0, so the page never talks to WireGuard. Live state is copied in by SyncVpnPeers on the provisioning queue (the only worker whose container owns the interface); the page polls the database and merely nudges that job, throttled so many open tabs cannot flood the queue. - enabled (operator intent) and present (observed on the hub) are stored separately, so a sync can never quietly undo a block and drift stays visible as "wird angewendet". - The sync adopts peers the host pipeline registered, naming them after their host — otherwise "who is on the VPN" would have blind spots. - Keypairs are generated in PHP via libsodium rather than shelling out to wg genkey (no interface in this container, and it keeps generation testable). The private key is shown once and never stored. - allocateIp() now also considers vpn_peers, which would otherwise be handed a tunnel address a host already holds. - vpn.manage is a new capability held by Owner/Admin only, and it hides the nav entry too — a VPN access reaches the management network. Verified end to end against the real hub, not just mocks: created an access in the browser, connected with the generated config, watched the console flip to "Verbunden" with real counters, then blocked it and confirmed the peer was gone from wg0 and the client could no longer handshake. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
e5aeb3989e |
feat(engine-b): ProxmoxClient VM-lifecycle extension (interface + fake + http)
nextVmid/cloneVm/setCloudInit/resizeDisk/startVm/vmStatus/guestAgentPing/ guestExec/taskStatus/applyFirewall. Fake is deterministic with configurable task/guest failure hooks; Http polls UPID + guest exec-status. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
a6a41719bb |
fix(engine): long SSH command timeout; wg peer removal on privileged worker
- PhpseclibRemoteShell sets a command timeout (default 2000s) so apt full-upgrade/install don't hit phpseclib's ~10s default and fail. - Host removal dispatches RemoveWireguardPeer to the provisioning queue, which runs in the worker that owns wg0 (the web container can't manage WireGuard). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
8a392bfd06 |
fix(engine): parse WireGuard CIDR for address allocation (any prefix length)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
65717bd3bd |
fix(engine): address Codex review (auth token bootstrap, tunnel recheck, ssh)
- CreateAutomationToken now bootstraps the pveum role/user/token over the authenticated SSH session (a fresh host has no API token yet); ProxmoxClient is read-only in A. - ConfigureWireguard re-verifies the handshake on the idempotent replay path, never advancing over a dead tunnel. - PhpseclibRemoteShell treats a missing SSH exit status as failure (255). - connectWithKey verifies the pinned host key fingerprint on later logins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
c06ca0ae5d |
feat(engine): SSH / WireGuard / Proxmox service layer
RemoteShell (phpseclib + fake), WireguardHub (local wg + fake), ProxmoxClient (REST + fake). Interfaces bound in AppServiceProvider; tests swap fakes via fakeServices() helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |