Troubleshooting¶
Start from the refusal, then read the machine's timeline. The commands use the walkthrough's setup.
Reading a refusal¶
Every refusal is application/problem+json. Its type is urn:bronzeward:problem:<code>, and
detail, when present, says which check refused. A 401 and an identity-revoked carry no
detail: go by the code. The response carries no deeper cause: a node or provider failure
behind it is recorded in the operation's events, a use-time-check timeline entry or an
observation's unread. The response headers are in $H.
| Code | Status | Usual cause | What to do |
|---|---|---|---|
idempotency-key-required |
428 | A write without Idempotency-Key |
Send one, unique per intended act |
invalid-request |
400 | A malformed body or header, such as an Idempotency-Key outside 16 to 128 characters of [A-Za-z0-9_-]; detail names it |
Fix the request |
idempotency-key-reused |
422 | The same key with a different request | Use a new key; the same key replays only the same request |
precondition-required |
428 | A draft change without If-Match |
Send the draft's current ETag |
precondition-failed |
412 | The draft moved since you read its ETag | Read the draft again, reapply your change |
stale-input |
409 | A publication input moved since it was read | Publish again |
scope-busy |
409 | An operation holds the machine's scope | Wait for it, or settle it (recovery) |
recovery-mode-active |
409 | Recovery mode, or a scope not yet released | Finish the restore |
conflict |
409 | A state the act does not apply to; detail names it |
Read the object, then act on its current state |
identity-revoked |
403 | The subject or service identity is revoked, or in auth.deniedSubjects |
Revocation is permanent and a new token does not lift it: act as another human subject, or issue a new service identity (bronzeward token issue) under a name auth.deniedSubjects does not list |
forbidden |
403 | The token lacks the role, or a human-only act from automation | Check auth.roles and the groups claim |
machine-identity-mismatch |
409 | The node's SMBIOS UUID, node ID or cluster ID is not the recorded one | Check the endpoint points at the right machine |
talos-access-unavailable |
503 | No talosconfig in the provider, access denied, or the node unreachable | Check OpenBao, the access path and the network |
dependency-unavailable |
503 | PostgreSQL, the identity provider or OpenBao is unreachable or not configured | Fix the dependency, then retry |
transient-conflict |
503 | Concurrent writes kept colliding | Retry with the same key |
epoch-superseded |
503 | This process started before the current recovery epoch | Restart it |
internal-error |
500 | Unknown; the write may or may not have happened | Retry with the same key, which returns the first answer if it was recorded |
Reading the timeline¶
Each machine has one append-only timeline: every plan, approval, observation, commitment, attempt, response, accounting, drift, freeze and ownership transition, in revision order.
call GET "/machines/$machine_w/timeline?limit=500" "$viewer" | jq -c '.items[] | [.revision, .at, .kind, .entry]'
Pages hold 50 entries by default and at most 500, oldest first, so recent entries are on the last
page: follow next with &cursor=<next>, or use the items helper.
Other lists, such as observations, page the same way but are in id order, not time order. Timeline
and operation-event responses carry no configuration text or secrets. Other reads can: source reads
(fragments, profiles, assignments) carry sanitized documents, and an ingestion's review carries the
staged document, which can hold a value nobody marked. That review is no-store and answers any
human with the author role, not only the ingestion's creator.
From it, for any operation, you can answer:
- Who approved what: the
planandapprovalentries, and anyapproval-revocation. - What each attempt carried, and when:
attemptentries, each with its number, route, controller and transport and verification deadlines. - How each attempt is accounted for: a
response, anaccountingdecision, or neither (still unknown). Anattemptis recorded before the send, so the timeline cannot always say whether the request left; an accounting records what was decided from observations, not that it was sent. - What was seen on the node:
observation-startedandobservationentries, each naming its purpose. - Why the scope was held:
commitment,operation-stateandscope-stateentries. - Who decided an accounting, and on what: a decided
accountingentry names who decided it, the start and end observations and the facts. - How it ended: the last
operation-stateentry. - Who owned it:
ownership-transitionentries, each with its generation.
To follow one operation:
call GET /operations/$op "$viewer" | jq .
call GET /operations/$op/events "$viewer" | jq -c '.items[]'
curl --disable -sS -N -H @- -H 'Accept: text/event-stream' "$A/api/v1/operations/$op/events" \
<<<"Authorization: Bearer $viewer"
The stream ends when the operation ends, or when the token expires. Resume with a fresh token and
Last-Event-ID set to the last event's id. An event: reset means the installation entered a
new recovery epoch: read the events again from the start.
Common situations¶
- An operation stays
unresolved. Find out why before taking it over: read its attempts and timeline. An unaccounted attempt, a verification deadline reached without a decisive observation, a closed scope gate or a revoked approval each leave it there with a healthy owner, and a takeover only advances the generation. Account, observe or cancel for the cause you find; take it over only when its owner stopped. See interrupted operations. - A plan is approved but nothing happens. Look on the timeline for a
refusalentry, which names the transaction and the comparison that failed, or ause-time-checkentry, which gives each dependency's result. - A machine is frozen. Its last
freezeentry says who froze it, in which role and why. An approver unfreezes it withPOST /machines/$machine/unfreezesand a body{"reason": "..."}. - Every call answers
409 recovery-mode-active. The instance was started with-recovery-startand no entry is recorded. Until the entry commits it serves only/livezandPOST /recovery/entries,GET /recoveryincluded; thedetailsays so. - Acts on one machine answer
409 recovery-mode-active. The installation is in recovery mode and that machine's scope is not released. On any instance past the entry:call GET /recovery "$viewer" | jq '{recoveryMode, scopes}'. serveexits at start. An unmigrated or differently migrated database: stop every server, runmigrate(migrate, then serve).