randomparity / randomparity/kdive

Epic: BYO host provider — adopt-only x86 bare metal and ppc64le PowerVM LPARs

Open
#1,814 3 comments 0 reactions 0 assignees View on GitHub
area:control-retrieve area:providers area:provisioning provider:baremetal-bmc provider:powervm type:epic
Dominant language
Python
Stars
0
Forks
0
Avg merge
1h 26m
Merged PRs (30d)
311

Description

# Epic: BYO host provider — adopt-only x86 bare metal and ppc64le PowerVM LPARs

## Problem

KDIVE drives the kernel build → boot → crash → debug lifecycle against machines it creates
itself. Every provider today is a hypervisor client: `local_libvirt` defines a domain on the
worker's own libvirtd, `remote_libvirt` defines one over `qemu+tls://`, `fault_inject` synthesizes
one. All three answer `Provisioner.provision` (`src/kdive/providers/ports/lifecycle.py:88`) by
*creating* a machine.

That excludes the hardware kernel developers actually need:

- **Machines the platform must not create.** Lab hosts, loaner systems, and certification rigs
are provisioned by a pipeline the operator already owns. KDIVE has no business re-imaging them,
and in most orgs no authority to.
- **Behavior that only reproduces on metal.** Real device drivers, real IOMMU, real NUMA, real
firmware. A bug that vanishes under QEMU cannot be debugged on any provider KDIVE has.
- **PowerVM LPARs.** A PowerVM LPAR is *never* created by KDIVE — PowerVM creates it. The only
sane model is adopt-an-existing-LPAR, which no current provider shape expresses.

The ppc64le work (epic #1139) proved KDIVE's *guest* stack on POWER — `arch_traits()`
(`src/kdive/domain/platform/arch_traits.py:85`), arch-keyed kernel artifacts (ADR-0343), multiarch
gdb (`src/kdive/providers/shared/debug_common/gdbmi/policy/arch.py:28`), and fadump
(`src/kdive/domain/capture.py:16`) all exist. But every one runs against a QEMU `pseries` guest
under TCG. There is no path to a real POWER LPAR, so the second architecture is proven only in
emulation.

The gap is not a missing feature on an existing provider. It is a provider *shape* KDIVE has never
had: one that adopts a machine instead of creating it, and reaches it out-of-band when the in-band
path is gone.

**Roadmap position.** This epic spans two roadmap milestones at once:
`docs/design/top-level-design.md:581` scopes M4 as bare metal (PXE/SoL/IPMI/Redfish) and `:583`
scopes M5 as PowerVM/HMC. The adopt-only decision removes PXE from M4, and the single-provider
decision merges the two control planes behind one seam — so the two land together rather than
sequentially.

## Relationship to epic #1760

**This epic is a competing approach to #1760, not a successor to it. Both are open; neither is
cancelled. The control-plane decision between them is deliberately still outstanding.**

#1760 selects **MAAS as an external provisioning control plane** — commissioning, PXE, base-image
deployment, and a claim/release saga against `ResourceKind.MAAS`. This epic selects **no control
plane at all**: the operator brings a running host and KDIVE reaches it directly over SSH and its
service processor.

Three of #1760's own sub-issues motivate having this alternative on the table:

- **#1793** — MAAS publishes no ppc64el custom-image template (`packer-maas` covers x86_64 and
aarch64 only), so #1760's Power half depends on unproven upstream image work.
- **#1792** — Beaker is a closer fit than MAAS, and an already-populated ppc64le PowerVM Beaker
fleet exists with the `lpar` (IBM HMC) power type working.
- **#1808** — neither MAAS nor Beaker can compose an LPAR, and both HMC drivers are thin SSH
screen-scrapers: MAAS's raises `NotImplementedError` for reset, and Beaker's `lpar` script
rejects `interrupt` outright. **A control plane that cannot issue an NMI cannot drive KDIVE's
force-crash plane**, which is the whole point of the product.

Adopt-only sidesteps all three: no image pipeline, no commissioning saga, and direct OOB access to
the crash triggers a wrapped driver refuses to expose.

### Known overlap — deliberate, not accidental

Roughly ten of #1760's sub-issues describe **host-facing planes that do not depend on how the
machine was provisioned**, and they overlap this epic's entries 11–16 and 19–20:

