#!/usr/bin/env bash
# scripts/dc-dc-rbd-mirror.sh -- rbd-mirror pool peer bootstrap for D-108's
# Glance replication mechanism (tooling gap register item #5). Extracts
# runbooks/dc-dc-phase5-dr-failover-drill.md Step 5 (stage ONE-WAY: rx-only
# peer bootstrap) and Step 8's rbd-mirror half (flip to rx-tx -- TWO-WAY)
# into a callable, parameterized script.
#
# STANDARD, well-documented upstream Ceph administration (trust these): every
# `rbd mirror pool` subcommand/flag below (enable image, peer bootstrap
# create/import, --direction rx-only|rx-tx) is copied verbatim from the
# runbook, which is itself copied from upstream rbd-mirror docs -- nothing
# here is invented.
#
# GENUINELY REPO-SPECIFIC (needs review before trusting; none of it exists
# yet): the Glance POOL name (runbook Step 5 explicitly warns "do not assume
# glance -- confirm against the live ceph-mon / `juju config glance
# rbd-pool`"), the site names, and the juju model/unit this targets. All
# REQUIRED arguments below -- no invented defaults.
#
# SECRET-ADJACENT TOKEN HANDLING: the bootstrap token is credential material
# (whoever holds it can join the mirror relationship). This script NEVER
# reads the token's contents into its own context -- bootstrap-primary
# redirects the remote command's stdout straight to a file ON THE REMOTE
# UNIT (via the ssh'd shell's own `>`), and bootstrap-secondary expects the
# token file to already exist on ITS target unit (transferred out of band by
# the operator, per the runbook's own "transfer the token to DC2 out of band
# ... never printed into this session's context" instruction) -- this script
# does not perform that transfer itself.
#
# NO LIVE RBD-MIRROR DAEMON EXISTS THIS SESSION (2026-07-10, prep-only) --
# never run for real, cannot be tonight. Default mode is a DRY-RUN plan
# print; --apply executes, one command at a time, aborting on first failure.
# Its matching harness (tests/dc-dc-rbd-mirror/run-tests.sh) validates ONLY
# this script's own logic (argument parsing, guard clauses, dry-run output,
# the required --direction choice) -- it does NOT and CANNOT validate real
# rbd-mirror daemon behavior.
#
# $DC gate: sources scripts/lib-net.sh and calls `lib_net_select_dc "$DC"`
# once (DOCFIX-151 convention), immediately after argument parsing --
# deliberately makes `--dc dc2` FAIL LOUD today (D-101/gap #3: DC2 has no
# assigned network literals, so no real DC2 Ceph cluster exists to target).
# Also prints the replication-plane CIDR key (PLANE_NAME in lib-net.sh) as a
# REMINDER that rbd-mirror daemon peering must ride that plane, per D-108's
# explicit correction of the DR-seed's original v4 carrier language -- never
# substituted into a command, the real ULA peer address is always separately
# measured.
#
# Usage:
# dc-dc-rbd-mirror.sh bootstrap-primary --dc dc1 --model M --unit U \
# --pool NAME --site-name NAME [--token-out /remote/path] [--apply]
# dc-dc-rbd-mirror.sh bootstrap-secondary --dc dc2 --model M --unit U \
# --pool NAME --site-name NAME --direction rx-only|rx-tx \
# --token-in /remote/path [--apply]
# dc-dc-rbd-mirror.sh --help
#
# Exit: 0 ok | 1 bad/missing args | 2 juju not on PATH (apply only) |
# 3 $DC gate refused | 5 apply-mode command failed. ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
usage() {
cat <<'EOF'
usage:
dc-dc-rbd-mirror.sh bootstrap-primary --dc dc1|dc2 --model M --unit U \
--pool NAME --site-name NAME [--token-out /remote/path] [--apply]
dc-dc-rbd-mirror.sh bootstrap-secondary --dc dc1|dc2 --model M --unit U \
--pool NAME --site-name NAME --direction rx-only|rx-tx \
--token-in /remote/path [--apply]
dc-dc-rbd-mirror.sh --help
Default mode is dry-run (prints the plan, executes nothing). --apply executes,
one command at a time, aborting on the first failure. --direction has no
default -- rx-only (Step 5, one-way staging) and rx-tx (Step 8, two-way) are
a deliberate, conscious choice each time, never assumed.
EOF
}
SUBCMD="${1:-}"
case "$SUBCMD" in
-h|--help) usage; exit 0 ;;
bootstrap-primary|bootstrap-secondary) shift ;;
"") echo "FAIL: a subcommand is required" >&2; usage >&2; exit 1 ;;
*) echo "FAIL: unknown subcommand '$SUBCMD'" >&2; usage >&2; exit 1 ;;
esac
DC=""; MODEL="${MODEL:-openstack}"; UNIT=""; APPLY=0
POOL=""; SITE_NAME=""; DIRECTION=""
TOKEN_OUT="/tmp/rbd-mirror-bootstrap-token"
TOKEN_IN=""
while [ $# -gt 0 ]; do
case "$1" in
-h|--help) usage; exit 0 ;;
--dc) DC="${2:-}"; shift 2 ;;
--model) MODEL="${2:-}"; shift 2 ;;
--unit) UNIT="${2:-}"; shift 2 ;;
--pool) POOL="${2:-}"; shift 2 ;;
--site-name) SITE_NAME="${2:-}"; shift 2 ;;
--direction) DIRECTION="${2:-}"; shift 2 ;;
--token-out) TOKEN_OUT="${2:-}"; shift 2 ;;
--token-in) TOKEN_IN="${2:-}"; shift 2 ;;
--apply) APPLY=1; shift ;;
*) echo "FAIL: unknown argument '$1'" >&2; usage >&2; exit 1 ;;
esac
done
# --- guard clauses: required args, no invented defaults ---
[ -n "$DC" ] || { echo "FAIL: --dc is required (dc1|dc2)" >&2; exit 1; }
case "$DC" in dc1|dc2) ;; *) echo "FAIL: --dc must be dc1 or dc2 (got '$DC')" >&2; exit 1 ;; esac
[ -n "$UNIT" ] || { echo "FAIL: --unit is required (e.g. ceph-rbd-mirror/0 or ceph-mon/0 -- measured, never assumed)" >&2; exit 1; }
[ -n "$POOL" ] || { echo "FAIL: --pool is required (confirm against 'ceph osd pool ls' + 'juju config glance rbd-pool' -- do not assume 'glance', per the runbook)" >&2; exit 1; }
[ -n "$SITE_NAME" ] || { echo "FAIL: --site-name is required" >&2; exit 1; }
case "$SUBCMD" in
bootstrap-secondary)
[ -n "$DIRECTION" ] || { echo "FAIL: --direction is required for bootstrap-secondary (rx-only|rx-tx -- a conscious choice, never defaulted)" >&2; exit 1; }
case "$DIRECTION" in rx-only|rx-tx) ;; *) echo "FAIL: --direction must be rx-only or rx-tx (got '$DIRECTION')" >&2; exit 1 ;; esac
[ -n "$TOKEN_IN" ] || { echo "FAIL: --token-in is required for bootstrap-secondary (the path, ON THIS UNIT, where the out-of-band-transferred bootstrap token file lives -- this script does not transfer it)" >&2; exit 1; }
;;
esac
# --- $DC gate (DOCFIX-151 convention) ---
# Informational in dry-run (a plan preview is not a mutation); HARD BLOCKING
# before any --apply execution below.
# shellcheck source=scripts/lib-net.sh
. "$HERE/lib-net.sh"
DC_GATE_MSG="$(lib_net_select_dc "$DC" 2>&1 1>/dev/null)"; DC_GATE_RC=$?
if [ "$DC_GATE_RC" -eq 0 ]; then
DC_GATE_LINE="\$DC gate: OK ($DC)"
else
DC_GATE_LINE="\$DC gate: FAILED for '$DC' -- $DC_GATE_MSG"
fi
REPL_CIDR=""
for c in "${PLANE_CIDRS[@]}"; do
[ "${PLANE_NAME[$c]:-}" = "replication" ] && REPL_CIDR="$c"
done
hdr() { echo; echo "=== $* ==="; }
note() { echo " $*"; }
hdr "dc-dc-rbd-mirror subcmd=$SUBCMD dc=$DC mode=$([ "$APPLY" -eq 1 ] && echo apply || echo dry-run)"
note "juju model=$MODEL unit=$UNIT pool=$POOL site-name=$SITE_NAME"
note "$DC_GATE_LINE"
note "reminder: rbd-mirror daemon peering must ride the REPLICATION plane (${REPL_CIDR:-unassigned}, per D-101/D-108/lib-net.sh) -- not substituted into any command, confirm the real ULA peer address separately"
# --- build the remote rbd command sequence (verbatim upstream ops) ---
SSH_CMDS=()
case "$SUBCMD" in
bootstrap-primary)
SSH_CMDS+=("rbd mirror pool enable $POOL image")
SSH_CMDS+=("rbd mirror pool peer bootstrap create --site-name $SITE_NAME $POOL > $TOKEN_OUT")
;;
bootstrap-secondary)
SSH_CMDS+=("rbd mirror pool enable $POOL image")
SSH_CMDS+=("rbd mirror pool peer bootstrap import --site-name $SITE_NAME --direction $DIRECTION $POOL $TOKEN_IN")
;;
esac
hdr "PLAN"
n=0
for cmd in "${SSH_CMDS[@]}"; do
n=$((n+1))
note "STEP $n: juju ssh -m $MODEL $UNIT -- \"$cmd\""
done
if [ "$SUBCMD" = "bootstrap-primary" ]; then
note "NOTE: the token now lives at $TOKEN_OUT ON THE REMOTE UNIT -- transfer it to the DC2 unit OUT OF BAND (never printed/piped through this session), then run bootstrap-secondary there"
fi
if [ "$APPLY" -ne 1 ]; then
echo
echo "re-run with --apply to execute (one command at a time, aborts on first failure)."
echo "OK (dry-run)"
exit 0
fi
[ "$DC_GATE_RC" -eq 0 ] || { echo "FAIL: \$DC gate refused for '$DC' -- STOP, do not mutate an unassigned DC ($DC_GATE_MSG)" >&2; exit 3; }
command -v juju >/dev/null 2>&1 || { echo "FAIL: juju required on PATH for --apply" >&2; exit 2; }
hdr "EXECUTE (apply mode -- one command at a time, abort on first failure)"
n=0
for cmd in "${SSH_CMDS[@]}"; do
n=$((n+1))
echo " DO STEP $n: juju ssh -m $MODEL $UNIT -- \"$cmd\""
juju ssh -m "$MODEL" "$UNIT" -- "$cmd" </dev/null || { echo "FAIL: step $n failed -- STOP (no further commands run)" >&2; exit 5; }
done
echo "OK (apply) -- verify with: rbd mirror pool info/status --verbose (per runbook Step 5/8 VERIFY)"