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(); } }