option('dry-run'); $started = 0; /** @var array reason => count */ $skipped = []; $query = Instance::query()->with(['order', 'host']); if ($uuid = $this->option('instance')) { $query->where('uuid', $uuid); } foreach ($query->cursor() as $instance) { $reason = $this->reasonToSkip($instance); if ($reason !== null) { $skipped[$reason] = ($skipped[$reason] ?? 0) + 1; continue; } $started++; // The whole allowance, packs included — the figure the run will // actually write. Printing `quota_gb` would report the package while // the sweep applied something larger. $owed = StorageAllowance::for($instance)->totalGb(); $this->line(($dryRun ? '[dry-run] ' : '') ."{$instance->subdomain}: {$owed} GB " .'('.($instance->quota_applied_gb === null ? 'noch nie gesetzt' : "zuletzt {$instance->quota_applied_gb} GB").')'); if (! $dryRun) { $this->startQuotaRun($instance); } } foreach ($skipped as $reason => $count) { $this->line("übersprungen ({$reason}): {$count}"); } $this->info($dryRun ? "Probelauf: {$started} Instanz(en) bekämen ihr Kontingent, ".array_sum($skipped).' übersprungen. Nichts wurde geändert.' : "{$started} Kontingent-Lauf/Läufe gestartet, ".array_sum($skipped).' übersprungen.'); return self::SUCCESS; } /** * Why this instance is being left alone, or null when it is not. * * The reason is the report — a sweep that says "skipped 34" and nothing else * is a sweep nobody can act on, because "34 machines have no allowance * enforced" and "34 machines are already fine" look identical from here. */ private function reasonToSkip(Instance $instance): ?string { // Nothing remote work can reach: a reservation with no VM, a failed // build, an ended service. The same rule every other maintenance action // asks — see Instance::hasLiveMachine(). if (! $instance->hasLiveMachine()) { return 'keine erreichbare Maschine'; } // No allowance on the row at all. Writing "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 — the same decision the step // itself makes, made here too so the machine is not visited for nothing. if ((int) $instance->quota_gb <= 0) { return 'kein Kontingent hinterlegt'; } if ($instance->quotaIsEnforced()) { return 'bereits gesetzt'; } // Whatever is in flight against this order gets to finish. A quota run // beside an unfinished build would race the same occ calls, and the // build applies the quota itself anyway. if ($this->hasRunInFlight($instance)) { return 'anderer Lauf aktiv'; } return null; } private function startQuotaRun(Instance $instance): void { $run = ProvisioningRun::create([ // The instance's own purchase order, as every customer pipeline // uses — that is what CustomerStep::order() and ::instance() // resolve from, and what the in-flight check above looks at. 'subject_type' => Order::class, 'subject_id' => $instance->order_id, 'pipeline' => 'quota', 'status' => ProvisioningRun::STATUS_PENDING, 'current_step' => 0, 'context' => [ 'instance_id' => $instance->id, 'host_id' => $instance->host_id, 'node' => $instance->host?->node, 'vmid' => $instance->vmid, 'subdomain' => $instance->subdomain, ], ]); AdvanceRunJob::dispatch($run->uuid); } /** * ANY run, deliberately — unlike the narrower "will it do this work?" question * App\Provisioning\WorkInFlight answers for the two actions that must not lose * a change they were asked to make. This is a repair sweep: it runs again, it * skips instances whose quota is already enforced, and an instance passed over * today is picked up on the next pass. Nothing is forgotten by waiting. */ private function hasRunInFlight(Instance $instance): bool { return ProvisioningRun::query() ->where('subject_type', Order::class) ->where('subject_id', $instance->order_id) ->inFlight() ->exists(); } }