fix(upgrade): keep the cluster serviceable across the Antelope -> Caracal rolling window - #1431
Conversation
df990a0 to
43fd1d4
Compare
SekiXu
left a comment
There was a problem hiding this comment.
The shim keeps subnet reads working across the mixed window, but I think it breaks subnet
creation on the still-Antelope servers.
Antelope declares the column with a python-side default (neutron/db/models_v2.py:54,
unmaintained/2023.1):
in_use = sa.Column(sa.Boolean(), nullable=False,
server_default=sql.false(), default=False)
default=False means SQLAlchemy names in_use in the INSERT for every new subnet, and MariaDB
refuses a value supplied for a generated column. Measured on 10.11.19 against the schema this
ALTER produces:
- Caracal-style INSERT (does not name
in_use) -> OK, reads back 0 - full-model SELECT,
FOR UPDATE,LOCK IN SHARE MODE-> all OK, so the read half does work,
and you are right that neither lock register writes the column - Antelope-style INSERT naming
in_use = 0:sql_mode=TRADITIONAL->ERROR 1906 (HY000)sql_mode=''-> warning only, insert succeeds
oslo.db's mysql_sql_mode defaults to TRADITIONAL and nothing in the tree overrides it (only
the commented #mysql_sql_mode = TRADITIONAL in the *.conf.sample files), so the strict path is
the one that applies.
The good news is that this one does not need a generated column. The portforwardings shim did,
because the values had to be derived from the new columns. Here the value only has to read
false, and Caracal's HasInUse in 2024.1 has no in_use attribute at all, so a plain column
answers both dialects:
ALTER TABLE subnets ADD COLUMN in_use tinyint(1) NOT NULL DEFAULT 0
Antelope's INSERT names it and is accepted, Caracal's does not and takes the default, and
migrate_neutron_db_post() drops it unchanged.
The rest of this one looks right to me. CONFIG_REQUIRES(neutron_last, pacemaker_last) really
does order CommitLast after the ovndb promotion, $HEX_SDK shared_id resolves
(core/main/proj_functions:170, sourced unconditionally by the dispatcher), and writing the OVN
sync marker only on success is the fix that stops one early failure riding onto the next
partition.
…l window 2023.2/expand/93f394357a27_remove_in_use_on_subnets.py drops subnets.in_use. Upstream put it in the expand branch -- it declares an expand_drop_exceptions() to opt out of the no-drops-in-expand rule -- so there is no phased form that leaves the column standing. The moment the first node migrates the shared schema, every still-Antelope neutron-server answers 500 to any subnet query with (1054, "Unknown column 'subnets.in_use' in 'SELECT'"), because Antelope's models_v2.HasInUse declares it as a real column and Subnet mixes it in. That is not one broken call: it takes out the subnet API on 2 of 3 servers behind the VIP, and with it the live migration that rolling_upgrade drains each node with. Deferring the migration only inverts which servers are broken, and shrinks the healthy pool as the roll proceeds instead of growing it. Caracal stopped using the column (it takes the row lock with SELECT ... FOR UPDATE and keeps the attribute only so back-ports need no schema change), so it is enough that the value reads false: re-add it as a generated column and the migrated schema answers both dialects. Antelope then reads the flag as "not in use", so read/write_lock_register stop guarding concurrent subnet deletes for the length of the window -- the accepted trade, and the same one the Yoga shim made for port forwardings. migrate_neutron_db_post() drops it once os_neutron_version_uniform reports no Antelope server left. Retire the Yoga <-> Antelope portforwardings shim in the same change. The supported path is stepwise -- 3.1.0 (Yoga) -> 3.1.10 (Antelope) -> 3.1.20 (Caracal), no jumping -- so a Caracal build can never meet a Yoga server and that shim was dead code here. Exactly one shim is carried at a time; the comment now says so, so the next hop swaps rather than accumulates. Verified on cube4510 2026-09-05: with the schema migrated and no shim, 0 of 13 VMs could be evacuated off cube452 and neutron on the two Antelope nodes returned HTTP 500 while the Caracal node returned 200. With the generated column in place all three nodes and the VIP return 200 and the drain runs. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
/etc/ovn lives on the A/B root partition, so an upgraded node boots with an empty northbound and the networks exist only in neutron's MySQL until migrate_neutron_ovn_sync rebuilds them. Until it does, the OVN mechanism driver fails every port bind with RowNotFound: Cannot find Logical_Switch with name=neutron-<network-id> which takes out port binding, and with it the live migration that rolling_upgrade drains each node with. The sync could never have worked where it was. config_neutron's Commit() runs at bootstrap stage 14; ovndb_servers is promoted by pacemaker_last at stage 19, and CONFIG_REQUIRES(neutron_last, pacemaker_last) is what orders anything after it. Measured on cube4510: the sync ran at 19:08:58 and the northbound ovsdb-server did not start until 19:17 -- it synced against a database that was not listening and silently did nothing. Move the call to CommitLast(), which is already the "ovn-northd is now managed by pacemaker" point. Then make it impossible for this to wedge a roll. It runs inside a hex_config commit, so blocking here blocks the node's whole bootstrap and its slot in a rolling upgrade -- and HexUtilSystemF only arms alarm() when its timeout argument is non-zero, so the old call (0) had no bound at all. Now: the northbound probe is a bounded safety net for a slow promotion rather than the mechanism (24 * (5s connect + 5s sleep) = 4 minutes), the sync itself gets timeout 600, and the outer HexUtilSystemF gets 900s as a backstop above both. Finally, only mark the migration done when the sync actually succeeded. Marking it unconditionally turned that one early failure into a permanent skip: the marker lives under /etc/appliance/state, which is CONFIG_MIGRATE'd, so it rode onto the next partition and no later boot ever retried. Every failure path now logs and returns without the marker, leaving it to the next boot. Verified on cube4510 2026-09-05: cube451 came up on 3.1.20 with 0 logical switches against 17 neutron networks, and port binding failed cluster-wide. Running the fixed migrate_neutron_ovn_sync rebuilt all 17 and set the marker on success; cube452 then picked them up from the promoted master by ovsdb replication, and the drain that had moved 0 of 13 VMs completed. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The OVN databases live under /etc/ovn on the A/B root partition, so an upgraded node boots with an empty northbound. Normally survivable -- ovsdb-server comes back as a backup and syncs from the promoted master -- but rolling_update rolls the master FIRST, so the node holding the only live copy is the one that reboots into the empty one. Pacemaker re-promotes it, the backups sync the empty DB from it, and the last good copy is gone cluster-wide. Belt to migrate_neutron_ovn_sync()'s braces rather than a replacement: the northbound is derived state and neutron's MySQL is the source of truth, so the sync still reconciles drift. It earns its place by removing the single point of total loss -- with only the sync, one failed sync leaves no OVN data anywhere. Safe across an OVN version bump: ovn-ctl's upgrade_db converts in place (NB 7.0.0 -> 7.3.0 and SB 20.27.0 -> 20.33.0 verified byte-identical in ovn-nbctl/ovn-sbctl show), and on a failed convert creates an empty database -- degrading to exactly the behaviour without this line. The /var/lib/ovn entry it replaces migrated nothing live: neither ovn23.03 nor ovn24.03 owns anything under that path, 3.1.10 nodes have no such directory, and rpm -qf on the files there reports they are not owned by any package -- unowned leftovers from an older layout, hauled partition to partition since the DBs moved. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Caracal ships cinder.volume.drivers.dell_emc.sc.storagecenter_fc.SCFCDriver with SUPPORTED = False, so cinder-volume's manager refuses to initialize the backend unless enable_unsupported_driver is set on it. Antelope's copy of the driver carries no SUPPORTED attribute at all, so this only bites on the hop. The failure is indirect and easy to misread. The backend never completes do_setup, so the first symptom is not a driver error but every attach failing with Unable to create attachment for volume ... Driver initialize connection failed (error: 'SCFCDriver' object has no attribute '_client') and the service reporting itself down while systemd still shows it active. Any live migration of a VM holding a Compellent volume fails with it. Upstream sets the flag on drivers whose third-party CI has stopped reporting; 12 drivers carry it in Caracal, including three other Dell EMC ones. The SC Series is a supported external storage model for us, so we accept the CI non-compliance and ship the opt-in with the model rather than asking operators to add it. Of the five models we ship, only this one is affected -- PowerStore, both Fujitsu drivers and NFS are still supported upstream. Verified on cube4510 after its 3.1.10 -> 3.1.20 upgrade: cinder-volume logged "Unsupported drivers are disabled" at startup and dell-emc-sc-fc@dell-emc-sc-fc sat down; with the opt-in it reports "Driver post RPC initialization completed successfully", the service shows enabled/up, and live migration of a Compellent-backed instance succeeds. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…driver gate 87cba37 put enable_unsupported_driver in the built-in SC Series model, but a model only reaches backends created or re-applied after the upgrade. /etc/cinder/backends is CONFIG_MIGRATE'd, so an upgraded cluster carries its pre-Caracal backend config forward verbatim and the driver stays dead -- every attach failing with 'SCFCDriver' object has no attribute '_client', and with it the live migration a rolling upgrade drains each node with. Add migrate_cinder_ext_storage_unsupported() and call it from SetStorageBackend(), after CINDER_BACKEND_DIR is settled -- written by the storage API, migrated forward by CONFIG_MIGRATE -- and immediately before that function wipes cinder.d/ext_storage_*.conf and re-copies them from there. That placement is what makes the fix both durable and effective in the same commit: backends/ is the source of truth, so rewriting it survives the copy, and the copy carries the opt-in into the running config now rather than on the next commit. Only backends/ is rewritten, for the same reason -- anything written to cinder.d would be deleted by the rm seconds later. The key is inserted after volume_driver rather than appended: these files hold one section each today, but appending would land outside the section the day one does not. iSCSI is matched too -- same upstream flag, same failure -- though only the FC model ships built in. Marker-gated and idempotent, and the call is bounded at 120s so a wedged migration cannot block the commit. Verified against the real backend config off cube4510: the key lands immediately after volume_driver inside [dell-emc-sc-fc], a second pass is a no-op, and a ceph backend is skipped. Applied by hand to all three nodes during the 3.1.10 -> 3.1.20 roll, after which cinder-volume reports dell-emc-sc-fc@dell-emc-sc-fc enabled/up and Compellent-backed instances live migrate again. Signed-off-by: Jim Lin <jim.lin@bigstack.co> Co-authored-by: Eandalf <clinah@connect.ust.hk> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ndoing it
cube-cos-login.spec already builds the image with
`kc.sh build --transaction-xa-enabled=false`, because MariaDB refuses XA whenever
wsrep is on -- "This version of MariaDB doesn't yet support 'XA transactions with
Galera replication'" -- which aborts keycloak's first bootstrap after the default
client scopes are committed but before MIGRATION_MODEL is stamped, leaving the
realm half-migrated. That build is correct and the shipped image really does bake
the option.
The server then throws it away at startup. Keycloak compares its baked build
config against env plus defaults, and every other baked build option is mirrored
in the environment -- db, cache, health, metrics, http-relative-path -- while
transaction-xa-enabled was not. So it saw baked=false against default=xa, decided
the optimized image was stale, re-augmented, and the rebuild reset the option:
Changes detected in configuration. Updating the server image.
The previous optimized build will be overridden with the following build options:
- transaction-xa-enabled=false > transaction-xa-enabled=xa
Asserting the value here makes current == baked, so nothing re-augments over it.
This is why the fix looked like a regression while the source was untouched:
06893e78 in cube-cos-ui is still on the default branch, and the deployed image was
built on 2026-09-04 with the flag in it. Nothing reverted -- the option was being
overridden on every single pod start.
Verified on cube4510 after its 3.1.10 -> 3.1.20 upgrade. Before: all three
keycloak pods logged the XA/Galera error, keycloak's admin API answered 401 to a
valid admin token (the token carried no realm_access claim at all), cube-cos-api
on cube453 crash-looped on "failed to init oidc auth in keycloak(401)", and
cluster check reported ApiService NG with api(1 control api down). After: the
override line is gone, 0 XA errors across all three pods, /auth/admin/realms
returns 200, cube-cos-api starts with 0 OIDC failures, and health_api_check
returns 0.
Signed-off-by: Jim Lin <jim.lin@bigstack.co>
Co-authored-by: Eandalf <clinah@connect.ust.hk>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Logstash renders a missing field as the literal "%{name}", and es-index feeds
straight into the opensearch output as "logs-%{es-index}", so an event that
reaches the end of the filter without [program] is indexed into a real index
called "logs-<date>-%{program}", braces and all.
a56f9a2 already fixed the cause by defaulting [program], and it holds -- but
only on a build that carries it. cube4510 ran 3.1.10, which predates a56f9a2,
and accumulated seven such indices totalling 2.3GB, 3.5M documents on 09-05
alone. They stopped the moment it was upgraded to 3.1.20: over a 70s window the
malformed index gained 0 documents while logs-20260905-keystone_access, the very
source that had been feeding it, gained 370.
So this guard fixes nothing that is broken today, and it is deliberately not a
substitute for the fallbacks. It is here because the failure mode is silent,
unbounded and invisible until someone reads an index list: any future field that
goes missing upstream of the index name mints a new literal-braced index every
day, and the documents are then only findable by someone who thinks to search for
braces. Collapsing any unresolved reference to "unknown" keeps them searchable
and makes the condition obvious.
Verified with logstash --config.test_and_exit against the rendered config on
cube451: Configuration OK. The substitution itself: "20260905-%{program}" ->
"20260905-unknown", well-formed names untouched. The seven historical indices
were reindexed into correctly named ones and removed.
Signed-off-by: Jim Lin <jim.lin@bigstack.co>
Co-authored-by: Eandalf <clinah@connect.ust.hk>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
43fd1d4 to
5a47a16
Compare
…d one
Antelope's models_v2.HasInUse declares in_use with a *python-side* default
(default=False), so SQLAlchemy names it in the INSERT for every subnet
neutron creates -- INSERT INTO subnets (id, network_id, in_use) VALUES
('s1', 'n1', false) -- and MariaDB refuses a supplied value for a generated
column. That is ERROR 1906 under any strict sql_mode, which covers both
oslo.db's mysql_sql_mode=TRADITIONAL default (nothing in this tree overrides
it) and our own server-wide STRICT_TRANS_TABLES, so it does not depend on an
oslo.db setting at all. Under sql_mode='' it degrades to a warning and the
row inserts, which is why a loose-mode scratch test looks fine.
The generated column therefore kept subnet *reads* alive across the mixed
window -- which is what it was verified against -- and would have broken
subnet *create* on every still-Antelope server for the window's whole
length. The read half was never the problem: full-model SELECT, FOR UPDATE
and LOCK IN SHARE MODE all pass against either shape.
Nothing here has to be derived, because the value only has to read false, so
a plain column answers both dialects: Antelope names it and the value is
accepted, Caracal never names it (2024.1's HasInUse carries no in_use
attribute) and takes the DEFAULT. tinyint(1) NOT NULL DEFAULT 0 is
byte-identical in information_schema to what
ussuri/expand/d8bdf05313f4_add_in_use_to_subnet.py originally created, and
migrate_neutron_db_post()'s DROP COLUMN IF EXISTS is unchanged.
Correct the claim the previous message made while here: the shim costs no
locking guarantee on either side. In both 22.2.1 and 24.2.2
read_lock_register and write_lock_register only take a row lock with
SELECT ... FOR UPDATE / LOCK IN SHARE MODE and never read or write the flag,
so subnet-delete guarding keeps working throughout. (Neutron 22.0.x did
UPDATE ... SET in_use=True, which a generated column also rejects with
ERROR 1906 -- but 3.1.10 ships 22.2.1, past that change.)
Reported by SekiXu in review on #1431.
Verified on jim-1cc against MariaDB 10.11.18, first on a CREATE TABLE LIKE
copy of the live neutron.subnets and then on the real table: SQLAlchemy
1.4.51 -- the shipped one -- emits the two INSERT shapes above; the generated
column rejects Antelope's with ERROR 1906 under TRADITIONAL and under
STRICT_TRANS_TABLES; the plain column accepts both and reads back 0 either
way; and the ALTER plus DROP COLUMN IF EXISTS ran clean against the real
table and left the schema as it was.
Signed-off-by: Jim Lin <jim.lin@bigstack.co>
Co-authored-by: Eandalf <clinah@connect.ust.hk>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@SekiXu thank you — this was a real catch, and a precise one. You were right that the generated column only fixes half the problem, and right about the mechanism: the I reproduced all of it on [cc1 ~]# # what each release's ORM actually emits (SQLAlchemy 1.4.51, the shipped one)
antelope INSERT INTO subnets (id, network_id, in_use) VALUES ('s1', 'n1', false)
caracal INSERT INTO subnets (id, network_id) VALUES ('s1', 'n1')
[cc1 ~]# # generated column, Antelope-style INSERT
sql_mode=TRADITIONAL ERROR 1906 row not inserted
sql_mode=STRICT_TRANS_TABLES ERROR 1906 row not inserted
sql_mode='' Warning 1906 ... has been ignored (row inserted)
[cc1 ~]# # plain column, both dialects, every sql_mode
antelope INSERT OK in_use reads back 0
caracal INSERT OK in_use reads back 0
[cc1 ~]# # the read half was never the problem -- both shapes pass
full-model SELECT / FOR UPDATE / LOCK IN SHARE MODE ok
[cc1 ~]# # information_schema: the plain ALTER vs Antelope's own BOOL NOT NULL DEFAULT false
in_use tinyint(1) NO 0 <- identicalThree things worth adding to what you found: It does not depend on the oslo.db setting at all. You are right that nothing in the tree overrides The column shape is byte-identical to the original. Your remark that neither lock register writes the column also retires the trade-off I claimed. I had written that the shim costs Antelope's concurrent-subnet-delete guarding; checking both releases, that was simply wrong. In 22.2.1 and 24.2.2 alike, The general rule I have taken from this, and recorded in the handbook: a generated column can only shim a dropped column if the old ORM never names it in a write, so the thing to check is a python-side Thanks also for verifying the other half — |
SekiXu
left a comment
There was a problem hiding this comment.
3824776a is exactly right. I checked the reasoning against the upstream tags rather than
taking it on trust:
22.2.1models_v2.py:55—in_use = sa.Column(sa.Boolean(), nullable=False, server_default=sql.false(), default=False). The python-sidedefault=Falseis there.24.2.2models_v2.py:33-52—HasInUse's body is the two classmethods and nothing
else, nosa.Columnat all, so the new side never names it.- Both tags' lock registers are
.enable_eagerloads(False).with_for_update()/
.with_for_update(read=True)and nothing more. HasInUseis mixed into exactly one model in both tags (models_v2.py:202/:196,
Subnet), and93f394357a27'supgrade()is the singleop.drop_column, so the blast
radius is one table.
Approving. One thing left, and it is the description rather than the code — the comment at
sdk_migrate.sh:283 reads correctly now and the handbook entry is clean, so the body is the
only surface still describing the generated column: the summary bullet, the "Why
subnets.in_use is re-added" paragraph with the locking trade-off you already retired, and
the evidence section ("With the generated column in place all three nodes and the VIP return
200"). Your comment supersedes it for anyone reading the thread, but the AC is written in the
body, so I would rather it not need the thread.
No re-run needed for that last one: on the read path the plain column is a strict superset of
the generated one, and that is all the 200/drain result rests on, so the evidence transfers
and only the wording has to stop naming the shape.
One note for whoever re-tests. migrate_neutron_db() returns early on
$STATE_DIR/neutron_db_migrated, and the guard at sdk_migrate.sh:297 counts the column
without checking its shape — so a node that already ran the old shim keeps the generated
column under the fixed build. cube4510 and jim-1cc are both in that state: drop the column
and remove the state file first, or the re-test silently exercises the old shape. Not asking
for a code change to defend an unreleased intermediate commit.
What type of PR is this?
What this PR does / why we need it
Everything here came out of the first real v3.1.10 -> v3.1.20 rolling upgrade (OpenStack Antelope -> Caracal), run end to end on
cube4510. The roll wedged three separate times, each on a different service, and each time on the same underlying shape: a rolling upgrade runs a mixed-version cluster for its whole duration, and it drains every node by live migration. Anything that breaks port binding, volume attach, or the subnet API during that window does not degrade the roll — it stops it, because the drain cannot move a single VM.2023.2/expand/93f394357a27_remove_in_use_on_subnets.pydropssubnets.in_use, and upstream put it in the expand branch with anexpand_drop_exceptions()opt-out, so there is no phased form that leaves the column standing. The moment the first node migrates the shared schema, every still-Antelopeneutron-serveranswers 500 to any subnet query —(1054, "Unknown column 'subnets.in_use' in 'SELECT'")— because Antelope'smodels_v2.HasInUsedeclares it as a real column. That is 2 of 3 servers behind the VIP, and with them the live migration the drain depends on. Re-adding it as a generated column answers both dialects; Caracal already stopped reading it, so the value only has to read false.migrate_neutron_db_post()drops it again onceos_neutron_version_uniformreports no Antelope server left/etc/ovnwas not inCONFIG_MIGRATE, so an upgraded node boots with an empty northbound. Survivable in isolation — a backup syncs from the promoted master — exceptrolling_upgraderolls the master first, so the node holding the only live copy is the one that reboots into the empty one. Pacemaker re-promotes it, the backups sync the empty DB from it, and the last good copy is gone cluster-wide. Every port bind then fails withRowNotFound: Cannot find Logical_Switch with name=neutron-<network-id>ovndb, and only claims success when it succeeded. It was called fromconfig_neutron'sCommit()at bootstrap stage 14;ovndb_serversis promoted at stage 19. Measured on cube4510: the sync ran at 19:08:58 and the northboundovsdb-serverdid not start until 19:17 — it synced against a database that was not listening and silently did nothing, then marked itself done. The marker lives under/etc/appliance/state, which isCONFIG_MIGRATE'd, so that one early failure rode onto the next partition and no later boot ever retried. Moved toCommitLast(), bounded at every level, and the marker is now only written on successSCFCDriverwithSUPPORTED = False, socinder-volumerefuses to initialize it withoutenable_unsupported_driver. Antelope's copy has noSUPPORTEDattribute at all, so this only bites on the hop. Two halves: the built-in model carries the opt-in, andmigrate_cinder_ext_storage_unsupported()rewrites the backends an upgraded cluster brought forward (see the note below — we are deliberately shipping a driver upstream labels unsupported)cube-cos-login.specbuilds the image withkc.sh build --transaction-xa-enabled=falsebecause MariaDB refuses XA whenever wsrep is on. That build is correct and the shipped image really does bake it — but Keycloak compares baked build config against env plus defaults, and this was the one baked option not mirrored in the environment. So it sawbaked=falsevsdefault=xa, decided the optimized image was stale, and re-augmented back to the default on every start%{name}, andes-indexfeeds straight intologs-%{es-index}, so an event reaching the end of the filter without[program]is indexed into a real index calledlogs-<date>-%{program}, braces and allWhich issue(s) this PR fixes
Special notes for your reviewer
We are knowingly shipping a driver upstream labels unsupported. Caracal marks
SCFCDriverwithSUPPORTED = False— upstream sets that flag on drivers whose third-party CI has stopped reporting, and 12 drivers carry it in Caracal including three other Dell EMC ones. The SC Series is a supported external storage model for us, so we accept the CI non-compliance and ship the opt-in with the model rather than asking operators to add it themselves and discover the requirement through a failed attach. Of the five external models we ship, only this one is affected — PowerStore, both Fujitsu drivers and NFS are still supported upstream. The practical consequence to be aware of: upstream is no longer gating changes to this driver on hardware CI, so regressions in it will reach us unannounced.Why the fix is in two commits and not one. A built-in model only reaches backends created or re-applied after the upgrade.
/etc/cinder/backendsisCONFIG_MIGRATE'd, so an upgraded cluster carries its pre-Caracal backend config forward verbatim and the model alone would leave the driver dead on exactly the clusters that matter.migrate_cinder_ext_storage_unsupported()is placed inSetStorageBackend()afterCINDER_BACKEND_DIRis settled and immediately before that function wipescinder.d/ext_storage_*.confand re-copies from it — which is what makes one commit both durable and effective now:backends/is the source of truth so the rewrite survives, and the copy carries the opt-in into the running config on this commit rather than the next.Why
subnets.in_useis re-added rather than the migration deferred. Deferring only inverts which servers are broken, and shrinks the healthy pool as the roll proceeds instead of growing it. The accepted trade with the generated column is that Antelope reads the flag as "not in use", soread/write_lock_registerstop guarding concurrent subnet deletes for the length of the window — the same trade the Yoga shim made for port forwardings. That Yoga shim is retired in the same commit: the supported path is stepwise, 3.1.0 (Yoga) -> 3.1.10 (Antelope) -> 3.1.20 (Caracal) with no jumping, so a Caracal build can never meet a Yoga server. Exactly one shim is carried at a time, and the comment now says so, so the next hop swaps rather than accumulates.The
/etc/ovnmigrate and the OVN sync are belt and braces, deliberately. The northbound is derived state and neutron's MySQL is the source of truth, so the sync alone is defensible — but with only the sync, one failed sync leaves no OVN data anywhere. The migrate removes the single point of total loss. It is safe across an OVN version bump:ovn-ctl'supgrade_dbconverts in place (NB 7.0.0 -> 7.3.0 and SB 20.27.0 -> 20.33.0 verified byte-identical inovn-nbctl/ovn-sbctl show), and a failed convert creates an empty database — degrading to exactly the behaviour without the line. The/var/lib/ovnentry it replaces migrated nothing live: neitherovn23.03norovn24.03owns anything there, 3.1.10 nodes have no such directory, andrpm -qfreports the files as owned by no package.The logstash guard fixes nothing that is broken today, and that is the point.
a56f9a24already fixed the cause by defaulting[program], and it holds — but only on a build that carries it. This is a guard against the class: any future field that goes missing upstream of the index name would mint a new literal-braced index every day, silently and unbounded, findable only by someone who thinks to search for braces. It is explicitly not a substitute for the fallbacks.Additional documentation
Verified on
cube4510(cube451/cube452/cube453, control-converged, HA) across a full v3.1.10 -> v3.1.20 rolling upgrade, unless stated otherwise.For the requirement "the subnet API stays up while the cluster is mixed-version",
With the generated column in place all three nodes and the VIP return 200 and the drain runs.
migrate_neutron_db_post()then drops the column once no Antelope server is left — confirmed byos_neutron_version_uniformreporting uniform 24.x after the third node finished.For the requirement "an upgraded node does not lose the OVN northbound",
That is the state the old ordering produced. Running the fixed
migrate_neutron_ovn_syncrebuilt all 17 logical switches and set the marker;cube452then picked them up from the promoted master by ovsdb replication, and the drain that had moved 0 of 13 VMs completed. The ordering itself is the control: the sync ran at 19:08:58 and the northboundovsdb-serverdid not start until 19:17.For the requirement "the sync cannot wedge a roll and cannot skip itself forever",
HexUtilSystemFonly armsalarm()when its timeout argument is non-zero, so the old call — passed0— had no bound at all while running inside ahex_configcommit, where blocking blocks the node's whole bootstrap and its slot in the roll. Every failure path now logs and returns without the marker, so the next boot retries.For the requirement "Compellent-backed instances live migrate on Caracal",
Note the failure is indirect: the backend never completes
do_setup, so the first symptom is not a driver error but every attach failing, with the service reporting itself down while systemd still shows itactive.For the requirement "an upgraded cluster's carried-forward backends get the opt-in",
The key is inserted after
volume_driverrather than appended: these files hold one section each today, but appending would land outside the section the day one does not. iSCSI is matched too — same upstream flag, same failure — though only the FC model ships built in. Applied by hand to all three nodes mid-roll, after which Compellent-backed instances live migrate again.For the requirement "keycloak keeps the option its image was built with",
This is why it looked like a regression while the source was untouched:
06893e78incube-cos-uiis still on the default branch and the deployed image really was built with the flag. Nothing reverted — the option was being overridden on every single pod start.For the requirement "no index is named after an unresolved field reference",
On the historical damage: cube4510 ran 3.1.10, which predates
a56f9a24, and accumulated seven literal-braced indices totalling 2.3GB — 3.5M documents on 09-05 alone. They stopped the moment it upgraded to 3.1.20: over a 70s window the malformed index gained 0 documents whilelogs-20260905-keystone_access, the very source that had been feeding it, gained 370. All seven were reindexed into correctly named indices (0 failures, sampled IDs confirmed present in the correct targets with correct routing) and removed.Worth recording, because it is the reason no repair tooling is shipped here: the malformed names are cosmetic, not a health risk. Tested directly rather than assumed —
The one shard that did wedge during the roll (
logs-20260831-%{program},REALLOCATED_REPLICAstuck at 0%) was a rolling-reboot recovery casualty that happened to land on that index, not a consequence of its name. So an upgraded cluster does not need a cleanup pipeline to stay healthy, and none is added.End state after the full roll: