#!/usr/bin/env bash
# scripts/dc-node-etchosts.sh -- per-DC cloudinit-userdata that gives every node a
# LOCAL /etc/hosts reverse mapping for its metal-internal address, so a charm that
# derives its TLS common_name from get_hostname(local_address) can resolve it even
# though the metal-internal plane is deliberately ISOLATED (no gateway, no route to
# the region resolver -- D-052). PROVEN CAUSE + FIX (2026-08-04, live):
#   * charm-ovn-central (rev 311) computes cert CN = get_hostname(its metal-internal
#     address); that address has NO reachable reverse-DNS resolver on the isolated
#     plane -> get_hostname() returns None -> empty common_name -> vault issues no
#     server cert -> ovn-central blocks "'certificates' awaiting server certificate
#     data" and the OVN NB/SB cluster never forms (6641/6642 never listen).
#   * MEASURED on the live dc0 model with a controlled test: adding
#     `<metal-internal-ip> <hostname>` to /etc/hosts on ONE unit made get_hostname
#     resolve, the charm published common_name, vault issued the per-unit server
#     cert, and /etc/ovn/{cert_host,key_host,ovn-central.crt} were written -- while
#     the two control units without the entry stayed broken. The entry is the sole
#     cause of the fix.
#
# This is a NEW mechanism. It BORROWS the SHAPE of D-008's static-/etc/hosts
# bootstrap but is NOT that: D-008's /etc/hosts is FQDN->VIP for os-public-hostname
# (unset in VR1); this supplies each node's OWN metal-internal address->name reverse
# mapping. rdns_mode=2 and the MAAS forward record already exist; only the reverse
# is unreachable from the isolated plane, which the local files entry supplies.
# OVN does not check the server-cert CN content and vault signs any non-empty CN
# (2026-08-04 research), so the name value only has to be non-empty and stable.
#
# DELIVERY: juju model-config cloudinit-userdata (model-wide, applied at machine
# provision). It MUST be set BEFORE `juju deploy` (a gated pre-deploy step in
# runbooks/phase-01-bundle-deploy.md / dc-dc-phase4). Scoped to the metal-internal
# CIDR (pinned per DC from lib-net.sh) so metal-admin et al. keep their DNS reverse
# untouched -- blast radius is exactly the plane that lacks reverse DNS.
#
# Usage: dc-node-etchosts.sh <render|apply|check> <site>
#   render  print the cloudinit-userdata YAML (pure; no juju) -- what the harness grades
#   apply   set it on the DC's juju model (juju client; run on the DC rack, D-138)
#   check   read it back and verify the postruncmd + this DC's metal-internal CIDR are present
# env: JUJU_MODEL (default: derived <site>, e.g. vr1-dc0) ; MODEL_ARG override
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODE="${1:-}"; SITE="${2:-}"
case "$MODE" in render|apply|check) ;; *)
  echo "usage: dc-node-etchosts.sh <render|apply|check> <site>   e.g. dc-node-etchosts.sh render vr1-dc0" >&2; exit 2 ;;
esac
[ -n "$SITE" ] || { echo "REFUSE: <site> required" >&2; exit 2; }

# --- derive THIS DC's metal-internal CIDR from lib-net (never typed) ---
# shellcheck source=/dev/null
. "$HERE/lib-net.sh"
lib_net_select_dc "$SITE" || { echo "REFUSE: lib_net_select_dc $SITE failed" >&2; exit 2; }
MI_CIDR=""
for c in "${PLANE_CIDRS[@]}"; do
  [ "${PLANE_NAME[$c]:-}" = "metal-internal" ] && { MI_CIDR="$c"; break; }
done
[ -n "$MI_CIDR" ] || { echo "REFUSE: no metal-internal plane in lib-net for $SITE" >&2; exit 2; }

MODEL="${JUJU_MODEL:-$SITE}"

# --- render the cloudinit-userdata YAML ---
# Built with printf, one line per statement, the CIDR passed as a %s ARGUMENT (the
# repo's heredoc-whitespace rule). The postruncmd is deterministic + idempotent: for each
# of the node's own IPv4 addresses that lies IN this DC's metal-internal CIDR, append
# `<ip> <hostname>.maas <hostname>` to /etc/hosts unless already present. The CIDR
# membership test uses python3 (present on every Ubuntu node image) so a /22 is handled
# exactly, not by fragile string-prefix matching.
render() {
  printf '%s\n' '#cloud-config'
  printf '%s\n' '# GENERATED by scripts/dc-node-etchosts.sh -- do not hand-edit the deployed value.'
  printf '%s\n' '# Gives each node a local /etc/hosts reverse mapping for its metal-internal'
  printf '%s\n' '# address so cert-CN-from-get_hostname resolves on the isolated plane (D-052).'
  # postruncmd, NOT runcmd: juju cloudinit-userdata REJECTS a top-level `runcmd`
  # (it owns runcmd for the machine-agent bootstrap: "runcmd not allowed, use
  # preruncmd or postruncmd instead", measured live 2026-08-04). postruncmd runs
  # AFTER juju's bootstrap, still long before the ovn-central cert hook. Do NOT
  # revert this to runcmd -- the harness T10 fails if you do.
  printf '%s\n' 'postruncmd:'
  printf '%s\n' '- - /bin/bash'
  printf '%s\n' '  - -c'
  printf '%s\n' "  - 'for ip in \$(hostname -I); do if python3 -c \"import ipaddress,sys; sys.exit(0 if ipaddress.ip_address(sys.argv[1]) in ipaddress.ip_network(sys.argv[2]) else 1)\" \"\$ip\" \"${MI_CIDR}\" 2>/dev/null; then grep -qw \"\$ip\" /etc/hosts || echo \"\$ip \$(hostname).maas \$(hostname)\" >> /etc/hosts; fi; done'"
}

case "$MODE" in
  render)
    render
    ;;
  apply)
    command -v juju >/dev/null 2>&1 || { echo "REFUSE: no juju client on this host (run apply on the DC rack, D-138)" >&2; exit 2; }
    UD="$(render)"
    echo "== applying cloudinit-userdata to model '$MODEL' (metal-internal ${MI_CIDR}) =="
    printf '%s\n' "$UD"
    juju model-config -m "$MODEL" "cloudinit-userdata=$UD" || { echo "FAIL: juju model-config set failed" >&2; exit 1; }
    echo "OK: set. VERIFY at provision time -- this affects machines created AFTER this point only."
    ;;
  check)
    command -v juju >/dev/null 2>&1 || { echo "REFUSE: no juju client on this host" >&2; exit 2; }
    GOT="$(juju model-config -m "$MODEL" cloudinit-userdata 2>/dev/null)"
    fail=0
    printf '%s' "$GOT" | grep -q "$MI_CIDR"     || { echo "FAIL: model cloudinit-userdata missing this DC's metal-internal CIDR $MI_CIDR"; fail=1; }
    printf '%s' "$GOT" | grep -q 'postruncmd'   || { echo "FAIL: model cloudinit-userdata missing postruncmd (juju rejects bare runcmd)"; fail=1; }
  printf '%s' "$GOT" | grep -q '/etc/hosts'   || { echo "FAIL: model cloudinit-userdata has no /etc/hosts postruncmd"; fail=1; }
    printf '%s' "$GOT" | grep -q 'ip_network'   || { echo "FAIL: model cloudinit-userdata missing the CIDR-membership guard"; fail=1; }
    [ "$fail" -eq 0 ] && { echo "OK: model '$MODEL' cloudinit-userdata carries the metal-internal /etc/hosts mapping for $MI_CIDR"; exit 0; } || exit 1
    ;;
esac
