Newer
Older
openstack-caracal-ipv4 / bundle.yaml
# ============================================================
# Caracal 2024.1 -- VR0 DC0 Omega Cloud testcloud rebuild bundle
# ============================================================
# Generated:        2026-05-22 (rebuild revision 2026-06-01, bundle-cleanup change-set)
# Replaces:         bundle-pre-destroy.yaml (Bobcat 2023.2)
# Charm channels:   verified against Charmhub 2026-05-22 (see Caracal_Rebuild handoff D-002)
# Bindings:         public:provider, else:metal for API charms; all-metal for backend charms.
#                   Ceph data nets via public/cluster BINDINGS on ceph-mon/ceph-osd (these provision the
#                   container/host NICs; ceph-*-network config would NOT). Ceph CLIENTS bind ceph->storage,
#                   and each subordinate's storage/data binding is mirrored on its PRINCIPAL (subset rule). (C2)
# Endpoints:        IP-ONLY -- os-public-hostname dropped on all API charms; the dual VIPs ARE the
#                   catalog endpoints (public 10.12.4.N / internal+admin 10.12.8.N). Vault issues
#                   per-VIP IP-SAN certs. No control-plane DNS dependency. (B5)
# HA chain:         hacluster subordinates + dual VIPs + :ha relations ACTIVE for 11 API charms
#                   (10 prior + ceph-radosgw, un-deferred). VIPs front-loaded into the MAAS-reserved
#                   /26: provider 10.12.4.2-.63, metal 10.12.8.2-.63 (supersedes .224-.254). (B1)
# Vault:            single unit, MYSQL storage backend (via vault-mysql-router). etcd + easyrsa
#                   REMOVED -- the etcd backend was never used (live storage = mysql) and is moot at
#                   1 unit; HA backend (Raft vs etcd) is a Roosevelt rehearsal item. (C1; revises D-006)
# Ceph networks:    FULL separation via network-space BINDINGS -- ceph-mon/ceph-osd public->storage
#                   (10.12.16.0/22), ceph-osd cluster->replication (10.12.20.0/22). Bindings, NOT
#                   ceph-*-network config, so the LXD-contained mon actually gets a storage NIC.
#                   Clients bind ceph->storage; container principals carry it too (subset rule). (C2)
# Magnum:           Layer A only -- CAPI driver graft is Layer B (runbooks/04a + 05)
# Octavia:          lb-mgmt PKI options supplied via overlays/octavia-pki.yaml (gitignored).
#                   Amphora-pipeline options baked (use-internal-endpoints etc.). (B4)
# OVN tunnels:      geneve overlay on the DATA space (10.12.12.0/22) -- ovn-chassis + ovn-chassis-octavia
#                   'data' binding; their principals also carry data (nova-compute:neutron-plugin bare-metal,
#                   octavia:ovsdb-cms provisions the container NIC) per the subset rule. Prereq: enp8s0
#                   link-subnet to 10.12.12.4N (rebuild-prep, machines Ready).
# Resources:        omitted -- let charms use latest available resource revisions
# ============================================================

name: vr0-dc0-omega-caracal-testcloud
description: |
  Charmed OpenStack Caracal (2024.1) on Ubuntu 22.04 LTS (Jammy) deployed via Juju 3.6 bundle
  against MAAS-managed VMs (openstack0-3, virsh).
  Decisions referenced (see Caracal_Rebuild handoff + 2026-06-01 bundle-cleanup change-set):
    D-001 Path 2A (Juju-bundle paradigm)
    D-002 channel matrix
    D-003 Option B (provider /22 carries FIPs + API VIPs)
    D-005 Ceph Squid
    D-006 Vault HA backend -- REVISED: etcd/easyrsa dropped for testcloud; Raft-vs-etcd is a Roosevelt item (C1)
    D-007 Magnum Layer A + Layer B graft
    D-019 (supersedes D-008) Designate deferred to v2
    D-009 hacluster subordinates (decorative on testcloud)
    D-016 IPv4-only v1
    D-018 MAAS-release-direct teardown
  Bundle-cleanup (2026-06-01): B5 IP-only endpoints; C1 vault-on-mysql (etcd/easyrsa removed);
    C2 full Ceph network separation; B1 VIP front-load + radosgw HA un-defer; B2 ovn prefer-chassis-as-gw;
    B3 nova Ceph-RBD ephemeral; B4 octavia amphora-pipeline options. C3 radosgw unchanged (already correct).

