Newer
Older
openstack-caracal-dc-dc / .claude / skills / openstack-cloud-ops / SKILL.md

name: openstack-cloud-ops

description: "Operate, install, extend, and troubleshoot the Omega Cloud - a commercial multi-tenant Charmed OpenStack (Caracal 2024.1) deployment managed with Juju and MAAS, with Vault TLS, OVN, Ceph, Octavia, and Magnum/CAPI tenant Kubernetes. Use this skill for ANY work touching OpenStack, Juju, MAAS, Magnum, CAPI, Ceph, OVN, Octavia, Keystone, Vault-for-OpenStack, tenant onboarding, or the repository this skill operates against - including writing or reviewing bash/python operational scripts, debugging failed deploys or cluster creates, runbook work, design-decision (D-NNN) discussion, and incident triage. Use it even for seemingly simple OpenStack questions: this deployment has strict operating discipline and known charm traps that make generic answers wrong."

openstack-cloud-ops

Operating skill for the Omega Cloud: a commercial, multi-tenant, tenant self-administered OpenStack cloud, built as virtual rehearsals (VR0 single-DC "testcloud"; VR1 two-DC + Office1 headend) for a future bare-metal multi-datacenter deployment ("Roosevelt"). The CURRENT phase/stage lives in repo docs/CURRENT-STATE.md ONLY (GA-R1) -- never trust a phase claim from this file or memory. The governing design constraint is MINIMIZE DELTA TO ROOSEVELT: the runbooks and scripts are primary deliverables alongside the running cloud, so transferable answers beat quick fixes.

Step 0 - locate the source of truth

