CluPilotCloud/app/Provisioning/Steps/Host/VerifyVmTemplate.php

86 lines
3.6 KiB
PHP

<?php
namespace App\Provisioning\Steps\Host;
use App\Models\PlanVersion;
use App\Models\ProvisioningRun;
use App\Provisioning\StepResult;
use App\Services\Proxmox\ProxmoxClient;
use Throwable;
/**
* Confirms the VM templates the plan catalogue sells actually exist on this node.
*
* Without this, a host went `active` while it could not serve a single customer.
* Every plan version points at a `template_vmid`, nothing in the host pipeline
* creates, copies or verifies that template, and there is no console command that
* does either. The first paid order placed on a freshly onboarded host therefore
* died in CloneVirtualMachine — after the customer had paid, in the one place
* where nobody is watching and the only honest recovery is a refund.
*
* So the gap is reported HERE, during onboarding, with an operator in front of
* the run. This step deliberately does NOT build a template: what goes into the
* golden image (Nextcloud version, cloud-init, guest agent, disk layout) is a
* product decision, and a step that invented one would produce customers running
* something nobody chose.
*
* Placed straight after VerifyProxmoxApi because it is the first thing worth
* asking the API once the API answers, and before RegisterCapacity so a host that
* cannot serve anyone never reaches the capacity table at all.
*/
class VerifyVmTemplate extends HostStep
{
public function __construct(private ProxmoxClient $pve) {}
public function key(): string
{
return 'verify_vm_template';
}
public function execute(ProvisioningRun $run): StepResult
{
$host = $this->host($run);
$required = PlanVersion::requiredTemplateVmids();
// A catalogue with nothing published yet requires no template. This is a
// real state — an installation being set up for the first time onboards
// its host before it publishes its plans — and failing here would be
// inventing a requirement nobody has stated.
if ($required === []) {
return StepResult::advance();
}
$client = $this->pve->forHost($host);
try {
$nodes = $client->listNodes();
$node = $nodes[0]['node'] ?? 'pve';
// isTemplate, not vmExists. `template: 1` is the property a clone
// needs; the number on its own is not. A build that died halfway
// leaves an ordinary VM numbered 9000 behind, and an existence
// check calls that fine — which puts the gap back exactly where
// this step was written to take it away from: the first paid order.
$missing = array_values(array_filter(
$required,
fn (int $vmid) => ! $client->isTemplate($node, $vmid),
));
} catch (Throwable $e) {
// The API was answering one step ago; a hiccup here is transient.
return StepResult::retry(15, 'could not ask Proxmox about the VM templates: '.$e->getMessage());
}
if ($missing !== []) {
return StepResult::fail(
'This host has no VM template to clone from, so it could not serve a single order. Absent on node '.
$node.', or present but not marked as a template: VMID '.implode(', ', $missing).', which '.
(count($missing) === 1 ? 'a published plan version points' : 'published plan versions point').' at. '.
'Copy the golden template onto this host (or restore it from another node) and retry — otherwise the '.
'first paid order placed here fails in clone_vm, after the customer has paid.'
);
}
return StepResult::advance();
}
}