| #1760 sub-issue | Overlaps this epic's entry |
|---|---|
| #1774 Define shared serial endpoint leasing and arbitration | 11, 14 |
| #1775 Capture physical x86 Redfish/IPMI SoL artifacts | 11 |
| #1776 Capture PowerVM HMC virtual-terminal artifacts | 6, 11 |
| #1777 Add KGDB over the shared serial endpoint | 14 |
| #1779 Add fail-closed x86 hardware crash mechanisms | 12 |
| #1780 Add authenticated SysRq crash control | 12 |
| #1781 Retrieve baseline kdump vmcores on both architectures | 13 |
| #1782 Prove PowerVM FADUMP through the shared vmcore path | 13, 20 |
| #1784 Recover stale bare-metal serial and KGDB sessions | 16 |
| #1785 Prove and document the two-ecosystem bare-metal spine | 19, 20 |

These are **not** adopted into this epic: each is already a native sub-issue of #1760, and
re-parenting them would mutate that epic while its approach is still live. Whichever control-plane
direction wins, the losing epic's overlapping entries should be closed as duplicates at that point
— not before. Until then, do not work an overlapping pair in parallel.

## Goals

1. **Adopt, never create.** An operator declares an already-running host in `systems.toml`; KDIVE
validates it, mints a System against it, and drives the full spine — install → boot →
force-crash → capture vmcore → debug → release. No OS install, no re-image, no PXE.
2. **Out-of-band control as a first-class plane.** Power and console reach the host through its
service processor — Redfish/IPMI on x86, the HMC on a PowerVM LPAR — so a wedged kernel is
recoverable without operator hands.
3. **Both architectures, one provider.** A single `providers/byo_host/` package with a pluggable
OOB driver, following the ppc64le precedent (one provider, arch-keyed) rather than the
remote-libvirt precedent (a package per platform).
4. **Live debug on metal.** KGDB over the OOB serial console gives breakpoints and single-step on
real hardware; in-target drgn and worker-side vmcore postmortem cover the rest.
5. **A host that returns to a known state.** Release restores the operator's baseline kernel,
power-cycles out-of-band, and re-verifies — or cordons the host with a reason. A crashed host
never silently becomes the next allocation's starting point.
6. **Real proof on real hardware, both arches.** An x86 host with a live BMC and a PowerVM LPAR
with HMC access each produce a real vmcore before this epic closes.

## Non-goals

- **No OS install or re-image.** No PXE, kickstart, NIM, or Beaker integration. The operator brings
a host that already boots a kdive-ready OS.
- **No LPAR lifecycle management.** KDIVE does not create, delete, or DLPAR-resize an LPAR. It
adopts an existing LPAR and uses the HMC only for power and vterm.
- **No firmware management** — no BIOS/UEFI settings, firmware updates, or boot-order changes as a
normal operation.
- **No snapshot or restore.** No hypervisor to checkpoint; `supports_snapshots` stays `False`,
exactly as `src/kdive/providers/core/runtime.py:78` anticipates.
- **No host-side traffic capture** (`supports_traffic_capture` stays `False`) and **no `host_dump`
capture method** — both need a hypervisor vantage that does not exist here.
- **No PowerNV / OpenPOWER bare metal.** ppc64le scope is PowerVM LPARs only.
- **Not a build host.** A BYO host is a target; ADR-0099 build-host targets are unaffected.
- **No shared `libvirt_common` refactor** and no re-litigation of ADR-0076's bounded-duplication
decision. BYO touches no libvirt code.
- **No new dispatch architecture.** This rides the ADR-0063 typed `ProviderRuntime` seam, as
AGENTS.md directs for PowerVM absent a new ADR.

## Requirements

### R1 — Declaration (config-owned, bind-only)