default-base: ubuntu@22.04/stable

variables:
  # ----- UCA pocket + Ceph source ----------------------------------------------
  openstack-origin: &openstack-origin cloud:jammy-caracal
  ceph-source:      &ceph-source      cloud:jammy-caracal

  # ----- Bindings for external-API-facing charms (public on provider) ----------
  api-bindings: &api-bindings
    "":            metal
    public:        provider

  # ----- Bindings for backend / internal-only charms (all metal) ---------------
  # Used for ovn-central, mysql-innodb-cluster, rabbitmq-server, vault, memcached, and the
  # mysql-router subordinates. (ceph-mon / ceph-osd now use explicit public/cluster bindings, see C2.)
  internal-bindings: &internal-bindings
    "":            metal

machines:
  "8":
    constraints: arch=amd64 tags=openstack
  "9":
    constraints: arch=amd64 tags=openstack
  "10":
    constraints: arch=amd64 tags=openstack
  "11":
    constraints: arch=amd64 tags=openstack

applications:

  # =====================================================================
  # Datastores: MySQL InnoDB Cluster, RabbitMQ, Vault
  # =====================================================================
  # C1: etcd + easyrsa REMOVED. Vault is single-unit and uses the MySQL storage backend via
  # vault-mysql-router (matches the live deploy; the etcd HA backend was never exercised and is
  # moot at one unit). Vault HA backend (Raft vs etcd) is a Roosevelt rehearsal item.

  mysql-innodb-cluster:
    charm: mysql-innodb-cluster
    channel: 8.0/stable
    num_units: 3
    to: [lxd:8, lxd:9, lxd:10]
    bindings: *internal-bindings
    constraints: arch=amd64

  rabbitmq-server:
    charm: rabbitmq-server
    channel: 3.9/stable
    num_units: 1
    to: [lxd:10]
    bindings: *internal-bindings
    constraints: arch=amd64

  vault:
    charm: vault
    channel: 1.8/stable
    num_units: 1                       # 3 on Roosevelt (D-009); HA backend decided there (C1)
    to: [lxd:11]
    bindings: *internal-bindings
    constraints: arch=amd64

  vault-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  # =====================================================================
  # Identity: Keystone
  # =====================================================================

  keystone:
    charm: keystone
    channel: 2024.1/stable
    num_units: 1                       # 3 on Roosevelt (D-009)
    to: [lxd:8]
    options:
      vip: "10.12.4.10 10.12.8.10"     # B1 front-loaded VIP; IS the catalog endpoint (B5, no os-public-hostname)
    bindings: *api-bindings
    constraints: arch=amd64

  keystone-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  # =====================================================================
  # Image: Glance + simplestreams-sync
  # =====================================================================

  glance:
    charm: glance
    channel: 2024.1/stable
    num_units: 1
    to: [lxd:11]
    options:
      vip: "10.12.4.13 10.12.8.13"     # B1
    bindings:                          # api-bindings + ceph->storage (C2; glance is a Ceph client)
      "":            metal
      public:        provider
      ceph:          storage
    constraints: arch=amd64

  glance-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  glance-simplestreams-sync:
    charm: glance-simplestreams-sync
    channel: 2024.1/stable
    num_units: 1
    to: [lxd:8]
    options:                           # B4 amphora-pipeline
      use-internal-endpoints: true     # use internal (IP) catalog endpoints
      use_swift: false                 # skip swift index; sidesteps radosgw object path for the amphora seed
    bindings: *internal-bindings
    constraints: arch=amd64

  # =====================================================================
  # Compute: Nova cloud-controller + compute + Placement
  # =====================================================================

  nova-cloud-controller:
    charm: nova-cloud-controller
    channel: 2024.1/stable
    num_units: 1
    to: [lxd:11]
    options:
      console-access-protocol: novnc
      network-manager: Neutron
      vip: "10.12.4.16 10.12.8.16"     # B1
    bindings: *api-bindings
    constraints: arch=amd64

  nova-compute:
    charm: nova-compute
    channel: 2024.1/stable
    num_units: 3
    to: ["9", "10", "11"]
    options:
      config-flags: default_ephemeral_format=ext4
      enable-live-migration: true      # now genuinely usable -- shared Ceph storage = memory-only migrate (B3)
      enable-resize: true
      libvirt-image-backend: rbd       # B3 Ceph-RBD ephemeral: DISK_GB from the Ceph pool, not local fs; unlocks Magnum
      migration-auth-type: ssh
      resume-guests-state-on-host-boot: true
      virt-type: qemu                  # Testcloud nested-KVM; Roosevelt will use 'kvm'
    bindings:                          # C2 ceph/ceph-access -> storage. OVN-on-data: neutron-plugin -> data
      "":            metal             #   puts 'data' in this principal's binding set so ovn-chassis' data
      ceph:          storage           #   binding is a valid SUBSET (subordinate subset rule). nova-compute is
      ceph-access:   storage           #   bare metal -- enp8s0 (data) is already present, so this only needs
      neutron-plugin: data             #   to satisfy the rule, not provision a NIC.
    constraints: arch=amd64

  ncc-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  placement:
    charm: placement
    channel: 2024.1/stable
    num_units: 1
    to: [lxd:11]
    options:
      vip: "10.12.4.19 10.12.8.19"     # B1
    bindings: *api-bindings
    constraints: arch=amd64

  placement-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  # =====================================================================
  # Networking: Neutron + OVN
  # =====================================================================

  neutron-api:
    charm: neutron-api
    channel: 2024.1/stable
    num_units: 1
    to: [lxd:9]
    options:
      enable-ml2-port-security: true
      flat-network-providers: physnet1
      neutron-security-groups: true
      vip: "10.12.4.15 10.12.8.15"     # B1
    bindings: *api-bindings
    constraints: arch=amd64

  neutron-api-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  neutron-api-plugin-ovn:
    charm: neutron-api-plugin-ovn
    channel: 2024.1/stable

  ovn-central:
    charm: ovn-central
    channel: 24.03/stable
    num_units: 3
    to: [lxd:8, lxd:9, lxd:10]
    bindings: *internal-bindings
    constraints: arch=amd64

  # ovn-chassis: subordinate to nova-compute. MAC-based bridge-interface-mappings captured from
  # MAAS 2026-05-22 (Bobcat used hardcoded 'enp1s0' -- anti-pattern fix). The charm picks whichever
  # MAC is found locally per unit; non-matching MACs are ignored.
  ovn-chassis:
    charm: ovn-chassis
    channel: 24.03/stable
    options:
      ovn-bridge-mappings: physnet1:br-ex
      prefer-chassis-as-gw: true       # B2 -- elects gateway chassis so tenant routers get external egress
      bridge-interface-mappings: >-
        br-ex:52:54:00:3d:fd:54
        br-ex:52:54:00:9d:63:77
        br-ex:52:54:00:89:7f:ce
        br-ex:52:54:00:99:fc:c2
    bindings:                          # OVN-on-data (fidelity): geneve encap onto the data space.
      "":   metal                      #   'data' endpoint verified on the charm (was the overlay-suffix bug).
      data: data                       #   Prereq: enp8s0 link-subnet to 10.12.12.0/22 (rebuild-prep, machines Ready).

  # ovn-chassis-octavia: separate ovn-chassis app, subordinate to octavia. No bridge-interface-mappings
  # and NO prefer-chassis-as-gw -- Octavia mgmt traffic rides the Neutron tenant overlay; it needs no
  # external physnet bridge or gateway here.
  ovn-chassis-octavia:
    charm: ovn-chassis
    channel: 24.03/stable
    bindings:                          # OVN-on-data: octavia chassis must share the compute chassis'
      "":   metal                      #   encap network or cross-chassis geneve tunnels break. The octavia
      data: data                       #   CONTAINER gets its data NIC Juju-provisioned at deploy.

  # =====================================================================
  # Block Storage: Cinder + cinder-ceph
  # =====================================================================

  cinder:
    charm: cinder
    channel: 2024.1/stable
    num_units: 1
    to: [lxd:9]
    options:
      block-device: None
      glance-api-version: 2
      vip: "10.12.4.12 10.12.8.12"     # B1
    bindings:                          # api-bindings + ceph -> storage. cinder's container needs a storage NIC
      "":            metal             #   for Ceph; binding the regular 'ceph' endpoint provisions it AND puts
      public:        provider          #   'storage' in cinder's binding set, so cinder-ceph's ceph->storage is a
      ceph:          storage           #   valid subset (subset rule). cinder:ceph is unrelated here -- cinder-ceph
    constraints: arch=amd64            #   owns the relation -- but the binding still provisions the NIC.

  cinder-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  cinder-ceph:
    charm: cinder-ceph
    channel: 2024.1/stable
    bindings:                          # C2: Ceph client traffic -> storage. Subordinate to cinder; the principal
      "":            metal             #   (cinder:ceph -> storage) now carries 'storage', so this is a valid
      ceph:          storage           #   subset and the shared container gets a 10.12.16.x NIC.

  # =====================================================================
  # Ceph: mon + osd + radosgw (Squid release per D-005)
  # =====================================================================
  # C2: full network separation via network-space BINDINGS (public/cluster) -- NOT ceph-*-network config,
  # which selects the net but never provisions the LXD-contained mon a storage NIC. Hosts carry enp9s0
  # (storage) + enp10s0 (replication); clients bind ceph->storage to reach the Ceph public net.

  ceph-mon:
    charm: ceph-mon
    channel: squid/stable
    num_units: 3
    to: [lxd:8, lxd:9, lxd:10]
    options:
      source: *ceph-source
      expected-osd-count: 4
      monitor-count: 3
    bindings:                          # C2 -- public BINDING (NOT ceph-public-network config). The config
      "":            metal             #   selects the net but does NOT give the LXD-contained mon a storage
      public:        storage           #   NIC, so the mon can't listen on 10.12.16.0/22. The binding both
    constraints: arch=amd64            #   provisions the NIC and sets the Ceph public net. Mons use only the
                                       #   public net (no cluster binding needed).

  ceph-osd:
    charm: ceph-osd
    channel: squid/stable
    num_units: 4
    to: ["8", "9", "10", "11"]
    options:
      source: *ceph-source
      osd-devices: /dev/vdb            # libvirt-attached, MAAS-untracked, wiped pre-deploy
    bindings:                          # C2 -- public/cluster BINDINGS (NOT ceph-*-network config). Bare-metal
      "":            metal             #   OSDs already carry enp9s0 (storage) + enp10s0 (replication); the
      public:        storage           #   bindings select them as Ceph public (client) and cluster (OSD-to-OSD
      cluster:       replication       #   replication/recovery), and keep parity with ceph-mon's method.
    constraints: arch=amd64 tags=openstack

  ceph-radosgw:
    charm: ceph-radosgw
    channel: squid/stable
    num_units: 1
    to: [lxd:8]
    options:
      source: *ceph-source
      vip: "10.12.4.20 10.12.8.20"     # B1 -- radosgw HA un-deferred for Roosevelt fidelity (decorative HA on testcloud)
    bindings:                          # api-bindings + mon->storage (C2). radosgw IS externally-facing (S3/Swift API).
      "":            metal
      public:        provider
      mon:           storage
    constraints: arch=amd64

  # =====================================================================
  # Dashboard: openstack-dashboard (Horizon)
  # =====================================================================

  openstack-dashboard:
    charm: openstack-dashboard
    channel: 2024.1/stable
    num_units: 1
    to: [lxd:10]
    options:
      debug: "false"
      vip: "10.12.4.18 10.12.8.18"     # B1 -- browse HTTPS by IP (B5); ALLOWED_HOSTS must permit the VIP IP (verify at deploy)
    bindings: *api-bindings
    constraints: arch=amd64

  dashboard-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  # =====================================================================
  # Load Balancer: Octavia
  # =====================================================================
  # CRITICAL: vault:certificates must be in bundle from day-one (post-deploy add causes the
  # documented apache2/octavia-api masking bug -- see test deployment v3 handoff).

  octavia:
    charm: octavia
    channel: 2024.1/stable
    num_units: 1
    to: [lxd:11]
    options:
      debug: false
      openstack-origin: *openstack-origin
      amp-image-tag: octavia-amphora   # B4 -- MUST match the tag octavia-diskimage-retrofit stamps
      # ----- PKI material -------------------------------------------------
      # 5 lb-mgmt-* options are supplied via overlays/octavia-pki.yaml
      # (gitignored). Generated per runbooks/01a-octavia-pki-generation.md.
      # Deploy with:
      #   juju deploy ./bundle.yaml \
      #     --overlay overlays/vr0-dc0-testcloud.yaml \
      #     --overlay overlays/octavia-pki.yaml
      vip: "10.12.4.17 10.12.8.17"     # B1
    bindings:                          # api-bindings + ovsdb-cms -> data. octavia's CONTAINER needs a data NIC so
      "":            metal             #   ovn-chassis-octavia can geneve-encap on the overlay; ovsdb-cms is a
      public:        provider          #   REGULAR (octavia<->ovn-central) endpoint -- unused in the amphora-driver
      ovsdb-cms:     data              #   setup, so binding it just provisions the NIC AND makes 'data' a valid
    constraints: arch=amd64            #   subset for the subordinate's data binding (subset rule).

  octavia-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  octavia-dashboard:
    charm: octavia-dashboard
    channel: 2024.1/stable

  octavia-diskimage-retrofit:
    charm: octavia-diskimage-retrofit
    channel: 2024.1/stable
    options:
      amp-image-tag: octavia-amphora
      use-internal-endpoints: true     # B4 -- charm ships FALSE; required so the retrofit glance client uses the internal (IP) endpoint
      image-format: raw                # B4 -- RAW, not the qcow2 default: glance is Ceph-backed, and the charm
                                       #   + Ceph docs recommend raw so RBD can fast-clone the amphora (qcow2
                                       #   forces a convert-on-import and defeats CoW).

  # =====================================================================
  # Secrets: Barbican
  # =====================================================================

  barbican:
    charm: barbican
    channel: 2024.1/stable
    num_units: 1
    to: [lxd:11]
    options:
      openstack-origin: *openstack-origin
      vip: "10.12.4.11 10.12.8.11"     # B1
    bindings: *api-bindings
    constraints: arch=amd64

  barbican-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  barbican-vault:
    charm: barbican-vault
    channel: 2024.1/stable

  # =====================================================================
  # Kubernetes-as-a-Service: Magnum (Layer A -- CAPI graft is Layer B)
  # =====================================================================
  # NOTE: After bundle deploys, magnum/0 will show active/idle but CANNOT create K8s clusters.
  # Layer B (post-deploy) brings it to life:
  #   1. capi-mgmt VM with k3s + CAPI operators              (runbook 04a)
  #   2. pip install magnum-capi-helm==1.1.0 into magnum venv (runbook 05)
  #   3. /etc/magnum/magnum.conf.d/99-capi.conf with enabled_drivers
  #   4. Install kubeconfig at /etc/magnum/kubeconfig
  #   5. Create Keystone capi-mgmt project + capo user + app credential

  magnum:
    charm: magnum
    channel: 2024.1/stable
    num_units: 1
    to: [lxd:9]
    options:
      openstack-origin: *openstack-origin
      region: RegionOne
      vip: "10.12.4.14 10.12.8.14"     # B1
    bindings: *api-bindings
    constraints: arch=amd64

  magnum-mysql-router:
    charm: mysql-router
    channel: 8.0/stable
    bindings: *internal-bindings

  magnum-dashboard:
    charm: magnum-dashboard
    channel: 2024.1/stable

  # =====================================================================
  # HA Cluster Subordinates (11 active for v1: 10 API charms + ceph-radosgw)
  # =====================================================================
  # Channel: 2.4/stable (per Caracal Charm Delivery table, D-002 verified 2026-05-22).
  # cluster_count: 1 (decorative on single-unit testcloud, D-009 / BUNDLEFIX-003).
  # VIPs front-loaded into the MAAS-reserved provider/metal /26 per B1 (.2-.63).
  # vault-hacluster stays commented (vault single-unit on mysql, C1 / BUNDLEFIX-002).
  # designate-hacluster stays deferred (D-019).
  #
  keystone-hacluster:              { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }
  glance-hacluster:                { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }
  neutron-api-hacluster:           { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }
  nova-cloud-controller-hacluster: { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }
  placement-hacluster:             { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }
  openstack-dashboard-hacluster:   { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }
  cinder-hacluster:                { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }
  octavia-hacluster:               { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }
  barbican-hacluster:              { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }
  magnum-hacluster:                { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }
  ceph-radosgw-hacluster:          { charm: hacluster, channel: 2.4/stable, options: { cluster_count: 1 } }   # B1 -- un-deferred
  # vault-hacluster:                 { charm: hacluster, channel: 2.4/stable }   # C1: vault single-unit on mysql; HA at Roosevelt
  # v2-deferred (D-019): designate-hacluster: { charm: hacluster, channel: 2.4/stable }

  # 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

