diff --git a/bundle.yaml b/bundle.yaml index a5e7dfa..e9242da 100644 --- a/bundle.yaml +++ b/bundle.yaml @@ -93,6 +93,7 @@ vault-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings etcd: charm: etcd @@ -130,6 +131,7 @@ keystone-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings # ===================================================================== # Image: Glance + simplestreams-sync @@ -149,6 +151,7 @@ glance-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings glance-simplestreams-sync: charm: glance-simplestreams-sync @@ -195,6 +198,7 @@ ncc-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings placement: charm: placement @@ -210,6 +214,7 @@ placement-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings # ===================================================================== # Networking: Neutron + OVN @@ -232,6 +237,7 @@ neutron-api-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings neutron-api-plugin-ovn: charm: neutron-api-plugin-ovn @@ -288,6 +294,7 @@ cinder-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings cinder-ceph: charm: cinder-ceph @@ -352,6 +359,7 @@ dashboard-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings # ===================================================================== # Load Balancer: Octavia @@ -382,6 +390,7 @@ octavia-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings octavia-dashboard: charm: octavia-dashboard @@ -412,6 +421,7 @@ barbican-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings barbican-vault: charm: barbican-vault @@ -444,6 +454,7 @@ magnum-mysql-router: charm: mysql-router channel: 8.0/stable + bindings: *internal-bindings magnum-dashboard: charm: magnum-dashboard diff --git a/bundle.yaml.bak-20260529-004120 b/bundle.yaml.bak-20260529-004120 new file mode 100644 index 0000000..a5e7dfa --- /dev/null +++ b/bundle.yaml.bak-20260529-004120 @@ -0,0 +1,603 @@ +# ============================================================ +# Caracal 2024.1 — VR0 DC0 Omega Cloud testcloud rebuild bundle +# ============================================================ +# Generated: 2026-05-22 +# 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 +# HA chain: ALL hacluster subordinates + vip configs + :ha relations COMMENTED OUT +# until NetBox VIP allocations land in 10.12.4.224-.254 +# Vault HA: etcd backend + easyrsa CA bootstrap live; vault-hacluster commented +# Magnum: Layer A only — CAPI driver graft is Layer B (runbooks/04a + 05) +# Octavia: lb-mgmt PKI options present but VALUES commented out — source from +# either Bobcat backup (~/backups/pre-caracal-destroy-2026-05-22/) +# or fresh octavia-cert-runbook (TBD) +# OVN tunnels: remain on metal space (Bobcat-proven); enp8s0 3_data v2 improvement +# 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): + 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 via etcd + easyrsa + 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 + +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 ceph-mon (Ceph public network IS metal, not OpenStack public), + # ceph-osd, ovn-central, mysql-innodb-cluster, rabbitmq-server, nova-compute, etc. + 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 + HA backend + # ===================================================================== + + 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) + to: [lxd:11] + bindings: *internal-bindings + constraints: arch=amd64 + + vault-mysql-router: + charm: mysql-router + channel: 8.0/stable + + etcd: + charm: etcd + channel: latest/stable # support charm; not in OS delivery table + num_units: 3 # Vault HA backend (D-006) + to: [lxd:8, lxd:9, lxd:10] + bindings: *internal-bindings + constraints: arch=amd64 + # Note: etcd charm has its OWN `channel:` config option (controls etcd snap). + # Leaving at charm default; revisit if a specific etcd binary version is needed. + + easyrsa: + charm: easyrsa + channel: latest/stable + num_units: 1 # One-shot CA for etcd bootstrap (D-006) + to: [lxd:8] + bindings: *internal-bindings + constraints: arch=amd64 + + # ===================================================================== + # 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.229 + os-public-hostname: keystone.omega.dc0.vr0.cloud.neumatrix.local + bindings: *api-bindings + constraints: arch=amd64 + + keystone-mysql-router: + charm: mysql-router + channel: 8.0/stable + + # ===================================================================== + # Image: Glance + simplestreams-sync + # ===================================================================== + + glance: + charm: glance + channel: 2024.1/stable + num_units: 1 + to: [lxd:11] + options: + vip: 10.12.4.228 + os-public-hostname: glance.omega.dc0.vr0.cloud.neumatrix.local + bindings: *api-bindings + constraints: arch=amd64 + + glance-mysql-router: + charm: mysql-router + channel: 8.0/stable + + glance-simplestreams-sync: + charm: glance-simplestreams-sync + channel: 2024.1/stable + num_units: 1 + to: [lxd:8] + 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.232 + os-public-hostname: nova.omega.dc0.vr0.cloud.neumatrix.local + 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 + enable-resize: true + migration-auth-type: ssh + resume-guests-state-on-host-boot: true + virt-type: qemu # Testcloud nested-KVM; Roosevelt will use 'kvm' + bindings: *internal-bindings + constraints: arch=amd64 + storage: + ephemeral-device: loop,10240M + + ncc-mysql-router: + charm: mysql-router + channel: 8.0/stable + + placement: + charm: placement + channel: 2024.1/stable + num_units: 1 + to: [lxd:11] + options: + vip: 10.12.4.235 + os-public-hostname: placement.omega.dc0.vr0.cloud.neumatrix.local + bindings: *api-bindings + constraints: arch=amd64 + + placement-mysql-router: + charm: mysql-router + channel: 8.0/stable + + # ===================================================================== + # 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.231 + os-public-hostname: neutron.omega.dc0.vr0.cloud.neumatrix.local + bindings: *api-bindings + constraints: arch=amd64 + + neutron-api-mysql-router: + charm: mysql-router + channel: 8.0/stable + + 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 ignored. + ovn-chassis: + charm: ovn-chassis + channel: 24.03/stable + options: + ovn-bridge-mappings: physnet1:br-ex + 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 + + # ovn-chassis-octavia: separate ovn-chassis app, subordinate to octavia. + # No bridge-interface-mappings — matches Bobcat-proven pattern (Octavia mgmt + # traffic rides Neutron tenant overlay; no external physnet bridge needed here). + ovn-chassis-octavia: + charm: ovn-chassis + channel: 24.03/stable + + # ===================================================================== + # 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.226 + os-public-hostname: cinder.omega.dc0.vr0.cloud.neumatrix.local + bindings: *api-bindings + constraints: arch=amd64 + storage: + block-devices: loop,10240M + + cinder-mysql-router: + charm: mysql-router + channel: 8.0/stable + + cinder-ceph: + charm: cinder-ceph + channel: 2024.1/stable + + # ===================================================================== + # Ceph: mon + osd + radosgw (Squid release per D-005) + # ===================================================================== + + 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: *internal-bindings # Ceph 'public' here = clients on metal, NOT OS public API + constraints: arch=amd64 + + 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 2026-05-22 + bindings: *internal-bindings + constraints: arch=amd64 tags=openstack + + ceph-radosgw: + charm: ceph-radosgw + channel: squid/stable + num_units: 1 + to: [lxd:8] + options: + source: *ceph-source + # v2-deferred: ceph-radosgw HA deferred to v2 per workstream-2 decision. + # vip slot 10.12.4.225 reserved for ceph-radosgw VIP in v2. + # See also commented ceph-radosgw-hacluster app + :ha relation below. + bindings: *api-bindings # radosgw IS externally-facing (S3/Swift API) + 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.234 + os-public-hostname: horizon.omega.dc0.vr0.cloud.neumatrix.local + bindings: *api-bindings + constraints: arch=amd64 + + dashboard-mysql-router: + charm: mysql-router + channel: 8.0/stable + + # ===================================================================== + # Load Balancer: Octavia + # ===================================================================== + # CRITICAL: vault:certificates must be in bundle from day-one (post-deploy add + # causes 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 + # ----- 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.233 + os-public-hostname: octavia.omega.dc0.vr0.cloud.neumatrix.local + bindings: *api-bindings + constraints: arch=amd64 + + octavia-mysql-router: + charm: mysql-router + channel: 8.0/stable + + 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 + + # ===================================================================== + # Secrets: Barbican + # ===================================================================== + + barbican: + charm: barbican + channel: 2024.1/stable + num_units: 1 + to: [lxd:11] + options: + openstack-origin: *openstack-origin + vip: 10.12.4.224 + os-public-hostname: barbican.omega.dc0.vr0.cloud.neumatrix.local + bindings: *api-bindings + constraints: arch=amd64 + + barbican-mysql-router: + charm: mysql-router + channel: 8.0/stable + + 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.230 + os-public-hostname: magnum.omega.dc0.vr0.cloud.neumatrix.local + bindings: *api-bindings + constraints: arch=amd64 + + magnum-mysql-router: + charm: mysql-router + channel: 8.0/stable + + magnum-dashboard: + charm: magnum-dashboard + channel: 2024.1/stable + + # ===================================================================== + # HA Cluster Subordinates (11 active for v1; ceph-radosgw + designate deferred to v2) + # ===================================================================== + # Channel: 2.4/stable (per Caracal Charm Delivery table, D-002 verified 2026-05-22). + # VIPs allocated from provider /22 range 10.12.4.224-.254 per D-003. + # NetBox IPAddress records queued post-deployment (engineer review pending). + # See workstream-2 decision (2026-05-22). + # + 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 } } + # vault-hacluster: { charm: hacluster, channel: 2.4/stable } + # v2-deferred: ceph-radosgw-hacluster: { charm: hacluster, channel: 2.4/stable } + # v2-deferred (D-019): designate-hacluster: { charm: hacluster, channel: 2.4/stable } + +relations: + + # ---- Vault HA backend chain (NEW for Caracal v1; chicken-and-egg via easyrsa) + - [easyrsa:client, etcd:certificates] # easyrsa issues etcd TLS one-time + - [vault:etcd, etcd:db] # vault uses etcd as HA backend + - [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] + + # ---- 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] + # v2-deferred: - [ceph-radosgw:ha, ceph-radosgw-hacluster:ha] + + # ---- 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] diff --git a/fix-api-bindings.py b/fix-api-bindings.py new file mode 100644 index 0000000..e2ba9cb --- /dev/null +++ b/fix-api-bindings.py @@ -0,0 +1,163 @@ +#!/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) diff --git a/fix-bundle-haclusters.py b/fix-bundle-haclusters.py new file mode 100644 index 0000000..e3d70e5 --- /dev/null +++ b/fix-bundle-haclusters.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +fix-bundle-haclusters.py - BUNDLEFIX-003 + +Add `options: { cluster_count: 1 }` to the 10 *active* testcloud haclusters so +the committed bundle matches the running model (we already set this at runtime +via `juju config`). Single-unit principals on the testcloud cannot form the +default 3-peer cluster; cluster_count=1 lets a 1-node cluster form and bring up +the (reachable, public->provider) VIP. Roosevelt's separate 3-unit bundle keeps +the default. + +Text/line based - never round-trips YAML, so anchors/comments/formatting are +preserved. Only touches the named, *uncommented* hacluster lines; the commented +v2-deferred ones (vault-hacluster, ceph-radosgw-hacluster, designate-hacluster) +are left untouched. Idempotent: skips a line that already has cluster_count, and +aborts cleanly if nothing needs changing. + +Usage: python3 fix-bundle-haclusters.py [path-to-bundle.yaml] (default ./bundle.yaml) +""" +import sys, os, re, shutil, difflib, datetime + +PATH = sys.argv[1] if len(sys.argv) > 1 else "bundle.yaml" +HACLUSTERS = ["keystone", "glance", "nova-cloud-controller", "neutron-api", + "cinder", "octavia", "barbican", "magnum", "placement", + "openstack-dashboard"] +INSERT_AFTER = "channel: 2.4/stable }" +INSERT_WITH = "channel: 2.4/stable, options: { cluster_count: 1 } }" + + +def abort(msg): + sys.stderr.write("ABORT (no changes written): %s\n" % msg) + sys.exit(1) + + +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: + orig = fh.readlines() +lines = list(orig) + +changed = [] +for name in HACLUSTERS: + # uncommented inline def line for this hacluster + pat = re.compile(r'^\s*%s-hacluster:\s*\{\s*charm:\s*hacluster' % re.escape(name)) + hits = [i for i, ln in enumerate(lines) + if pat.match(ln) and not ln.lstrip().startswith("#")] + if len(hits) != 1: + abort("expected exactly 1 uncommented '%s-hacluster' inline def, found %d" + % (name, len(hits))) + i = hits[0] + if "cluster_count" in lines[i]: + abort("%s-hacluster already has cluster_count - already applied? inspect." + % name) + if INSERT_AFTER not in lines[i]: + abort("%s-hacluster line not in expected inline shape: %r" + % (name, lines[i].strip())) + lines[i] = lines[i].replace(INSERT_AFTER, INSERT_WITH, 1) + changed.append(name) + +if len(changed) != len(HACLUSTERS): + abort("only changed %d of %d haclusters" % (len(changed), len(HACLUSTERS))) + +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("") + +try: + import yaml +except Exception: + print("NOTE: PyYAML not importable - semantic verification skipped; re-verify on jumphost.") + sys.exit(0) + +apps = yaml.safe_load(open(PATH))["applications"] +print("=== verification ===") +print("YAML parses: PASS") +ok = True +for name in HACLUSTERS: + a = apps.get("%s-hacluster" % name, {}) + cc = (a.get("options") or {}).get("cluster_count") + p = (cc == 1) + ok &= p + print(" %-30s cluster_count==1 : %s" % (name + "-hacluster", "PASS" if p else "FAIL (%r)" % cc)) +# deferred ones must NOT have appeared +for absent in ("vault-hacluster", "ceph-radosgw-hacluster", "designate-hacluster"): + p = absent not in apps + ok &= p + print(" %-30s stays absent : %s" % (absent, "PASS" if p else "FAIL")) +print("\nRESULT:", "ALL CHECKS PASS" + if ok else "FAILURES - revert: cp %s %s" % (bak, PATH)) +sys.exit(0 if ok else 2) diff --git a/fix-bundle-router-bindings.py b/fix-bundle-router-bindings.py new file mode 100644 index 0000000..8fc8f0b --- /dev/null +++ b/fix-bundle-router-bindings.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +fix-bundle-router-bindings.py (BUNDLEFIX-005, part 2 / option A) + +Adds `bindings: *internal-bindings` to every mysql-router application block in the +Caracal bundle, so the router subordinates bind to the metal space -- matching the +live `juju bind metal` fix already applied to the running model. + +Why: without an explicit binding the routers default to the empty 'alpha' space, +which resolves to the container's PROVIDER address. The cluster then grants +mysqlrouteruser@, but the router's actual TCP connection to the +metal-only cluster egresses the metal interface -> grant host != source -> +"Access denied 1045" -> mysqlrouter never bootstraps. Binding to metal makes the +advertised address == the connection source. + +Safe by construction: + - pure line edits (NO YAML round-trip; preserves anchors, comments, formatting) + - timestamped .bak + - prints a unified diff + - idempotent (skips any router that already carries a bindings line) + - yaml.safe_load verification of the result, asserting every mysql-router app + resolves to bindings {'': 'metal'} via the *internal-bindings anchor + - aborts unless it finds the expected mysql-router blocks and they verify + +Usage: + python3 fix-bundle-router-bindings.py [path/to/bundle.yaml] (default ./bundle.yaml) +""" +import sys, os, difflib, datetime + +DEFAULT = "bundle.yaml" + + +def transform(lines): + """Insert `bindings: *internal-bindings` after the channel line of + every `charm: mysql-router` app block that doesn't already have a bindings line.""" + out = [] + prev_is_mr_charm = False + found = inserted = skipped = 0 + for idx, line in enumerate(lines): + out.append(line) + stripped = line.strip() + if prev_is_mr_charm and stripped.startswith("channel:"): + found += 1 + nxt = lines[idx + 1].strip() if idx + 1 < len(lines) else "" + if nxt.startswith("bindings:"): + skipped += 1 + else: + indent = line[: len(line) - len(line.lstrip())] + out.append(f"{indent}bindings: *internal-bindings") + inserted += 1 + prev_is_mr_charm = (stripped == "charm: mysql-router") + return out, found, inserted, skipped + + +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 + + with open(path, "r", encoding="utf-8") as f: + original = f.read() + lines = original.splitlines() + + out, found, inserted, skipped = transform(lines) + new = "\n".join(out) + ("\n" if original.endswith("\n") else "") + + if found == 0: + print("[ABORT] no `charm: mysql-router` + `channel:` blocks found - unexpected structure.") + return 3 + if inserted == 0 and skipped == found: + print(f"[OK/IDEMPOTENT] all {found} mysql-router apps already bound; no change.") + return 0 + + print("=== unified diff ===") + diff = "\n".join(difflib.unified_diff( + original.splitlines(), new.splitlines(), + fromfile=f"{path} (orig)", tofile=f"{path} (new)", lineterm="")) + print(diff or "(no diff)") + print(f"=== mysql-router blocks: {found} | inserted: {inserted} | already-bound: {skipped} ===") + + # semantic verification (anchors resolve under safe_load) + try: + import yaml + doc = yaml.safe_load(new) + apps = (doc or {}).get("applications", {}) or {} + mr = {k: v for k, v in apps.items() + if isinstance(v, dict) and v.get("charm") == "mysql-router"} + bad = {k: v.get("bindings") for k, v in mr.items() if v.get("bindings") != {"": "metal"}} + if bad: + print(f"[ABORT] verification failed; not bound to {{'': 'metal'}}: {bad}") + return 4 + print(f"[VERIFY] yaml.safe_load OK; all {len(mr)} mysql-router apps -> bindings {{'': 'metal'}}.") + except ImportError: + print("[WARN] PyYAML missing; skipped semantic verify (re-verify on jumphost after pull).") + except Exception as e: + print(f"[ABORT] yaml verification error: {e}") + return 5 + + ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + bak = f"{path}.bak-{ts}" + with open(bak, "w", encoding="utf-8") as f: + f.write(original) + with open(path, "w", encoding="utf-8") as f: + f.write(new) + print(f"[WROTE] {path} (backup: {bak})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/fix-bundle-v1.py b/fix-bundle-v1.py new file mode 100644 index 0000000..7c3200b --- /dev/null +++ b/fix-bundle-v1.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +""" +fix-bundle-v1.py - Option-A bundle fix for the Caracal v1 deploy. + +SUPERSEDES fix-api-bindings.py (which did only edits 1-2). Run this against the +*original* bundle.yaml (i.e. after `git restore bundle.yaml` if the earlier +script was already applied). + +All edits are text/line based - YAML is never round-tripped, so anchors, +aliases, comments and formatting are preserved. Verification uses safe_load +(read only). Aborts WITHOUT writing if the bundle is not in the expected +pre-fix shape. + +Edits: + 1. Shrink &api-bindings to the two keys that carry meaning: + "": metal + public: provider + (drops admin/internal/shared-db/amqp/certificates/cluster/ha - all 'metal', + i.e. the "" default - which were causing 'unknown endpoint' deploy errors + on keystone / ceph-radosgw / openstack-dashboard.) + 2. vault bindings: *api-bindings -> *internal-bindings (vault is internal-only). + 3. Remove vault's options: block (vip + os-public-hostname) - vault is a + single unit on the testcloud (3 at Roosevelt); a provider VIP is both + unreachable from metal-bound vault and pointless at one unit. + 4. Comment out the vault-hacluster subordinate. + 5. Comment out the [vault:ha, vault-hacluster:ha] relation. + +Net effect on counts: VIPs 11->10, apps 51->50, relations 98->97. +Vault HA is restored at Roosevelt where it is genuinely 3-unit with a real +(metal) VIP from NetBox. + +Usage: python3 fix-bundle-v1.py [path-to-bundle.yaml] (default ./bundle.yaml) +""" +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"} +VAULT_OPTS_EXPECTED = {"vip", "os-public-hostname"} + + +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: + orig = fh.readlines() +lines = list(orig) + +# ---------- Edit 1: shrink &api-bindings ---------- +anchor = next((i for i, ln in enumerate(lines) + if re.match(r'^api-bindings:\s*&api-bindings$', ln.strip())), None) +if anchor is None: + abort("could not locate 'api-bindings: &api-bindings'") +a_indent = indent_of(lines[anchor]) +j, kept, dropped = anchor + 1, [], [] +while j < len(lines): + raw = lines[j] + if raw.strip() == "" or indent_of(raw) <= a_indent: + break + key = raw.strip().split(":", 1)[0].strip() + (dropped if key in DROP else kept).append(key if key in DROP else raw) + j += 1 +kept_keys = {l.strip().split(":", 1)[0].strip() for l in kept} +if kept_keys != KEEP or set(dropped) != DROP: + abort("api-bindings not in expected pre-fix shape (kept=%s dropped=%s)" + % (sorted(kept_keys), sorted(dropped))) +lines = lines[:anchor + 1] + kept + lines[j:] + +# ---------- locate vault app block (post edit-1 indices) ---------- +vault = next((i for i, ln in enumerate(lines) + if ln.strip() == "vault:" and indent_of(ln) == 2), None) +if vault is None: + abort("could not locate ' vault:' application block") +# block end = next line at indent <= 2 (non-blank) +vend = vault + 1 +while vend < len(lines): + if lines[vend].strip() and indent_of(lines[vend]) <= 2: + break + vend += 1 + +# ---------- Edit 2: vault bindings -> internal-bindings ---------- +b_fixed = False +for k in range(vault + 1, vend): + if re.match(r'^\s*bindings:\s*\*api-bindings\b', lines[k]): + lines[k] = lines[k].replace("*api-bindings", "*internal-bindings", 1) + b_fixed = True + break +if not b_fixed: + abort("vault 'bindings: *api-bindings' not found (already changed?)") + +# ---------- Edit 3: remove vault options: block ---------- +opt = next((k for k in range(vault + 1, vend) + if re.match(r'^\s{4}options:\s*$', lines[k])), None) +if opt is None: + abort("vault 'options:' line not found") +opt_indent = indent_of(lines[opt]) +c = opt + 1 +opt_children = [] +while c < vend and lines[c].strip() and indent_of(lines[c]) > opt_indent: + opt_children.append(lines[c].strip().split(":", 1)[0].strip()) + c += 1 +if set(opt_children) != VAULT_OPTS_EXPECTED: + abort("vault options are %s, expected %s - inspect by hand (won't blind-delete)" + % (sorted(opt_children), sorted(VAULT_OPTS_EXPECTED))) +del lines[opt:c] # remove 'options:' + its children + +# ---------- Edit 4: comment out the vault-hacluster subordinate ---------- +hac = [i for i, ln in enumerate(lines) + if re.match(r'^\s*vault-hacluster:', ln) and not ln.lstrip().startswith("#")] +if len(hac) != 1: + abort("expected exactly 1 uncommented 'vault-hacluster:' line, found %d" % len(hac)) +i = hac[0] +ind = indent_of(lines[i]) +lines[i] = lines[i][:ind] + "# " + lines[i][ind:] + +# ---------- Edit 5: comment out the vault:ha relation ---------- +rel = [i for i, ln in enumerate(lines) + if ("vault:ha" in ln and "vault-hacluster:ha" in ln + and not ln.lstrip().startswith("#"))] +if len(rel) != 1: + abort("expected exactly 1 uncommented vault:ha relation, found %d" % len(rel)) +i = rel[0] +ind = indent_of(lines[i]) +lines[i] = lines[i][:ind] + "# " + lines[i][ind:] + +# ---------- 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 ---------- +try: + import yaml +except Exception: + print("NOTE: PyYAML not importable - semantic verification skipped; re-verify on jumphost.") + sys.exit(0) + +doc = yaml.safe_load(open(PATH)) +apps = doc["applications"] +rels = doc.get("relations") or [] +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: + p = apps.get(a, {}).get("bindings") == MIN + ok &= p + print(" %-22s minimal api-bindings : %s" % (a, "PASS" if p else "FAIL")) +checks = [ + ("vault bindings == internal-bindings", apps.get("vault", {}).get("bindings") == INT), + ("vault has no options block", apps.get("vault", {}).get("options") in (None, {})), + ("vault-hacluster removed from apps", "vault-hacluster" not in apps), + ("vault:ha relation removed", not any("vault:ha" in pair for pair in rels)), +] +for desc, p in checks: + ok &= p + print(" %-34s : %s" % (desc, "PASS" if p else "FAIL")) +nvip = sum(1 for ap in apps.values() + if isinstance(ap, dict) and isinstance(ap.get("options"), dict) + and str(ap["options"].get("vip", "")).startswith("10.12.4.")) +pv = (nvip == 10) +ok &= pv +print(" %-34s : %s" % ("VIP count == 10 (was 11)", "PASS (%d)" % nvip if pv else "FAIL (%d)" % nvip)) +print("\nRESULT:", "ALL CHECKS PASS" + if ok else "FAILURES - revert: cp %s %s" % (bak, PATH)) +sys.exit(0 if ok else 2)