instance($run); if ($instance === null) { return StepResult::fail('no_instance'); } $node = (string) $run->context('node'); $vmid = (int) $run->context('vmid'); $pve = $this->pve->forHost($instance->host); // setCloudInit() is the generic `PUT /nodes/{node}/qemu/{vmid}/config` // despite its name — it writes whatever parameters it is given, and // cloud-init keys are simply what the first caller happened to need. // cores and memory are ordinary VM configuration and go the same way. // Idempotent: writing the same values again changes nothing. $pve->setCloudInit($node, $vmid, [ 'cores' => (int) $instance->cores, 'memory' => (int) $instance->ram_mb, ]); $this->growDisk($run, $pve, $instance, $node, $vmid); $this->settlePendingRestart($run, $pve, $instance, $node, $vmid); return StepResult::advance(); } /** * Grow the disk to what the customer is now entitled to, and say so out loud * when that is less than the machine already has. * * The target is the customer's WHOLE allowance — package plus booked storage * packs — with TWO overheads left on top of it, never one. The package's own * system overhead is read off the package and never invented: the catalogue's * four packages ship 120/100, 540/500, 1050/1000 and 2100/2000 GB, which is * 20, 40, 50 and 100 GB of room beside the customer's files and is a decision * the owner makes per package, not a ratio anybody may derive. Every booked * pack brings ITS OWN overhead the same way, frozen on the booking rather * than on the package — what it gives the customer and what it takes on the * host are two different figures, and only their difference belongs here. * Without it, a stack of packs would sit on the package's headroom alone and * thin it out with every pack added, which is exactly the shape the rule * exists to refuse. * * With no packs booked the sum is exactly the package's own `disk_gb`, which * is what this step has always used. */ private function growDisk(ProvisioningRun $run, ProxmoxClient $pve, Instance $instance, string $node, int $vmid): void { $plan = $this->plan($run); $planDisk = (int) ($plan['disk_gb'] ?? 0); $planQuota = (int) ($plan['quota_gb'] ?? 0); // No contract behind this run, or a package without a size: nothing to // grow towards, and guessing at one would resize somebody's disk from a // default. if ($planDisk <= 0) { return; } $allowance = StorageAllowance::for($instance); // Kontingent + Blöcke + Kopfraum des Pakets + Kopfraum der Blöcke. // // Der letzte Summand ist der neue: ein Block gibt dem Kunden 20 GB und // belegt 22, damit die Regel max(10 GB, 12 %) auch für ein gestapeltes // Paket gilt. Ohne ihn hätte ein Start mit drei Blöcken 90 GB auf 100 GB // Platte — zehn Gigabyte Kopfraum, wo die Regel zwölf verlangt. // // Beide Kopfräume werden gelesen, nicht abgeleitet: der des Pakets aus // dem eingefrorenen Vertrag, der der Blöcke aus den Buchungen. $target = $allowance->totalGb() > 0 ? $allowance->totalGb() + max(0, $planDisk - $planQuota) + max(0, $allowance->packDiskGb() - $allowance->packGb()) : $planDisk; $actual = (int) $instance->disk_gb; if ($target > $actual) { // Absolute target, not '+…', so a retry cannot grow it twice. $pve->resizeDisk($node, $vmid, 'scsi0', $target.'G'); $instance->update(['disk_gb' => $target]); return; } if ($target < $actual) { $run->events()->create([ 'step' => $this->key(), 'attempt' => $run->attempt, 'outcome' => 'info', 'message' => "Festplatte bleibt bei {$actual} GB: ein kleineres Paket ({$target} GB) verkleinert " .'keine Platte — das Kontingent des Kunden sinkt stattdessen.', ]); } } /** * Record — or clear — the restart this change is waiting on. * * Keyed on whether the machine is RUNNING rather than on reading back what * Proxmox says the guest currently has. Without hotplug the answer is the * same either way and this one needs no second API shape to be faked in * tests; and where the two could differ, being pessimistic is the safe * direction. A "restart pending" that turns out to have been unnecessary is * a visible nuisance, while a resize that silently never happened is exactly * the failure this step exists to stop. */ private function settlePendingRestart(ProvisioningRun $run, ProxmoxClient $pve, Instance $instance, string $node, int $vmid): void { $fromCores = (int) $run->context('plan_change.from_cores', $instance->cores); $fromRam = (int) $run->context('plan_change.from_ram_mb', $instance->ram_mb); // Nothing about CPU or memory moved, so this change is waiting on // nothing. An older pending restart is left exactly where it is — it is // still pending, and clearing it here would hide it. if ($fromCores === (int) $instance->cores && $fromRam === (int) $instance->ram_mb) { return; } $running = ($pve->vmStatus($node, $vmid)['status'] ?? '') === 'running'; if (! $running) { // A stopped machine reads its configuration when it starts, so the // new size is already true for it — and anything pending from an // earlier change is settled by the same start. if ($instance->restartIsPending()) { $instance->update(['restart_required_since' => null]); } return; } // First pending change keeps its timestamp: how long somebody has been // paying for cores they do not have is the interesting number, not when // the most recent change was applied. if (! $instance->restartIsPending()) { $instance->update(['restart_required_since' => now()]); } $run->events()->create([ 'step' => $this->key(), 'attempt' => $run->attempt, 'outcome' => 'info', 'message' => "Neue Ausstattung ({$instance->cores} vCPU, {$instance->ram_mb} MB RAM) ist in der VM-Konfiguration " .'hinterlegt und wird beim nächsten Neustart wirksam. Die laufende Maschine wurde nicht neu gestartet.', ]); } }