List substrate quadlet services + live systemd/podman state.
curl --request GET \ --url https://stratum.example.com/v1/admin/substrate/services \ --header 'Authorization: Bearer <token>'Bootstrap-admin endpoint returning, per substrate quadlet (postgres, openbao, ovn-central, stratum-controlplane, victoriametrics, vmagent, victorialogs, otel-collector, tempo, vmalert, alertmanager, node-exporter, stratum-ui,…), the runtime state systemd + podman report: ActiveState, SubState, UnitFileState, NRestarts, last error snippet, the container’s image ref + restart count.
Lets operators answer “is anything crash-looping on this
node?” from the product without SSH’ing for systemctl status.
name is the service’s log-pipeline identity, not its systemd unit
name: a quadlet drops .container, a native unit drops .service
(ovn-controller, not ovn-controller.service), and any other unit
type keeps its suffix (podman.socket). It can be passed straight to
the per-service logs endpoint below — before it could not,
and doing so returned an empty 200 for every native unit.
The controlplane shells out to systemctl show + podman inspect against the host’s sockets, which are bind-mounted
into the stratum-controlplane container. Single-node at v1;
multi-node aggregation is post-v1 work..
Authorizations
Section titled “Authorizations”Responses
Section titled “Responses”Per-quadlet runtime status.
object
object
Service base name. For quadlets, the .container template stripped (e.g. postgres); for native systemd units the unit name; for OVS bridges the bridge name (e.g. br-int); for cephadm daemons a normalized group name (ceph-mon, ceph-mgr, ceph-crash, ceph-osd.<id>).
One synthetic name exists: ceph-foreign-fsid, raised on a node that carries Ceph daemon units belonging to a cluster other than the one it is a member of. It is always unhealthy, at most one per node, and its lastError names the offending fsids. Daemons of a foreign fsid are NOT reported under their own group names, so the live daemon keeps ceph-mon / ceph-mgr rather than being displaced by a ghost.
Stratum node id the row was sampled on.
Mirrors systemd’s ActiveState.
Mirrors systemd’s SubState; sub-classifies state (e.g. running, exited, failed).
Operator-facing roll-up. active -> healthy; failed / inactive / deactivating -> unhealthy; reloading / parse-failure -> unknown.
activating is unknown — a unit on its way up — EXCEPT when subState is auto-restart, which is unhealthy. systemd only enters auto-restart after a start has failed, so it is a crash loop rather than a cold start; reporting it as unknown is how a ghost daemon looped 931 times over 13 hours without failing any check.
Container image ref the host’s podman runtime is using, read through the podman API socket. Empty when there is no container right now (a stopped unit, or one between restarts), or when podman could not be asked, in which case lastError says so.
Max of systemd’s NRestarts and the podman runtime restart
count. Captures both unit-level restarts (systemd) and
in-unit container recycles (podman) — both indicate
instability even though systemd considers the unit active
in the second case.
Systemd’s ActiveEnterTimestamp. Empty when the unit never entered active.
Short failure summary surfaced from systemd’s Result field
(e.g. systemd result: exit-code). Otherwise, for a quadlet
whose container podman could not be asked about,
podman inspect <container>: <reason>: its image and podman
restart count are then unknown, not absent, and state and
health still come from systemd. Empty when there is nothing
to report.
Systemd’s unit-file enablement state (enabled, disabled, static).
True when the row’s health is rolled up from a
.socket sibling instead of its own .service
ActiveState. UIs should render the pill as “idle / active”
for these rows: inactive is the steady state for a
socket-activated daemon (the.socket is listening for the
first client connect).
Modular-libvirt drill-down: per-daemon state behind a socket-activated front door (e.g. libvirtd’s virtproxyd / virtqemud / virtnetworkd / virtstoraged). UIs hide these in steady state; expanding the row surfaces them during incident debugging. Does not affect the parent row’s health by design — a failing virtnetworkd doesn’t render libvirtd as unhealthy because the operator workflow that depends on it might be unrelated.
object
Daemon unit prefix (the corresponding units are <name>.socket and <name>.service).
ActiveState of the daemon’s .socket. active is healthy; anything else is broken.
ActiveState of the daemon’s .service. Informational
only — inactive is the steady-state for a socket-
activated daemon (it’s waiting for the first connect).
When this row was last sampled on its node. The
endpoint aggregates per-node samples across the fleet, so a
node that stopped answering keeps its last sample with an
ageing sampledAt; UIs should flag rows older than a couple
of poll intervals as stale (the node may be down). Absent only
on the live-local fallback path (no sampler store wired).
Example
{ "items": [ { "name": "postgres", "state": "active", "health": "healthy", "backingDaemons": [ { "name": "virtqemud", "socketState": "active", "serviceState": "active" } ] } ]}Authenticated but not authorized.
object
The findings of a rejected IAM policy document (custom and resource policies).
One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.
object
The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.
Index of the statement at fault; -1 for the document as a whole.
The offending action, where one is at fault (ACTION_NOT_APPLICABLE).
Field errors, for codes that carry them: each problem at its field
(see FieldError). Documented on the responses that set it, e.g.
approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED.
Absent otherwise.
One field’s problem, the stage-2 installer API’s shape.
field is the field’s path, message what is wrong with it.
object
The field’s path, e.g. links[0].interface, disks[1].confirm,
mgmt.address or role; empty for the whole request.
Structured facts about the refusal, for codes that carry them. The
shape is fixed per code and documented on the response that answers
it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and
IMAGE_IN_USE. Absent for codes that carry none.
object
Example
{ "issues": [ { "code": "ACTION_NOT_APPLICABLE", "action": "compute:CreateVpc" } ]}