relations:
  - [nova-cloud-controller:memcache, memcached:cache]

  # ---- Vault (single unit, MySQL storage backend via vault-mysql-router; C1 -- etcd+easyrsa removed)
  - [vault-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [vault:shared-db, vault-mysql-router:shared-db]
  - [mysql-innodb-cluster:certificates, vault:certificates]
  # - [vault:ha, vault-hacluster:ha]                   # vault de-HA'd on testcloud (C1/BUNDLEFIX-002); HA backend a Roosevelt item

  # ---- Keystone (identity, hub of all OS service relations)
  - [keystone-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [keystone-mysql-router:shared-db, keystone:shared-db]
  - [keystone:certificates, vault:certificates]
  - [keystone:ha, keystone-hacluster:ha]

  # ---- Glance (image)
  - [glance-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [glance-mysql-router:shared-db, glance:shared-db]
  - [glance:identity-service, keystone:identity-service]
  - [glance:certificates, vault:certificates]
  - [glance:ha, glance-hacluster:ha]

  # ---- Glance simplestreams sync (Octavia amphora pipeline source)
  - [glance-simplestreams-sync:identity-service, keystone:identity-service]
  - [glance-simplestreams-sync:certificates, vault:certificates]

  # ---- Nova cloud controller (NCC)
  - [ncc-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [ncc-mysql-router:shared-db, nova-cloud-controller:shared-db]
  - [nova-cloud-controller:identity-service, keystone:identity-service]
  - [nova-cloud-controller:amqp, rabbitmq-server:amqp]
  - [nova-cloud-controller:image-service, glance:image-service]
  - [nova-cloud-controller:neutron-api, neutron-api:neutron-api]
  - [nova-cloud-controller:cloud-compute, nova-compute:cloud-compute]
  - [nova-cloud-controller:cinder-volume-service, cinder:cinder-volume-service]
  - [nova-cloud-controller:certificates, vault:certificates]
  - [nova-cloud-controller:ha, nova-cloud-controller-hacluster:ha]

  # ---- Nova compute
  - [nova-compute:amqp, rabbitmq-server:amqp]
  - [nova-compute:image-service, glance:image-service]

  # ---- Placement
  - [placement-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [placement-mysql-router:shared-db, placement:shared-db]
  - [placement:identity-service, keystone:identity-service]
  - [placement:placement, nova-cloud-controller:placement]
  - [placement:certificates, vault:certificates]
  - [placement:ha, placement-hacluster:ha]

  # ---- Neutron API + OVN
  - [neutron-api-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [neutron-api-mysql-router:shared-db, neutron-api:shared-db]
  - [neutron-api:identity-service, keystone:identity-service]
  - [neutron-api:amqp, rabbitmq-server:amqp]
  - [neutron-api:certificates, vault:certificates]
  - [neutron-api-plugin-ovn:neutron-plugin, neutron-api:neutron-plugin-api-subordinate]
  - [neutron-api-plugin-ovn:ovsdb-cms, ovn-central:ovsdb-cms]
  - [neutron-api-plugin-ovn:certificates, vault:certificates]
  - [ovn-central:certificates, vault:certificates]
  - [ovn-chassis:ovsdb, ovn-central:ovsdb]
  - [ovn-chassis:nova-compute, nova-compute:neutron-plugin]
  - [ovn-chassis:certificates, vault:certificates]
  - [neutron-api:ha, neutron-api-hacluster:ha]

  # ---- Cinder + cinder-ceph
  - [cinder-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [cinder-mysql-router:shared-db, cinder:shared-db]
  - [cinder:identity-service, keystone:identity-service]
  - [cinder:amqp, rabbitmq-server:amqp]
  - [cinder:image-service, glance:image-service]
  - [cinder:certificates, vault:certificates]
  - [cinder-ceph:storage-backend, cinder:storage-backend]
  - [cinder-ceph:ceph, ceph-mon:client]
  - [cinder-ceph:ceph-access, nova-compute:ceph-access]
  - [cinder:ha, cinder-hacluster:ha]

  # ---- Ceph mon + osd + radosgw
  - [ceph-mon:osd, ceph-osd:mon]
  - [ceph-mon:client, nova-compute:ceph]
  - [ceph-mon:client, glance:ceph]
  - [ceph-radosgw:mon, ceph-mon:radosgw]
  - [ceph-radosgw:identity-service, keystone:identity-service]
  - [ceph-radosgw:certificates, vault:certificates]
  - [ceph-radosgw:ha, ceph-radosgw-hacluster:ha]      # B1 -- un-deferred

  # ---- OpenStack Dashboard (Horizon)
  - [dashboard-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [dashboard-mysql-router:shared-db, openstack-dashboard:shared-db]
  - [openstack-dashboard:identity-service, keystone:identity-service]
  - [openstack-dashboard:certificates, vault:certificates]
  - [openstack-dashboard:ha, openstack-dashboard-hacluster:ha]

  # ---- Octavia (LBaaS)
  # CRITICAL: octavia:certificates <-> vault:certificates MUST be present at deploy time
  - [octavia-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [octavia-mysql-router:shared-db, octavia:shared-db]
  - [octavia:identity-service, keystone:identity-service]
  - [octavia:amqp, rabbitmq-server:amqp]
  - [octavia:neutron-api, neutron-api:neutron-load-balancer]
  - [octavia:certificates, vault:certificates]
  - [octavia-dashboard:dashboard, openstack-dashboard:dashboard-plugin]
  - [ovn-chassis-octavia:ovsdb, ovn-central:ovsdb]
  - [ovn-chassis-octavia:ovsdb-subordinate, octavia:ovsdb-subordinate]
  - [ovn-chassis-octavia:certificates, vault:certificates]
  # Octavia amphora image pipeline
  - [octavia-diskimage-retrofit:juju-info, glance-simplestreams-sync:juju-info]
  - [octavia-diskimage-retrofit:identity-credentials, keystone:identity-credentials]
  - [octavia:ha, octavia-hacluster:ha]

  # ---- Barbican (secrets)
  - [barbican-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [barbican-mysql-router:shared-db, barbican:shared-db]
  - [barbican:identity-service, keystone:identity-service]
  - [barbican:amqp, rabbitmq-server:amqp]
  - [barbican:certificates, vault:certificates]
  - [barbican:secrets, barbican-vault:secrets]
  - [barbican-vault:certificates, vault:certificates]
  - [barbican-vault:secrets-storage, vault:secrets]
  - [barbican:ha, barbican-hacluster:ha]

  # ---- Magnum (Layer A only; CAPI graft is Layer B/runbook 05)
  - [magnum-mysql-router:db-router, mysql-innodb-cluster:db-router]
  - [magnum:shared-db, magnum-mysql-router:shared-db]
  - [magnum:identity-service, keystone:identity-service]
  - [magnum:amqp, rabbitmq-server:amqp]
  - [magnum:certificates, vault:certificates]
  - [magnum-dashboard:dashboard, openstack-dashboard:dashboard-plugin]
  - [magnum:ha, magnum-hacluster:ha]