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 the package's own system overhead left on top of it. That * 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. A pack of extra * storage is that much more FOR THE CUSTOMER, so it is added to the * allowance and the package's overhead rides along unchanged. * * 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); $target = $allowance->totalGb() > 0 ? $allowance->totalGb() + max(0, $planDisk - $planQuota) : $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.', ]); } }