105 lines
3.7 KiB
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'];
|
|
}
|
|
}
|