#!/usr/bin/env python3
"""
fix-bundle-add-memcached.py (BUNDLEFIX-004, part 2)
Adds the `memcached` application AND the
`nova-cloud-controller:memcache <-> memcached:cache` relation to the Caracal
bundle, matching the live `juju deploy memcached` + `juju integrate` already
applied to the running model.
Why: nova-cloud-controller treats `memcache` as a required relation. The Caracal
rebuild omitted memcached entirely, so a fresh `juju deploy` of the bundle would
leave nova-cc blocked on "Missing relations: memcache" (no instance scheduling).
App block added (placement to: [lxd:8] = openstack0, where it landed live; metal
space; latest/stable, the only stable channel for the memcached charm):
memcached:
charm: memcached
channel: latest/stable
num_units: 1
to: [lxd:8]
bindings: *internal-bindings
constraints: arch=amd64
Relation added:
- [nova-cloud-controller:memcache, memcached:cache]
Safe by construction: line edits (preserve anchors/comments/formatting),
timestamped .bak, unified diff, idempotent, yaml.safe_load verification.
Usage: python3 fix-bundle-add-memcached.py [path/to/bundle.yaml] (default ./bundle.yaml)
"""
import sys, os, difflib, datetime
DEFAULT = "bundle.yaml"
APP_BLOCK = [
"",
" # memcached: nova-cloud-controller token/cell caching (BUNDLEFIX-004)",
" memcached:",
" charm: memcached",
" channel: latest/stable",
" num_units: 1",
" to: [lxd:8]",
" bindings: *internal-bindings",
" constraints: arch=amd64",
"",
]
RELATION_LINE = " - [nova-cloud-controller:memcache, memcached:cache]"
def main():
path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT
if not os.path.isfile(path):
print(f"[ABORT] not found: {path}")
return 2
original = open(path, encoding="utf-8").read()
lines = original.splitlines()
have_app = any(l.strip().startswith("memcached:") for l in lines)
have_rel = "memcached:cache" in original
if have_app and have_rel:
print("[OK/IDEMPOTENT] memcached app and relation already present; no change.")
return 0
if have_app != have_rel:
print(f"[ABORT] partial state (app={have_app}, relation={have_rel}); fix by hand to avoid duplication.")
return 3
# Bundle order here is description -> variables -> machines -> applications -> relations,
# so `relations:` is the END of the applications section. Anchor BOTH inserts to it:
# the app block goes immediately before `relations:` (last app), the relation immediately after.
rel_idx = next((i for i, l in enumerate(lines) if l.rstrip() == "relations:"), None)
if rel_idx is None:
print("[ABORT] could not find top-level 'relations:' key.")
return 4
out = []
for i, l in enumerate(lines):
if i == rel_idx:
out.extend(APP_BLOCK) # app block: end of applications (just before relations:)
out.append(l)
if i == rel_idx:
out.append(RELATION_LINE) # relation: first entry after relations:
new = "\n".join(out) + ("\n" if original.endswith("\n") else "")
print("=== unified diff ===")
print("\n".join(difflib.unified_diff(
original.splitlines(), new.splitlines(),
fromfile=f"{path} (orig)", tofile=f"{path} (new)", lineterm="")))
try:
import yaml
d = yaml.safe_load(new)
a = d["applications"]
rels = d.get("relations", [])
assert "memcached" in a, "memcached app missing after edit"
assert a["memcached"].get("charm") == "memcached", "charm != memcached"
assert a["memcached"].get("bindings") == {"": "metal"}, f"bindings={a['memcached'].get('bindings')}"
mc = [r for r in rels if any("memcache" in str(x) for x in r)]
assert mc, "memcache relation missing after edit"
print(f"[VERIFY] OK: memcached app present, bindings {{'': 'metal'}}, relation {mc}")
print(f"[VERIFY] totals now: apps={len(a)} relations={len(rels)}")
except ImportError:
print("[WARN] PyYAML missing; skipped semantic verify (re-verify on jumphost after pull).")
except Exception as e:
print(f"[ABORT] verification failed: {e}")
return 5
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
bak = f"{path}.bak-{ts}"
open(bak, "w", encoding="utf-8").write(original)
open(path, "w", encoding="utf-8").write(new)
print(f"[WROTE] {path} (backup: {bak})")
return 0
if __name__ == "__main__":
sys.exit(main())