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 contract now includes, and say so out loud when * the contract asks for less than the machine already has. */ private function growDisk(ProvisioningRun $run, ProxmoxClient $pve, Instance $instance, string $node, int $vmid): void { $target = (int) ($this->plan($run)['disk_gb'] ?? 0); $actual = (int) $instance->disk_gb; // 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 ($target <= 0) { return; } 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.', ]); } }