CluPilotCloud/app/Provisioning/Steps/Customer/CompleteRestart.php

118 lines
4.3 KiB
PHP

<?php
namespace App\Provisioning\Steps\Customer;
use App\Models\ProvisioningRun;
use App\Provisioning\StepResult;
use App\Services\Proxmox\ProxmoxClient;
/**
* The machine is back — check that it came back as the machine that was sold,
* and only then stop saying a restart is due.
*
* `restart_required_since` is the one thing a customer and an operator both
* read to answer "am I paying for cores I do not have". Clearing it because
* somebody pressed a button, or because a run reached its last step, would turn
* it from a fact into a rumour — and the rumour points the wrong way, because it
* says the problem is gone.
*
* So the proof is taken from the machine. Proxmox's status/current reports what
* the RUNNING guest has (`cpus`, `maxmem`), not what its definition says, and
* those two disagree for exactly as long as a pending restart lasts. If the
* guest is back with at least what the instance row promises, the flag goes; if
* it is back smaller, something did not take and the run fails loudly rather
* than quietly declaring success.
*
* Where an older Proxmox reports neither figure, the fallback is the pipeline
* itself: this step only runs after ShutDownVirtualMachine saw the guest
* genuinely stopped and StartVirtualMachine started it again, and a stopped
* machine reads its definition when it starts. That is the same reasoning
* ResizeVirtualMachine already uses to decide there is nothing pending on a
* stopped guest.
*/
class CompleteRestart extends CustomerStep
{
/** How long the guest may take to show up as running before this gives up. */
private const RUNNING_DEADLINE = 120;
public function __construct(private ProxmoxClient $pve) {}
public function key(): string
{
return 'complete_restart';
}
public function maxDuration(): int
{
return 180;
}
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');
$status = $this->pve->forHost($instance->host)->vmStatus($node, $vmid);
if (($status['status'] ?? '') !== 'running') {
if ($run->started_at !== null && $run->started_at->copy()->addSeconds(self::RUNNING_DEADLINE)->isPast()) {
return StepResult::fail('restart_did_not_come_back');
}
return StepResult::poll(5, 'waiting for the machine to come back');
}
if (($short = $this->shortfall($status, $instance->cores, $instance->ram_mb)) !== null) {
return StepResult::fail('restart_applied_nothing: '.$short);
}
// Only here. The machine is running, its guest agent has answered (the
// step before this one), and it is running on what was sold.
if ($instance->restartIsPending()) {
$instance->update(['restart_required_since' => null]);
}
$run->events()->create([
'step' => $this->key(),
'attempt' => $run->attempt,
'outcome' => 'info',
'message' => "Die Maschine läuft wieder mit {$instance->cores} vCPU und {$instance->ram_mb} MB RAM. "
.'Der offene Neustart ist damit erledigt.',
]);
return StepResult::advance();
}
/**
* What the running guest is short of, or null when it is not short of
* anything we can see.
*
* Compared with `>=` rather than `===`: Proxmox rounds memory and a host may
* legitimately give a guest more than was asked for, and refusing a machine
* for being too big would be a strange way to protect a customer.
*
* @param array<string, mixed> $status
*/
private function shortfall(array $status, ?int $cores, ?int $ramMb): ?string
{
if (isset($status['cpus']) && $cores > 0 && (int) $status['cpus'] < $cores) {
return "{$status['cpus']} statt {$cores} vCPU";
}
if (isset($status['maxmem']) && $ramMb > 0) {
$actualMb = (int) ((int) $status['maxmem'] / 1048576);
if ($actualMb < $ramMb) {
return "{$actualMb} statt {$ramMb} MB RAM";
}
}
return null;
}
}