*/ 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', 'status', 'last_seen_at', ]; 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); } /** 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); } /** Storage already committed to non-failed instances. */ public function committedGb(): int { return (int) $this->instances()->where('status', '!=', 'failed')->sum('quota_gb'); } /** Storage still available for new instances. */ public function availableGb(): int { return max(0, $this->freeGb() - $this->committedGb()); } /** * 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); } }