CluPilotCloud/app/Models/Host.php

140 lines
4.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Models;
use App\Models\Concerns\HasUuid;
use App\Provisioning\Contracts\ProvisioningSubject;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Host extends Model implements ProvisioningSubject
{
/** @use HasFactory<\Database\Factories\HostFactory> */
use HasFactory, HasUuid;
protected $fillable = [
'name', 'datacenter', 'cluster', 'public_ip', 'wg_ip', 'wg_pubkey',
'ssh_host_key', 'api_token_ref', 'total_gb', 'total_ram_mb', 'cpu_cores',
'cpu_weight', 'reserve_pct', 'pve_version', 'node', 'status', 'last_seen_at', 'dns_name', 'dns_record_id',
];
protected $hidden = ['api_token_ref'];
protected function casts(): array
{
return [
'api_token_ref' => 'encrypted',
'last_seen_at' => 'datetime',
'total_gb' => 'integer',
'total_ram_mb' => 'integer',
'cpu_cores' => 'integer',
'cpu_weight' => 'integer',
'reserve_pct' => 'integer',
];
}
/** Placement runs (polymorphic subject). */
public function runs(): MorphMany
{
return $this->morphMany(ProvisioningRun::class, 'subject');
}
/** Runner hook: a failed onboarding run moves the host to the error status. */
public function onProvisioningFailed(): void
{
$this->update(['status' => 'error']);
}
public function instances(): HasMany
{
return $this->hasMany(Instance::class);
}
public function maintenanceWindows(): BelongsToMany
{
return $this->belongsToMany(MaintenanceWindow::class);
}
/** Free committable storage: total minus reserve. */
public function freeGb(): int
{
if ($this->total_gb === null) {
return 0;
}
return (int) floor($this->total_gb * (100 - $this->reserve_pct) / 100);
}
/**
* Host storage already committed, counted as the VM disk allocation
* (disk_gb) since that is what is actually placed on the host — not the
* (smaller) Nextcloud user quota. A failed instance still counts while its VM
* exists (has a vmid); a failure before any VM was created releases it.
*/
public function committedGb(): int
{
// Answer a preloaded sum when the caller supplied one (see
// Instance::scopeOccupyingHost) — listing every host otherwise costs a
// query per host, twice over once usedPct() asks again.
//
// Tested for PRESENCE, not for null: SUM over no rows is null, so a
// host with nothing on it arrives preloaded-but-null. Reading that as
// "not preloaded" would re-query for exactly the empty hosts, which is
// the case the preload exists for.
if (array_key_exists('committed_disk_gb', $this->getAttributes())) {
return (int) $this->getAttributes()['committed_disk_gb'];
}
return (int) $this->instances()->occupyingHost()->sum('disk_gb');
}
/** Storage still available for new instances. */
public function availableGb(): int
{
return max(0, $this->freeGb() - $this->committedGb());
}
/** Used-storage percentage (committed vs committable free), 0100. */
public function usedPct(): int
{
$free = $this->freeGb();
return $free > 0 ? min(100, (int) round($this->committedGb() / $free * 100)) : 0;
}
/**
* Heartbeat health from last_seen_at: online (≤5 min), stale (≤30 min),
* offline (older or never seen). Drives the health dot in the console.
*/
public function healthState(): string
{
if ($this->last_seen_at === null) {
return 'offline';
}
$minutes = $this->last_seen_at->diffInMinutes(now());
return match (true) {
$minutes <= 5 => 'online',
$minutes <= 30 => 'stale',
default => 'offline',
};
}
/**
* Placement (spec §1): first active host in the datacenter with enough free
* committable storage. Cluster is ignored in v1.0.
*/
public static function placeableIn(string $datacenter, int $quotaGb): ?self
{
return self::query()
->where('datacenter', $datacenter)
->where('status', 'active')
->orderBy('name')
->get()
->first(fn (self $host) => $host->availableGb() >= $quotaGb);
}
}