93 lines
4.1 KiB
PHP
93 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace App\Provisioning\Steps\Host;
|
|
|
|
use App\Models\ProvisioningRun;
|
|
use App\Provisioning\StepResult;
|
|
use App\Services\Ssh\RemoteShell;
|
|
|
|
/**
|
|
* Creates the automation role + user + API token on the host via `pveum` over
|
|
* SSH. A fresh host has no API token yet, so this bootstrap must run through the
|
|
* authenticated root SSH session, not the (token-less) REST API. The secret is
|
|
* shown only once, so the token id is persisted before advancing; a re-run
|
|
* deletes and re-mints the token to stay idempotent after a crash.
|
|
*/
|
|
class CreateAutomationToken extends HostStep
|
|
{
|
|
public function __construct(private RemoteShell $shell) {}
|
|
|
|
public function key(): string
|
|
{
|
|
return 'create_automation_token';
|
|
}
|
|
|
|
public function execute(ProvisioningRun $run): StepResult
|
|
{
|
|
$host = $this->host($run);
|
|
|
|
$role = (string) config('provisioning.proxmox.role_id');
|
|
$privs = (string) config('provisioning.proxmox.role_privs');
|
|
$user = (string) config('provisioning.proxmox.user');
|
|
$tokenName = (string) config('provisioning.proxmox.token_name');
|
|
|
|
$this->keyLogin($this->shell, $host);
|
|
|
|
// The role is converged BEFORE the token short-circuit below, deliberately.
|
|
//
|
|
// `pveum role add … || true` only ever applied the privilege list on the
|
|
// run that first created the role, so adding a privilege to
|
|
// provisioning.role_privs reached new hosts and no existing one. That is
|
|
// how Sys.Modify would have been missing on every host onboarded before
|
|
// it was added, with the symptom appearing much later and somewhere else
|
|
// entirely: a 403 from POST /cluster/backup failing a paying customer's
|
|
// provisioning at register_backup. `role modify` without -append REPLACES
|
|
// the list, so this line makes the host match the config rather than
|
|
// accumulating whatever any past version of it once granted.
|
|
// Two separate commands rather than one `add || modify` line, so a
|
|
// failure can be attributed: `add` failing means the role is already
|
|
// there, which is the ordinary case and not an error.
|
|
$roleArgs = escapeshellarg($role).' -privs '.escapeshellarg($privs);
|
|
if (! $this->shell->run('pveum role add '.$roleArgs)->ok()
|
|
&& ! $this->shell->run('pveum role modify '.$roleArgs)->ok()) {
|
|
return StepResult::retry(20, 'could not converge the Proxmox automation role privileges');
|
|
}
|
|
|
|
// Idempotent: token already minted and persisted. Below the role
|
|
// convergence, so a replay still repairs the privileges.
|
|
if ($this->hasResource($run, 'pve_token') && filled($host->api_token_ref)) {
|
|
return StepResult::advance();
|
|
}
|
|
|
|
// User and ACL are idempotent and have nothing to converge (unlike the
|
|
// role, whose CONTENT changes as the pipeline grows), so "already exists"
|
|
// stays tolerated here.
|
|
$this->shell->run('pveum user add '.escapeshellarg($user).' || true');
|
|
$this->shell->run('pveum acl modify / -user '.escapeshellarg($user).' -role '.escapeshellarg($role).' || true');
|
|
|
|
// Drop any half-created token from a prior crashed attempt, then mint fresh.
|
|
$this->shell->run('pveum user token remove '.escapeshellarg($user).' '.escapeshellarg($tokenName).' || true');
|
|
$result = $this->shell->run(
|
|
'pveum user token add '.escapeshellarg($user).' '.escapeshellarg($tokenName).' -privsep 0 --output-format json'
|
|
);
|
|
|
|
if (! $result->ok()) {
|
|
return StepResult::retry(20, 'Proxmox token creation failed');
|
|
}
|
|
|
|
$data = json_decode($result->stdout, true);
|
|
$secret = $data['value'] ?? null;
|
|
|
|
if (blank($secret)) {
|
|
return StepResult::retry(20, 'Proxmox token secret was empty');
|
|
}
|
|
|
|
$tokenId = $data['full-tokenid'] ?? "{$user}!{$tokenName}";
|
|
|
|
$host->update(['api_token_ref' => $tokenId.'='.$secret]);
|
|
$this->recordResource($run, $host, 'pve_token', $tokenId);
|
|
|
|
return StepResult::advance();
|
|
}
|
|
}
|