- A `[[byo_host]]` array-of-tables parsed by a `ByoHostInstance` model beside
`RemoteLibvirtInstance` (`src/kdive/inventory/model.py:208`) and registered on `InventoryDoc`
(`:283`). Fields: `name`, `arch`, `ssh_target`, a `[byo_host.oob]` sub-table (`kind` =
`redfish`|`ipmi`|`hmc`, `endpoint`, credential refs, plus `managed_system`/`lpar_name` for
`hmc`), `baseline_kernel`, `vcpus`, `memory_mb`, `cost_class`, `pool`,
`concurrent_allocation_cap`.
- `vcpus`/`memory_mb` **required**, matching the remote-libvirt rule
(`src/kdive/inventory/model.py:208`) — a host without a declared ceiling is un-grantable.
- `ResourceKind.BYO_HOST = "byo-host"` (`src/kdive/domain/catalog/resources.py:17`) plus one
migration widening the `CHECK (kind IN (...))` established at
`src/kdive/db/schema/0020_resources_kind_remote_libvirt.sql:9`.
- **The migration must also seed a cost-class coefficient.** Admission resolves the coefficient
fail-closed, so a cost class with no row denies every allocation.
`src/kdive/db/schema/0032_remote_cost_class_coefficient.sql:1-9` is the record of this exact bug
shipping once for remote-libvirt; BYO must not repeat it.
- **`arch` must reach `Resource.capabilities`.** `resolve_accel()`
(`src/kdive/services/systems/validation.py:50`) fail-*opens* when a resource advertises no guest
arches, so without this a ppc64le profile is admitted onto an x86 BYO host and fails late at
install. BYO advertises its single declared arch so admission rejects the mismatch up front.
- Registration is **bind-only** (`creates=False`,
`src/kdive/providers/core/discovery_registration.py:39`): `reconcile_resources`
(`src/kdive/inventory/reconcile/resources.py:150`) is the sole creator.
- Secrets are refs only, never material (ADR-0012 / ADR-0087).

### R2 — Provisioning profile

A provider section is not optional — `ProviderRuntime.profile_policy`
(`src/kdive/providers/core/runtime.py:137`) is a required field, and `ProviderSection`
(`src/kdive/profiles/provisioning.py:283`) currently knows only `remote_libvirt_section`.

- `ByoHostProfile` + `ProviderSection.byo_host_section` + a `ByoHostProfilePolicy`.
- **`boot_method` has no correct existing value.** `_pair_boot_method_with_provider`
(`src/kdive/profiles/provisioning.py:394`) binds `disk-image` ⟺ remote-libvirt, and
`direct-kernel` requires `kernel_source_ref` (`:454`). An adopted host boots neither: it has an
existing OS and an existing bootloader, and the kernel arrives *after* provision. This needs a
resolved answer (see Open Questions), and the validator pairing rules must be extended rather
than bypassed.

### R3 — Verification, at two distinct moments

- **Deploy time (schema only):** `reconcile-systems --check`
(`src/kdive/inventory/cli.py:66`) accepts a well-formed `[[byo_host]]` block and rejects a
malformed one, touching neither Postgres nor S3. ADR-0121's no-I/O contract is preserved — no
live probing is added there.
- **Operator pre-flight (live):** a contribution registered through
`diagnostic_provider_contributions()` (`src/kdive/providers/assembly/diagnostics.py:12`),
modeled on `src/kdive/providers/remote_libvirt/diagnostics/reachability.py`. Covers SSH
reachability, OOB endpoint reachability, credential validity, arch match, bootloader flavor,
kdump/fadump readiness, and baseline-kernel presence — **before** any allocation.
- **Adopt time (live, re-checked):** the same preconditions inside `Provisioner.provision`,
because the world drifts between pre-flight and use. Results recorded on the System row.

### R4 — Out-of-band control

- A typed OOB driver port with three implementations: Redfish (x86, primary), IPMI (x86, legacy
labs), HMC (PowerVM LPAR).
- Drives `Controller.power()` (`src/kdive/providers/ports/lifecycle.py:251`) and is the escalation
path when in-band SSH is unreachable. Provides serial console: SoL on x86, vterm on the HMC.
- Credentials resolve through the SecretRegistry and register for redaction for the op's lifetime.
**These are username/password, not the on-disk x509 files ADR-0077 materializes** — the
resolution path differs and the redaction obligation is stricter, since a password can surface
in a console transcript in a way a TLS key never does.
- Fail-closed: an unreachable OOB endpoint fails with a specific existing `ErrorCategory`
(`src/kdive/domain/errors.py`), never a silent in-band fallback.

