107 lines
4.2 KiB
PHP
107 lines
4.2 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 = $this->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';
|
|
|
|
$missing = array_values(array_filter(
|
|
$required,
|
|
fn (int $vmid) => ! $client->vmExists($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. Missing on node '.
|
|
$node.': 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();
|
|
}
|
|
|
|
/**
|
|
* Every template a currently sellable plan version points at.
|
|
*
|
|
* Published versions whose window has not closed, rather than
|
|
* PlanVersion::available() — a version scheduled to launch next week is not
|
|
* "available" yet and its template gap would then surface at its first order
|
|
* instead of here, which is the whole failure this step removes. A closed
|
|
* window is different: nobody can buy it any more, so its template is not
|
|
* this host's problem.
|
|
*
|
|
* @return array<int, int>
|
|
*/
|
|
private function requiredTemplateVmids(): array
|
|
{
|
|
return PlanVersion::query()
|
|
->whereNotNull('published_at')
|
|
->whereNotNull('template_vmid')
|
|
->where(fn ($q) => $q->whereNull('available_until')->orWhere('available_until', '>', now()))
|
|
->pluck('template_vmid')
|
|
->map(fn ($vmid) => (int) $vmid)
|
|
->unique()
|
|
->sort()
|
|
->values()
|
|
->all();
|
|
}
|
|
}
|