Newer
Older
openstack-caracal-ipv4 / scripts / phase-06-bootstrap.sh
#!/usr/bin/env bash
# scripts/phase-06-bootstrap.sh
#
# Phase-06 Step 6.0-BOOT: fresh-deploy tenant bootstrap. Verify-or-create (idempotent;
# all [SKIP] on an existing cloud):
#   - domain capi  - project capi-mgmt in capi
#   - admin roles member + load-balancer_member + reader on capi-mgmt (D-039)
#   - five flavors (as-built specs)
#   - image ubuntu-24.04-noble via STAGE-AND-VERIFY (FINDING-3): download to $HOME (snap-
#     readable; NOT /tmp -- L7) if missing/checksum-stale, sha256 vs published SHA256SUMS,
#     then client-safe `image create --file --import` (image-conversion lands it raw, D-021).
# D-056 script: the image stage-and-verify is the amphora-pipeline shape; the rest is
# idempotent verify-or-create. Human-gated by invocation.
#
# Tunables via env: PROJ_DOMAIN PROJECT IMG_NAME IMG_URL SUM_URL IMG_FILE SRC POLL_TRIES POLL_SLEEP
# Requires: jumphost; admin-openrc (sourced or at ~/admin-openrc); openstack jq curl wget sha256sum awk.
# Usage:  source ~/admin-openrc && bash scripts/phase-06-bootstrap.sh
# Exit:   0 all present/created + image active | 1 gate/checksum/poll fail | 2 precondition
# ASCII + LF.

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

PROJ_DOMAIN="${PROJ_DOMAIN:-capi}"
PROJECT="${PROJECT:-capi-mgmt}"
IMG_NAME="${IMG_NAME:-ubuntu-24.04-noble}"
IMG_URL="${IMG_URL:-https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img}"
SUM_URL="${SUM_URL:-https://cloud-images.ubuntu.com/noble/current/SHA256SUMS}"
IMG_FILE="${IMG_FILE:-noble-server-cloudimg-amd64.img}"
SRC="${SRC:-$HOME/$IMG_FILE}"
POLL_TRIES="${POLL_TRIES:-40}"; POLL_SLEEP="${POLL_SLEEP:-15}"
ROLES="member load-balancer_member reader"
FLAVOR_SPECS=("gp.large 4 16384 80" "gp.mid 2 8192 40" "capi.node 2 4096 40" "gp.small 1 4096 20" "m1.lbtest 1 1024 4")

# --- preconditions ------------------------------------------------------------------
for c in openstack jq curl wget sha256sum awk; do
  command -v "$c" >/dev/null 2>&1 || { echo "FAIL: $c not found" >&2; exit 2; }
done
if [ -z "${OS_AUTH_URL:-}" ] && [ -f "$HOME/admin-openrc" ]; then
  # shellcheck disable=SC1091
  . "$HOME/admin-openrc"
fi
[ -n "${OS_AUTH_URL:-}" ] || { echo "FAIL: OS_AUTH_URL unset and no ~/admin-openrc" >&2; exit 2; }
[ -n "${OS_USERNAME:-}" ] && [ -n "${OS_USER_DOMAIN_NAME:-}" ] || { echo "FAIL: OS_USERNAME/OS_USER_DOMAIN_NAME unset" >&2; exit 2; }
openstack token issue >/dev/null 2>&1 || { echo "FAIL: no scoped token (admin-openrc)" >&2; exit 2; }

# 1. domain
if openstack domain show "$PROJ_DOMAIN" -f value -c id >/dev/null 2>&1; then
  echo "[SKIP] domain $PROJ_DOMAIN exists"
else
  openstack domain create --description "CAPI/Magnum workload identity" "$PROJ_DOMAIN" >/dev/null
  echo "[OK] domain $PROJ_DOMAIN"
fi

# 2. project
if openstack project show "$PROJECT" --domain "$PROJ_DOMAIN" -f value -c id >/dev/null 2>&1; then
  echo "[SKIP] project $PROJECT exists"
