81 lines
3.1 KiB
PHP
81 lines
3.1 KiB
PHP
<?php
|
|
|
|
namespace App\Provisioning\Steps\Customer;
|
|
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\StepResult;
|
|
use App\Services\Billing\StorageAllowance;
|
|
use App\Services\Proxmox\ProxmoxClient;
|
|
|
|
/**
|
|
* The storage a plan change actually delivers.
|
|
*
|
|
* This is the half of a plan change that a downgrade can carry out in full: a
|
|
* disk cannot be shrunk, but a quota can, and the quota is what was sold. The
|
|
* customer on a 200 GB package gets 200 GB whatever size the underlying disk
|
|
* happens to be, and Nextcloud is what enforces it.
|
|
*
|
|
* What is written is the customer's whole allowance — the package PLUS every
|
|
* storage pack booked onto their contract — and it is asked of
|
|
* StorageAllowance rather than read off `instances.quota_gb`. That column is the
|
|
* package alone, and taking it as the answer is how a booked pack came to change
|
|
* nothing at all: the customer paid ten euros a month and Nextcloud went on
|
|
* enforcing the figure the package included.
|
|
*
|
|
* `files default_quota`, not a per-user quota. An account with an explicit quota
|
|
* stops following the default, so writing one onto the admin account would
|
|
* freeze that account at today's figure and quietly exclude it from the next
|
|
* plan change. The default applies to every account on the instance — which is
|
|
* what a package's storage allowance means here, since a package is a whole
|
|
* Nextcloud and not a seat on a shared one.
|
|
*/
|
|
class ApplyStorageQuota extends CustomerStep
|
|
{
|
|
public function __construct(private ProxmoxClient $pve) {}
|
|
|
|
public function key(): string
|
|
{
|
|
return 'apply_storage_quota';
|
|
}
|
|
|
|
public function maxDuration(): int
|
|
{
|
|
return 120;
|
|
}
|
|
|
|
public function execute(ProvisioningRun $run): StepResult
|
|
{
|
|
$instance = $this->instance($run);
|
|
|
|
if ($instance === null) {
|
|
return StepResult::fail('no_instance');
|
|
}
|
|
|
|
$quota = StorageAllowance::for($instance)->totalGb();
|
|
|
|
// No allowance recorded: setting "0 GB" would lock the customer out of
|
|
// their own files, which is a far worse answer than leaving what is
|
|
// there and letting somebody notice.
|
|
if ($quota <= 0) {
|
|
return StepResult::advance();
|
|
}
|
|
|
|
$pve = $this->pve->forHost($instance->host);
|
|
$occ = 'cd /opt/nextcloud && docker compose exec -T app php occ ';
|
|
|
|
// Idempotent, like every other occ call in this pipeline: writing the
|
|
// same value again is a no-op and exits 0.
|
|
$this->guest($pve, $run, $occ.'config:app:set files default_quota --value='.escapeshellarg($quota.' GB'));
|
|
|
|
// Recorded only AFTER the guest accepted it — guest() throws on a
|
|
// non-zero exit, so nothing below runs for a call that failed. Without
|
|
// this line there is no way to tell a machine whose allowance is
|
|
// enforced from one where the figure has only ever been a row in our
|
|
// database, which is precisely how every instance built before this step
|
|
// existed came to have the whole disk. See clupilot:apply-quotas.
|
|
$instance->update(['quota_applied_gb' => $quota]);
|
|
|
|
return StepResult::advance();
|
|
}
|
|
}
|