Newer
Older
openstack-caracal-ipv4 / scripts / lib-validate.sh
# scripts/lib-validate.sh
#
# Shared library for the D-011 validation check suite (scripts/checks/*.sh) and
# the orchestrator (scripts/validate.sh). SOURCED, never executed. ASCII + LF.
#
# WHY THIS EXISTS: every check needs the same env hygiene, structured-output
# capture, and result vocabulary. Authoring them once here (and harnessing them
# once) is the difference between "modular and consistent" and "modular and each
# piece re-implements -- and re-breaks -- the same env/parse bugs".
#
# THE STANDARD EXIT CONTRACT (every check script and the orchestrator conform):
#   0 PASS               all assertions held
#   1 FAIL               a real failure -- the thing under test is broken
#   2 HOLD               could not determine (missing scope/precondition/headroom);
#                        NOT a pass and NOT a failure -- operator must resolve
#   3 PASS_PENDING_MANUAL a manual/attestation item is outstanding but all else green
#   4 SKIPPED            deliberately not run (e.g. disruptive path not requested)
#
# Checks emit ONE machine-readable result line via `emit`, then exit with the
# matching code. The orchestrator parses those lines; humans read them too.
#
# shellcheck shell=bash
# shellcheck disable=SC2034  # constants consumed by sourcing scripts

if [ "${BASH_SOURCE[0]:-}" = "${0}" ]; then
  echo "lib-validate.sh is a sourced library; do not run it directly." >&2
  exit 2
fi

# --- exit contract constants (use these names, never bare integers) ----------
readonly VR_PASS=0 VR_FAIL=1 VR_HOLD=2 VR_MANUAL=3 VR_SKIP=4
# map code -> word (for reporting); index by code.
VR_WORD=( [0]=PASS [1]=FAIL [2]=HOLD [3]=PASS_PENDING_MANUAL [4]=SKIPPED )

# --- result emission ---------------------------------------------------------
# emit <check-id> <code> <message>
# Prints a single parseable line: "RESULT <id> <WORD> <code> <elapsed> <message>".
# _VR_T0 (epoch seconds) is set by vr_begin; elapsed is "-" if unset.
emit() {
  local id="$1" code="$2"; shift 2
  local word="${VR_WORD[$code]:-UNKNOWN}" el="-"
  if [ -n "${_VR_T0:-}" ]; then el="$(( $(date +%s) - _VR_T0 ))s"; fi
  printf 'RESULT %s %s %s %s %s\n' "$id" "$word" "$code" "$el" "$*"
}
# vr_begin <check-id>: start the timer + announce (checks call this first).
vr_begin() { _VR_ID="$1"; _VR_T0="$(date +%s)"; echo "== check $_VR_ID =="; }

# --- structured-output capture (DOCFIX-085: NEVER merge stderr into JSON) -----
# vr_json <var> <cmd...>: run cmd, capture STDOUT into <var>, stderr to $_VR_ERR.
# Returns the command's exit status. Caller checks status AND that <var> parses.
_VR_ERR=""
vr_json() {
  local __var="$1"; shift
  _VR_ERR="$(mktemp)"
  local __out __rc
  __out="$("$@" 2>"$_VR_ERR")"; __rc=$?
  printf -v "$__var" '%s' "$__out"
  return $__rc
}
# vr_err_tail: print (indented) the last stderr captured by vr_json, if any.
vr_err_tail() { [ -s "${_VR_ERR:-/dev/null}" ] && sed 's/^/    stderr: /' "$_VR_ERR"; return 0; }

# --- run(): capture-then-test mutation helper (DOCFIX-082 regime) ------------
# run <cmd...>: run with stdin closed, capture combined output, print it
# indented, return the command's status. NO pipeline on the mutating command
# (so status is the command's, not a downstream tee/sed -- SIGPIPE-safe).
run() {
  local __out __rc
  __out="$("$@" </dev/null 2>&1)"; __rc=$?
  [ -n "$__out" ] && printf '%s\n' "$__out" | sed 's/^/    /'
  return $__rc
}

# --- env hygiene: scrub then scope (subshell callers only) --------------------
vr_scrub_os() { local v; for v in $(env | awk -F= '/^OS_/{print $1}'); do unset "$v"; done; }
# vr_admin_env: clean admin scope from ~/admin-openrc. Returns 2 if absent.
vr_admin_env() {
  vr_scrub_os
  [ -r "$HOME/admin-openrc" ] || { echo "HOLD: ~/admin-openrc not readable" >&2; return 2; }
  # shellcheck source=/dev/null
  . "$HOME/admin-openrc"; return 0
}
# vr_tenant_env <cred-file>: clean tenant password scope from a cred file
# (username=/user_domain_id=/project_id=/password=/auth_url= lines). Returns 2 if unusable.
vr_tenant_env() {
  local cf="$1"; vr_scrub_os
  [ -s "$cf" ] || { echo "HOLD: tenant cred $cf missing" >&2; return 2; }
  local au un ud pid pw
  au="$(awk -F= '/^auth_url=/{print $2}'  "$cf")"; un="$(awk -F= '/^username=/{print $2}' "$cf")"
  ud="$(awk -F= '/^user_domain_id=/{print $2}' "$cf")"; pid="$(awk -F= '/^project_id=/{print $2}' "$cf")"
  pw="$(awk -F= '/^password=/{print $2}' "$cf")"
  [ -n "$au" ] && [ -n "$un" ] && [ -n "$pid" ] && [ -n "$pw" ] || { echo "HOLD: $cf missing fields" >&2; return 2; }
  export OS_AUTH_URL="$au" OS_IDENTITY_API_VERSION=3 OS_USERNAME="$un" \
         OS_USER_DOMAIN_ID="$ud" OS_PROJECT_ID="$pid" OS_PASSWORD="$pw" \
         OS_CACERT="${OS_CACERT:-$HOME/vault-init/vault-ca-root.pem}"
  return 0
}

# --- small validators (consistent, tested) -----------------------------------
vr_is_hex32() { [[ "${1:-}" =~ ^[0-9a-f]{32}$ ]]; }
vr_is_ipv4()  { [[ "${1:-}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; }
# vr_need <tool...>: HOLD (return 2) if any tool is missing on PATH.
vr_need() { local t; for t in "$@"; do command -v "$t" >/dev/null || { echo "HOLD: missing tool: $t" >&2; return 2; }; done; return 0; }

# --- disruptive gate ---------------------------------------------------------
# vr_disruptive_ok: true only if the check was invoked with --disruptive (or
# VR_DISRUPTIVE=1 passed by the orchestrator's --include-disruptive). Checks call
# this to guard their destructive path and SKIP (return 4) otherwise.
vr_disruptive_ok() { [ "${VR_DISRUPTIVE:-0}" = "1" ]; }