#!/usr/bin/env python3
"""
fix-api-bindings.py - surgical fix for the api-bindings phantom-endpoint defect.
Two text edits (line-based: does NOT round-trip YAML, so anchors, aliases,
comments and formatting are all preserved):
1. Shrink the &api-bindings anchor from 9 keys to the two that actually carry
meaning:
"": metal
public: provider
The seven dropped keys (admin/internal/shared-db/amqp/certificates/cluster/
ha) all mapped to 'metal', which is exactly what the "" default already
provides - and naming an endpoint a charm lacks is a hard `juju deploy`
error. Dropping them un-breaks keystone, ceph-radosgw and
openstack-dashboard, which each lack one or more of those endpoints, while
being functionally identical for the charms that have them.
2. Repoint `vault` from *api-bindings to *internal-bindings. Vault has no
`public` endpoint, so even the 2-key anchor would error on it; it is an
internal-only service and belongs on the metal-default anchor.
Usage: python3 fix-api-bindings.py [path-to-bundle.yaml] (default ./bundle.yaml)
Aborts WITHOUT writing if the bundle is not in the expected pre-fix shape
(already fixed, or structure changed). Creates a timestamped .bak, prints a
unified diff, and - if PyYAML is importable - verifies the resolved bindings.
"""
import sys, os, re, shutil, difflib, datetime
PATH = sys.argv[1] if len(sys.argv) > 1 else "bundle.yaml"
DROP = {"admin", "internal", "shared-db", "amqp", "certificates", "cluster", "ha"}
KEEP = {'""', "public"}
def abort(msg):
sys.stderr.write("ABORT (no changes written): %s\n" % msg)
sys.exit(1)
def indent_of(line):
return len(line) - len(line.lstrip())
if not os.path.isfile(PATH):
abort("file not found: %s (run from the repo root, or pass the path)" % PATH)
with open(PATH, "r", newline="") as fh: # newline="" -> keep original EOLs
orig = fh.readlines()
lines = list(orig)
# ---------- Edit 1: shrink &api-bindings ----------
anchor = None
for i, ln in enumerate(lines):
if re.match(r'^api-bindings:\s*&api-bindings$', ln.strip()):
anchor = i
break
if anchor is None:
abort("could not locate the 'api-bindings: &api-bindings' anchor line")
a_indent = indent_of(lines[anchor])
j = anchor + 1
kept, dropped = [], []
while j < len(lines):
raw = lines[j]
s = raw.strip()
if s == "" or indent_of(raw) <= a_indent:
break
key = s.split(":", 1)[0].strip()
if key in DROP:
dropped.append(key)
else:
kept.append(raw)
j += 1
block_end = j
kept_keys = {l.strip().split(":", 1)[0].strip() for l in kept}
if kept_keys != KEEP or set(dropped) != DROP:
abort("api-bindings block not in expected pre-fix shape "
"(kept=%s dropped=%s) - already fixed or bundle changed; inspect by hand"
% (sorted(kept_keys), sorted(dropped)))
lines = lines[:anchor + 1] + kept + lines[block_end:]
# ---------- Edit 2: vault -> *internal-bindings ----------
vault = None
for i, ln in enumerate(lines):
if ln.strip() == "vault:" and indent_of(ln) == 2:
vault = i
break
if vault is None:
abort("could not locate the ' vault:' application block")
fixed_vault = False
k = vault + 1
while k < len(lines):
raw = lines[k]
if raw.strip() and indent_of(raw) <= 2:
break
if re.match(r'^\s*bindings:\s*\*api-bindings\b', raw):
lines[k] = raw.replace("*api-bindings", "*internal-bindings", 1)
fixed_vault = True
break
k += 1
if not fixed_vault:
abort("did not find vault's 'bindings: *api-bindings' line "
"(already on internal-bindings, or block changed)")
# ---------- Collateral-damage guard ----------
if len(orig) - len(lines) != 7:
abort("unexpected line-count delta %d (expected exactly -7); aborting"
% (len(orig) - len(lines)))
# ---------- Backup + write ----------
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
bak = "%s.bak-%s" % (PATH, ts)
shutil.copy2(PATH, bak)
with open(PATH, "w", newline="") as fh:
fh.writelines(lines)
print("Backup written: %s" % bak)
print("=== unified diff ===")
sys.stdout.writelines(
difflib.unified_diff(orig, lines,
fromfile="bundle.yaml (before)",
tofile="bundle.yaml (after)"))
print("")
# ---------- Verify (best-effort; authoritative re-check is on the jumphost) ----------
try:
import yaml
except Exception:
print("NOTE: PyYAML not importable here - semantic verification skipped.")
print(" Re-verify on the jumphost after pull (it has PyYAML).")
sys.exit(0)
with open(PATH) as fh:
doc = yaml.safe_load(fh)
apps = doc["applications"]
MIN = {"": "metal", "public": "provider"}
INT = {"": "metal"}
on_min = ["keystone", "ceph-radosgw", "openstack-dashboard", "octavia",
"glance", "nova-cloud-controller", "placement", "neutron-api",
"cinder", "barbican", "magnum"]
print("=== verification ===")
print("YAML parses: PASS")
ok = True
for a in on_min:
got = apps.get(a, {}).get("bindings")
p = (got == MIN)
ok = ok and p
print(" %-22s -> minimal api-bindings : %s"
% (a, "PASS" if p else "FAIL %r" % (got,)))
gv = apps.get("vault", {}).get("bindings")
pv = (gv == INT)
ok = ok and pv
print(" %-22s -> internal-bindings : %s"
% ("vault", "PASS" if pv else "FAIL %r" % (gv,)))
print("")
print("RESULT:", "ALL CHECKS PASS"
if ok else "FAILURES - revert with: cp %s %s" % (bak, PATH))
sys.exit(0 if ok else 2)