96 lines
3.3 KiB
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;
|
|
}
|
|
}
|