CluPilotCloud/app/Provisioning/Steps/Customer/ApplyStorageQuota.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();
}
}