163 lines
6.6 KiB
PHP
163 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace App\Provisioning\Steps\Customer;
|
|
|
|
use App\Models\Instance;
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\StepResult;
|
|
use App\Services\Proxmox\ProxmoxClient;
|
|
|
|
/**
|
|
* The hardware half of a plan change: CPU, memory and disk.
|
|
*
|
|
* Two things here are not symmetrical, and both are deliberate.
|
|
*
|
|
* **A disk only grows.** Proxmox resizes a virtual disk upwards on a running
|
|
* guest and has no way back down — there is no shrink, online or offline, and
|
|
* asking for one is an error, not a slow operation. So a downgrade does not try.
|
|
* What shrinks instead is the customer's QUOTA, which Nextcloud enforces and
|
|
* which is the thing that was actually sold; the guest keeps the disk it already
|
|
* has, and `instances.disk_gb` keeps saying so because that column is what the
|
|
* machine HAS and the host's storage accounting reads it. What the smaller
|
|
* package includes is on the contract, where it belongs — nothing reads a disk
|
|
* size as proof of what was sold.
|
|
*
|
|
* **CPU and memory are applied but not made true.** The configuration PUT lands
|
|
* immediately and a running guest takes the new size at its next cold boot;
|
|
* nothing in this codebase turns Proxmox's CPU or memory hotplug on, so there is
|
|
* no live path. Rebooting a customer's cloud as a side effect of a purchase is
|
|
* not something to do quietly, and applying a value that only becomes real
|
|
* months later with nobody knowing is not either — so the machine is left
|
|
* running and the gap is recorded on the instance, where the console's instance
|
|
* list and the customer's own cloud page both show it.
|
|
*/
|
|
class ResizeVirtualMachine extends CustomerStep
|
|
{
|
|
public function __construct(private ProxmoxClient $pve) {}
|
|
|
|
public function key(): string
|
|
{
|
|
return 'resize_vm';
|
|
}
|
|
|
|
public function maxDuration(): int
|
|
{
|
|
return 300;
|
|
}
|
|
|
|
public function execute(ProvisioningRun $run): StepResult
|
|
{
|
|
$instance = $this->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.',
|
|
]);
|
|
}
|
|
}
|