Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
251 lines
6.4 KiB
PHP
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);
|
|
}
|
|
}
|