Phase 2: docs/use-cases/openstack.md¶
Master plan: PLAN-use-case-docs.md
Planned at high effort. Phase 1 was rated high because the material already existed elsewhere and the work was deciding what not to say. This phase inherits that hazard in a milder form and adds three of its own: OpenStack is the first permutation that is not a scraped source, so the page cannot borrow either existing page's shape; the deployment story is genuinely in flight upstream and must be written so that it does not rot; and phase 1's page carries two uncorrected factual errors, one of which this page would directly contradict.
Review effort: medium. The master plan sets no review effort for any page; oVirt's equivalent was reviewed at medium, phase 1 followed, and this phase does the same.
Situation¶
Two use-case pages exist. docs/use-cases/ovirt.md landed
2026-08-10 as PLAN-two-tier-ci-phase-04-docs.md's deliverable
and settles the format; docs/use-cases/shaken-fist.md landed
2026-09-18 as phase 1 of this plan, merge commit 2f0e526, and
confirms it. Both have identical section headings, which is what
makes the format a convention rather than a coincidence.
This phase writes the third, for OpenStack. It is the permutation
with the largest existing footprint in the documentation —
docs/index.md:106-131 carries a full prose section on it — and
the only one whose deployment path is still moving upstream.
Mission¶
Write docs/use-cases/openstack.md: what Kerbside is worth in a
Nova spice-direct deployment, how the push model differs from the
two scraped sources already documented, how to set it up given
that the Kolla-Ansible deployment change has not yet merged, and
what is honestly not proven yet.
Along the way, correct the two factual errors the PR #443 review found in phase 1's page. Neither was auto-filed as an issue, so nothing else tracks them.
Scope¶
In scope:
docs/use-cases/openstack.md, new.- Two corrections to
docs/use-cases/shaken-fist.md(survey finding 7), per decision 4. - One row added to the OpenStack option table in
docs/console-sources.md, per decision 6. - Inbound links from the four places phase 1 used, plus the
sibling link from
shaken-fist.md. - Filing an issue for the code defect in survey finding 6.
Out of scope, each with a reason:
- The
docs/index.mdintroduction slim-down. Phase 5's job, per decision 7. - Fixing the
KeyErrorin survey finding 6. This is a documentation plan; the fix is a code change with its own test. File it, do not do it. - Any change to the
openstack_matrixlane. The page describes what the lane proves; it does not extend it. - Anything about Proxmox, aggregation or placement. Phases 3 and 4.
What the survey found¶
Checked against develop at dcd62d0 on 2026-09-19. The master
plan's phase row reads, in full: "Nova 2025.1 spice-direct;
Kolla-Ansible deployment via kerbside-patches; much of
docs/index.md's OpenStack section moves here". All three clauses
survive, but the third is a phase boundary question rather than a
fact, and is settled by decision 7.
1. There is no kerbside/sources/openstack.py. The sources/
directory holds base.py, ovirt.py, shakenfist.py and
static.py only. kerbside/main.py:139-142 matches
type == 'openstack' in the scrape loop and does
skipped_sources.add(source['source']); continue. Every piece of
OpenStack handling lives inline in kerbside/api.py:536-663
(class NovaToken), routed at api.py:887 as /nova-console.vv.
Consoles are created at exchange time by the db.add_console()
call at api.py:630, not by discovery. This is the finding that
shapes the page: it cannot follow either existing page's
scrape-then-exchange narrative.
2. docs/console-sources.md's OpenStack section is accurate.
Worth stating explicitly, because phase 1's central hazard was an
accurate eighty-line section it risked duplicating. Here the
equivalent is docs/console-sources.md:182-214 — thirty-three
lines, correctly describing the on-demand model at :29-38 and
documenting type: openstack with eight options whose names match
what api.py and main.py:104-129 actually read. The url key
is the Keystone endpoint and is spelled url, not auth_url, in
both the table and the code. One gap only, finding 5.
3. OpenStack consoles are never subject-pinned on the backend
leg. api.py:630-636 calls db.add_console() with uuid,
source, hypervisor, insecure_port and secure_port and
nothing else, so host_subject takes its default of None
(kerbside/db.py:214-216). Nova's validate_console_auth_token
response carries no certificate subject, so there is nothing to
populate it with. The host-subject that does appear in the
generated .vv at api.py:646 is config.PROXY_HOST_SUBJECT —
the proxy's own subject, which pins the client-to-Kerbside leg,
a different leg entirely. Both other sources populate backend
host_subject from the platform. This is the sharpest difference
between this deployment and the two already documented, and
decision 3 puts it in the value proposition rather than only in
the limitations table.
4. The upstream deployment state, queried today. The Gerrit
REST API for topic:spice-direct-consoles returns:
openstack/kolla change 975495, Implement container image build
for kerbside, merged; openstack/kolla-ansible change
976889, Deploy Kerbside with Kolla-Ansible, open; and open
alongside it 988189 (CI scenario jobs), 988913 and 989614 (the two
tempest changes) and 967801 (routable SPICE console IP). Two
related kolla-ansible changes have merged: 967800, Add additional
SPICE configuration options, and 967802, Allow requiring secure
channels with SPICE. So docs/index.md:127-131's claim — image
build merged, deployment code not — is still true, which was the
one claim in that section at risk of having gone stale. Decision 5
covers how the page records it.
5. verify is an undocumented source option.
kerbside/api.py:581-591 reads source.get('verify'), accepting
a bool or the strings 'true'/'false' case-insensitively and
defaulting to True, and passes it to the Keystone session. It
does not appear in the option table at
docs/console-sources.md:197-207, which documents ca_cert
instead. Two TLS knobs on the same source, one documented.
6. A latent KeyError in the same block.
kerbside/api.py:595 formats its error message with
source["name"], but sources.yaml entries are keyed source —
per the documented table, per main.py, and per every other read
in api.py. A source whose verify is neither a bool nor a
string therefore raises KeyError: 'name' instead of logging the
configuration error it was about to report. Out of scope to fix;
step 2d files it.
7. Two uncorrected errors in phase 1's page. PR #443's second
review round raised both, classified both document, and — unlike
round 1 — no issues were auto-filed for them, so nothing tracks
them:
docs/use-cases/shaken-fist.md:19-27says the Shaken Fist path has "no portal to write and no ticket plumbing to build, which is not true of the oVirt or OpenStack paths". True of oVirt. False of OpenStack, which embeds the broker in Nova exactly as Shaken Fist embeds it in its own API, and whose exchange Kerbside serves itself at/nova-console.vv. The real distinctions are that Shaken Fist's verification is offline where Nova's is a callback, and that its client library does mint-and-exchange in one call.docs/use-cases/shaken-fist.md:48,:81and:128-136present the backend leg as unconditionally TLS with CA verification and subject pinning.rust/kerbside-proxy/src/backend.rs:78-110dialstarget.insecure_portwithtls_port: Noneand retries with TLS onlyif is_need_secured(&first_err) && target .secure_port != 0. Where a node's qemu does not demand TLS the leg stays plaintext and neither verification nor pinning happens.ovirt.md:68and:98-104get this right.
8. The index scaffolding already exists. docs/index.md:158
carries an OpenStack row in the Use Cases table, with the CI lane
already named. As in phase 1, the page phase links an existing row
rather than adding one.
9. The lane is real and merge-tier. openstack_matrix
(.github/workflows/functional-tests.yml:861) builds an all-in-one
Kolla deployment from kerbside-patches on a Debian 13 Shaken Fist
guest and runs a curated Tempest subset; docs/testing.md:485-530
is the authority, including why the guest distro is load-bearing.
It runs in the merge queue only, never per-PR — which is a real
limitation for this page to state, since it means OpenStack
regressions are caught later than Shaken Fist or static ones.
Nothing was corrected at source during planning this time: the
master plan's phase row and the index.md description both hold
as written. Phase 1 corrected four claims; this phase corrects
none, which is itself worth one sentence.
Decisions¶
1. The page follows ovirt.md's section structure exactly.
Both existing pages already do. A third that diverged would turn a
convention back into a coincidence, and phase 5 has to slim the
index against a predictable shape.
2. "How it works" is built around push, not scrape. The
diagram and the prose start from a user asking Nova for a console
and end at the relayed session; they do not open with discovery,
because there is none. The page says plainly that there is no
OpenStack source driver and that type: openstack entries are
skipped by the scrape loop, because an operator who has read
console-sources.md will otherwise reasonably expect the
discovery interval to apply.
3. The backend-pinning gap goes in the value proposition, not
only the limitations table. This is the decision most likely to
be argued with: it writes a weakness into the part of the page
that is closest to a sales pitch, and the obvious alternative is a
limitations row where it would still be technically disclosed. It
goes up front anyway, for three reasons. It is the single largest
difference between this deployment and the two already written, so
an operator comparing pages needs it where they are comparing. It
is not a defect to be embarrassed about but a consequence of what
Nova's API returns, which is worth explaining once, properly. And
burying exactly this kind of qualification is how phase 1 shipped
an incorrect security claim — the page said "the backend leg is
pinned" because the qualification had been pushed out of the
narrative. The mitigation is stated alongside: the client-facing
leg is still pinned via PROXY_HOST_SUBJECT, and the backend leg
is still TLS-escalated on demand and firewall-inspected.
4. Phase 1's two errors are corrected in this phase. Not a
separate PR, and not left to the auto-filing, which did not
happen. One of the two is the sentence this page would have to
contradict, so correcting it is a precondition for writing the
page honestly rather than a courtesy. The other is a security
misstatement live on develop right now. The cost is that this
phase's diff touches a file it did not create, which is worth
paying once.
5. Upstream state is recorded as change numbers and a checked
date. docs/index.md:127 currently says "At the time of last
update to this document...", which no reader can falsify and
which has no expiry. The page names change 976889 and says what
its status was on a stated date, so a reader can open it and check
in one click. The same treatment for the merged image-build change
975495.
6. verify is documented in console-sources.md as part of
this phase. One row in an existing table. The page delegates all
source configuration to that table rather than restating it —
which is the phase-1 lesson applied — and delegating to an
incomplete table is the same failure as duplicating a complete
one, just harder to see.
7. The docs/index.md introduction slim-down stays in phase
5. The master plan's page table says "much of docs/index.md's
OpenStack section moves here", which reads like phase 2 work.
Phase 5 exists to do the index slim-down once, after every page
that could absorb material exists; doing OpenStack's share now
leaves the index half-slimmed for three phases and means editing
the same introduction twice. This page is written to supersede
docs/index.md:106-131 — covering everything it covers, better —
so that phase 5's job is a deletion and a link, not a rewrite.
8. The KeyError is filed, not fixed. Per the skill's rule
that a plan records a defect it finds rather than scope-creeping
into fixing it, and per phase 1's precedent with #444.
Steps¶
| Step | Effort | Model | Isolation | Brief for sub-agent |
|---|---|---|---|---|
| 2a | high | opus | none | Write docs/use-cases/openstack.md. Read first, in this order: docs/use-cases/ovirt.md in full (the format authority — sections are Value proposition, How it works, How to set it up with platform-side / Network / Kerbside side / A worked example subsections, User interaction model, Status and limitations, See also; "How it works" opens with a mermaid flowchart TD, and "Status and limitations" is a table of what is not proven, one row each, naming why); docs/use-cases/shaken-fist.md (the sibling, for tone and for how it delegates to the reference pages); docs/console-sources.md:182-214 (what you must not duplicate); kerbside/api.py:536-663, the NovaToken class, which is the entire OpenStack implementation; kerbside/main.py:139-142; docs/testing.md:485-530. The page must cover: the value proposition against Nova's own console story (spice-direct returns a URL pointing at Kerbside, so Kerbside is the console endpoint rather than a bolt-on, and OpenStack deliberately does not expose hypervisor ports to client networks); the push model — no source driver, no scrape, consoles created at exchange time at api.py:630 — per decision 2; the /nova-console.vv exchange at a level that links to console-sources.md#openstack for the option table rather than restating it; the backend-pinning gap per decision 3, in the value proposition and as a limitations row; the merge-tier-only lane per survey finding 9, as a limitations row; setup split into OpenStack side, network, and Kerbside side, with the upstream state per decision 5. Re-query the upstream state yourself rather than trusting this plan's snapshot — curl -s 'https://review.opendev.org/changes/?q=topic:spice-direct-consoles' \| tail -c +6 \| python3 -m json.tool — and use the date you ran it. Constraints: relative links from docs/use-cases/ need ../; mermaid is linted by tools/mermaid-lint.sh, so run it; wrap prose at the width ovirt.md uses; do not touch any other file in this step. |
| 2b | medium | opus | none | Correct two passages in docs/use-cases/shaken-fist.md, both from survey finding 7, and verify each against the code before editing rather than applying the review's suggestion on trust. (i) The "no portal to write and no ticket plumbing to build" contrast at :19-27 is false for OpenStack. Read kerbside/api.py:536-663 and confirm that Kerbside serves the Nova exchange itself, then scope the contrast to oVirt and the scraped sources and replace the OpenStack half with the distinction that does hold: Nova also embeds the broker, but its Kerbside-side validation is a callback to Nova's /os-console-auth-tokens/ API rather than an offline signature check. (ii) The backend leg at :48, :81 and :128-136 is presented as unconditionally TLS with CA verification and subject pinning. Read rust/kerbside-proxy/src/backend.rs:78-110 and confirm the insecure-first dial and the is_need_secured escalation, then mirror how docs/use-cases/ovirt.md:68 and :98-104 phrase it: the diagram edge names the escalation, the prose paragraph spells it out, and the value-proposition bullet names the condition. Note that the page's own Network subsection already requires both ports to be reachable, which only makes sense given insecure-first dialing — the page currently contradicts itself, and after this step it should not. Do not touch anything else in that file. |
| 2c | medium | sonnet | none | Two mechanical edits, no prose invention. (i) docs/console-sources.md — add a verify row to the OpenStack option table at :197-207, between password and project_name or wherever the existing ordering puts it, describing it as: optional, accepts a boolean or the strings true/false, defaults to true, and controls TLS certificate verification for the Keystone session. Read kerbside/api.py:581-591 to confirm the semantics before writing the row. Do not touch the rest of that file except as item (iv) below requires. (ii) docs/index.md:158 — the | OpenStack | ... | row of the Use Cases table: turn the bare scenario cell into a markdown link whose text stays OpenStack and whose target is the new page's path relative to docs/, mirroring exactly how the oVirt and Shaken Fist rows link theirs. (This plan describes the link rather than showing it; see the note under this table.) Leave the description and CI columns alone. (iii) README.md — the curated list has two use-case entries, both in the Per-deployment guide: form; add a third for OpenStack in the same form, using the absolute https://github.com/shakenfist/kerbside/blob/develop/docs/use-cases/openstack.md URL that every link in that list uses. (iv) add the new page to docs/console-sources.md's ## Related Documentation list, and name it in the ## See also section of both docs/use-cases/ovirt.md and docs/use-cases/shaken-fist.md. Do not add a row to any table, and do not touch docs/index.md's introduction — that is phase 5's, per decision 7. |
| 2d | low | sonnet | none | Verification and filing, no edits except to fix what it finds. Run, from the repository root: tools/mermaid-lint.sh; grep -rln 'openstack\.md' docs/ README.md (expect docs/index.md, docs/console-sources.md, docs/use-cases/ovirt.md, docs/use-cases/shaken-fist.md, README.md, and the plan files — note the -l and the escaped dot, which matter: a sibling link inside docs/use-cases/ correctly carries no directory prefix, so a pattern including one can never match it); grep -n 'NEED_SECURED\|need_secured' docs/use-cases/shaken-fist.md (expect at least one hit after 2b); grep -n 'no portal to write' docs/use-cases/shaken-fist.md (expect either no hit, or a hit scoped to oVirt alone); and for every relative link in the new page, resolve it from docs/use-cases/ and confirm both the file and the anchor exist. Then pre-commit run --all-files. Finally, file a GitHub issue for survey finding 6 — kerbside/api.py:595 formats an error with source["name"] where sources.yaml entries are keyed source, so a non-bool, non-string verify raises KeyError instead of logging the configuration error — quoting the line and naming the documented key. Report anything that fails; do not paper over a broken anchor by deleting the link. |
Steps run in order. 2a is the phase; 2b is separable and could in principle be its own change, but see decision 4. 2c and 2d exist so that 2a's and 2b's briefs can both say "do not touch any other file", which is the cheapest way to stop a writing step from wandering.
The gotcha phase 1 recorded still applies and applies to this file
too: kerbside/tests/unit/test_docs_links.py resolves every
relative .md link in every tracked markdown file, plan files
included, against that file's own directory. A plan cannot contain
a literal markdown link to a page under docs/use-cases/ — it
resolves against docs/plans/ and fails tox -e py3. Every
reference to a use-case page in this plan is therefore a code
span, not a link. Phases 3 and 4 inherit this.
Risks and mitigations¶
- The page duplicates
console-sources.md#openstack. The phase-1 hazard, milder — thirty-three lines rather than eighty — but the same failure. Mitigation: decision 2 fixes what the page owns (the push narrative and its consequences) against what it delegates (the option table and the API mechanics); step 2d's reviewer checks the option names do not appear in the new page at all. - Correcting phase 1's page reopens reviewed text and invites a
review round on work already merged. Mitigation: the
corrections are the reviewer's own suggestions, and step 2b
verifies each against
backend.rsandapi.pybefore applying rather than trusting them — which is what turned twodocumentitems into confirmed defects in the first place. - The upstream Gerrit state changes between this plan and the page. Entirely likely; 976889 could merge next week. Mitigation: step 2a re-queries the REST API and dates its answer, and decision 5's change-number form means a stale sentence is one click from being caught rather than unfalsifiable.
- Writing the pinning gap into the value proposition reads as undermining the product. Mitigation: decision 3 requires the mitigation stated alongside the gap; the review checks that the paragraph explains why Nova cannot supply a subject, rather than leaving it as an unexplained absence.
- The phase-1 rule bites again. Forbidding duplication caused phase 1 to assert the negation of a mechanism it was not allowed to describe. Mitigation: carried into the definition of done below, unchanged.
Definition of done¶
Each of these is falsifiable from the tree:
-
docs/use-cases/openstack.mdexists, and its^##headings matchdocs/use-cases/ovirt.md's exactly, in order. - Its "How it works" section contains a mermaid
flowchart TD, andtools/mermaid-lint.shexits zero. - None of the eight option names from
docs/console-sources.md:197-207(source,type,url,username,password,project_name,user_domain_id,project_domain_id) appears in the new page. It links to the table instead. - The page states that there is no OpenStack source driver and
that
type: openstackentries are skipped by the scrape loop. - The absent backend
host_subjectappears both in the value proposition and as a row in the Status and limitations table, and the page distinguishes it fromPROXY_HOST_SUBJECT, which pins a different leg. - A limitations row names
openstack_matrixas merge-tier only, and says what that means for when regressions surface. - The page names at least one Gerrit change number with a status and the date it was checked.
-
grep -n 'no portal to write' docs/use-cases/shaken-fist.mdreturns nothing, or returns a sentence scoped to oVirt alone. -
grep -in 'need.secured' docs/use-cases/shaken-fist.mdreturns at least one hit, and the page no longer says the backend leg is unconditionally pinned. -
verifyappears in the OpenStack option table indocs/console-sources.md. -
grep -rln 'openstack\.md' docs/ README.mdlistsdocs/index.md,docs/console-sources.md,docs/use-cases/ovirt.md,docs/use-cases/shaken-fist.mdandREADME.md. -
pre-commit run --all-filespasses, which includestox -e py3and thereforetest_docs_links.py. - An open GitHub issue exists for survey finding 6: #451, filed by step 2d. Phase 1's two review findings were never auto-filed, which is why this phase had to carry them, so the issue number is recorded here rather than left to the filing.
- The rule forbidding duplication has not been read as licence to deny. No sentence in either page asserts the absence of a mechanism merely because this plan forbade describing it. Forbidding a description is not permission to claim the thing does not happen. (Carried from phase 1, where exactly this produced a false security claim.)
Registration¶
Done in the planning commit, not deferred:
PLAN-use-case-docs.md's Execution table: phase 1 moves toCompletewith merge commit2f0e526recorded, and phase 2 links this file and moves toIn progress.docs/plans/index.md: the plan's row gains phase 1's outcome and this phase's description, reflecting what the survey found.- There is still no
docs/plans/order.ymlin this repository to update.
Back brief¶
Before executing any step of this plan, please back brief the operator as to your understanding of the plan and how the work you intend to do aligns with that plan.
One gate, cheap to raise now and expensive to undo later:
- Before step 2a writes any prose, state the section outline and describe the diagram's nodes and edges in one paragraph. Decision 2 says the narrative runs from the user's request rather than from discovery, and that shape is the thing a reader either follows or does not. Agreeing it before the prose exists is much cheaper than restructuring a finished page, and this is the first page in the set that cannot copy an existing shape.
Steps 2b, 2c and 2d need no gate: 2b is two passages whose target state is specified above, and 2c and 2d are mechanical.