CluPilotCloud/app/Services/Provisioning/HostCapacity.php

96 lines
3.3 KiB
PHP

<?php
namespace App\Services\Provisioning;
use App\Models\Host;
use App\Services\Billing\PlanCatalogue;
use Throwable;
/**
* What the estate can still take, and which packages it can no longer deliver.
*
* The platform books thick: `disk_gb` — the sold quota plus the machine's own
* overhead — is reserved against a host at placement, not the customer's actual
* usage. That is the safe choice for a small operator (nothing can quietly
* overfill), and it has one consequence worth surfacing: the catalogue can go
* on offering a package that no host has room for, and nobody finds out until a
* paid order fails at the reservation step.
*
* So this asks the question BEFORE the order does.
*/
class HostCapacity
{
/** Is there a host in this datacenter that could take this much? */
public function canPlace(string $datacenter, int $diskGb): bool
{
return Host::placeableIn($datacenter, $diskGb) !== null;
}
/**
* The largest single reservation the estate could still accept.
*
* The largest FREE HOST, not the sum: a package goes on one machine, so two
* hosts with 300 GB each cannot take a 540 GB instance between them.
*/
public function largestPlaceableGb(?string $datacenter = null): int
{
return (int) Host::query()
->where('status', 'active')
->when($datacenter !== null, fn ($q) => $q->where('datacenter', $datacenter))
->get()
->map(fn (Host $host) => $host->availableGb())
->max() ?? 0;
}
/**
* Sellable packages that no host could currently take.
*
* Keyed by plan key, valued by what it needs and what the roomiest host has
* left — an operator deciding whether to buy a machine needs the shortfall,
* not a yes/no.
*
* @return array<string, array{name: string, needs: int, largest: int}>
*/
public function unplaceablePlans(?string $datacenter = null): array
{
try {
$sellable = app(PlanCatalogue::class)->sellable();
} catch (Throwable) {
// A catalogue that cannot be read is its own alarm elsewhere. This
// is a capacity warning; it does not get to be the thing that takes
// the console's overview page down.
return [];
}
// No host at all is not a capacity problem — it is an installation
// nobody has onboarded a machine into yet, and it has its own notices.
// Without this, a fresh console greeted the operator with one warning
// per sellable package before they had done anything at all.
$hasHost = Host::query()
->where('status', 'active')
->when($datacenter !== null, fn ($q) => $q->where('datacenter', $datacenter))
->exists();
if (! $hasHost) {
return [];
}
$largest = $this->largestPlaceableGb($datacenter);
$atRisk = [];
foreach ($sellable as $key => $plan) {
$needs = (int) ($plan['disk_gb'] ?? 0);
if ($needs > 0 && $needs > $largest) {
$atRisk[$key] = [
'name' => (string) ($plan['name'] ?? $key),
'needs' => $needs,
'largest' => $largest,
];
}
}
return $atRisk;
}
}