Files
ladill-servers/app/Models/HostingNode.php
T
isaaccladandCursor b6c8ac343f Extract Ladill Servers as standalone app at servers.ladill.com.
VPS and dedicated server ordering, managed panels, SSO, billing, and launcher integration — forked from hosting infrastructure with server-focused routes and dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 19:18:30 +00:00

251 lines
6.4 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class HostingNode extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'hostname',
'ip_address',
'ipv6_address',
'type',
'role',
'primary_hosting_node_id',
'segment',
'provider',
'provider_instance_id',
'region',
'datacenter',
'cpu_cores',
'ram_mb',
'disk_gb',
'oversell_ratio',
'allocated_disk_gb',
'used_disk_gb',
'bandwidth_tb',
'max_accounts',
'current_accounts',
'current_load_percent',
'status',
'features',
'installed_software',
'ssh_port',
'ssh_private_key',
'control_panel',
'last_health_check_at',
'health_status',
];
protected $casts = [
'features' => 'array',
'installed_software' => 'array',
'health_status' => 'array',
'last_health_check_at' => 'datetime',
'cpu_cores' => 'integer',
'ram_mb' => 'integer',
'disk_gb' => 'integer',
'oversell_ratio' => 'decimal:2',
'allocated_disk_gb' => 'integer',
'used_disk_gb' => 'integer',
'bandwidth_tb' => 'integer',
'max_accounts' => 'integer',
'current_accounts' => 'integer',
'current_load_percent' => 'decimal:2',
];
protected $hidden = [
'ssh_private_key',
];
public const ROLE_PRIMARY = 'primary';
public const ROLE_EXTENSION = 'extension';
public const TYPE_SHARED = 'shared';
public const TYPE_VPS = 'vps';
public const TYPE_DEDICATED = 'dedicated';
public const SEGMENT_GENERAL = 'general';
public const SEGMENT_STARTER = 'starter';
public const SEGMENT_WORDPRESS = 'wordpress';
public const SEGMENT_HIGH_RESOURCE = 'high_resource';
public const PROVIDER_CONTABO = 'contabo';
public const PROVIDER_LOCAL = 'local';
public const PROVIDER_MANUAL = 'manual';
public const PROVIDER_LEGACY_RC = 'legacy_rc';
public const STATUS_PROVISIONING = 'provisioning';
public const STATUS_ACTIVE = 'active';
public const STATUS_MAINTENANCE = 'maintenance';
public const STATUS_FULL = 'full';
public const STATUS_DECOMMISSIONED = 'decommissioned';
public function primary(): BelongsTo
{
return $this->belongsTo(self::class, 'primary_hosting_node_id');
}
public function extensions(): HasMany
{
return $this->hasMany(self::class, 'primary_hosting_node_id');
}
public function accounts(): HasMany
{
return $this->hasMany(HostingAccount::class);
}
public function isPrimary(): bool
{
return ($this->role ?? self::ROLE_PRIMARY) === self::ROLE_PRIMARY;
}
public function isExtension(): bool
{
return $this->role === self::ROLE_EXTENSION;
}
public function poolRoot(): self
{
return $this->isExtension() && $this->primary
? $this->primary
: $this;
}
public function isAvailable(): bool
{
if ($this->status !== self::STATUS_ACTIVE) {
return false;
}
if ($this->max_accounts && $this->current_accounts >= $this->max_accounts) {
return false;
}
return true;
}
public function supportsSegment(?string $segment): bool
{
if ($segment === null || $segment === '') {
return true;
}
return in_array($this->segment, [$segment, self::SEGMENT_GENERAL], true);
}
public function logicalCapacityGb(): float
{
if (! $this->disk_gb || $this->disk_gb <= 0) {
return 0;
}
$ratio = (float) ($this->oversell_ratio ?: 1);
return round($this->disk_gb * max($ratio, 1), 2);
}
public function logicalCapacityPercent(): float
{
$logicalCapacity = $this->logicalCapacityGb();
if ($logicalCapacity <= 0) {
return 0;
}
return round(($this->allocated_disk_gb / $logicalCapacity) * 100, 2);
}
public function physicalDiskPercent(): float
{
if (! $this->disk_gb || $this->disk_gb <= 0) {
return 0;
}
return round(($this->used_disk_gb / $this->disk_gb) * 100, 2);
}
public function accountCapacityPercent(): float
{
if (! $this->max_accounts || $this->max_accounts <= 0) {
return 0;
}
return round(($this->current_accounts / $this->max_accounts) * 100, 2);
}
public function hasLogicalCapacityFor(int $requestedDiskGb = 0): bool
{
if ($requestedDiskGb <= 0) {
return true;
}
if (! $this->disk_gb || $this->disk_gb <= 0) {
return false;
}
return ($this->used_disk_gb + $requestedDiskGb) <= $this->disk_gb;
}
public function availableLogicalDiskGb(): float
{
if (! $this->disk_gb || $this->disk_gb <= 0) {
return 0;
}
return max($this->disk_gb - $this->used_disk_gb, 0);
}
public function canProvision(int $requestedDiskGb = 0, ?string $segment = null): bool
{
if (! $this->isAvailable()) {
return false;
}
if (! $this->supportsSegment($segment)) {
return false;
}
return $this->hasLogicalCapacityFor($requestedDiskGb);
}
public function incrementAccountCount(): void
{
$this->increment('current_accounts');
if ($this->max_accounts && $this->current_accounts >= $this->max_accounts) {
$this->update(['status' => self::STATUS_FULL]);
}
}
public function decrementAccountCount(): void
{
$this->decrement('current_accounts');
if ($this->status === self::STATUS_FULL && $this->current_accounts < $this->max_accounts) {
$this->update(['status' => self::STATUS_ACTIVE]);
}
}
public function scopeAvailable($query)
{
return $query->where('status', self::STATUS_ACTIVE)
->whereRaw('current_accounts < COALESCE(max_accounts, 999999)');
}
public function scopeOfType($query, string $type)
{
return $query->where('type', $type);
}
}