### R5 — The spine on an adopted host

- **Install:** the kernel goes into the running OS's bootloader over SSH — `grubby` on x86 EFI,
grub2-PReP/petitboot on PowerVM — pulled in-target via presigned GET (ADR-0082,
`src/kdive/providers/remote_libvirt/lifecycle/install.py:187`). The worker never pushes a kernel.
- **Boot + readiness:** boot-id change is the readiness signal for an out-of-band-console host
(`Booter.boot`, `src/kdive/providers/ports/lifecycle.py:194`; the remote precedent's boot method
is `install.py:255` and its boot-id read is `install.py:381`), with OOB console evidence on the
failure path.
- **Control:** `force_crash` via magic SysRq, gated by `src/kdive/security/authz/gate.py`
unchanged; power delegates to the OOB driver.
- **Capture:** `CaptureMethod.KDUMP` (`src/kdive/domain/capture.py:12`) on both arches, plus
`CaptureMethod.FADUMP` (`:16`) on ppc64le. `src/kdive/providers/shared/fadump_detect.py:17` gates
fadump on a **QEMU version floor** that is meaningless on a real LPAR, where platform firmware
provides `ibm,configure-kernel-dump` unconditionally — that detection needs a non-QEMU path.
- **Required runtime wiring** (`src/kdive/providers/core/runtime.py:135`), where the naive defaults
are actively wrong:
- `binding=ResourceBindingCapabilities(...)` is **mandatory** — BYO serves many hosts, so
without a rebind hook every op resolves to one arbitrary host. Remote sets this at
`src/kdive/providers/remote_libvirt/composition.py:391`.
- `platform_root_cmdline=None` is **mandatory** — the field defaults to `"root=/dev/vda"`
(`runtime.py:143`), and an adopted host's own bootloader owns its root device. Remote
overrides it for the same reason (`composition.py:390`, ADR-0183).
- `supports_snapshots=False`, `supports_traffic_capture=False`, `supports_diagnostic_sysrq=True`.
`supports_crash_watch` is **conditional on the console-multiplexing answer** (Open Question 3):
if KGDB takes exclusive console ownership, crash-watch cannot be advertised unconditionally.

### R6 — Debug

- KGDB over the OOB serial console. **`DebugTransportKind` is a closed literal**
(`src/kdive/providers/ports/lifecycle.py:27-28`: `Literal["gdbstub", "drgn-live"]`), so a KGDB
transport adds a third kind and touches the debug-session registrar under `mcp/tools/` — a
change to the provider-agnostic core, declared here rather than discovered (see R9).
- Driven through the existing engine
(`src/kdive/providers/shared/debug_common/gdbmi/core/engine.py`) with arch selection via
`select_gdb_binary()` (`src/kdive/providers/shared/debug_common/gdbmi/policy/arch.py:59`).
Requires `kgdboc` on the target cmdline.
- In-target drgn live introspection (ADR-0085) and worker-side vmcore postmortem
(`src/kdive/providers/shared/debug_common/crash_postmortem.py`).

### R7 — Release returns the host

- Teardown re-points the bootloader at the declared `baseline_kernel`, power-cycles via OOB,
re-runs the R3 adopt preconditions, and only then frees the Resource.
- Any failure **cordons** the Resource with a reason rather than re-offering it
(`Resource.cordoned`, `src/kdive/domain/catalog/resources.py:34`).
- The reconciler (ADR-0021) gains a BYO drift path for a host left mid-teardown by a dead worker.

### R8 — Cross-cutting invariants (AGENTS.md)

Not restated per entry, but binding on every one: the uniform `ToolResponse` envelope with
`error_category` on failure; the five-part limit contract on any deadline handed to an agent
(unit, reference clock, scope, consequence, recovery) — BYO's OOB power and reboot windows are
new limits and need all five; guarded state transitions via `domain/capacity/state.py`; the
existing `ErrorCategory` taxonomy with no invented strings; secrets-by-reference with mandatory
redaction of all console/gdb output; per-Allocation and per-System advisory-lock serialization;
and the agent-facing wrapper docstring as the contract of record.

### R9 — Portability

