CluPilotCloud/app/Provisioning/Jobs/PingHosts.php

69 lines
2.6 KiB
PHP

<?php
namespace App\Provisioning\Jobs;
use App\Models\Host;
use App\Services\Proxmox\ProxmoxClient;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
/**
* Asks every host whether it is still there.
*
* `hosts.last_seen_at` existed and drove the health dot in the console, and
* nothing ever wrote it after onboarding — RegisterCapacity stamped it once and
* that was the only write in the codebase. So every host read "offline" thirty
* minutes after it was added, permanently, and the dot meant nothing. That is
* also why host reachability could not appear on the status page: it was never
* being measured.
*
* nodeStatus() is the cheapest question the API answers, and it is the right
* one: it goes through the same credentials, the same network path and the same
* token that provisioning depends on. A host that answers this can be worked
* with; a host that cannot is down as far as this platform is concerned,
* whatever a separate ICMP probe would say.
*
* Runs on the provisioning queue, which is where the Proxmox credentials are
* usable.
*/
class PingHosts implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct()
{
$this->onQueue('provisioning');
}
public function handle(ProxmoxClient $proxmox): void
{
$hosts = Host::query()
// Hosts still being onboarded, or already retired, are not a
// statement about the platform's health.
->whereIn('status', ['active', 'error'])
->get();
foreach ($hosts as $host) {
try {
$proxmox->forHost($host)->nodeStatus($host->node);
// Only the timestamp. Whether a host is "active" or "error" is
// an operator's decision and a provisioning outcome — a
// heartbeat must not quietly promote a host somebody has
// deliberately taken out of service.
$host->forceFill(['last_seen_at' => now()])->save();
} catch (Throwable) {
// Deliberately silent, and deliberately not writing anything.
// Absence IS the signal: healthState() reads the age of the
// last successful answer, so a failure is recorded by the
// timestamp not moving. Writing a failure count here would be a
// second version of the same fact.
}
}
}
}