else
  openstack project create --domain "$PROJ_DOMAIN" --description "CAPI management project" "$PROJECT" >/dev/null
  echo "[OK] project $PROJECT (domain $PROJ_DOMAIN)"
fi

# 3. roles (D-039: trustor must hold load-balancer_member or CAPO 403s on Octavia at LB provisioning)
for ROLE in $ROLES; do
  if openstack role assignment list --user "$OS_USERNAME" --user-domain "$OS_USER_DOMAIN_NAME" \
       --project "$PROJECT" --project-domain "$PROJ_DOMAIN" --role "$ROLE" -f value 2>/dev/null | grep -q .; then
    echo "[SKIP] role $ROLE on $PROJECT"
  else
    openstack role add --user "$OS_USERNAME" --user-domain "$OS_USER_DOMAIN_NAME" \
      --project "$PROJECT" --project-domain "$PROJ_DOMAIN" "$ROLE"
    echo "[OK] role $ROLE on $PROJECT"
  fi
done

# 4. flavors (as-built specs; public)
for spec in "${FLAVOR_SPECS[@]}"; do
  read -r fname fv fr fd <<<"$spec"
  if openstack flavor show "$fname" -f value -c id >/dev/null 2>&1; then
    echo "[SKIP] flavor $fname exists"
  else
    openstack flavor create --vcpus "$fv" --ram "$fr" --disk "$fd" --public "$fname" >/dev/null
    echo "[OK] flavor $fname ($fv vcpu / $fr MB / $fd GB)"
  fi
done

# 5. image (stage-and-verify)
if openstack image show "$IMG_NAME" -f value -c id >/dev/null 2>&1; then
  echo "[SKIP] image $IMG_NAME exists"
else
  EXP=$(curl -fsSL "$SUM_URL" | awk -v f="$IMG_FILE" '$2=="*"f || $2==f {print $1}')
  [ -n "$EXP" ] || { echo "GATE FAIL: no published checksum for $IMG_FILE"; exit 1; }
  if [ -f "$SRC" ] && [ "$(sha256sum "$SRC" | awk '{print $1}')" = "$EXP" ]; then
    echo "[OK] staged $IMG_FILE present + checksum-valid; skipping download"
  else
    echo "[..] downloading $IMG_FILE to $SRC (snap-readable; NOT /tmp)"
    wget -q -O "$SRC" "$IMG_URL"
    GOT=$(sha256sum "$SRC" | awk '{print $1}')
    [ "$EXP" = "$GOT" ] || { echo "GATE FAIL: checksum mismatch exp=$EXP got=$GOT"; exit 1; }
    echo "[OK] checksum verified ($GOT)"
  fi
  openstack image create "$IMG_NAME" --file "$SRC" --import \
    --container-format bare --disk-format qcow2 --public \
    --property os_distro=ubuntu --property os_version=24.04 >/dev/null
  echo "[OK] image $IMG_NAME import submitted"
fi

# poll to active (import + glance image-conversion to raw)
echo "=== poll $IMG_NAME -> active ==="
ACTIVE=0
for i in $(seq 1 "$POLL_TRIES"); do
  ST=$(openstack image show "$IMG_NAME" -f value -c status 2>/dev/null || echo '?')
  echo "[$i] status=$ST"
  if [ "$ST" = active ]; then ACTIVE=1; break; fi
  sleep "$POLL_SLEEP"
done
[ "$ACTIVE" = 1 ] || { echo "GATE FAIL: $IMG_NAME not active after $POLL_TRIES tries"; exit 1; }

echo "=== confirm ==="
openstack image show "$IMG_NAME" -f json | jq -r '"image: status=\(.status) disk_format=\(.disk_format) visibility=\(.visibility)"'
echo "Summary: phase-06 bootstrap complete (domain/project/roles/flavors/image)."