This repository (GitBucket, git.baldurkeep.com) is authoritative for everything: bundle, runbooks, scripts, design decisions, as-built values. This skill is a discipline-and-routing layer OVER that repo, not a substitute for it. Never hardcode the repo's own name/path in a command -- it has already been renamed/forked once (see D-110) and will again; discover it fresh each session.

  1. Look for a local clone: a repo dir in the working tree, or a $HOME entry containing bundle.yaml + docs/design-decisions.md. If found, confirm identity with git remote -v, then git log -1 to note HEAD and work from it.
  2. No clone and you have shell + network: ask the operator for the exact clone URL (org/group and repo name -- do not guess or reuse one from memory; it may have moved, per this same repo's own history). The repo may be private; if the clone fails, ask the operator to provide access or the relevant files.
  3. No clone obtainable (e.g. chat without sandbox network): say so, ask the operator to paste the relevant runbook/script, and proceed only on what is actually in front of you.

Divergence rule: if this skill and repo HEAD disagree, the repo wins - but FLAG the divergence to the operator rather than silently following either. The repo is a living draft; this skill's invariants (discipline, hardening) change slowly, its facts (IPs, versions, phase status) go stale fast.

Session bookends (GA-R1/GA-R4). OPEN: read docs/CURRENT-STATE.md in full BEFORE any other doc (it is the only status authority), then docs/session-ledger.md

  • bash scripts/ledger-scan.sh to recover what is in flight and reconcile it against repo ground truth (open decisions, open SEC count, next-free numbers). Re-do this after any compaction. CLOSE: a bounded ledger summary (15-line hard cap, GA-R4 rule 1); the full session body goes to docs/archive/ in the same close commit; commit and push -- a session ending without its bookend is treated as NOT durable by the next session. See references/operating-discipline.md (Session continuity).

    Step 1 - detect the environment

  • Live shell to the jumphost / infra (Claude Code on vcloud or similar): you may RUN read-only audits directly. Every mutation remains individually human-gated - present the command, state what it changes, wait for approval. A live shell relaxes the transport, never the discipline.

  • Chat / no infra shell: operate the gated copy-paste model - prepare labeled blocks, the operator runs them and pastes output back. Never assume a block ran or succeeded; wait for the pasted evidence.

Read references/operating-discipline.md before doing either.

The three hard operating rules (non-negotiable)

  1. Execute only the current runbook step, exactly as written. No added scope, no adjacent improvements, no live re-architecture mid-step. Findings and improvement ideas are LOGGED (changelog / D-NNN proposal), never executed live mid-step.
  2. Never use an inferred value. No IP, ID, name, or scope goes into a command unless it was measured this session or carried from confirmed as-built. If a value would be inferred: stop and measure it. Never run a destructive or session-altering command from memory without confirming it is the minimal correct action for the current live state.
  3. Prefer dynamic lookups over hardcoded literals. Discover VIPs, project names, IDs, and version sets at runtime. Where a literal is unavoidable it is tagged and centralized (scripts/lib-net.sh, lib-hosts.sh), keyed by stable identity (CIDR, hostname) - never by drifting IDs.

Corollary that governs everything: verify before mutate. A read-only audit precedes every mutation; destructive and secret-handling steps are gated individually, never batched.

Routing - where to go for what

Task Read first
Any command block, script, or paste block you are about to write references/script-authoring.md
Deploy / redeploy / teardown (VR0, single-DC testcloud) repo runbooks/README.md, then the phase-NN runbook; conventions in references/operating-discipline.md
VR1 DC-DC buildout (two-DC + Office1 headend -- the current live mission) repo docs/CURRENT-STATE.md FIRST (stage/gate STATUS lives there only), then docs/dc-dc-deployment-workflow.md (stage identity, links, gap register), then runbooks/README.md's "VR1 DC-DC track" section for the dc-dc-phase0..6 runbooks + dc-dc-teardown-rollback.md. SEPARATE track from VR0's phase-NN above -- do not conflate stage numbers between the two.
Something is broken (triage, incidents) references/troubleshooting.md, then repo runbooks/appendix-A-troubleshooting.md
CAPI / Magnum / mgmt-VM recovery repo runbooks/ops-capi-recovery.md
Deliver ANY repo change (script, runbook, doc) run bash scripts/repo-lint.sh + the touched script's tests/<name>/run-tests.sh BEFORE handing it over
Pre-deploy gate (before add-model) bash scripts/preflight.sh -- THE single entry; do not run the sub-gates piecemeal
Is the cloud actually healthy? (post-deploy, post-restart, pre-change baseline, incident) bash scripts/cloud-assert.sh (add --capture at deploy completion for the committed BOM)
Full-cloud restart after outage/maintenance repo runbooks/ops-restart-procedure.md
VR1 site unreachable after a vcloud reboot (ssh voffice1 / office1-* time out) bash scripts/site-baseleg.sh check office1 -- office1-local is an ISOLATED libvirt net, so the host's base L3 leg (10.10.0.10/24 + compose route 10.10.1.0/24 via 10.10.0.20) is dropped every reboot; the After=libvirtd unit re-adds it (D-126 amendment / D-128). If MISS and the unit is not yet installed: sudo bash scripts/site-baseleg.sh apply office1. This is the vcloud path; the workstation reaches the same over the tailnet (D-107).
Starting any consequential live session bash scripts/run-logged.sh <label> first (as-executed log; docs/as-executed-log-convention.md)
Credential exposures / security TODOs repo docs/security-ledger.md -- add a row, never only a script comment
Resuming work / what is still in flight (esp. after compaction) repo docs/CURRENT-STATE.md first, then docs/session-ledger.md + bash scripts/ledger-scan.sh to reconcile against repo ground truth
Tenant onboarding / tenant self-service repo scripts/tenant-onboard.sh + runbooks/tenant-onboarding-v2-DRAFT.md + appendix-C/D
Network / plane / IPAM questions references/environment.md, repo scripts/lib-net.sh, NetBox (the IPAM apex -- but during VR1 the WORKING apex is office1-netbox, 10.10.1.10, per DOCFIX-195; netbox.baldurkeep.com is a READ-ONLY v1 reference, write-back deferred to end-of-deployment)
ANY change request to a built surface grep repo docs/design-decisions.md for the governing D-NNN FIRST - PROPOSED/OPEN means the operator has not ruled: present options, do not implement
Why is it built this way? / proposing changes repo docs/design-decisions.md (D-NNN); grep before assigning a new number
Versions, channels, pins repo docs/CURRENT-STATE.md section 7 (VR1 substrate -- measured pin authority); runbooks/appendix-B-asbuilt-version-lock.md (VR0 charm channels)
Environment facts (hosts, repo, planes) references/environment.md
OpenTofu / libvirt / MAAS / OPNsense IaC work (repo opentofu/) references/opentofu-provider-docs.md FIRST (sources + fetch method), then repo opentofu/README.md (per-module ground truth, schema notes, audit findings -- but its OPNsense section is SUPERSEDED by D-112/D-113)
Creating/changing a libvirt_domain; anything about nested KVM, a libvirt storage pool, LXD versions, an OPNsense edge, or a VM that boots wrong references/platform-traps.md -- per-tool traps + version pins + a verbatim-error index (KiB-vs-MiB memory, acpi=off, missing cpu block = no svm, "in-place" bounces the guest, AppArmor on non-default pools, MAAS-vs-LXD 6.7, lxdbr0's dnsmasq, tcsh on the edge)

Standard loops (repeatable session shapes)

Session bootstrap (jumphost): Claude Code sessions launch from the repo root, always (GA-R7 rule 6 -- a session launched from a subdirectory gets its own empty project dir and a divergent auto-memory; the GA-F06 origin). git -C "$REPO" pull (set $REPO once per session -- see runbooks/README.md Conventions / references/environment.md) -> bash scripts/repo-lint.sh (0 fail expected) -> if touching the live cloud, bash scripts/run-logged.sh <label> to open the logged shell. Repo HEAD and a clean lint are the preconditions for everything else.

VR1 operating model (D-128 -- invariant; do not re-derive this each session): Claude runs ON the vcloud jumphost -- do NOT relocate the session to a workstation. Work splits in two, origins FIXED BY DESIGN: Plane 1 (substrate -- the OUTER tofu root qemu:///system + local virsh, creating the DC host VMs) runs on vcloud and has no Roosevelt analog; Plane 2 (MAAS / NetBox / the INNER tofu root qemu+ssh-from-Office1 / tenant tests) EXECUTES on voffice1 via ssh voffice1 '...' so runbooks read "from the Office1 headend" and transfer. Claude's host only picks the TRANSPORT, not where work runs. Durable Office1 reach = the D-126 base-leg oneshot (scripts/site-baseleg.sh check office1 FIRST if ssh voffice1 is dead post-reboot) -- NOT tailscale-on-vcloud (D-107 is the operator's WORKSTATION path / human GUI, left untouched). DCs sit outside the /22 carve and keep the D-126 -J-through-vvr1-dc0 shape. Durable host->site reach is standing per-SITE tooling, DCs included (so the gap does not reopen per-DC): site-baseleg.sh is site-keyed. At each DC standup, MEASURE how vcloud reaches that DC's containment/ service net -- if it is an ISOLATED host-local net, add a MEASURED leg row + install the unit as part of standup definition-of-done (NEVER an inferred DC IP -- the harness fails on an un-commented DC supernet); a routed/NAT net or an already-reachable qemu+ssh path needs none. Register item 20 tracks this. **Rack-side twin invariant (D-131 sub-1, RULED 2026-07-21): every internet-isolated DC rack gets the repo-carried scripts/dc-rack-net.sh install <site> at standup (reboot-persistent rack bridge legs

  • the node-DNS dnsmasq forwarder aliased on metal-admin, forwarding to region BIND over the rack's own transit) -- MAAS 3.7's rack-only agent resolver SERVFAILs on isolated racks, so WITHOUT this, commissioning silently times out (appendix-A carries the symptom pair).** Two measured MAAS traps with standing consequences: MAAS virsh PODS are refuted for this repo's DCs (per-machine power_type=virsh via scripts/maas-node-power.sh is the ruled shape -- D-103/D-123 amendments 2026-07-20), and a pod delete CASCADES to its linked machine records -- read the pod's machine list BEFORE any vm-host delete; non-empty = STOP (appendix-A, 2026-07-21 incident).

Change-delivery loop: grep for prior art (zeroth decision) -> grep design-decisions for the governing D-NNN -> edit -> bash scripts/repo-lint.sh -> run the touched script's harness (create one if missing -- no script change ships without its harness) -> deliver with an entry in the SESSION changelog (ONE changelog per session, GA-R2/D1 -- never per-topic files) carrying a per-item revert. Under blanket approval, the changelog IS the review surface: every item states what, why (evidence), and how to revert. Changelogs are session-scoped scratch: consolidated per-stage at stage close, NEVER citable as status or decision authority.

Deploy loop (VR0, single-DC testcloud): phase-00 runbook (D-061 destroy path) -> bash scripts/preflight.sh PASS -> phase-01..08 gated -> bash scripts/cloud-assert.sh --capture -> commit the asbuilt/ BOM.

Deploy loop (VR1 DC-DC, the current live mission): read docs/dc-dc-deployment-workflow.md's tooling gap register FIRST -- check whether the stage you're about to run has any open blocking item -> Stage 1 (dc-dc-phase0-vcloud-prep.md) gated -> Stage 2..7 gated in order, per runbooks/README.md's "VR1 DC-DC track" table -> if a stage's tofu apply fails partway, read runbooks/dc-dc-teardown-rollback.md's rollback decision tree BEFORE reaching for a teardown (fix-forward is usually correct). Run bash scripts/opentofu-validate.sh before any tofu step; whether a given root/step has been exercised for real is a STATUS question -- check CURRENT-STATE, never this file's memory of it. At STAGE CLOSE (GA-R6): a stage or verification-type gate CLOSES only on a NAMED executable check whose captured output the closing commit cites -- prose cannot close a stage, and there is NO conditional close (a remainder splits into its own gate row or the stage stays open, E3). Ruling-type gates close per GA-R5 (recorded, pushed ruling). The close-out set is executable: gauntlet ALL GREEN, repo-lint 0-fail, the stage's GA-R2 consolidation commit landed, the GA-R7 memory review run, MERGE the stage branch to main (merge commit, NOT squash; operator-gated), then RETIRE the branch (local + remote). ALSO sweep THIS skill in the same close: fold in any new INVARIANT the stage established and reconcile any divergence from repo HEAD (facts delegate to the repo and go stale; invariants live here and must be kept current). The next stage branches off post-merge main. main is trunk-of-record only for stages that have merged (precedents: Stage 2 -> PR #1 e468d2e 2026-07-15; Stage 3 -> merge commit 2026-07-21, vr1-dc0 scope, dc1 HELD under G12). See docs/dc-dc-deployment-workflow.md "Cross-cutting discipline".

Incident loop: capture the verbatim error -> bash scripts/cloud-assert.sh (the service-own-verdict sweep localizes the layer) -> appendix-A by exact message -> recorded fix, gated -> log the finding (new root causes become appendix-A/DOCFIX material).

Record authority and session discipline (GA-R1..R7, ratified 2026-07-18)

Ratified text: repo docs/audit/ga-rulings.md -- this is the operating summary.

  • docs/CURRENT-STATE.md is the ONLY status authority (GA-R1). Stage, gates, plan counts, version pins, decision-queue state live THERE and nowhere else; other surfaces point or are history. Any commit that changes a status it carries updates it in the SAME commit (C1; repo-lint L10 enforces). It is authoritative over documents, never over measurement -- a fresh capture wins and corrects it in the capture's commit (C2).
  • Plan counts and versions are quoted only from captured command output -- a dated capture file or a quoted command + output; never from memory or another document's prose (GA-R1 rule 2). Legal stage/gate status tokens are OPEN / BLOCKED / HELD / CLOSED, used only in CURRENT-STATE (vocabulary ruling, Option A: readiness is not a state; event words are history, never states).
  • Rulings are gated like mutations (GA-R5). ONE decision per operator exchange -- batch adoptions are invalid. A ruling exists only when its Status line quotes the question AND the operator's exact utterance, dated, committed and pushed before dependent work. Ambiguous/template answers rule NOTHING: stop and ask. Never adopt from an inferred or reconstructed ruling -- an operator ruling is a value under hard rule 2.
  • D-number admission (GA-R3): a new D-number needs architectural consequence beyond the stage, a Roosevelt-delta (the A1 test: a Roosevelt build session would grep it before touching a built surface), or supersession. Everything else is OPERATIONAL: runbook edit / session-changelog line / as-built row, never a D-number. Entries carry [ARCH]/[OPS] tags; doubt resolves DOWN to OPS.
  • Memory governance (GA-R7): auto-memory records observations and conventions ONLY -- never operator policy, priorities, or postures. Any memory claiming operator intent is PROPOSED until confirmed verbatim; remove on discovery. Durable observations GRADUATE to their repo surface; memory is never the only copy. Stage close includes a full memory review (rules 1-4).
  • Reconciliation circuit-breaker. If you have been fixing records to match records for more than one cycle without touching the actual deliverable, STOP and report to the operator. Record-churn presenting as progress is this project's measured failure mode (GA-F06); escalating is the correct move.

Posture

  • This is a commercial multi-tenant cloud with HARD tenant isolation (SCS Domain Manager persona). Treat tenant-visible surfaces and cross-domain boundaries as security-relevant in every change.
  • The operator community here values debate and industry best practice over quick fixes. Push back with sources when you disagree; own mistakes plainly and concisely. Fabricated flags, values, or version numbers are the cardinal sin - if you have not verified an option name or version, say so and verify.
  • Responses stay concise. Decisions get explicit rationale.