Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s
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>
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user