108 lines
4.5 KiB
PHP
108 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Provisioning\Steps\Customer;
|
|
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\StepResult;
|
|
use App\Services\Proxmox\ProxmoxClient;
|
|
|
|
/**
|
|
* Ask the guest to shut itself down, and wait for it to do so.
|
|
*
|
|
* The first half of a restart, and the half that carries all the risk. Two
|
|
* decisions are written into it.
|
|
*
|
|
* **It is a shutdown and a start, never a reboot.** An ACPI reset leaves the
|
|
* same qemu process running with the same command line, so the cores and memory
|
|
* a plan change wrote into the VM's definition would still not be there
|
|
* afterwards — the machine would go round and come back exactly as small as it
|
|
* was, and `restart_required_since` would be cleared over nothing. Only a cold
|
|
* boot reads the definition again. That is why this step exists at all instead
|
|
* of a single `status/reboot` call.
|
|
*
|
|
* **It never escalates to a power cut.** Proxmox will happily destroy the qemu
|
|
* process once the timeout expires (`forceStop`), and ProxmoxClient deliberately
|
|
* does not offer that — the guest is a Nextcloud, which is MariaDB plus a file
|
|
* tree, and killing one mid-write to make a CPU change take effect can turn a
|
|
* cosmetic delay into a restore from last night's backup. So the grace period
|
|
* below is a bound on WAITING, not a fuse: when it runs out the run fails, the
|
|
* machine is left running exactly as it was, and an operator gets a failed run
|
|
* to look at. A guest that has ignored ACPI for ten minutes is a guest that is
|
|
* doing something, and a person should decide what happens to it.
|
|
*/
|
|
class ShutDownVirtualMachine extends CustomerStep
|
|
{
|
|
/**
|
|
* How long the guest is given to go away by itself.
|
|
*
|
|
* Ten minutes. A Nextcloud stopping cleanly is docker compose stopping a
|
|
* handful of containers and MariaDB flushing — seconds to a minute or two on
|
|
* anything healthy. The rest of the allowance is for the unhealthy case that
|
|
* is still going to succeed: a long-running cron job inside the guest, a
|
|
* large InnoDB buffer pool being written out. Beyond that it is not slow,
|
|
* it is stuck.
|
|
*/
|
|
private const GRACE_SECONDS = 600;
|
|
|
|
public function __construct(private ProxmoxClient $pve) {}
|
|
|
|
public function key(): string
|
|
{
|
|
return 'shut_down_vm';
|
|
}
|
|
|
|
public function maxDuration(): int
|
|
{
|
|
// Above GRACE_SECONDS so this step's own deadline fires first and the
|
|
// run fails with a reason somebody can read, rather than being turned
|
|
// into a generic timeout-retry by the runner.
|
|
return 720;
|
|
}
|
|
|
|
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);
|
|
|
|
// Idempotent, and the ordinary way this step ends: the guest is gone.
|
|
if (($pve->vmStatus($node, $vmid)['status'] ?? '') !== 'running') {
|
|
return StepResult::advance();
|
|
}
|
|
|
|
// Asked once, remembered on the run. Re-sending ACPI to a guest that is
|
|
// already half-way through its own shutdown achieves nothing and reads,
|
|
// in the guest's log, like a second person pressing the power button.
|
|
if ($run->context('shutdown_upid') === null) {
|
|
$run->mergeContext([
|
|
'shutdown_upid' => $pve->shutdownVm($node, $vmid, self::GRACE_SECONDS),
|
|
]);
|
|
|
|
return StepResult::poll(10, 'waiting for the guest to shut down');
|
|
}
|
|
|
|
// A poll does not reset started_at, so this deadline genuinely
|
|
// accumulates across the whole wait rather than restarting every time.
|
|
if ($run->started_at !== null && $run->started_at->copy()->addSeconds(self::GRACE_SECONDS)->isPast()) {
|
|
$run->events()->create([
|
|
'step' => $this->key(),
|
|
'attempt' => $run->attempt,
|
|
'outcome' => 'info',
|
|
'message' => 'Der Gast hat sich in '.self::GRACE_SECONDS.' Sekunden nicht heruntergefahren. '
|
|
.'Die Maschine läuft unverändert weiter — sie wird bewusst nicht hart ausgeschaltet, '
|
|
.'weil eine Nextcloud eine laufende Datenbank ist.',
|
|
]);
|
|
|
|
return StepResult::fail('shutdown_timeout');
|
|
}
|
|
|
|
return StepResult::poll(10, 'waiting for the guest to shut down');
|
|
}
|
|
}
|