diff --git a/docs/uat.md b/docs/uat.md index 45a03d6..520cf32 100644 --- a/docs/uat.md +++ b/docs/uat.md @@ -21,52 +21,45 @@ Use `--type linux` for all remote nodes. Proxmox-specific claims (`doctor proxmox`, PVE role, sudoers) are **skipped** in this path. 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 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 export ORCA_HOME=~/orca-uat 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 ```sh orca node join --type proxmox \ --host pve01 \ - --ssh-user root \ - --ssh-key ~/.ssh/orca_ed25519 \ - --host-key-fingerprint SHA256: + --ssh-user root ``` **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 orca node join --type linux \ --host worker01 \ - --ssh-user root \ - --ssh-key ~/.ssh/orca_ed25519 \ - --host-key-fingerprint SHA256: + --ssh-user root ``` **Expected**: SSH bootstrap succeeds, orca user created, drift-events dir created, node registered as `ready` with `kind=linux`. diff --git a/internal/cli/init.go b/internal/cli/init.go index 46b73e8..90f7901 100644 --- a/internal/cli/init.go +++ b/internal/cli/init.go @@ -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))] + "..."}) if !jsonOutput { 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@\n\n", certpaths.SSHPubPath()) } // Step 4b: known_hosts file (empty, 0600). Without this, the TOFU diff --git a/internal/proxmox/bootstrap.go b/internal/proxmox/bootstrap.go index 71f83d5..2634d35 100644 --- a/internal/proxmox/bootstrap.go +++ b/internal/proxmox/bootstrap.go @@ -467,10 +467,12 @@ func createLinuxUser(user string) error { // createPVERole creates the OrcaOperator PVE role if it doesn't exist. // Idempotent: probes `pveum role list` before `pveum role add`. 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( - "pveum role list 2>/dev/null | grep -q '^%s' || pveum role add %s --privs '%s'", - shellQuote(role), shellQuote(role), OrcaOperatorPrivileges, + "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), OrcaOperatorPrivileges, ) if _, err := runRemote(cmd); err != nil { return err @@ -483,11 +485,10 @@ func createPVERole(role string) error { // Uses @pam realm (AD-019) since orca creates a Linux system user. func createPVEUser(user string) error { pveUserID := user + "@pam" - // F10c: shellQuote the PVE user id (validated upstream, but - // defense-in-depth). + // Idempotent: check if user exists, create if not, update comment if exists. cmd := fmt.Sprintf( - "pveum user list 2>/dev/null | grep -q %s || pveum user add %s -comment 'Orca automation user'", - shellQuote(pveUserID), shellQuote(pveUserID), + "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), ) if _, err := runRemote(cmd); err != nil { return err