The ADR-0076 rule holds: no provider-specific logic in core (`domain`, `db`, `jobs`, `reconciler`,
`services`, `store`, `security`, the whole `mcp` package). It is now **checked by review rather
than measured** — ADR-0543 (#1840) retired the M2 portability diff gate, which measured
`pre-M2..HEAD` and could not separate a Milestone's provider work from the platform work merging
to `main` beside it. There is no allowlist to extend and no diff to hold.

The discipline that survives is declaring the core touches up front, so a reviewer can check them
against this list rather than discovering them: the `ResourceKind` value, the migration (kind
CHECK + cost-class seed), the `ProviderComposition` descriptor
(`src/kdive/providers/assembly/composition.py:228`), the inventory model + reconcile arm, the
`ProviderSection` field, the `DebugTransportKind` literal + debug-session registrar, and
regenerated docs. A core touch outside this set is the signal the gate used to raise, and it is
now a review question.

**One automated guard remains**, and it is what criterion 8 turns on:
`tests/scripts/test_provider_capture_coverage.py` pins a per-provider `CAPTURE_COVERAGE` table and
imports the real `build_*_runtime` builders, so a provider whose advertised capture methods
disagree with its row reddens `just test`. It asserts two hardcoded keys and **enumerates
nothing**, so registering `byo-host` with no row stays green — closing that is entry 17.

## Success criteria

1. A real vmcore captured from a force-crashed kernel on **an x86 host with a live BMC**,
retrieved, and opened by drgn.
2. The same on **a PowerVM LPAR via HMC**, including fadump.
3. A KGDB session over the OOB console hits a breakpoint and reads a backtrace, on each arch.
4. Release restores the baseline kernel and returns the host to allocatable **without operator
intervention**; an induced teardown failure cordons the host with a readable reason.
5. `doctor` names the specific defect for each of: unreachable SSH, unreachable OOB endpoint, bad
OOB credential, arch mismatch, missing baseline kernel — before any allocation.
6. Admission **rejects** a ppc64le profile targeted at an x86 BYO host (and vice versa) at
`systems.create`, not at install.
7. `reconcile-systems --check` rejects a malformed `[[byo_host]]` block with an `entry.field: msg`
error and still touches neither Postgres nor S3.
8. The capture-coverage drift guard passes with a `byo-host` row, and that row is enforced — a
registered kind missing from the table fails `just test` rather than passing silently. R9's
core touches are reviewed against the list above; there is no longer a measured diff.
9. Every host tool the live tiers need is declared in the owning Ansible role in the same PR that
introduces it (AGENTS.md provisioning-parity rule).

## Decomposition

**1. #1815 — Record the BYO host provider design: ADR set and milestone design doc**
The ADR set (provider package + resource kind; OOB control port; adopt-only provisioning and its
boot-method semantics; baseline-restore teardown; KGDB-over-serial debug transport) plus the
milestone design doc under `docs/design/`, mirroring `docs/design/m2-remote-libvirt.md`. *Seam:*
AGENTS.md requires an ADR for architecture decisions; the OOB port and the adopt-only boot method
are genuinely new interface commitments, and Open Questions 1–4 are resolved here rather than
inside an implementation PR. *Depends on:* nothing.

**2. #1817 — Add ResourceKind.BYO_HOST, its migration, inventory schema, and provider package skeleton**
`ResourceKind.BYO_HOST`, the CHECK-widen + cost-class-seed migration (next free number after
`src/kdive/db/schema/0111_restrict_pinned_job_deletion.sql`), `ByoHostInstance` +
`InventoryDoc` field + reconcile arm, `systems.toml.example`, and
`src/kdive/providers/byo_host/` with a buildable `ProviderRuntime` whose ports are fail-closed
stubs, registered as a `_RuntimeDescriptor`
(`src/kdive/providers/assembly/composition.py:228`). *Seam:* these cannot be separated —
`tests/db/test_resource_kind_parity.py:25` asserts the CHECK set by **exact equality** and
`:33` asserts every admitted kind resolves to a buildable runtime, so a migration without a
registered runtime fails CI. `docs/design/m2-remote-libvirt.md:258` states this rule and M2's
issue 1 was cut the same way. ADR-0517 makes migration numbers strictly ascending across merges,
so this lands early and alone. *Depends on:* 1.

**3. #1819 — Add the BYO host provisioning profile section and profile policy**
`ByoHostProfile`, `ProviderSection.byo_host_section`
(`src/kdive/profiles/provisioning.py:283`), a `ByoHostProfilePolicy` for the required
`profile_policy` field, and the extended `boot_method` pairing rules
(`src/kdive/profiles/provisioning.py:394`, `:454`). *Seam:* the profile is the agent-facing
provision contract and the last required-port gap in the skeleton; it is separable from entry 2
only because entry 2's stub policy is replaced wholesale here. *Depends on:* 2.

**4. #1818 — Define the OOB control port seam and add the Redfish driver**
The typed OOB driver protocol under `src/kdive/providers/byo_host/oob/` plus Redfish, with
credentials resolved through the SecretRegistry and registered for redaction. *Seam:* the port
shape is what entries 5 and 6 are written against. Redfish first: it is the modern standard and
the x86 proof depends on it. *Depends on:* 1.

**5. #1821 — Add the IPMI out-of-band driver**
A second driver against the entry-4 seam for BMCs predating Redfish. *Seam:* purely additive
behind the port; parallelizable with entry 6. *Depends on:* 4.

**6. #1822 — Add the HMC out-of-band driver for PowerVM LPARs**
Power control, LPAR identity (managed system + partition), and vterm access. *Seam:* the HMC is a
managed-system-plus-partition model rather than a per-host BMC, so it is the driver most likely to
stress the entry-4 port shape — it lands early enough to force a port revision if it must.
*Depends on:* 4.

**7. #1816 — Survey the OOB capability surface beyond power and console**
Read-only investigation of what Redfish/IPMI/HMC expose that KDIVE could use: sensor and health
telemetry, boot-device override, virtual media, firmware inventory, HMC dump management. Produces
findings and follow-on issues, not code. *Seam:* the "investigate others" scope item, kept as a
spike so an unbounded survey cannot block the spine. *Depends on:* nothing.

**8. #1823 — Implement adopt: the BYO Provisioner, preconditions, and runtime binding**
Replace the stub `Provisioner.provision` (`src/kdive/providers/ports/lifecycle.py:88`) with adopt —
validate preconditions, record facts on the System row, return a stable handle — and wire the
runtime fields whose defaults are wrong for BYO: `binding=ResourceBindingCapabilities` (multi-host
rebinding, cf. `src/kdive/providers/remote_libvirt/composition.py:391`) and
`platform_root_cmdline=None` (`src/kdive/providers/core/runtime.py:143`). Advertise the declared
`arch` in `Resource.capabilities` so `resolve_accel()`
(`src/kdive/services/systems/validation.py:50`) stops fail-opening. *Seam:* adopt is where a
declared host becomes a usable System; the binding and cmdline fields belong with it because
both are per-resource facts adopt resolves. *Depends on:* 3, 4.

**9. #1824 — Add the BYO host doctor diagnostics contribution**
Register via `diagnostic_provider_contributions()`
(`src/kdive/providers/assembly/diagnostics.py:12`), modeled on
`src/kdive/providers/remote_libvirt/diagnostics/reachability.py`, covering the R3 pre-flight
checks. *Seam:* reuses the adopt precondition module from entry 8 rather than duplicating it —
that shared module is why this follows adopt instead of preceding it. *Depends on:* 8.

**10. #1825 — Add the BYO install and boot/readiness planes**
`Installer.install` (`src/kdive/providers/ports/lifecycle.py:178`) writes the kernel into the
running OS's bootloader over SSH, arch-keyed (grubby / grub2-PReP), pulling via presigned GET;
`Booter.boot` (`:194`) confirms readiness by boot-id change. *Seam:* install and boot are one
module in the closest precedent (`src/kdive/providers/remote_libvirt/lifecycle/install.py:187`
and `:255`) because readiness is the second half of a successful install. *Depends on:* 8.

**11. #1826 — Add the BYO console plane over the OOB serial channel**
Collect, rotate, and snapshot SoL/vterm output, reusing
`src/kdive/providers/console_parts/rotation.py` and `sidecar.py` and following
`src/kdive/providers/remote_libvirt/console/collector.py`; wire `ConsoleCapabilities`. *Seam:* the
console is an OOB-driver capability, so it depends on the driver seam rather than on install.
*Depends on:* 4, 8.

**12. #1827 — Add the BYO control plane: SysRq force-crash and OOB power**
`Controller.power()` (`src/kdive/providers/ports/lifecycle.py:251`) delegates to the OOB driver;
`force_crash` injects magic SysRq through the console under the existing
`src/kdive/security/authz/gate.py`. Sets `supports_diagnostic_sysrq`, and `supports_crash_watch`
per the Open Question 3 resolution. *Seam:* SysRq needs the console channel from entry 11; power
needs the driver from entry 4. *Depends on:* 11.

**13. #1829 — Add BYO crash capture and retrieve: kdump on both arches, fadump on POWER**
Arm kdump, confirm the reservation, retrieve the vmcore following
`src/kdive/providers/remote_libvirt/retrieve/kdump_capture.py`; add fadump for ppc64le. *Seam:*
this entry owns replacing the QEMU-version-floor fadump probe
(`src/kdive/providers/shared/fadump_detect.py:17`) with a platform-firmware probe, since a real
LPAR has no QEMU version to read. *Depends on:* 10, 12.

**14. #1828 — Add the KGDB-over-serial debug transport**
Add the third `DebugTransportKind` (`src/kdive/providers/ports/lifecycle.py:27-28`), the
debug-session registrar arm under `mcp/tools/`, and `Connector.open_transport` (`:215`) driving
the existing engine with arch selection from
`src/kdive/providers/shared/debug_common/gdbmi/policy/arch.py:59`. *Seam:* must multiplex KGDB
against log capture on one serial channel — the coupling that makes this separate from entry 11 —
and it is the only debug entry touching the provider-agnostic core, so R9 declares it. *Depends
on:* 11.

**15. #1831 — Wire BYO in-target drgn introspection and vmcore postmortem**
Wire `LiveIntrospector` over in-target drgn (ADR-0085) and `CrashPostmortem` via
`src/kdive/providers/shared/debug_common/crash_postmortem.py`. *Seam:* postmortem needs a real
vmcore from entry 13; live introspection needs a booted System from entry 10. *Depends on:* 13.

**16. #1830 — Add BYO teardown: baseline restore, OOB power-cycle, and reconciler drift repair**
Restore the declared `baseline_kernel`, power-cycle via OOB, re-run adopt preconditions, free or
cordon (`src/kdive/domain/catalog/resources.py:34`); add the reconciler path for a host left
mid-teardown. *Seam:* needs OOB power (entry 12) and the adopt preconditions (entry 8); the
cordon-on-failure rule is what keeps a crashed host out of the next allocation. *Depends on:* 12.

**17. #1820 — Add the `byo-host` capture-coverage row and make a missing row detectable**
Add the `byo-host` row to `CAPTURE_COVERAGE` in `tests/scripts/test_provider_capture_coverage.py`,
and the **registered-kinds completeness assertion** that iterates the resolver's registered kinds
and requires a row for each — without it a provider can register with no row and `just test` stays
green, which is what makes criterion 8 checkable rather than a convention. *Seam:* registering a
new kind changes what the drift guard compares the moment entry 2 lands, so this follows entry 2
directly rather than trailing the epic. **Re-scoped by #1840:** the BYO baseline tag, the R9
allowlist entries and the CI-wiring decision this entry used to carry are gone with the retired
gate; the issue was retitled and restated to match. *Depends on:* 2.

**18. #1832 — Write the BYO host operator runbook and agent-facing documentation**
A runbook under `docs/operating/runbooks/` covering host prerequisites, OOB setup per driver,
`systems.toml` declaration, and the pre-flight/cordon workflow; plus MCP wrapper docstrings and
`Field` text for every capability and limit an agent must know (AGENTS.md: the wrapper docstring
is the agent-facing contract, and R8's five-part limit rule applies to the OOB windows). *Seam:*
the surface is stable only once capture and teardown semantics settle. *Depends on:* 13, 16.

**19. #1833 — Live proof: full BYO spine on an x86 host with a BMC**
The blocking end-to-end proof — adopt → install → boot → force-crash → kdump → retrieve → drgn →
KGDB breakpoint → release-with-restore — on real hardware, with host tools declared in the owning
Ansible role. *Depends on:* 14, 15, 16.

**20. #1834 — Live proof: full BYO spine on a PowerVM LPAR via HMC**
The same arm on a real LPAR, including fadump. *Seam:* independent of entry 19 — different driver,
arch, and hardware — and gated on the HMC driver rather than on the x86 proof. *Depends on:* 6,
14, 15, 16.

### Dependency graph (validated acyclic)

```
1 ─┬─> 2 ─┬─> 3 ──> 8 ─┬─> 9
│ └─> 17 ├─> 10 ─────────> 13 ──> 15 ─┬─> 19
│ └─> 11 ─┬─> 12 ─┬─> 13 └─> 20
└─> 4 ─┬─> 5 │ └─> 16 ─┬─> 18
├─> 6 ────────────────────────> 20 ├─> 19
├─> 11 └─> 20
└─> 8 14 ─┬─> 19
11 ──> 14 └─> 20
13 ──> 18
7 (independent)
```

Entry → issue: 1=#1815, 2=#1817, 3=#1819, 4=#1818, 5=#1821, 6=#1822, 7=#1816, 8=#1823,
9=#1824, 10=#1825, 11=#1826, 12=#1827, 13=#1829, 14=#1828, 15=#1831, 16=#1830, 17=#1820,
18=#1832, 19=#1833, 20=#1834.

Full edge list: 1→2, 1→4, 2→3, 2→17, 3→8, 4→5, 4→6, 4→8, 4→11, 8→9, 8→10, 8→11, 10→13, 11→12,
11→14, 12→13, 12→16, 13→15, 13→18, 14→19, 14→20, 15→19, 15→20, 16→18, 16→19, 16→20, 6→20.
No cycles. Dependency-free at birth: **1 (#1815)** and **7 (#1816)**.

## Open questions

1. **How is `baseline_kernel` identified?** A version string the operator declares, or a fact
captured from the running host at first adopt? Declared is auditable but drifts when the
operator patches; captured is self-maintaining but records whatever was booted at adopt —
possibly a kernel a previous run left behind. Teardown cannot restore what it cannot name.
*Affects 2, 16.*
2. **What `boot_method` does an adopted host declare?** A new enum value, or a reinterpretation of
`direct-kernel` where `kernel_source_ref` becomes optional because the kernel arrives at install
time? The pairing validators at `src/kdive/profiles/provisioning.py:394` and `:454` must be
extended either way. *Affects 1, 3.*
3. **Console multiplexing between KGDB and log capture.** One serial channel, two consumers. Does
KGDB take exclusive ownership for the session (losing console logs while attached), or is the
stream demultiplexed? This also decides whether `supports_crash_watch` can be advertised
unconditionally. *Affects 11, 12, 14.*
4. **Does `arch_traits()` split?** `src/kdive/domain/platform/arch_traits.py:52` carries `machine`
(`q35`/`pseries`) — a libvirt concept meaningless on metal — alongside `console_device`, which
generalizes fine. Split the table, or have BYO read only the applicable fields? *Affects 1, 8,
10.*
5. **fadump detection on real firmware.** What replaces the QEMU version floor
(`src/kdive/providers/shared/fadump_detect.py:17`) on a real LPAR — a device-tree property, an
RTAS call, or an operator assertion? *Affects 13.*
6. **Cost-class coefficient value.** The migration must seed *a* coefficient (R1); the number is an
operator pricing decision. *Affects 2.*
7. **Concurrency ceiling.** Proposed `concurrent_allocation_cap = 1` — one System per physical
host, since a force-crash takes the whole machine. Confirm no partitioning case exists.
*Affects 2.*

Contributor guide

Open the contributing guide

Research direction

Start by reading the provider lifecycle seam in src/kdive/providers/ports/lifecycle.py, the inventory models in src/kdive/inventory/model.py, and the provisioning rules in src/kdive/profiles/provisioning.py. Trace the existing migrations and diagnostic contribution path before resolving the open boot-method and control-plane questions. Done means an adopted x86 host and PowerVM LPAR can complete the described lifecycle with real hardware proof, but #1760's competing approach remains undecided.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, python
Domain
backend, devops, infrastructure
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.