fix: PVE role/user idempotency + init pre-staging instructions

- createPVERole: use grep -qF + fallback to pveum role mod (was broken
  by single-quote-in-grep pattern: grep -q '^'OrcaOperator'')
- createPVEUser: same idempotency fix (grep -qF + fallback to mod)
- orca init: prints ssh-copy-id instructions with the orca public key
  path after generating the SSH keypair
- docs/uat.md: removed manual pre-staging (ssh-keygen, ssh-copy-id
  with operator key, host-key fingerprint pinning). orca init handles
  key generation; node join uses the orca key by default; TOFU is
  automatic. Updated node join examples to not pass --ssh-key or
  --host-key-fingerprint.

---ci---
project: orca
status: fix
---/ci---
This commit is contained in:
Jon Chery
2026-08-10 17:07:30 +00:00
parent 00efe25ce4
commit d324939699
3 changed files with 36 additions and 42 deletions
+26 -35
View File
@@ -21,52 +21,45 @@ Use `--type linux` for all remote nodes. Proxmox-specific claims
(`doctor proxmox`, PVE role, sudoers) are **skipped** in this path. (`doctor proxmox`, PVE role, sudoers) are **skipped** in this path.
The signoff script reports exercised vs. skipped claims. The signoff script reports exercised vs. skipped claims.
### Pre-staging
1. Build orca from the v0.13 tag:
```sh
git clone https://git.cloudinit.dev/coreci/orca.git
cd orca && git checkout v0.12.13
make build
# binary is at bin/orca
```
2. Generate the orca SSH keypair on the lead:
```sh
ssh-keygen -t ed25519 -f ~/.ssh/orca_ed25519 -N ""
```
3. Pre-stage the orca public key on pve01 and worker01:
```sh
ssh-copy-id -i ~/.ssh/orca_ed25519.pub root@pve01
ssh-copy-id -i ~/.ssh/orca_ed25519.pub root@worker01
```
4. Pin host-key fingerprints (optional but recommended):
```sh
ssh-keyscan pve01 | ssh-keygen -lf -
ssh-keyscan worker01 | ssh-keygen -lf -
```
## Step-by-step UAT ## Step-by-step UAT
### Step 1: Initialize the cluster ### Step 1: Install orca + initialize the cluster
Install orca (1-liner):
```sh
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash
```
Initialize the cluster:
```sh ```sh
export ORCA_HOME=~/orca-uat export ORCA_HOME=~/orca-uat
orca init orca init
``` ```
**Expected**: cluster directory created, CA cert generated, localhost node registered. **Expected**: orca init creates:
- CA cert + server cert
- SSH keypair (orca_ssh_key + orca_ssh_key.pub)
- known_hosts file (empty, for TOFU capture)
- Master key (for secrets encryption)
- Traefik data-plane ingress (binary + systemd unit + config)
- Localhost node registered
**Pre-staging remote nodes**: `orca init` prints the orca public key path.
Deploy it to each remote host before joining:
```sh
ssh-copy-id -i $ORCA_HOME/orca_ssh_key.pub root@pve01
ssh-copy-id -i $ORCA_HOME/orca_ssh_key.pub root@worker01
```
The TOFU host-key capture is automatic — no manual fingerprint pinning needed.
The first SSH connection captures and stores the remote host key.
### Step 2: Onboard the Proxmox host ### Step 2: Onboard the Proxmox host
```sh ```sh
orca node join --type proxmox \ orca node join --type proxmox \
--host pve01 \ --host pve01 \
--ssh-user root \ --ssh-user root
--ssh-key ~/.ssh/orca_ed25519 \
--host-key-fingerprint SHA256:<fingerprint>
``` ```
**Expected**: SSH bootstrap succeeds, orca user created, PVE role assigned, node registered as `ready` with `kind=proxmox`. **Expected**: SSH bootstrap succeeds, orca user created, PVE role assigned, node registered as `ready` with `kind=proxmox`.
@@ -76,9 +69,7 @@ orca node join --type proxmox \
```sh ```sh
orca node join --type linux \ orca node join --type linux \
--host worker01 \ --host worker01 \
--ssh-user root \ --ssh-user root
--ssh-key ~/.ssh/orca_ed25519 \
--host-key-fingerprint SHA256:<fingerprint>
``` ```
**Expected**: SSH bootstrap succeeds, orca user created, drift-events dir created, node registered as `ready` with `kind=linux`. **Expected**: SSH bootstrap succeeds, orca user created, drift-events dir created, node registered as `ready` with `kind=linux`.
+2
View File
@@ -158,6 +158,8 @@ func runInit(out interface{ Write([]byte) (int, error) }) error {
summary.Steps = append(summary.Steps, stepResult{Label: "ssh-key", Status: "ok", Detail: sshKeyFp[:min(16, len(sshKeyFp))] + "..."}) summary.Steps = append(summary.Steps, stepResult{Label: "ssh-key", Status: "ok", Detail: sshKeyFp[:min(16, len(sshKeyFp))] + "..."})
if !jsonOutput { if !jsonOutput {
fmt.Fprintf(out, "\xe2\x9c\x93 SSH keypair provisioned: fp=%s\n", sshKeyFp[:min(16, len(sshKeyFp))]+"...") fmt.Fprintf(out, "\xe2\x9c\x93 SSH keypair provisioned: fp=%s\n", sshKeyFp[:min(16, len(sshKeyFp))]+"...")
fmt.Fprintf(out, "\n To onboard remote nodes, deploy the orca public key first:\n")
fmt.Fprintf(out, " ssh-copy-id -i %s root@<remote-host>\n\n", certpaths.SSHPubPath())
} }
// Step 4b: known_hosts file (empty, 0600). Without this, the TOFU // Step 4b: known_hosts file (empty, 0600). Without this, the TOFU
+8 -7
View File
@@ -467,10 +467,12 @@ func createLinuxUser(user string) error {
// createPVERole creates the OrcaOperator PVE role if it doesn't exist. // createPVERole creates the OrcaOperator PVE role if it doesn't exist.
// Idempotent: probes `pveum role list` before `pveum role add`. // Idempotent: probes `pveum role list` before `pveum role add`.
func createPVERole(role string) error { func createPVERole(role string) error {
// F10c: shellQuote the role (validated upstream, but defense-in-depth). // Idempotent: check if the role already exists before creating.
// Use pveum role list with grep -qF (fixed string, not regex) to
// avoid shell-quoting issues with single quotes inside the pattern.
cmd := fmt.Sprintf( cmd := fmt.Sprintf(
"pveum role list 2>/dev/null | grep -q '^%s' || pveum role add %s --privs '%s'", "pveum role list 2>/dev/null | grep -qF %s && exit 0 || pveum role add %s --privs '%s' 2>/dev/null || pveum role mod %s --privs '%s'",
shellQuote(role), shellQuote(role), OrcaOperatorPrivileges, shellQuote(role), shellQuote(role), OrcaOperatorPrivileges, shellQuote(role), OrcaOperatorPrivileges,
) )
if _, err := runRemote(cmd); err != nil { if _, err := runRemote(cmd); err != nil {
return err return err
@@ -483,11 +485,10 @@ func createPVERole(role string) error {
// Uses @pam realm (AD-019) since orca creates a Linux system user. // Uses @pam realm (AD-019) since orca creates a Linux system user.
func createPVEUser(user string) error { func createPVEUser(user string) error {
pveUserID := user + "@pam" pveUserID := user + "@pam"
// F10c: shellQuote the PVE user id (validated upstream, but // Idempotent: check if user exists, create if not, update comment if exists.
// defense-in-depth).
cmd := fmt.Sprintf( cmd := fmt.Sprintf(
"pveum user list 2>/dev/null | grep -q %s || pveum user add %s -comment 'Orca automation user'", "pveum user list 2>/dev/null | grep -qF %s && exit 0 || pveum user add %s -comment 'Orca automation user' 2>/dev/null || pveum user mod %s -comment 'Orca automation user'",
shellQuote(pveUserID), shellQuote(pveUserID), shellQuote(pveUserID), shellQuote(pveUserID), shellQuote(pveUserID),
) )
if _, err := runRemote(cmd); err != nil { if _, err := runRemote(cmd); err != nil {
return err return err