Newer
Older
openstack-caracal-dc-dc / tests / provider-bundle-check / run-tests.sh
#!/usr/bin/env bash
# tests/provider-bundle-check/run-tests.sh
#
# Offline regression harness for scripts/provider-bundle-check.py. No live infra;
# fixtures are generated from the repo bundle by targeted mutation, so every check
# is proven to FAIL when its invariant is violated (bash -n / "it parsed" proves
# nothing -- behavior is the contract). Mutates NOTHING outside $TMP.
# Exit: 0 all pass | 1 any case failed.  ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/../.." && pwd)"
CHK="$REPO/scripts/provider-bundle-check.py"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PASS=0; FAIL=0

# fixture base: real bundle + the policies dir the DOCFIX-071 check resolves
mkdir -p "$TMP/policies"
cp "$REPO/bundle.yaml" "$TMP/good.yaml"
cp "$REPO/policies/domain-manager-policy.yaml" "$REPO/policies/overrides.zip" "$TMP/policies/"

mutate() { # mutate <out> <python-expr operating on dict b>
  python3 - "$TMP/good.yaml" "$TMP/$1" "$2" <<'PY'
import sys, yaml
src, dst, expr = sys.argv[1], sys.argv[2], sys.argv[3]
b = yaml.safe_load(open(src))
exec(expr)
yaml.safe_dump(b, open(dst, "w"))
PY
}

run() { # run <want_rc> <regex> <label> <bundle>
  local want="$1" rx="$2" label="$3" f="$4"
  local out rc
  out="$(python3 "$CHK" "$f" 2>&1)"; rc=$?
  if [[ "$rc" == "$want" ]] && grep -qE "$rx" <<<"$out"; then
    echo "  PASS  $label"; PASS=$((PASS+1))
  else
    echo "  FAIL  $label (rc=$rc want=$want)"; echo "$out" | sed 's/^/        /' | head -6; FAIL=$((FAIL+1))
  fi
}

# T1 happy path: the repo bundle itself must PASS
run 0 'PASS: Pattern A' "T1 repo bundle passes clean" "$TMP/good.yaml"

# T2 relation referencing a ghost app
mutate t2.yaml 'b["relations"].append(["ghost-app:amqp","rabbitmq-server:amqp"])'
run 1 'unknown app' "T2 ghost relation app FAILS" "$TMP/t2.yaml"

# T3 endpoint-less relation side (the missing-colon class)
mutate t3.yaml 'b["relations"].append(["keystone","rabbitmq-server:amqp"])'
run 1 'lacks explicit :endpoint' "T3 endpoint-less relation FAILS" "$TMP/t3.yaml"

# T4 D-062 regression: mysql at 1 unit
mutate t4.yaml 'b["applications"]["mysql-innodb-cluster"]["num_units"]=1'
run 1 'D-062 requires 3' "T4 mysql num_units=1 FAILS" "$TMP/t4.yaml"

# T5 shared VIP octet between two apps
mutate t5.yaml 'b["applications"]["glance"]["options"]["vip"]="10.12.4.50 10.12.8.50 10.12.12.50"'
run 1 'shared by' "T5 duplicate VIP octet FAILS" "$TMP/t5.yaml"

# T6 DOCFIX-071 regression: keystone resource stanza removed
mutate t6.yaml 'b["applications"]["keystone"].pop("resources",None)'
run 1 'DOCFIX-071' "T6 missing policyd resource FAILS" "$TMP/t6.yaml"

# T7 DOCFIX-071 drift: zip content differs from the yaml source
mutate t7.yaml 'pass'
printf '\n# drift\n' >> "$TMP/policies/domain-manager-policy.yaml"
run 1 'DIFFERS from policies' "T7 zip/source drift FAILS" "$TMP/t7.yaml"
cp "$REPO/policies/domain-manager-policy.yaml" "$TMP/policies/"   # restore

# T8 original invariant still guarded: octavia VIP off-band
mutate t8.yaml 'b["applications"]["octavia"]["options"]["vip"]="10.12.4.233 10.12.8.233 10.12.12.233"'
run 1 'outside 50-60' "T8 off-band VIP octet FAILS" "$TMP/t8.yaml"

