*/ 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']; } }