CluPilotCloud/app/Services/Proxmox/VmTemplateCheck.php

105 lines
3.7 KiB
PHP

<?php
namespace App\Services\Proxmox;
use App\Models\Host;
use App\Models\PlanVersion;
use Throwable;
/**
* Whether every published plan's VM template actually EXISTS on a node, not
* merely that a `template_vmid` is filled in somewhere.
*
* Asks the same question as the onboarding step VerifyVmTemplate, but over
* ALL active hosts instead of one, and BEFORE a purchase instead of after:
* ProvisioningChecks::vm_template (Task 8) is a field check only — it cannot
* tell a `template_vmid` that was typed by mistake from one that is really
* there, and asking Proxmox on every page load does not belong on a
* readiness page. This is the on-demand button that actually asks.
*/
final class VmTemplateCheck
{
public function __construct(private ProxmoxClient $pve) {}
/** @return array<string, mixed> */
public function run(): array
{
$required = PlanVersion::query()
->whereNotNull('published_at')
->whereNotNull('template_vmid')
->where(fn ($q) => $q->whereNull('available_until')->orWhere('available_until', '>', now()))
->pluck('template_vmid')->map(fn ($v) => (int) $v)->unique()->values()->all();
// A catalogue with no published version requires no template. The
// same case VerifyVmTemplate explicitly lets through.
if ($required === []) {
return ['ok' => true, 'reason' => 'nothing_published'];
}
$hosts = Host::query()->where('status', 'active')->whereNotNull('api_token_ref')->get();
if ($hosts->isEmpty()) {
return ['ok' => false, 'reason' => 'no_usable_host'];
}
$missing = [];
$unverifiable = [];
foreach ($required as $vmid) {
$found = false;
$aHostFailedToAnswer = false;
foreach ($hosts as $host) {
try {
if ($this->pve->forHost($host)->vmExists($host->node ?? 'pve', $vmid)) {
$found = true;
break;
}
} catch (Throwable) {
// A host that is not answering right now is not proof of
// a missing template — it might be sitting on exactly
// this host. Keep looking at the others; a later "yes"
// still wins outright (see $found above).
$aHostFailedToAnswer = true;
continue;
}
}
if ($found) {
continue;
}
// Two different verdicts, not one: every host answered "no" is a
// confirmed gap (build the template); some host never answered
// is merely unverified (ask again once it is back). Telling an
// operator to build something that may already exist on a host
// that is simply not responding right now sends them to fix the
// wrong problem.
if ($aHostFailedToAnswer) {
$unverifiable[] = $vmid;
} else {
$missing[] = $vmid;
}
}
if ($missing !== []) {
$result = ['ok' => false, 'reason' => 'template_missing', 'vmids' => $missing];
if ($unverifiable !== []) {
// Named separately, never merged into vmids: one is
// confirmed and actionable, the other merely unanswered.
$result['unverifiable'] = $unverifiable;
}
return $result;
}
if ($unverifiable !== []) {
return ['ok' => false, 'reason' => 'hosts_unreachable', 'vmids' => $unverifiable];
}
return ['ok' => true, 'reason' => 'present'];
}
}