# ---- Placement / anti-affinity checks (only fire on a role-separated bundle) ----
# Build a valid role-separated fixture from the base: 3 control / 2 compute / 4 storage,
# quorum trio + control containers on control, ceph-osd on storage, nova-compute on compute.
python3 - "$TMP/good.yaml" "$TMP/rolesep.yaml" <<'PY'
import sys, yaml
b = yaml.safe_load(open(sys.argv[1]))
roles = ["control","control","control","compute","compute","storage","storage","storage","storage"]
b["machines"] = {str(i): {"constraints": "arch=amd64 tags=openstack-vr1-dc0,%s" % r}
                 for i, r in enumerate(roles)}
for n, s in b["applications"].items():
    if not isinstance(s, dict) or "to" not in s:
        continue
    if   n == "ceph-osd":     s["num_units"] = 4; s["to"] = ["5","6","7","8"]
    elif n == "nova-compute": s["num_units"] = 2; s["to"] = ["3","4"]
    elif n in ("mysql-innodb-cluster","ovn-central","ceph-mon"): s["to"] = ["lxd:0","lxd:1","lxd:2"]
    else:                     s["to"] = ["lxd:0"]
yaml.safe_dump(b, open(sys.argv[2], "w"))
PY

mutate_rs() { python3 - "$TMP/rolesep.yaml" "$TMP/$1" "$2" <<'PY'
import sys, yaml
b = yaml.safe_load(open(sys.argv[1])); exec(sys.argv[3]); yaml.safe_dump(b, open(sys.argv[2], "w"))
PY
}
runargs() { # runargs <want_rc> <regex> <label> -- <checker args...>
  local want="$1" rx="$2" label="$3"; shift 3
  local out rc; out="$(python3 "$CHK" "$@" 2>&1)"; rc=$?
  if [[ "$rc" == "$want" ]] && grep -qE "$rx" <<<"$out"; then
    echo "  PASS  $label"; PASS=$((PASS+1))
  else
    echo "  FAIL  $label (rc=$rc want=$want)"; echo "$out" | sed 's/^/        /' | head -6; FAIL=$((FAIL+1))
  fi
}

# T9 valid role-separated bundle PASSES (placement active, all invariants hold)
run 0 'counts OK' "T9 valid role-separated bundle passes" "$TMP/rolesep.yaml"

# T10 dangling to: (undefined machine)
mutate_rs t10.yaml 'b["applications"]["glance"]["to"]=["lxd:99"]'
run 1 'undefined machine' "T10 dangling to: FAILS" "$TMP/t10.yaml"

# T11 off-role: ceph-osd on control/compute instead of storage
mutate_rs t11.yaml 'b["applications"]["ceph-osd"]["to"]=["0","1","2","3"]'
run 1 'off-role .want storage' "T11 ceph-osd off-role FAILS" "$TMP/t11.yaml"

# T12 anti-affinity defeat: a 3-unit app stacked on 2 machines
mutate_rs t12.yaml 'b["applications"]["mysql-innodb-cluster"]["to"]=["lxd:0","lxd:0","lxd:1"]'
run 1 'anti-affinity defeat' "T12 stacked control units FAIL" "$TMP/t12.yaml"

# T13 wrong bare-metal count: ceph-osd != number of storage nodes
mutate_rs t13.yaml 'b["applications"]["ceph-osd"]["num_units"]=3'
run 1 '!= 4 storage node' "T13 ceph-osd count mismatch FAILS" "$TMP/t13.yaml"

# T14 overlay merge + DC bands: base + dc1 VIPs, --dc vr1-dc1 PASSES
runargs 0 'PASS: Pattern A' "T14 dc1-vips overlay + --dc vr1-dc1 passes" \
  "$TMP/good.yaml" --overlay "$REPO/overlays/vr1-dc1-vips.yaml" --dc vr1-dc1

# T15 same overlay but default (dc0) bands -> dc1 VIPs off-band FAIL
runargs 1 'not in 10.12.4' "T15 dc1 VIPs vs dc0 bands FAILS" \
  "$TMP/good.yaml" --overlay "$REPO/overlays/vr1-dc1-vips.yaml"

echo
echo "RESULT: PASS=$PASS FAIL=$FAIL"
[[ "$FAIL" -eq 0 ]] && { echo "ALL PASS"; exit 0; } || exit 1