Newer
Older
openstack-caracal-ipv4 / scripts / phase-03-admin-openrc.sh
#!/usr/bin/env bash
# scripts/phase-03-admin-openrc.sh
#
# Phase-03 Step 3.2: build ~/admin-openrc from live cloud state -- fetch the vault root
# CA, the keystone admin password, discover the admin project (DOCFIX-022: the first
# candidate that issues a SCOPED token wins), and write a 0600 openrc. Encapsulates the
# do-doc block AS A SCRIPT (D-056): deterministic + run every session + carries the
# fiddly scope-discovery loop, so it benefits from being tested and paste-proof.
#
# SECRET-HANDLING: this fetches the admin password (which the operator already retrieves
# manually via juju) and writes it into ~/admin-openrc. The password stays inside the
# subshell + the 0600 file (umask 077 set before write); it is never echoed. The human
# gates by choosing to invoke the script.
#
# Tunables via env: KEYSTONE_VIP ADMIN_DOMAIN PROJECT_CANDIDATES CA RC MODEL
# Requires: jumphost; juju (authed); openstack; python3; jq; openssl.
# Usage:  scripts/phase-03-admin-openrc.sh   (then: source ~/admin-openrc)
# Exit:   0 openrc written + scoped token verified | 1 fetch/scope/confirm fail | 2 precondition
# ASCII + LF.

set -euo pipefail
shopt -s inherit_errexit 2>/dev/null || true

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
EXTRACT="$SCRIPT_DIR/extract_admin_password.py"

KEYSTONE_VIP="${KEYSTONE_VIP:-10.12.4.50}"     # keystone PUBLIC endpoint (provider VIP; verify vs bundle on rebuild)
ADMIN_DOMAIN="${ADMIN_DOMAIN:-admin_domain}"
PROJECT_CANDIDATES="${PROJECT_CANDIDATES:-admin admin_domain}"   # tried in order; first that SCOPES wins (DOCFIX-022)
CA="${CA:-$HOME/vault-init/vault-ca-root.pem}"
RC="${RC:-$HOME/admin-openrc}"
MODEL="${MODEL:-openstack}"

# --- preconditions ------------------------------------------------------------------
for c in juju openstack python3 jq openssl; do
  command -v "$c" >/dev/null 2>&1 || { echo "FAIL: $c not found" >&2; exit 2; }
done
[ -f "$EXTRACT" ] || { echo "FAIL: helper not found: $EXTRACT" >&2; exit 2; }
juju whoami >/dev/null 2>&1 || { echo "FAIL: juju not authed (stale macaroon? run 'juju login')" >&2; exit 2; }

( set -e
  mkdir -p "$(dirname "$CA")"

  # 1. Vault root CA -> file (JSON extract; DOCFIX-021 -- human output indents the PEM)
  juju run vault/leader get-root-ca -m "$MODEL" --format json \
    | jq -r '[.. | strings | select(test("-----BEGIN CERTIFICATE-----"))][0]' > "$CA"
  [ -s "$CA" ] || { echo "FATAL: vault root CA extract empty"; exit 1; }
  openssl x509 -in "$CA" -noout -subject -dates

  # 2. Admin password -> var (tested .py extractor; never echoed)
  ADMIN_PASS=$(juju run keystone/leader get-admin-password -m "$MODEL" --format json | python3 "$EXTRACT")
  [ -n "$ADMIN_PASS" ] || { echo "FATAL: password extract failed"; exit 1; }

  # 3. PROJECT LOOKUP: first candidate that issues a SCOPED token wins (DOCFIX-022)
  export OS_AUTH_URL="https://${KEYSTONE_VIP}:5000/v3" OS_USERNAME=admin OS_PASSWORD="$ADMIN_PASS"
  export OS_USER_DOMAIN_NAME="$ADMIN_DOMAIN" OS_PROJECT_DOMAIN_NAME="$ADMIN_DOMAIN"
  export OS_IDENTITY_API_VERSION=3 OS_REGION_NAME=RegionOne OS_CACERT="$CA"
  ADMIN_PROJECT=""
  for P in $PROJECT_CANDIDATES; do
    if OS_PROJECT_NAME="$P" openstack token issue >/dev/null 2>&1; then ADMIN_PROJECT="$P"; break; fi
  done
  [ -n "$ADMIN_PROJECT" ] || { echo "FATAL: no candidate project scoped (tried: $PROJECT_CANDIDATES)"; exit 1; }
  echo "[OK] admin project = $ADMIN_PROJECT ; password len ${#ADMIN_PASS}"

  # 4. Write ~/admin-openrc (back up any existing; 0600 via umask + chmod)
  [ -f "$RC" ] && mv "$RC" "$RC.pre-$(date -u +%Y%m%dT%H%M%SZ)"
  umask 077
  cat > "$RC" <<EOF
export OS_AUTH_URL=https://${KEYSTONE_VIP}:5000/v3
export OS_USERNAME=admin
export OS_PASSWORD='$ADMIN_PASS'
export OS_PROJECT_NAME=$ADMIN_PROJECT
export OS_USER_DOMAIN_NAME=$ADMIN_DOMAIN
export OS_PROJECT_DOMAIN_NAME=$ADMIN_DOMAIN
export OS_IDENTITY_API_VERSION=3
export OS_REGION_NAME=RegionOne
export OS_CACERT=$CA
EOF
  chmod 600 "$RC"
  echo "[OK] wrote $RC (0600)"
)

# 5. Verify from the written file (password stayed inside the subshell above)
echo "=== verify (sourced from $RC) ==="
# shellcheck disable=SC1090  # $RC is written above; runtime source is intentional
( source "$RC"; echo "auth -> $OS_AUTH_URL  project=$OS_PROJECT_NAME"; openstack token issue >/dev/null 2>&1 \
    && echo "[OK] scoped token issued" || { echo "VERIFY FAIL: token issue from written openrc"; exit 1; } )
# shellcheck disable=SC1090
( source "$RC"; openstack endpoint list -f value -c "Service Name" -c Interface -c URL 2>/dev/null | sort | head ) || true
echo "[OK] admin-openrc ready -- 'source $RC' to use"