From 9ff55ace8a14c296d4c64723bc467c9a83412aa9 Mon Sep 17 00:00:00 2001 From: Guy Godfroy Date: Wed, 24 Jun 2026 09:21:02 +0200 Subject: [PATCH] revert: drop commit-confirmed apply from 2.3.0 (defer to 2.4.0) Removes the commit-confirm wire surface added in a85a5cd (per-write ?confirm, /confirm endpoints, uapi:confirm scope, apply-confirm integration, OpenAPI confirm surface, tests). Keeps the scope-tree, per-token rate/burst, and platform-fidelity work. The mechanism works (built, shipped in 2.3.0-rc1, soaked on live hardware), but shipping it stable would freeze an unsettled confirm authz model into the permanent v2 contract: per-write arming rides the write's own resource :rw with no uapi:confirm requirement, ack/rollback are window-agnostic, and the package-granularity escalation analysis suggests these may need to change. With no first-party consumer (the Terraform provider ships 2.3.0 as Option A), deferring lets the whole feature ship once in 2.4.0 with one reviewed authz model rather than locking in a contract changeable only with a major bump. (apply-confirm 0.1.0 is released on the feed, so the dependency is not the blocker; the wire contract is.) Design and decision preserved in docs/commit-confirm.md and docs/roadmap.md; full implementation recoverable from a85a5cd. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 +- CLAUDE.md | 2 +- build/gen_openapi.uc | 112 +- build/openapi.json | 1667 +------------------- docs/architecture.md | 21 +- docs/commit-confirm.md | 222 ++- docs/errors.md | 5 - docs/installation.md | 4 - docs/roadmap.md | 52 +- docs/tokens.md | 2 +- src/lib/apply_confirm.uc | 136 -- src/lib/errors.uc | 8 - src/lib/handler.uc | 31 +- src/lib/scope.uc | 1 - src/lib/transaction.uc | 108 +- src/main.uc | 132 +- src/raw.uc | 27 +- tests/integration/45_apply_confirm_test.sh | 84 - tests/unit/apply_confirm_test.uc | 75 - tests/unit/handler_test.uc | 46 - tests/unit/raw_test.uc | 23 - 21 files changed, 194 insertions(+), 2568 deletions(-) delete mode 100644 src/lib/apply_confirm.uc delete mode 100755 tests/integration/45_apply_confirm_test.sh delete mode 100644 tests/unit/apply_confirm_test.uc diff --git a/CHANGELOG.md b/CHANGELOG.md index 490873c..a5f4c82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,9 @@ All notable changes to this project will be documented in this file. Format foll Surfaces the known scope tree through a sanctioned interface so external consumers (the upcoming `luci-app-uapi` LuCI frontend, fleet inventory tools, anything that wants to render a scope picker) can enumerate valid scopes without parsing `src/lib/scope.uc` or hardcoding a copy. Closes [openwrt-iac/uapi#5](https://github.com/openwrt-iac/uapi/issues/5). Also plumbs per-token `rate` / `burst` overrides through the mint surfaces, closing a "planned for v2.x" gap that has been carried in `docs/tokens.md` since 2.0. -### Added +Commit-confirmed apply (the `?confirm` / `/confirm` surface) was present in the 2.3.0-rc1 pre-release and has since been deferred; it is not part of the 2.3.0 stable surface. The confirm wire contract is intentionally not frozen into v2 until its authz model is settled and a consumer needs it, so it does not lock in a contract that could only be changed with a major bump. See `docs/commit-confirm.md` and `docs/roadmap.md`. -- Commit-confirmed apply (safe apply). A config write can arm a rollback deadline via `?confirm=` (or the `X-Uapi-Confirm` header behind a proxy that forwards it; uhttpd's CGI strips custom headers, so the query form is the portable interface): uapi snapshots the affected uci packages, commits, and returns `202 Accepted` with a `confirm` token; unless the client acks via `POST /confirm/` before the deadline, the pre-change snapshot is restored automatically (surviving reboot / process kill / a dead management path). The ack is client-driven, so a network blip that hides the response also prevents the ack, keeping the auto-revert and the client's view consistent. New endpoints `GET /confirm`, `GET|POST|DELETE /confirm/` and scope `uapi:confirm` (`:ro` for status/list, `:rw` for ack/rollback; arming needs no extra scope). The rollback timer and durable state live in a separate package, `apply-confirm` (uapi invokes its CLI, runs no daemon of its own); the integration is optional and feature-detected, returning `501 confirm_unavailable` when apply-confirm is not installed. See `docs/commit-confirm.md`. (This RC ships to GitHub Releases only; the stable 2.3.0 tag and apk-feed publication wait until apply-confirm reaches a stable feed release.) +### Added - New CLI subcommand `uapi-token scopes` printing one scope path per line (sorted, greppable). Pair with `--json` for a JSON array suitable for piping into `jq` or any other consumer. The CLI is the durable cross-package interface; it works from any shell, Ansible playbook, or fleet inventory tool that can `ssh` to the router. diff --git a/CLAUDE.md b/CLAUDE.md index 46067d6..e8b12f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,7 +67,7 @@ CLAUDE.md is the every-turn meta document (principles, style, workflow). Topic-s | Raw passthrough stability + semantics | `docs/raw.md` | Working on `/raw/` or considering using it. | | Non-uci resources (apk, leases, password, authorized_keys) | `docs/non-uci-state.md` | Adding or modifying a resource whose source of truth isn't `/etc/config/`. | | Token CLI, HTTP token mint, scope syntax, raw-access composition, per-token rate limit overrides | `docs/tokens.md` | Anything auth-shaped from the operator angle. | -| Commit-confirmed apply (X-Uapi-Confirm, /confirm endpoints, apply-confirm integration) | `docs/commit-confirm.md` | Working on the confirm surface, the write-path stage seam, or the apply-confirm integration. | +| Commit-confirmed apply: deferral decision + 2.4.0 design (built in rc1, removed before 2.3.0 stable) | `docs/commit-confirm.md` | Picking the feature back up, or understanding why the confirm surface is not in 2.3.0. | | Threat model, TLS posture, public endpoints, design exclusions | `docs/security.md` | Security review, threat-model questions, "why not rpcd sessions?" | | Error envelope, top-level + field-level codes, response headers | `docs/errors.md` | Defining a new error code or auditing error shapes. | | Observability (log categories, format, global rate limit, metrics, diagnostics, healthz, capacity) | `docs/operations.md` | Operator-facing setup, log forwarding, debugging in the field. Cross-references `docs/architecture.md` ยง Rate limit / Metrics for the implementation-side mechanics. | diff --git a/build/gen_openapi.uc b/build/gen_openapi.uc index 89e8710..29fd13c 100755 --- a/build/gen_openapi.uc +++ b/build/gen_openapi.uc @@ -195,21 +195,6 @@ function responses(verb, success) { return r; } -const CONFIRM_PARAM = { "$ref": "#/components/parameters/Confirm" }; -const CONFIRM_202 = { "$ref": "#/components/responses/PendingConfirm" }; - -// Mark a uci-config-write operation as commit-confirm-capable: it accepts the -// X-Uapi-Confirm header and may return 202 with an armed rollback window. -// Scoped to config writes (resource CRUD, singleton patch, batch) - NOT token / -// package / system-access writes, which apply-confirm does not snapshot. -function with_confirm(op) { - if (op == null) return op; - op.parameters = op.parameters ?? []; - push(op.parameters, CONFIRM_PARAM); - op.responses["202"] = CONFIRM_202; - return op; -} - function build_crud_paths(ep) { let schema_ref = schema_name(ep); let mod = load_resource(ep.file); @@ -301,18 +286,12 @@ function build_crud_paths(ep) { }, }; - with_confirm(paths[ep.path].post); - with_confirm(paths[ep.path + "/{id}"].put); - with_confirm(paths[ep.path + "/{id}"].patch); - with_confirm(paths[ep.path + "/{id}"].delete); - with_confirm(paths[ep.path + "/{id}/adopt"].post); - return paths; } function build_singleton_paths(ep) { let schema_ref = schema_name(ep); - let paths = { + return { [ep.path]: { "get": { "summary": sprintf("Get the %s singleton", ep.domain), "description": "Conditional GET via If-None-Match (or ?if_none_match=).", @@ -329,8 +308,6 @@ function build_singleton_paths(ep) { "responses": responses("patch", { "200": make_response(200, "Updated", schema_ref) }) }, }, }; - with_confirm(paths[ep.path].patch); - return paths; } function build_collection_paths(ep) { @@ -420,7 +397,6 @@ const TAGS = [ { name: "Operational / Metrics", group: "Operational endpoints", description: "Prometheus 0.0.4 text. Path-template labels normalize concrete ids.", path_prefix: "/metrics" }, { name: "Operational / Diagnostics", group: "Operational endpoints", description: "Lock state, uptime, loaded resources.", path_prefix: "/diagnostics" }, { name: "Operational / Batch", group: "Operational endpoints", description: "Multi-package atomic transaction (max 50 ops). 207 Multi-Status on success.", path_prefix: "/batch" }, - { name: "Operational / Commit-confirm", group: "Operational endpoints", description: "Confirm or roll back a commit-confirmed apply (apply-confirm). Requires the apply-confirm package.", path_prefix: "/confirm" }, ]; function build_tags() { @@ -705,55 +681,6 @@ function build_paths() { }), }, }; - // Confirm-capable; the batch 202 carries the BatchResponse plus one confirm - // window spanning all touched packages. - paths["/batch"].post.parameters = [CONFIRM_PARAM]; - paths["/batch"].post.responses["202"] = { - "description": "Accepted: every sub-request succeeded and a commit-confirmed rollback is armed over all touched packages. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "headers": { - "X-Confirm-Token": { "description": "The armed rollback token; pass to /confirm/{token}.", "schema": { "type": "string", "pattern": "^ac_[0-9]+_[0-9a-f]{8}$" } }, - "X-Confirm-Deadline": { "description": "Best-effort unix epoch of auto-rollback.", "schema": { "type": "integer" } }, - }, - "content": { "application/json": { "schema": { "allOf": [ - { "$ref": "#/components/schemas/BatchResponse" }, - { "type": "object", "required": ["confirm"], "properties": { "confirm": { "$ref": "#/components/schemas/ConfirmWindow" } } }, - ] } } }, - }; - - let confirm_token_param = { "name": "token", "in": "path", "required": true, - "schema": { "type": "string", "pattern": "^ac_[0-9]+_[0-9a-f]{8}$" } }; - paths["/confirm"] = { - "get": { - "summary": "List pending confirm windows", - "description": "Passthrough to `apply-confirm list --json`. Scope: uapi:confirm:ro (or *:ro). Requires the apply-confirm package. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "responses": responses("get", { - "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ConfirmWindow" } } } } }, - }), - }, - }; - paths["/confirm/{token}"] = { - "parameters": [confirm_token_param], - "get": { - "summary": "Status of a pending confirm window", - "description": "Passthrough to `apply-confirm status --json` (authoritative remaining time). Scope: uapi:confirm:ro. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "responses": responses("get", { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConfirmWindow" } } } } }), - }, - "post": { - "summary": "Confirm (ack) a pending apply", - "description": "Acks the window so the change is NOT rolled back. Scope: uapi:confirm:rw. 409 confirm_window_closed if the window already expired or closed. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "responses": responses("post", { "200": { "description": "Confirmed", "content": { "application/json": { "schema": { "type": "object", "properties": { "confirmed": { "type": "string" } } } } } } }), - }, - "delete": { - "summary": "Roll back a pending apply now", - "description": "Restores the snapshot immediately (early/forced revert). Scope: uapi:confirm:rw. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "responses": responses("delete", { "200": { "description": "Rolled back", "content": { "application/json": { "schema": { "type": "object", "properties": { "rolled_back": { "type": "string" } } } } } } }), - }, - }; // Tag non-curated paths. Longest-prefix-wins so /system/password doesn't // get the bare "System" tag. @@ -787,17 +714,6 @@ function build_paths() { function build_schemas() { let schemas = { - "ConfirmWindow": { - "type": "object", - "required": ["token", "timeout", "deadline", "packages"], - "description": "An armed commit-confirmed rollback window (apply-confirm). Returned in the 202 body of a confirmed write.", - "properties": { - "token": { "type": "string", "pattern": "^ac_[0-9]+_[0-9a-f]{8}$", "description": "Pass to POST /confirm/{token} to ack, or DELETE /confirm/{token} to roll back now." }, - "timeout": { "type": "integer", "description": "Seconds the window stays armed." }, - "deadline": { "type": "integer", "description": "Best-effort unix epoch of auto-rollback; GET /confirm/{token} is authoritative." }, - "packages": { "type": "array", "items": { "type": "string" }, "description": "uci packages snapshotted and restored on rollback." }, - }, - }, "ErrorEnvelope": { "type": "object", "required": ["code", "message", "request_id"], @@ -1243,30 +1159,8 @@ function build_doc() { "ValidationFailed": { "description": "Request body failed validation (per-field errors in `errors[]`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, "Locked": { "description": "Another write holds the same per-package lock; retry after Retry-After seconds", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }, "headers": { "Retry-After": { "$ref": "#/components/headers/RetryAfter" } } }, "TooManyRequests": { "description": "Per-token rate limit exceeded; retry after Retry-After seconds", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }, "headers": { "Retry-After": { "$ref": "#/components/headers/RetryAfter" } } }, - "InternalError": { "description": "Server error (codes: internal_error, reload_failed_restored, reload_failed_unrecovered, rollback_reload_failed)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, - "ServiceUnavailable": { "description": "Service unavailable (codes: service_unavailable, init_script_missing, confirm_stage_failed)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, - "PendingConfirm": { - "description": "Write committed with a commit-confirmed rollback armed. The body is the normal resource representation plus a `confirm` block; unless POST /confirm/{token} acks before the deadline, apply-confirm restores the pre-change snapshot. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "headers": { - "X-Confirm-Token": { "description": "The armed rollback token; pass to /confirm/{token}.", "schema": { "type": "string", "pattern": "^ac_[0-9]+_[0-9a-f]{8}$" } }, - "X-Confirm-Deadline": { "description": "Best-effort unix epoch of auto-rollback; poll GET /confirm/{token} for authoritative remaining time.", "schema": { "type": "integer" } }, - }, - "content": { "application/json": { "schema": { - "type": "object", - "required": ["confirm"], - "description": "The resource representation plus the armed `confirm` window.", - "properties": { "confirm": { "$ref": "#/components/schemas/ConfirmWindow" } }, - } } }, - }, - }, - "parameters": { - "Confirm": { - "name": "confirm", "in": "query", "required": false, - "schema": { "type": "integer", "minimum": 1, "maximum": 3600 }, - "x-uapi-requires": "apply-confirm", - "description": "Arm a commit-confirmed rollback on this write: snapshot the affected uci packages and auto-revert after this many seconds unless POST /confirm/{token} acks first. The query form is the portable interface; the `X-Uapi-Confirm` header also works but only behind a proxy that forwards it (uhttpd's CGI env strips custom headers via a hard-coded allowlist). When set, the success status is 202 with a `confirm` block. Requires the apply-confirm package; without it the write returns 501 confirm_unavailable.", - }, + "InternalError": { "description": "Server error (codes: internal_error, reload_failed_restored, reload_failed_unrecovered)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, + "ServiceUnavailable": { "description": "Service unavailable (codes: service_unavailable, init_script_missing)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }, }, }, }; diff --git a/build/openapi.json b/build/openapi.json index e25a3e3..43adedb 100644 --- a/build/openapi.json +++ b/build/openapi.json @@ -243,10 +243,6 @@ { "name": "Operational / Batch", "description": "Multi-package atomic transaction (max 50 ops). 207 Multi-Status on success." - }, - { - "name": "Operational / Commit-confirm", - "description": "Confirm or roll back a commit-confirmed apply (apply-confirm). Requires the apply-confirm package." } ], "x-tagGroups": [ @@ -352,8 +348,7 @@ "Operational / Schema discovery", "Operational / Metrics", "Operational / Diagnostics", - "Operational / Batch", - "Operational / Commit-confirm" + "Operational / Batch" ] } ], @@ -505,16 +500,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Rules" ] @@ -658,16 +645,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Rules" ] @@ -748,16 +727,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Rules" ] @@ -811,16 +782,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Rules" ] @@ -901,16 +864,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Rules" ] @@ -1057,16 +1012,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Zones" ] @@ -1210,16 +1157,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Zones" ] @@ -1300,16 +1239,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Zones" ] @@ -1363,16 +1294,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Zones" ] @@ -1453,16 +1376,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Zones" ] @@ -1609,16 +1524,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Redirects" ] @@ -1762,16 +1669,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Redirects" ] @@ -1852,16 +1751,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Redirects" ] @@ -1915,16 +1806,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Redirects" ] @@ -2005,16 +1888,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Redirects" ] @@ -2161,16 +2036,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Forwardings" ] @@ -2314,16 +2181,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Forwardings" ] @@ -2404,16 +2263,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Forwardings" ] @@ -2467,16 +2318,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Forwardings" ] @@ -2557,16 +2400,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Forwardings" ] @@ -2704,16 +2539,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Firewall / Defaults" ] @@ -2860,16 +2687,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Interfaces" ] @@ -3013,16 +2832,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Interfaces" ] @@ -3103,16 +2914,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Interfaces" ] @@ -3166,16 +2969,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Interfaces" ] @@ -3256,16 +3051,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Interfaces" ] @@ -3412,16 +3199,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Devices" ] @@ -3565,16 +3344,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Devices" ] @@ -3655,16 +3426,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Devices" ] @@ -3718,16 +3481,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Devices" ] @@ -3808,16 +3563,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Devices" ] @@ -3964,16 +3711,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Routes" ] @@ -4117,16 +3856,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Routes" ] @@ -4207,16 +3938,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Routes" ] @@ -4270,16 +3993,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Routes" ] @@ -4360,16 +4075,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Routes" ] @@ -4516,16 +4223,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Rules" ] @@ -4669,16 +4368,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Rules" ] @@ -4759,16 +4450,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Rules" ] @@ -4822,16 +4505,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Rules" ] @@ -4912,16 +4587,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Rules" ] @@ -5068,16 +4735,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Bridge Vlans" ] @@ -5221,16 +4880,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Bridge Vlans" ] @@ -5311,16 +4962,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Bridge Vlans" ] @@ -5374,16 +5017,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Bridge Vlans" ] @@ -5464,16 +5099,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Bridge Vlans" ] @@ -5620,16 +5247,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Wireguard Peers" ] @@ -5773,16 +5392,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Wireguard Peers" ] @@ -5863,16 +5474,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Wireguard Peers" ] @@ -5926,16 +5529,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Wireguard Peers" ] @@ -6016,16 +5611,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Network / Wireguard Peers" ] @@ -6172,16 +5759,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Wireless / Devices" ] @@ -6325,16 +5904,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Wireless / Devices" ] @@ -6415,16 +5986,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Wireless / Devices" ] @@ -6478,16 +6041,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Wireless / Devices" ] @@ -6568,16 +6123,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Wireless / Devices" ] @@ -6724,16 +6271,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Wireless / Interfaces" ] @@ -6877,16 +6416,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Wireless / Interfaces" ] @@ -6967,16 +6498,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Wireless / Interfaces" ] @@ -7030,16 +6553,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Wireless / Interfaces" ] @@ -7120,16 +6635,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Wireless / Interfaces" ] @@ -7276,16 +6783,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Hosts" ] @@ -7429,16 +6928,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Hosts" ] @@ -7519,16 +7010,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Hosts" ] @@ -7582,16 +7065,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Hosts" ] @@ -7672,16 +7147,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Hosts" ] @@ -8044,16 +7511,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Servers" ] @@ -8197,16 +7656,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Servers" ] @@ -8287,16 +7738,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Servers" ] @@ -8350,16 +7793,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Servers" ] @@ -8440,16 +7875,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Servers" ] @@ -8587,16 +8014,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Dnsmasq" ] @@ -8734,16 +8153,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dhcp / Odhcpd" ] @@ -8881,16 +8292,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "System" ] @@ -9037,16 +8440,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "System / Timeservers" ] @@ -9190,16 +8585,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "System / Timeservers" ] @@ -9280,16 +8667,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "System / Timeservers" ] @@ -9343,16 +8722,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "System / Timeservers" ] @@ -9433,16 +8804,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "System / Timeservers" ] @@ -9589,16 +8952,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dropbear / Instances" ] @@ -9742,16 +9097,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dropbear / Instances" ] @@ -9832,16 +9179,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dropbear / Instances" ] @@ -9895,16 +9234,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dropbear / Instances" ] @@ -9985,16 +9316,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Dropbear / Instances" ] @@ -10141,16 +9464,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Uhttpd / Instances" ] @@ -10294,16 +9609,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Uhttpd / Instances" ] @@ -10384,16 +9691,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Uhttpd / Instances" ] @@ -10447,16 +9746,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Uhttpd / Instances" ] @@ -10537,16 +9828,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Uhttpd / Instances" ] @@ -10693,16 +9976,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Uhttpd / Certs" ] @@ -10846,16 +10121,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Uhttpd / Certs" ] @@ -10936,16 +10203,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Uhttpd / Certs" ] @@ -10999,16 +10258,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Uhttpd / Certs" ] @@ -11089,16 +10340,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Uhttpd / Certs" ] @@ -11236,16 +10479,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Unbound / Server" ] @@ -11383,16 +10618,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Unbound / Srv" ] @@ -11530,16 +10757,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Unbound / Ext" ] @@ -11686,16 +10905,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Sqm / Queues" ] @@ -11839,16 +11050,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Sqm / Queues" ] @@ -11929,16 +11132,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Sqm / Queues" ] @@ -11992,16 +11187,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Sqm / Queues" ] @@ -12082,16 +11269,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Sqm / Queues" ] @@ -12238,16 +11417,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Agents" ] @@ -12391,16 +11562,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Agents" ] @@ -12481,16 +11644,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Agents" ] @@ -12544,16 +11699,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Agents" ] @@ -12634,16 +11781,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Agents" ] @@ -12790,16 +11929,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Com2secs" ] @@ -12943,16 +12074,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Com2secs" ] @@ -13033,16 +12156,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Com2secs" ] @@ -13096,16 +12211,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Com2secs" ] @@ -13186,16 +12293,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Com2secs" ] @@ -13342,16 +12441,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Groups" ] @@ -13495,16 +12586,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Groups" ] @@ -13585,16 +12668,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Groups" ] @@ -13648,16 +12723,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Groups" ] @@ -13738,16 +12805,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Groups" ] @@ -13894,16 +12953,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Accesses" ] @@ -14047,16 +13098,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Accesses" ] @@ -14137,16 +13180,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Accesses" ] @@ -14200,16 +13235,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Accesses" ] @@ -14290,16 +13317,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / Accesses" ] @@ -14437,16 +13456,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Snmpd / System" ] @@ -14584,16 +13595,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Lldpd / Config" ] @@ -14731,16 +13734,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Prometheus Node Exporter Lua / Config" ] @@ -14878,16 +13873,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Vnstat / Config" ] @@ -15034,16 +14021,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Vnstat / Interfaces" ] @@ -15187,16 +14166,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Vnstat / Interfaces" ] @@ -15277,16 +14248,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Vnstat / Interfaces" ] @@ -15340,16 +14303,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Vnstat / Interfaces" ] @@ -15430,16 +14385,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Vnstat / Interfaces" ] @@ -15577,16 +14524,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Globals" ] @@ -15733,16 +14672,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Interfaces" ] @@ -15886,16 +14817,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Interfaces" ] @@ -15976,16 +14899,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Interfaces" ] @@ -16039,16 +14954,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Interfaces" ] @@ -16129,16 +15036,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Interfaces" ] @@ -16285,16 +15184,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Members" ] @@ -16438,16 +15329,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Members" ] @@ -16528,16 +15411,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Members" ] @@ -16591,16 +15466,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Members" ] @@ -16681,16 +15548,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Members" ] @@ -16837,16 +15696,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Policies" ] @@ -16990,16 +15841,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Policies" ] @@ -17080,16 +15923,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Policies" ] @@ -17143,16 +15978,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Policies" ] @@ -17233,16 +16060,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Policies" ] @@ -17389,16 +16208,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Rules" ] @@ -17542,16 +16353,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Rules" ] @@ -17632,16 +16435,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Rules" ] @@ -17695,16 +16490,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Rules" ] @@ -17785,16 +16572,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Mwan3 / Rules" ] @@ -17932,16 +16711,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Usteer / Config" ] @@ -18088,16 +16859,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Openvpn / Instances" ] @@ -18241,16 +17004,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Openvpn / Instances" ] @@ -18331,16 +17086,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Openvpn / Instances" ] @@ -18394,16 +17141,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Openvpn / Instances" ] @@ -18484,16 +17223,8 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "$ref": "#/components/responses/PendingConfirm" } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Openvpn / Instances" ] @@ -20409,354 +19140,16 @@ }, "423": { "$ref": "#/components/responses/Locked" - }, - "202": { - "description": "Accepted: every sub-request succeeded and a commit-confirmed rollback is armed over all touched packages. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "headers": { - "X-Confirm-Token": { - "description": "The armed rollback token; pass to /confirm/{token}.", - "schema": { - "type": "string", - "pattern": "^ac_[0-9]+_[0-9a-f]{8}$" - } - }, - "X-Confirm-Deadline": { - "description": "Best-effort unix epoch of auto-rollback.", - "schema": { - "type": "integer" - } - } - }, - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/BatchResponse" - }, - { - "type": "object", - "required": [ - "confirm" - ], - "properties": { - "confirm": { - "$ref": "#/components/schemas/ConfirmWindow" - } - } - } - ] - } - } - } } }, - "parameters": [ - { - "$ref": "#/components/parameters/Confirm" - } - ], "tags": [ "Operational / Batch" ] } - }, - "/confirm": { - "get": { - "summary": "List pending confirm windows", - "description": "Passthrough to `apply-confirm list --json`. Scope: uapi:confirm:ro (or *:ro). Requires the apply-confirm package. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ConfirmWindow" - } - } - } - }, - "headers": { - "X-Request-Id": { - "$ref": "#/components/headers/XRequestId" - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "429": { - "$ref": "#/components/responses/TooManyRequests" - }, - "500": { - "$ref": "#/components/responses/InternalError" - }, - "503": { - "$ref": "#/components/responses/ServiceUnavailable" - } - }, - "tags": [ - "Operational / Commit-confirm" - ] - } - }, - "/confirm/{token}": { - "parameters": [ - { - "name": "token", - "in": "path", - "required": true, - "schema": { - "type": "string", - "pattern": "^ac_[0-9]+_[0-9a-f]{8}$" - } - } - ], - "get": { - "summary": "Status of a pending confirm window", - "description": "Passthrough to `apply-confirm status --json` (authoritative remaining time). Scope: uapi:confirm:ro. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConfirmWindow" - } - } - }, - "headers": { - "X-Request-Id": { - "$ref": "#/components/headers/XRequestId" - }, - "ETag": { - "$ref": "#/components/headers/ETag" - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "429": { - "$ref": "#/components/responses/TooManyRequests" - }, - "500": { - "$ref": "#/components/responses/InternalError" - }, - "503": { - "$ref": "#/components/responses/ServiceUnavailable" - } - }, - "tags": [ - "Operational / Commit-confirm" - ] - }, - "post": { - "summary": "Confirm (ack) a pending apply", - "description": "Acks the window so the change is NOT rolled back. Scope: uapi:confirm:rw. 409 confirm_window_closed if the window already expired or closed. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "responses": { - "200": { - "description": "Confirmed", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "confirmed": { - "type": "string" - } - } - } - } - }, - "headers": { - "X-Request-Id": { - "$ref": "#/components/headers/XRequestId" - }, - "X-Reload-Status": { - "$ref": "#/components/headers/XReloadStatus" - }, - "X-Reload-Services": { - "$ref": "#/components/headers/XReloadServices" - }, - "Idempotent-Replayed": { - "$ref": "#/components/headers/IdempotentReplayed" - }, - "ETag": { - "$ref": "#/components/headers/ETag" - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "429": { - "$ref": "#/components/responses/TooManyRequests" - }, - "500": { - "$ref": "#/components/responses/InternalError" - }, - "503": { - "$ref": "#/components/responses/ServiceUnavailable" - }, - "409": { - "$ref": "#/components/responses/Conflict" - }, - "412": { - "$ref": "#/components/responses/PreconditionFailed" - }, - "422": { - "$ref": "#/components/responses/ValidationFailed" - }, - "423": { - "$ref": "#/components/responses/Locked" - } - }, - "tags": [ - "Operational / Commit-confirm" - ] - }, - "delete": { - "summary": "Roll back a pending apply now", - "description": "Restores the snapshot immediately (early/forced revert). Scope: uapi:confirm:rw. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "responses": { - "200": { - "description": "Rolled back", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "rolled_back": { - "type": "string" - } - } - } - } - }, - "headers": { - "X-Request-Id": { - "$ref": "#/components/headers/XRequestId" - }, - "X-Reload-Status": { - "$ref": "#/components/headers/XReloadStatus" - }, - "X-Reload-Services": { - "$ref": "#/components/headers/XReloadServices" - }, - "ETag": { - "$ref": "#/components/headers/ETag" - } - } - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "429": { - "$ref": "#/components/responses/TooManyRequests" - }, - "500": { - "$ref": "#/components/responses/InternalError" - }, - "503": { - "$ref": "#/components/responses/ServiceUnavailable" - }, - "409": { - "$ref": "#/components/responses/Conflict" - }, - "412": { - "$ref": "#/components/responses/PreconditionFailed" - }, - "422": { - "$ref": "#/components/responses/ValidationFailed" - }, - "423": { - "$ref": "#/components/responses/Locked" - } - }, - "tags": [ - "Operational / Commit-confirm" - ] - } } }, "components": { "schemas": { - "ConfirmWindow": { - "type": "object", - "required": [ - "token", - "timeout", - "deadline", - "packages" - ], - "description": "An armed commit-confirmed rollback window (apply-confirm). Returned in the 202 body of a confirmed write.", - "properties": { - "token": { - "type": "string", - "pattern": "^ac_[0-9]+_[0-9a-f]{8}$", - "description": "Pass to POST /confirm/{token} to ack, or DELETE /confirm/{token} to roll back now." - }, - "timeout": { - "type": "integer", - "description": "Seconds the window stays armed." - }, - "deadline": { - "type": "integer", - "description": "Best-effort unix epoch of auto-rollback; GET /confirm/{token} is authoritative." - }, - "packages": { - "type": "array", - "items": { - "type": "string" - }, - "description": "uci packages snapshotted and restored on rollback." - } - } - }, "ErrorEnvelope": { "type": "object", "required": [ @@ -20790,11 +19183,6 @@ "reload_failed_unrecovered", "service_unavailable", "init_script_missing", - "confirm_unavailable", - "already_armed", - "confirm_window_closed", - "confirm_stage_failed", - "rollback_reload_failed", "batch_partial_failure" ], "description": "Machine-readable error code. Stable within a major. Clients should branch on HTTP status first and treat unknown codes gracefully (the project's additive contract permits new codes within a major)." @@ -25781,7 +24169,7 @@ } }, "InternalError": { - "description": "Server error (codes: internal_error, reload_failed_restored, reload_failed_unrecovered, rollback_reload_failed)", + "description": "Server error (codes: internal_error, reload_failed_restored, reload_failed_unrecovered)", "content": { "application/json": { "schema": { @@ -25791,7 +24179,7 @@ } }, "ServiceUnavailable": { - "description": "Service unavailable (codes: service_unavailable, init_script_missing, confirm_stage_failed)", + "description": "Service unavailable (codes: service_unavailable, init_script_missing)", "content": { "application/json": { "schema": { @@ -25799,55 +24187,6 @@ } } } - }, - "PendingConfirm": { - "description": "Write committed with a commit-confirmed rollback armed. The body is the normal resource representation plus a `confirm` block; unless POST /confirm/{token} acks before the deadline, apply-confirm restores the pre-change snapshot. (x-uapi-requires: apply-confirm)", - "x-uapi-requires": "apply-confirm", - "headers": { - "X-Confirm-Token": { - "description": "The armed rollback token; pass to /confirm/{token}.", - "schema": { - "type": "string", - "pattern": "^ac_[0-9]+_[0-9a-f]{8}$" - } - }, - "X-Confirm-Deadline": { - "description": "Best-effort unix epoch of auto-rollback; poll GET /confirm/{token} for authoritative remaining time.", - "schema": { - "type": "integer" - } - } - }, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "confirm" - ], - "description": "The resource representation plus the armed `confirm` window.", - "properties": { - "confirm": { - "$ref": "#/components/schemas/ConfirmWindow" - } - } - } - } - } - } - }, - "parameters": { - "Confirm": { - "name": "confirm", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 3600 - }, - "x-uapi-requires": "apply-confirm", - "description": "Arm a commit-confirmed rollback on this write: snapshot the affected uci packages and auto-revert after this many seconds unless POST /confirm/{token} acks first. The query form is the portable interface; the `X-Uapi-Confirm` header also works but only behind a proxy that forwards it (uhttpd's CGI env strips custom headers via a hard-coded allowlist). When set, the success status is 202 with a `confirm` block. Requires the apply-confirm package; without it the write returns 501 confirm_unavailable." } } } diff --git a/docs/architecture.md b/docs/architecture.md index 92be995..baf6deb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -236,12 +236,11 @@ current ETag doesn't match, return `412 precondition_failed` BEFORE any uci write. `If-Match: *` matches any existing resource. Absent header preserves last-write-wins (opt-in concurrency). -uhttpd's CGI env strips `If-Match`, `If-None-Match`, `X-Request-Id`, -`Idempotency-Key`, and `X-Uapi-Confirm` (hard-coded allowlist in uhttpd -source). All have query-string fallbacks (`?if_match=` / `?if_none_match=` / -`?request_id=` / `?idempotency_key=` / `?confirm=`) which are the portable -interface; a reverse proxy in front of uhttpd that forwards the headers also -works via the header path. +uhttpd's CGI env strips `If-Match`, `If-None-Match`, `X-Request-Id`, and +`Idempotency-Key` (hard-coded allowlist in uhttpd source). All four have +`?if_match=` / `?if_none_match=` / `?request_id=` / `?idempotency_key=` +query-string fallbacks; a reverse proxy in front of uhttpd that forwards +the headers still works via the header path. ## Rate limit token bucket @@ -354,16 +353,6 @@ batch's request_id with each sub-request's `.` line. | Apk install lock | (apk-internal) | Per-operation | | uapi global flock | `/var/lock/uapi.lock` | Per-transaction | | uapi per-package flock | `/var/lock/uapi.pkg..lock` | Per-transaction | -| Commit-confirm pending window | apply-confirm-owned (its durable state) | Until ack/timeout | - -The commit-confirm pending window (snapshot + deadline) is owned entirely by -the `apply-confirm` package, not uapi: uapi stages it via the CLI and surfaces -it read-only through `/confirm`. uapi holds no timer and no persisted confirm -state. Note the unconfirmed-window lock gap: the per-package flock is released -when the write's fork exits, so a second normal write to an armed package can -commit and then be clobbered by the auto-revert. apply-confirm's one-pending- -apply rule bounds this, and the Terraform provider serializes its own applies; -see `docs/commit-confirm.md`. `/tmp` is tmpfs on OpenWrt: rate-limit buckets, idempotency entries, and metrics counters reset on reboot. This is acceptable: operational diff --git a/docs/commit-confirm.md b/docs/commit-confirm.md index 2287180..34bd1ed 100644 --- a/docs/commit-confirm.md +++ b/docs/commit-confirm.md @@ -1,112 +1,110 @@ -# Commit-confirmed apply - -A uapi write is atomic per request (stage, validate, commit, reload, with an -in-band rollback if the reload command fails). That protects against a *failed* -reload. It does not protect against a reload that succeeds yet severs the -operator's only path to the box: a firewall or network change can reload -cleanly (the init script exits 0) and still lock you out, with no one left to -undo it. - -Commit-confirmed apply closes that gap. A write can arm a deadline: uapi -snapshots the affected uci packages and commits the change, and unless the -client confirms within the window, the snapshot is restored automatically. The -rollback fires locally and survives a reboot, a process kill, and the -management interface going down. - -uapi does not implement the timer or the durable state itself; that would mean a -long-running daemon, which the zero-bloat / no-aux-process principle forbids. -The supervisor lives in a separate package, `apply-confirm`, and uapi integrates -by invoking its CLI. See `apply-confirm`'s own docs for the rollback mechanism. - -## Optional, feature-detected - -The integration is optional. uapi probes for `/usr/sbin/apply-confirm` per -request: - -- Installed: the confirm surface is live. -- Absent: a write that asks to confirm returns `501 confirm_unavailable`, and an - ordinary write (no confirm) behaves exactly as before. There is no hard - package dependency; `apk add apply-confirm` is the opt-in. - -## Arming a write - -Add `?confirm=` to any config write (`POST`/`PUT`/`PATCH`/`DELETE` on a -resource, or `POST /batch`). The value is 1..3600. Arming needs no extra scope: -it is a safety modifier on a write the token is already authorized to perform. - -```sh -# Arm a 60s rollback on a firewall change. -curl -fsS -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ - -X PUT 'https://router/api/v2/firewall/rules/?confirm=60' -d @rule.json -``` - -The query parameter is the portable interface. uapi also reads an -`X-Uapi-Confirm: ` header, but uhttpd's CGI env strips it via a -hard-coded allowlist (the same reason `If-Match` and `Idempotency-Key` have -`?if_match=` / `?idempotency_key=` fallbacks); the header path only works when a -reverse proxy in front of uhttpd forwards it. - -A confirmed write returns **202 Accepted** instead of 200/204. The body is the -normal resource representation plus a `confirm` block, and the token is also in -the `X-Confirm-Token` response header: - -```json -{ - "...": "normal resource body", - "confirm": { - "token": "ac_1718900000_a1b2c3d4", - "timeout": 60, - "deadline": 1718900060, - "packages": ["firewall"] - } -} -``` - -`deadline` is best-effort (`now + timeout`); poll `GET /confirm/` for the -authoritative remaining time. - -## Confirming or rolling back - -Verify the management path still works, then confirm. If you cannot reach the -box, do nothing: the deadline fires and the change is reverted. - -| Method + path | Action | Scope | -|--------------------------|-------------------------------------|------------------| -| `POST /confirm/` | ack: keep the change | `uapi:confirm:rw`| -| `DELETE /confirm/`| roll back now (early/forced) | `uapi:confirm:rw`| -| `GET /confirm/` | status (authoritative remaining) | `uapi:confirm:ro`| -| `GET /confirm` | list pending windows | `uapi:confirm:ro`| - -A console operator with no network can still force the revert directly: -`apply-confirm rollback`. - -## What uapi reloads vs what apply-confirm reloads - -uapi owns the forward reload (applying the change) and the in-band rollback (if -that reload fails). apply-confirm owns the out-of-band rollback reload (timeout -or forced) because uapi's process is gone by then. uapi passes its own -authoritative reload set (`resource.reload`, unioned across a batch) to -`apply-confirm stage --service ...`, so a rollback reloads exactly what the -apply reloaded. - -## Failure modes - -| Condition | Response | -|-----------|----------| -| apply-confirm not installed | `501 confirm_unavailable`; nothing staged | -| another apply already armed (one pending at a time, across uapi/LuCI/shell) | `409 already_armed`; reverted | -| bad timeout | `400 bad_request`; reverted | -| snapshot/state write failed | `503 confirm_stage_failed`; nothing committed | -| apply reload fails while uapi is alive | in-band `reload_failed_restored`, and the now-pointless window is disarmed | -| apply bricks so hard the request never returns | apply-confirm's supervisor auto-reverts at the deadline, surviving reboot | -| ack after the window closed / double ack | `409 confirm_window_closed` | -| forced rollback restored config but a reload failed | `500 rollback_reload_failed` (the config WAS restored) | - -## Known limit: the unconfirmed window - -While a window is armed, the write's per-package lock has already been released -(uapi forks per request). A *second* normal write to the same package during the -window will commit, and then the pending auto-rollback can clobber it. The -single-pending-apply rule (one armed window globally) bounds the blast radius, -and the Terraform provider serializes its own applies, so this is a non-issue -for the primary consumer. Do not interleave manual writes with an armed window. +# Commit-confirmed apply (deferred design) + +**Status: DEFERRED. Not shipped in 2.3.0.** Targeted for a 2.4.0 minor once +the authz model below is settled and a concrete consumer wants it +(`apply-confirm` 0.1.0 is already released on the apk feed, so the +dependency is no longer a blocker). The full per-write implementation was +built, shipped in the +2.3.0-rc1 pre-release, soaked on live hardware, and then removed before +stable; it is recoverable from commit `a85a5cd`. This file is the design and +decision record for the eventual 2.4.0 work, not a description of shipped +behavior. + +## Why it was deferred from 2.3.0 + +uapi writes are atomic per request (stage, validate, commit, reload, with +in-band rollback if the reload command fails). What that does not cover is a +change that reloads cleanly yet severs the operator's only management path +(the classic firewall/network lockout). Commit-confirmed apply closes that +gap: snapshot the affected uci packages, arm a deadline, and auto-restore if +no ack arrives. The mechanism works (validated end to end on real hardware). +It was deferred for contract-commitment reasons, not because it is broken: + +- **No first-party consumer.** The Terraform provider ships 2.3.0 without + consuming confirm. Shipping the surface stable would freeze a permanent v2 + contract that nothing exercises. +- **Unsettled authz (the deciding factor).** Freezing the current authz into + v2 would cost a major bump to change later. See "Authz model to settle". + +(The dependency is not a factor: `apply-confirm` 0.1.0 is released and on the +apk feed. The hold is the wire-contract commitment, not availability.) + +## Architecture (the chosen shape) + +A separate package, `apply-confirm` (procd-supervised, durable state under +`/var/lib`), owns the rollback timer and the uci-package snapshots. uapi +gains no daemon of its own; it integrates by invoking the `apply-confirm` +CLI (`stage` / `ack` / `rollback` / `status` / `list`). The snapshot is a +per-uci-package `uci export`; rollback is `uci import` + commit + a service +reload. A single box-global pending window is enforced by an exclusive flock +(`stage` refuses with `already_armed` if one is already armed). + +## Wire surface (as built in rc1, the per-write half) + +- A write carries `?confirm=` (1..3600). uapi snapshots the + affected packages, commits, reloads, and returns `202 Accepted` with a + `ConfirmWindow {token, timeout, deadline, packages}` body plus + `X-Confirm-Token` / `X-Confirm-Deadline` headers. The header form + `X-Uapi-Confirm` also works behind a proxy that forwards it, but uhttpd's + CGI strips custom headers, so the query form is the portable interface. +- `GET /confirm` (list), `GET /confirm/` (status), `POST + /confirm/` (ack), `DELETE /confirm/` (roll back now). +- Scope `uapi:confirm` (`:ro` for status/list, `:rw` for ack/rollback). +- Optional and feature-detected: `501 confirm_unavailable` when + `apply-confirm` is not installed. + +**The ack is client-driven; uapi never auto-acks.** The client confirms only +after verifying the management path still works. A network blip that hides +the `202` also prevents the ack, so the auto-revert and the client's view of +its own apply stay consistent (this is what resolves the original +state-divergence objection that killed the v1-era design). + +## The missing half: a standalone arm (the 2.4.0 ask) + +The per-write form cannot wrap a whole `terraform apply`: a DAG apply is N +isolated RPCs with no apply-level hook, and each `?confirm` mints a separate +last-writer-wins window. The Terraform-useful shape is `apply-confirm`'s +`stage` primitive exposed over HTTP so a wrapper can arm once, run the apply, +then ack once. Locked design (see `docs/roadmap.md`): + +- New `POST /confirm` (the bare-collection slot, today a 405). +- Body names curated **resources/scopes, never raw packages**; uapi derives + the package set and reload-service union from `RESOURCE_SOURCES` (the same + fold `/batch` does). +- Returns the same `ConfirmWindow` 202 body; ack/rollback/status/list reuse + the existing `/confirm` endpoints unchanged. + +## Authz model to settle (the reason for one coherent 2.4.0 cut) + +Ship per-write and standalone together so the authz is decided once: + +- Per-write arming currently needs no extra scope; it rides the write's own + resource `:rw`. But `apply-confirm` reverts the whole uci **package**, + while uapi scopes are per **resource**, and one package backs many + resources (`network` backs 7, `firewall` 5, `dhcp` 6). So a per-write arm + with only `network:routes:rw` can cause a deadline revert of the whole + `network` package, including `network:interfaces` the token cannot write. +- The standalone arm must therefore require `uapi:confirm:rw` **and** `:rw` + on every curated resource backed by the *derived* package set + (package-granularity authz). +- ack/rollback are window-agnostic today (a `uapi:confirm:rw` token can ack + any window). Decide whether that should become window/package-scoped. + +Any of these decided differently than rc1 shipped would be a breaking change +once frozen into v2. That is why the whole feature waits for one reviewed +authz model rather than shipping the per-write half now. + +## Operator wrapper guidance (for the eventual consumer) + +The whole-apply wrap is operator-driven (a script around `terraform apply`), +owned by the provider repo. Two rules it must follow: + +- **Mint the wrap token at package granularity** (the token is necessarily + broader than the apply it guards), or a narrow token gets a 403 on arm. +- **Key ack-vs-rollback on management-path reachability, not the apply's + exit code.** A partial failure where the box is still reachable should ack + (Terraform already recorded the resources that succeeded, so acking keeps + the box consistent with state); only an unreachable box should be left to + auto-revert. `apply; if reachable then ack else let-expire`, never `if exit + 0 then ack`. Otherwise a forgotten ack reverts the whole armed package to + its arm-time snapshot, silently undoing committed sibling-resource changes. diff --git a/docs/errors.md b/docs/errors.md index 173ae33..1d6a5e0 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -49,13 +49,8 @@ For DELETE success, the response is `204 No Content` with the `X-Request-Id` hea | 500 | `internal_error` | Bug or unexpected condition | | 500 | `reload_failed_restored` | Daemon reload failed; uapi rolled back the uci change | | 500 | `reload_failed_unrecovered` | Reload AND restore failed. Loudest case; manual recovery | -| 500 | `rollback_reload_failed` | Forced commit-confirm rollback restored uci but a reload failed (config WAS restored) | -| 501 | `confirm_unavailable` | `X-Uapi-Confirm` used but the apply-confirm package is not installed | | 503 | `service_unavailable` | ubus unreachable, service not running | | 503 | `init_script_missing` | `/etc/init.d/` not present for a resource's reload list | -| 503 | `confirm_stage_failed` | apply-confirm could not snapshot/arm the rollback window | -| 409 | `already_armed` | A commit-confirm window is already pending (one at a time) | -| 409 | `confirm_window_closed` | ack/rollback of a token with no open window (expired/unknown)| `batch_partial_failure` is special: it appears only in the body of a `POST /batch` abort response, with the HTTP status taken from the failing diff --git a/docs/installation.md b/docs/installation.md index a549ca6..a263139 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -48,10 +48,6 @@ The feed carries **stable releases only**. Release candidates (`-rc`, `-alpha`, The stable line currently is `1.2.x` (`v2.0.0` is in RC at time of writing). `v1.2.1` stays available indefinitely for operators who need to pin to the v1 wire contract; `apk add 'uapi<2.0.0'` (or `apk add uapi=1.2.1-r1`) gets you there. -## Optional: commit-confirmed apply - -uapi does not depend on it, but installing the `apply-confirm` package enables the safe-apply (commit-confirmed rollback) surface: `apk add apply-confirm`. Without it, an ordinary write works as usual and a write that sets `X-Uapi-Confirm` returns `501 confirm_unavailable`. See `docs/commit-confirm.md`. - ## TLS uapi inherits TLS from the `main` uhttpd instance. By default OpenWrt ships a self-signed certificate (regenerated at first boot via `px5g`); browsers and curl complain, and over a real network this is **not adequate**. Two well-trodden options on OpenWrt: diff --git a/docs/roadmap.md b/docs/roadmap.md index f095826..a1da858 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -49,33 +49,45 @@ Items that are interesting but conflict with an architectural principle or have an unresolved design question. Not "later"; "later if the right shape appears". -### `commit-confirmed` timed rollback (shipped 2.3.0) +### `commit-confirmed` timed rollback (built, deferred from 2.3.0) -Resolved by the sidecar path (option 1 below), built as a separate package -(`apply-confirm`) so uapi gains no daemon of its own; uapi integrates by -invoking its CLI. See `docs/commit-confirm.md`. The original -state-divergence objection ("the router reverts but Terraform thinks it's OK") -is handled by making the ack client-driven: a confirmed write returns 202 + a -token, and the client confirms only after verifying reachability. If the client -never sees the response, it also never acks, so its own apply is not marked -complete; the auto-revert and the client's view stay consistent. - -Sequencing: the wire surface ships in 2.3.0 but the 2.3.0 tag is held until -`apply-confirm` reaches a stable, feed-published release (it is a safety -primitive that soaks RC-first). The integration is optional and -feature-detected, so an install without apply-confirm is unaffected. - -The original v2-planning analysis, kept for context: +Built and validated, then deferred out of 2.3.0 stable. The sidecar path +won: a separate package (`apply-confirm`) owns the durable rollback timer +and snapshot state so uapi gains no daemon of its own; uapi integrates by +invoking its CLI. A confirmed write returns `202` + a token via per-write +`?confirm=`, and the ack is client-driven, which resolves the +original state-divergence objection (the client acks only after verifying +reachability, so a lost response prevents both the ack and the client +marking its own apply complete): > If the router gets no confirmation because of a network temporary issue > between the router and Terraform, it reverts, but Terraform thinks it's > OK. That state-divergence is worse than the original race we're trying > to solve. -The webhook-on-revert refinement (push a rollback notification to the client) -remains open as a future enhancement, not a requirement. The fully-synchronous -"stage-and-test" pattern is now specified concretely as a standalone HTTP arm -endpoint under Features below. +The full per-write surface shipped in 2.3.0-rc1 and was soaked on live +hardware, then removed before stable (recoverable from commit `a85a5cd`). +Why deferred rather than shipped: + +- No first-party consumer: the Terraform provider ships 2.3.0 without + consuming confirm, so the surface would enter a permanent v2 contract + with nothing exercising it. +- The authz model is unsettled and freezing it would cost a major bump to + fix. Per-write arming currently rides the write's own resource `:rw` with + no `uapi:confirm` requirement, and ack/rollback are window-agnostic (a + `uapi:confirm:rw` token can ack any window); the package-granularity + escalation analysis (a per-write arm snapshots and reverts the whole uci + package, not just the resource written) suggests these may need to change. + Committing them to v2 now forecloses that without a 3.0.0. + +The dependency is not the blocker: `apply-confirm` 0.1.0 is released and on +the apk feed. The hold is the wire-contract commitment. + +Plan: ship the whole feature once, coherently, in a 2.4.0 (per-write +`?confirm` plus the standalone `POST /confirm` arm under Features below, +with one reviewed authz model), gated on a settled authz model and a +concrete consumer. Design reference: `docs/commit-confirm.md`. + ## Features (additive, future minor bumps in v2.x) diff --git a/docs/tokens.md b/docs/tokens.md index d3cc83e..06952e7 100644 --- a/docs/tokens.md +++ b/docs/tokens.md @@ -122,7 +122,7 @@ The authoritative source is `src/lib/scope.uc` `KNOWN_PATHS`. | `prometheus_node_exporter_lua` | `config` | | `vnstat` | `config`, `interfaces` | | `packages` | `installed`, `feeds` | -| `uapi` | `tokens`, `metrics`, `diagnostics`, `confirm` | +| `uapi` | `tokens`, `metrics`, `diagnostics` | | `raw` | `` (composes with the curated domain tree) | ## Deepest-match-wins diff --git a/src/lib/apply_confirm.uc b/src/lib/apply_confirm.uc deleted file mode 100644 index ad081d4..0000000 --- a/src/lib/apply_confirm.uc +++ /dev/null @@ -1,136 +0,0 @@ -let fs = require('fs'); - -// Thin wrapper around the apply-confirm CLI (a separate OpenWrt package). uapi -// invokes it, never absorbs it: the rollback timer and durable state live in -// apply-confirm's procd supervisor, so uapi keeps its zero-daemon footprint. -// The integration is optional - when the binary is absent the confirm surface -// degrades to 501 and ordinary writes are unaffected. -const BIN = "/usr/sbin/apply-confirm"; - -// Package and service names that reach the shell. Matches transaction.uc's -// SAFE_NAME_RE. popen runs the string through sh, so every interpolated value -// is validated against this BEFORE it is concatenated - that is the injection -// control (ucode fs.popen has no argv-vector form). -const NAME_RE = /^[A-Za-z0-9_-]+$/; -// apply-confirm token: ac__<8 lowercase hex> (see its cli-contract). -const TOKEN_RE = /^ac_[0-9]+_[0-9a-f]{8}$/; - -function ac_present() { - return fs.stat(BIN) != null; -} - -function _run(args) { - let p = fs.popen(BIN + " " + args, "r"); - if (p == null) return { exit: -1, out: "" }; - let out = trim(p.read("all") ?? ""); - let exit = p.close(); - return { exit, out }; -} - -// Snapshot `packages`, arm a `timeout`-second rollback that reloads `services` -// on restore. Returns { ok: true, token, timeout, deadline } or -// { ok: false, kind, message } with kind a registered error code. -function ac_stage(packages, services, timeout) { - if (type(timeout) != "int" || timeout <= 0) - return { ok: false, kind: "bad_request", - message: "confirm timeout must be a positive integer" }; - if (type(packages) != "array" || length(packages) == 0) - return { ok: false, kind: "confirm_stage_failed", - message: "no packages to stage" }; - - let args = "stage --timeout " + timeout; - for (let pkg in packages) { - if (type(pkg) != "string" || !match(pkg, NAME_RE)) - return { ok: false, kind: "confirm_stage_failed", - message: sprintf("unsafe package name %J", pkg) }; - args += " --package " + pkg; - } - for (let svc in (services ?? [])) { - if (type(svc) != "string" || !match(svc, NAME_RE)) - return { ok: false, kind: "confirm_stage_failed", - message: sprintf("unsafe service name %J", svc) }; - args += " --service " + svc; - } - - if (!ac_present()) - return { ok: false, kind: "confirm_unavailable", - message: "apply-confirm is not installed" }; - - let r = _run(args); - if (r.exit == 0) { - if (!match(r.out, TOKEN_RE)) - return { ok: false, kind: "confirm_stage_failed", - message: sprintf("apply-confirm returned an unrecognized token %J", r.out) }; - return { ok: true, token: r.out, timeout, deadline: time() + timeout }; - } - if (r.exit == 3) - return { ok: false, kind: "already_armed", - message: "another apply is already armed (one pending apply at a time)" }; - if (r.exit == 2) - return { ok: false, kind: "bad_request", - message: "apply-confirm rejected the stage parameters" }; - return { ok: false, kind: "confirm_stage_failed", - message: sprintf("apply-confirm stage failed (exit %d)", r.exit) }; -} - -function _control(verb, token) { - if (!match(token ?? "", TOKEN_RE)) - return { ok: false, kind: "bad_request", message: "malformed confirm token" }; - if (!ac_present()) - return { ok: false, kind: "confirm_unavailable", - message: "apply-confirm is not installed" }; - let r = _run(verb + " " + token); - return { ok: r.exit == 0, exit: r.exit, out: r.out }; -} - -// ack: confirm the pending apply so it is NOT rolled back. -function ac_ack(token) { - let r = _control("ack", token); - if (r.kind != null) return r; - if (r.exit == 0) return { ok: true }; - if (r.exit == 4) - return { ok: false, kind: "confirm_window_closed", - message: "no such token, or the confirm window already closed" }; - return { ok: false, kind: "confirm_stage_failed", - message: sprintf("apply-confirm ack failed (exit %d)", r.exit) }; -} - -// rollback: restore the snapshot now (early/forced revert). -function ac_rollback(token) { - let r = _control("rollback", token); - if (r.kind != null) return r; - if (r.exit == 0) return { ok: true }; - if (r.exit == 4) - return { ok: false, kind: "confirm_window_closed", - message: "no such token, or nothing pending" }; - if (r.exit == 5) - return { ok: false, kind: "rollback_reload_failed", - message: "config WAS restored but a service reload failed; check logread" }; - return { ok: false, kind: "confirm_stage_failed", - message: sprintf("apply-confirm rollback failed (exit %d)", r.exit) }; -} - -// status / list are read passthroughs returning apply-confirm's JSON verbatim. -function ac_status(token) { - if (!match(token ?? "", TOKEN_RE)) - return { ok: false, kind: "bad_request", message: "malformed confirm token" }; - if (!ac_present()) - return { ok: false, kind: "confirm_unavailable", message: "apply-confirm is not installed" }; - let r = _run("status " + token + " --json"); - if (r.exit == 0) return { ok: true, json: r.out }; - if (r.exit == 4) - return { ok: false, kind: "not_found", message: "no such token" }; - return { ok: false, kind: "confirm_stage_failed", - message: sprintf("apply-confirm status failed (exit %d)", r.exit) }; -} - -function ac_list() { - if (!ac_present()) - return { ok: false, kind: "confirm_unavailable", message: "apply-confirm is not installed" }; - let r = _run("list --json"); - if (r.exit == 0) return { ok: true, json: r.out }; - return { ok: false, kind: "confirm_stage_failed", - message: sprintf("apply-confirm list failed (exit %d)", r.exit) }; -} - -return { ac_present, ac_stage, ac_ack, ac_rollback, ac_status, ac_list }; diff --git a/src/lib/errors.uc b/src/lib/errors.uc index be25a8e..2338aa2 100644 --- a/src/lib/errors.uc +++ b/src/lib/errors.uc @@ -26,12 +26,6 @@ const STATUS_BY_CODE = { reload_failed_unrecovered: 500, service_unavailable: 503, init_script_missing: 503, - // commit-confirmed apply (apply-confirm integration) - confirm_unavailable: 501, - already_armed: 409, - confirm_window_closed: 409, - confirm_stage_failed: 503, - rollback_reload_failed: 500, }; const FIELD_CODES = { @@ -57,8 +51,6 @@ const ALL_CODES = [ "validation_failed", "locked", "too_many_requests", "internal_error", "reload_failed_restored", "reload_failed_unrecovered", "service_unavailable", "init_script_missing", - "confirm_unavailable", "already_armed", "confirm_window_closed", - "confirm_stage_failed", "rollback_reload_failed", "batch_partial_failure", ]; diff --git a/src/lib/handler.uc b/src/lib/handler.uc index 2ab4b61..2057bcc 100644 --- a/src/lib/handler.uc +++ b/src/lib/handler.uc @@ -337,24 +337,8 @@ function attach_reload_headers(resp, result) { function translate_tx(ctx, result) { if (result.ok) { let resp = attach_reload_headers(errors.ok(ctx, result.body), result); - if (result.body != null) resp = set_etag_header(resp, result.body); - // A commit-confirmed write returns 202 with the armed window so the - // client knows it must confirm (or the change auto-rolls back). - if (result.confirm != null) { - resp.status = 202; - // DELETE's result body is null; synthesize one so the 202 carries the - // required `confirm` block (PendingConfirm schema), not just the header. - if (type(resp.body) != "object") resp.body = {}; - resp.body.confirm = result.confirm; - resp.headers["X-Confirm-Token"] = result.confirm.token; - resp.headers["X-Confirm-Deadline"] = "" + result.confirm.deadline; - } - return resp; + return (result.body != null) ? set_etag_header(resp, result.body) : resp; } - if (result.kind == "confirm_unavailable" || result.kind == "already_armed" - || result.kind == "confirm_window_closed" || result.kind == "confirm_stage_failed" - || result.kind == "rollback_reload_failed" || result.kind == "bad_request") - return errors.error(ctx, result.kind, result.message); if (result.kind == "locked") return errors.locked_from(ctx, null, result); if (result.kind == "lock_unavailable") @@ -523,7 +507,6 @@ function make(resource, opts) { function create(conn, ctx, body) { let result = transaction.transaction(conn, tx_params({ - confirm: ctx.confirm, fn: function(c, p) { // Section-name resolution: caller's body.id wins, else the // per-resource id_for_create hook (e.g. network.interfaces @@ -570,7 +553,6 @@ function make(resource, opts) { function replace(conn, ctx, id, body) { let result = transaction.transaction(conn, tx_params({ - confirm: ctx.confirm, fn: function(c, p) { let errs = _validate_with_schema(resource, body, body, c, id); if (length(errs) > 0) @@ -607,7 +589,6 @@ function make(resource, opts) { function patch(conn, ctx, id, body) { let result = transaction.transaction(conn, tx_params({ - confirm: ctx.confirm, fn: function(c, p) { let existing = load_section(c, p, id); if (!existing || !type_predicate(existing['.type'])) @@ -649,7 +630,6 @@ function make(resource, opts) { function remove(conn, ctx, id) { let result = transaction.transaction(conn, tx_params({ - confirm: ctx.confirm, fn: function(c, p) { let existing = load_section(c, p, id); if (!existing || !type_predicate(existing['.type'])) @@ -668,12 +648,7 @@ function make(resource, opts) { }, })); - // A confirmed delete armed a rollback window; route through translate_tx - // so the 202 + X-Confirm-Token reaches the client. Without this the 204 - // short-circuit would drop the token: the section is deleted and the - // window armed, but the client has no token to ack and the delete - // silently auto-reverts at the deadline. - if (result.ok && result.confirm == null) + if (result.ok) return attach_reload_headers(errors.no_content(ctx), result); return translate_tx(ctx, result); } @@ -699,7 +674,6 @@ function make(resource, opts) { // name, so we go through the full transaction (snapshot + commit + // reload + restore-on-failure). let result = transaction.transaction(conn, tx_params({ - confirm: ctx.confirm, fn: function(c, p) { let existing = load_section(c, p, id); if (!existing || !type_predicate(existing['.type'])) @@ -766,7 +740,6 @@ function make_singleton(resource, opts) { function patch(conn, ctx, body) { let result = transaction.transaction(conn, tx_params({ - confirm: ctx.confirm, fn: function(c, p) { let existing = find(c); if (!existing) { diff --git a/src/lib/scope.uc b/src/lib/scope.uc index 91be111..4bfc262 100644 --- a/src/lib/scope.uc +++ b/src/lib/scope.uc @@ -68,7 +68,6 @@ const KNOWN_PATHS = { "uapi:tokens": true, "uapi:metrics": true, "uapi:diagnostics": true, - "uapi:confirm": true, "raw": true, }; diff --git a/src/lib/transaction.uc b/src/lib/transaction.uc index 7ef43f5..a269d55 100644 --- a/src/lib/transaction.uc +++ b/src/lib/transaction.uc @@ -1,5 +1,4 @@ let fs = require('fs'); -let apply_confirm = require('apply_confirm'); // Lock layout (introduced to let concurrent writes to different uci packages // proceed in parallel without losing the apk-vs-uci serialization guarantee): @@ -128,24 +127,7 @@ function _finalize_after_reload(reload_err, restore_fn, body, services) { reload_error: reload_err }; } -// Attach the confirm block to a successful result, or disarm the window when -// uapi already reverted in-band (the apply reload failed, so the staged -// rollback is now pointless and must not fire later). Disarm with ac_ack, not -// ac_rollback: uapi has already restored the pre-change config in-band, so we -// only need to cancel apply-confirm's timer. ac_rollback would re-import the -// (identical) snapshot and run a redundant second service reload whose failure -// we could not surface. `armed` is ac_stage's return; null when no confirm. -function _attach_confirm(result, armed, packages) { - if (armed == null) return result; - if (result.ok) - result.confirm = { token: armed.token, timeout: armed.timeout, - deadline: armed.deadline, packages }; - else - apply_confirm.ac_ack(armed.token); - return result; -} - -function run_inner(conn, pkg, services, fn, snapshot, reload, confirm) { +function run_inner(conn, pkg, services, fn, snapshot, reload) { let result = fn(conn, pkg); if (!result || result.ok === false) { @@ -153,34 +135,13 @@ function run_inner(conn, pkg, services, fn, snapshot, reload, confirm) { return result ?? { ok: false, kind: "unknown" }; } - // Arm the commit-confirmed rollback BEFORE commit: apply-confirm snapshots - // the on-disk pre-change config, which uci_commit is about to overwrite. - let armed = null; - if (confirm != null) { - armed = apply_confirm.ac_stage([pkg], services, confirm); - if (!armed.ok) { - conn.uci_revert(pkg); - return { ok: false, kind: armed.kind, message: armed.message }; - } - } + conn.uci_commit(pkg); - // If commit or reload throws after the window is armed, disarm it before - // propagating: otherwise a stale rollback fires at the deadline and reverts - // whatever state exists then, with no token ever delivered to ack. - let fin; - try { + return _finalize_after_reload(reload(services), function() { + conn.uci_import(pkg, snapshot); conn.uci_commit(pkg); - fin = _finalize_after_reload(reload(services), function() { - conn.uci_import(pkg, snapshot); - conn.uci_commit(pkg); - return reload(services); - }, result.body, services); - } catch (e) { - if (armed != null) apply_confirm.ac_rollback(armed.token); - die(e); - } - - return _attach_confirm(fin, armed, [pkg]); + return reload(services); + }, result.body, services); } function transaction(conn, params) { @@ -220,7 +181,7 @@ function transaction(conn, params) { let caught = null; try { let snapshot = conn.uci_export(pkg); - result = run_inner(conn, pkg, services, fn, snapshot, reload, params.confirm); + result = run_inner(conn, pkg, services, fn, snapshot, reload); } catch (e) { caught = e; } @@ -286,7 +247,6 @@ function multi_transaction(conn, params) { let snapshots = {}; let result = null; let caught = null; - let armed = null; try { for (let pkg in sorted) snapshots[pkg] = conn.uci_export(pkg); let inner = fn(conn); @@ -294,50 +254,34 @@ function multi_transaction(conn, params) { for (let pkg in sorted) conn.uci_revert(pkg); result = inner ?? { ok: false, kind: "unknown" }; } else { - // Arm the commit-confirmed rollback over all packages before any - // commit (apply-confirm snapshots the pre-change on-disk config). - if (params.confirm != null) { - armed = apply_confirm.ac_stage(sorted, services, params.confirm); - if (!armed.ok) { - for (let pkg in sorted) conn.uci_revert(pkg); - result = { ok: false, kind: armed.kind, message: armed.message }; + // Commit each package, but capture the first failure so we can + // still attempt a restore on every package (committed or not). + // Without this, a mid-loop commit throw leaves earlier packages + // committed and breaks the across-packages atomicity contract. + let commit_err = null; + for (let pkg in sorted) { + let caught_commit = null; + try { conn.uci_commit(pkg); } catch (e) { caught_commit = "" + e; } + if (caught_commit != null) { + commit_err = sprintf("uci_commit(%s) failed: %s", + pkg, caught_commit); + break; } } - if (result == null) { - // Commit each package, but capture the first failure so we can - // still attempt a restore on every package (committed or not). - // Without this, a mid-loop commit throw leaves earlier packages - // committed and breaks the across-packages atomicity contract. - let commit_err = null; + let reload_err = (commit_err == null) ? reload(services) : commit_err; + result = _finalize_after_reload(reload_err, function() { for (let pkg in sorted) { - let caught_commit = null; - try { conn.uci_commit(pkg); } catch (e) { caught_commit = "" + e; } - if (caught_commit != null) { - commit_err = sprintf("uci_commit(%s) failed: %s", - pkg, caught_commit); - break; - } + conn.uci_import(pkg, snapshots[pkg]); + conn.uci_commit(pkg); } - let reload_err = (commit_err == null) ? reload(services) : commit_err; - result = _attach_confirm(_finalize_after_reload(reload_err, function() { - for (let pkg in sorted) { - conn.uci_import(pkg, snapshots[pkg]); - conn.uci_commit(pkg); - } - return reload(services); - }, inner.body, services), armed, sorted); - } + return reload(services); + }, inner.body, services); } } catch (e) { caught = e; } for (let h in acquired) _release_one(h); _release_one(g); - if (caught != null) { - // A throw after arming (e.g. reload throwing) would leave the window - // armed; disarm before propagating so a stale rollback can't fire. - if (armed != null) apply_confirm.ac_rollback(armed.token); - die(caught); - } + if (caught != null) die(caught); return result; } diff --git a/src/main.uc b/src/main.uc index 36fa5b5..63ed79a 100644 --- a/src/main.uc +++ b/src/main.uc @@ -19,7 +19,6 @@ let ratelimit = require("ratelimit"); let metrics = require("metrics"); let idempotency = require("idempotency"); let error_ring = require("error_ring"); -let apply_confirm = require("apply_confirm"); // /schema endpoint needs the raw resource modules; handler.make hides them. const RESOURCE_SOURCES = {}; @@ -107,7 +106,6 @@ const INSECURE_MARKER = "/etc/uapi.insecure"; const REASON = { "200": "OK", - "202": "Accepted", "204": "No Content", "207": "Multi-Status", "304": "Not Modified", @@ -123,7 +121,6 @@ const REASON = { "422": "Unprocessable Entity", "423": "Locked", "500": "Internal Server Error", - "501": "Not Implemented", "503": "Service Unavailable", }; @@ -563,7 +560,6 @@ function batch_dispatch(conn, ctx, token, method, body) { r = transaction.multi_transaction(conn, { packages: pkgs, reload_services: reloads, - confirm: ctx.confirm, fn: run_ops, }); } @@ -596,25 +592,16 @@ function batch_dispatch(conn, ctx, token, method, body) { if (r.kind == "lock_unavailable") return errors.error(ctx, "internal_error", sprintf("batch lock unavailable: %s", r.error)); - if (r.kind == "confirm_unavailable" || r.kind == "already_armed" - || r.kind == "confirm_stage_failed" || r.kind == "bad_request") - return errors.error(ctx, r.kind, r.message); if (!r.ok) return errors.error(ctx, "internal_error", sprintf("batch returned unknown kind %J", r.kind)); - let headers = { "Content-Type": "application/json", - "X-Request-Id": ctx.request_id }; - let resp_body = { results, request_id: ctx.request_id }; - // A commit-confirmed batch returns 202 + the armed window (one token for - // all packages) instead of the usual 207. - if (r.confirm != null) { - resp_body.confirm = r.confirm; - headers["X-Confirm-Token"] = r.confirm.token; - headers["X-Confirm-Deadline"] = "" + r.confirm.deadline; - return { status: 202, headers, body: resp_body }; - } - return { status: 207, headers, body: resp_body }; + return { + status: 207, + headers: { "Content-Type": "application/json", + "X-Request-Id": ctx.request_id }, + body: { results, request_id: ctx.request_id }, + }; } function metrics_response(ctx) { @@ -629,41 +616,6 @@ function metrics_response(ctx) { }; } -// /confirm/* control endpoints: thin passthroughs to the apply-confirm CLI. -function confirm_ack_response(ctx, tok) { - let r = apply_confirm.ac_ack(tok); - if (r.ok) return errors.ok(ctx, { confirmed: tok }); - return errors.error(ctx, r.kind, r.message); -} - -function confirm_rollback_response(ctx, tok) { - let r = apply_confirm.ac_rollback(tok); - if (r.ok) return errors.ok(ctx, { rolled_back: tok }); - return errors.error(ctx, r.kind, r.message); -} - -function confirm_status_response(ctx, tok) { - let r = apply_confirm.ac_status(tok); - if (!r.ok) return errors.error(ctx, r.kind, r.message); - let parsed = null; - try { parsed = json(r.json); } catch (e) {} - if (parsed == null) - return errors.error(ctx, "confirm_stage_failed", - "apply-confirm status returned unparseable JSON"); - return errors.ok(ctx, parsed); -} - -function confirm_list_response(ctx) { - let r = apply_confirm.ac_list(); - if (!r.ok) return errors.error(ctx, r.kind, r.message); - let parsed = null; - try { parsed = json(r.json); } catch (e) {} - if (parsed == null) - return errors.error(ctx, "confirm_stage_failed", - "apply-confirm list returned unparseable JSON"); - return errors.ok(ctx, parsed); -} - function diagnostics_response(ctx) { let resources_loaded = []; for (let k in RESOURCE_SOURCES) push(resources_loaded, k); @@ -776,23 +728,6 @@ function dispatch(env) { } ctx.body_text = body_text; ctx.idempotency_key = env.HTTP_IDEMPOTENCY_KEY ?? qs.idempotency_key ?? null; - // Commit-confirmed apply: ?confirm= arms an apply-confirm rollback - // window on the write. The X-Uapi-Confirm header is also read but uhttpd's - // CGI env strips custom headers, so the query form is the portable path (see - // the If-Match / Idempotency-Key fallbacks). Cap at 3600 so uapi's - // best-effort deadline matches apply-confirm's own max_timeout clamp. - ctx.confirm = null; - // confirm only modifies a write; ignore the param on reads (a stray - // ?confirm= on a GET must not 400). - let is_write = (method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE"); - let confirm_raw = is_write ? (env.HTTP_X_UAPI_CONFIRM ?? qs.confirm ?? null) : null; - if (confirm_raw != null && confirm_raw != "") { - if (type(confirm_raw) != "string" || !match(confirm_raw, /^[0-9]{1,4}$/) - || int(confirm_raw) < 1 || int(confirm_raw) > 3600) - return { ctx, resp: errors.error(ctx, "bad_request", - "confirm must be a positive integer 1..3600 (seconds)") }; - ctx.confirm = int(confirm_raw); - } // PATCH with Content-Type: application/json-patch+json switches the // handler from merge-patch (RFC 7396, the default) to RFC 6902 ops. let ctype = env.CONTENT_TYPE ?? ""; @@ -870,15 +805,6 @@ function dispatch(env) { let parts = split_path(path); - // commit-confirm only wraps uci-config transactions. Reject ?confirm= on - // the non-uci write endpoints (apk shell-out, /etc/shadow, authorized_keys, - // token mint) rather than silently ignoring it and reporting plain success. - if (ctx.confirm != null && (parts[0] == "packages" || parts[0] == "tokens" - || (parts[0] == "system" && length(parts) >= 2 - && (parts[1] == "password" || parts[1] == "authorized_keys")))) - return { ctx, token, resp: errors.error(ctx, "bad_request", - "confirm is not supported on this endpoint (uci-config writes only)") }; - if (path == "/metrics") { if (method != "GET") return { ctx, token, @@ -905,45 +831,6 @@ function dispatch(env) { return { ctx, token, resp: diagnostics_response(ctx) }; } - if (length(parts) >= 1 && parts[0] == "confirm") { - // GET /confirm -> list pending; {GET,POST,DELETE} /confirm/ -> - // status / ack / rollback. ro for reads, rw for ack+rollback. - if (length(parts) == 1) { - if (method != "GET") - return { ctx, token, resp: errors.error(ctx, "method_not_allowed", - "confirm collection only supports GET") }; - let denied = scope.require_or_deny(errors, ctx, token.scopes, ["uapi", "confirm"], "ro", - "listing confirm windows"); - if (denied != null) return { ctx, token, resp: denied }; - return { ctx, token, resp: confirm_list_response(ctx) }; - } - if (length(parts) == 2) { - let tok = parts[1]; - if (method == "GET") { - let denied = scope.require_or_deny(errors, ctx, token.scopes, ["uapi", "confirm"], "ro", - "reading a confirm window"); - if (denied != null) return { ctx, token, resp: denied }; - return { ctx, token, resp: confirm_status_response(ctx, tok) }; - } - if (method == "POST") { - let denied = scope.require_or_deny(errors, ctx, token.scopes, ["uapi", "confirm"], "rw", - "confirming an apply"); - if (denied != null) return { ctx, token, resp: denied }; - return { ctx, token, resp: confirm_ack_response(ctx, tok) }; - } - if (method == "DELETE") { - let denied = scope.require_or_deny(errors, ctx, token.scopes, ["uapi", "confirm"], "rw", - "rolling back an apply"); - if (denied != null) return { ctx, token, resp: denied }; - return { ctx, token, resp: confirm_rollback_response(ctx, tok) }; - } - return { ctx, token, resp: errors.error(ctx, "method_not_allowed", - "confirm token supports GET, POST, DELETE") }; - } - return { ctx, token, resp: errors.error(ctx, "not_found", - sprintf("Unknown confirm sub-path %J", path)) }; - } - if (length(parts) >= 1 && parts[0] == "auth") { if (length(parts) == 2 && parts[1] == "whoami") { if (method != "GET") @@ -1145,14 +1032,9 @@ global.handle_request = function(env) { // Idempotency store: cache 2xx POST responses so a repeated key replays. // Only when the request actually carried a key (not on replay itself, the // replay path returns before metrics/idempotency-store). - // 202 = a commit-confirmed write with a live, single-use rollback token. - // Caching it would replay a dead token on retry (the window auto-reverts - // and closes long before the 24h idempotency TTL) without re-arming, which - // silently defeats the retry-after-network-blip case confirm exists for. A - // retry of a confirmed write must re-dispatch (re-arm, or 409 already_armed). if (method == "POST" && token != null && ctx != null && ctx.idempotency_key != null - && resp.status >= 200 && resp.status < 300 && resp.status != 202 + && resp.status >= 200 && resp.status < 300 && (resp.headers == null || resp.headers["Idempotent-Replayed"] == null)) { try { idempotency.store(token.name, ctx.idempotency_key, ctx.body_text ?? "", resp); } diff --git a/src/raw.uc b/src/raw.uc index 6d9c799..63e475b 100644 --- a/src/raw.uc +++ b/src/raw.uc @@ -96,22 +96,7 @@ function build_response_body(view, reload_info) { } function translate_raw_tx(ctx, result) { - if (result.ok) { - let resp = errors.ok(ctx, result.body); - // Commit-confirmed raw write: 202 + armed window (mirrors handler.translate_tx). - if (result.confirm != null) { - resp.status = 202; - if (type(resp.body) != "object") resp.body = {}; - resp.body.confirm = result.confirm; - resp.headers["X-Confirm-Token"] = result.confirm.token; - resp.headers["X-Confirm-Deadline"] = "" + result.confirm.deadline; - } - return resp; - } - if (result.kind == "confirm_unavailable" || result.kind == "already_armed" - || result.kind == "confirm_window_closed" || result.kind == "confirm_stage_failed" - || result.kind == "rollback_reload_failed" || result.kind == "bad_request") - return errors.error(ctx, result.kind, result.message); + if (result.ok) return errors.ok(ctx, result.body); if (result.kind == "locked") return errors.locked_from(ctx, null, result); if (result.kind == "lock_unavailable") @@ -206,7 +191,6 @@ function create(conn, ctx, scopes, pkg, body) { let result = transaction.transaction(conn, { package: pkg, reload_services: reload.services, - confirm: ctx.confirm, fn: function(c, p) { if (client_supplied_id && load_section(c, p, new_id) != null) return { ok: false, kind: "conflict", @@ -249,7 +233,6 @@ function replace(conn, ctx, scopes, pkg, id, body) { let result = transaction.transaction(conn, { package: pkg, reload_services: reload.services, - confirm: ctx.confirm, fn: function(c, p) { let existing = load_section(c, p, id); if (!existing) @@ -297,7 +280,6 @@ function patch(conn, ctx, scopes, pkg, id, body) { let result = transaction.transaction(conn, { package: pkg, reload_services: reload.services, - confirm: ctx.confirm, fn: function(c, p) { let existing = load_section(c, p, id); if (!existing) @@ -327,7 +309,6 @@ function remove(conn, ctx, scopes, pkg, id) { let result = transaction.transaction(conn, { package: pkg, reload_services: reload.services, - confirm: ctx.confirm, fn: function(c, p) { let existing = load_section(c, p, id); if (!existing) @@ -337,10 +318,7 @@ function remove(conn, ctx, scopes, pkg, id) { return { ok: true, body: null }; }, }); - // A confirmed delete armed a window; route through translate_raw_tx so the - // 202 + X-Confirm-Token reaches the client instead of a bare 204 that drops - // the token and silently auto-reverts at the deadline (cf. handler.uc remove). - if (result.ok && result.confirm == null) return errors.no_content(ctx); + if (result.ok) return errors.no_content(ctx); return translate_raw_tx(ctx, result); } @@ -351,7 +329,6 @@ return { replace, patch, remove, - translate_raw_tx, inferred_domain_path, TYPE_DOMAIN_MAP, }; diff --git a/tests/integration/45_apply_confirm_test.sh b/tests/integration/45_apply_confirm_test.sh deleted file mode 100755 index 0f2a782..0000000 --- a/tests/integration/45_apply_confirm_test.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/sh -set -eu - -. tests/integration/lib/install_uapi.sh -install_uapi - -URL=http://127.0.0.1:8080/api/v2 -ADMIN="Authorization: Bearer $ADMIN_TOKEN" -fail() { echo "FAIL: $*"; exit 1; } - -# apply-confirm is a separate package (RC-soak; not on the stable feed yet). -# When it is installable, run the full stage -> ack / stage -> timeout-rollback -# happy path; otherwise verify the graceful-degrade contract (501 when absent, -# ordinary writes unaffected). Same shape as 40_unbound_uci_ext_test.sh. -if $SSH 'apk add apply-confirm 2>/dev/null' && $SSH 'test -x /usr/sbin/apply-confirm'; then - HAVE_AC=1 - echo "[apply-confirm] installed; running full commit-confirm path" -else - HAVE_AC=0 - echo "[apply-confirm] not installable on this VM; running 501 graceful-degrade smoke only" -fi - -cleanup() { $SSH 'uci -q delete firewall.acc_test 2>/dev/null; uci -q commit firewall 2>/dev/null || true'; } -trap cleanup EXIT INT TERM - -if [ "$HAVE_AC" = 0 ]; then - echo "--- confirmed write returns 501 confirm_unavailable when apply-confirm is absent ---" - # Use the ?confirm= query fallback: uhttpd CGI drops custom request headers - # (the reason the fallback exists), so X-Uapi-Confirm is unreliable here. - code=$(curl -sS -o /tmp/ac_body.json -w '%{http_code}' -H "$ADMIN" \ - -H 'Content-Type: application/json' \ - -X POST "$URL/firewall/rules?confirm=60" \ - -d '{"id":"acc_test","target":"ACCEPT","match":{"src_zone":"lan"}}') - [ "$code" = "501" ] || { cat /tmp/ac_body.json; fail "expected 501, got $code"; } - grep -q '"code": "confirm_unavailable"' /tmp/ac_body.json || fail "expected confirm_unavailable" - - echo "--- the rejected confirmed write committed nothing ---" - [ -z "$($SSH 'uci -q get firewall.acc_test.target' 2>/dev/null || true)" ] \ - || fail "501 confirmed write must not have committed the section" - - echo "--- a normal (unconfirmed) write is unaffected ---" - code=$(curl -sS -o /dev/null -w '%{http_code}' -H "$ADMIN" \ - -H 'Content-Type: application/json' \ - -X POST "$URL/firewall/rules" \ - -d '{"id":"acc_test","target":"ACCEPT","match":{"src_zone":"lan"}}') - [ "$code" = "200" ] || fail "normal write expected 200, got $code" - - echo "--- GET /confirm also degrades to 501 ---" - code=$(curl -sS -o /dev/null -w '%{http_code}' -H "$ADMIN" "$URL/confirm") - [ "$code" = "501" ] || fail "GET /confirm expected 501 when absent, got $code" - - echo "apply-confirm graceful-degrade smoke ok." - exit 0 -fi - -echo "--- confirmed write returns 202 + a confirm token ---" -resp=$(curl -sS -D /tmp/ac_hdr.txt -H "$ADMIN" -H 'Content-Type: application/json' \ - -X POST "$URL/firewall/rules?confirm=30" \ - -d '{"id":"acc_test","target":"ACCEPT","match":{"src_zone":"lan"}}') -echo "$resp" | grep -q '"confirm"' || fail "202 body missing confirm block: $resp" -head -1 /tmp/ac_hdr.txt | grep -q '202' || fail "expected 202 status" -token=$(echo "$resp" | jq -r '.confirm.token') -[ -n "$token" ] && [ "$token" != "null" ] || fail "no confirm token in body" -[ "$($SSH 'uci -q get firewall.acc_test.target')" = "ACCEPT" ] || fail "confirmed write did not commit" - -echo "--- POST /confirm/ acks; the change persists ---" -code=$(curl -sS -o /dev/null -w '%{http_code}' -H "$ADMIN" -X POST "$URL/confirm/$token") -[ "$code" = "200" ] || fail "ack expected 200, got $code" -sleep 2 -[ "$($SSH 'uci -q get firewall.acc_test.target')" = "ACCEPT" ] || fail "acked change was rolled back" -$SSH 'uci -q delete firewall.acc_test; uci commit firewall' - -echo "--- confirmed write left unacked rolls back at the deadline ---" -resp=$(curl -sS -H "$ADMIN" -H 'Content-Type: application/json' \ - -X POST "$URL/firewall/rules?confirm=3" \ - -d '{"id":"acc_test","target":"ACCEPT","match":{"src_zone":"lan"}}') -token=$(echo "$resp" | jq -r '.confirm.token') -[ -n "$token" ] && [ "$token" != "null" ] || fail "no token for timeout test" -[ "$($SSH 'uci -q get firewall.acc_test.target')" = "ACCEPT" ] || fail "write not committed pre-timeout" -sleep 8 -[ -z "$($SSH 'uci -q get firewall.acc_test.target' 2>/dev/null || true)" ] \ - || fail "unacked change was not rolled back after the deadline" - -echo "apply-confirm commit-confirm path ok." diff --git a/tests/unit/apply_confirm_test.uc b/tests/unit/apply_confirm_test.uc deleted file mode 100644 index f12571d..0000000 --- a/tests/unit/apply_confirm_test.uc +++ /dev/null @@ -1,75 +0,0 @@ -let t = require('harness'); - -let ac = loadfile('src/lib/apply_confirm.uc')(); - -// The build/unit environment has no /usr/sbin/apply-confirm, so ac_present() is -// false. That lets every guard path be reached: input validation runs before -// the presence check, and the presence check itself yields confirm_unavailable. - -t.describe('apply_confirm.ac_present', () => { - t.it('is false when the binary is not installed (unit env)', () => { - t.assert_equal(ac.ac_present(), false); - }); -}); - -t.describe('apply_confirm.ac_stage validation', () => { - t.it('rejects a non-positive timeout with bad_request', () => { - let r = ac.ac_stage(["network"], ["network"], 0); - t.assert_equal(r.kind, "bad_request"); - }); - - t.it('rejects a non-integer timeout with bad_request', () => { - let r = ac.ac_stage(["network"], ["network"], "60"); - t.assert_equal(r.kind, "bad_request"); - }); - - t.it('rejects an empty package list with confirm_stage_failed', () => { - let r = ac.ac_stage([], [], 60); - t.assert_equal(r.kind, "confirm_stage_failed"); - }); - - t.it('rejects an unsafe package name before it reaches the shell', () => { - let r = ac.ac_stage(["net; rm -rf /"], [], 60); - t.assert_equal(r.kind, "confirm_stage_failed"); - }); - - t.it('rejects an unsafe service name', () => { - let r = ac.ac_stage(["network"], ["svc;evil"], 60); - t.assert_equal(r.kind, "confirm_stage_failed"); - }); - - t.it('returns confirm_unavailable when inputs are valid but the binary is absent', () => { - let r = ac.ac_stage(["network"], ["network"], 60); - t.assert_equal(r.kind, "confirm_unavailable"); - }); -}); - -t.describe('apply_confirm control verbs (token guard + presence)', () => { - t.it('ac_ack rejects a malformed token with bad_request', () => { - t.assert_equal(ac.ac_ack("not-a-token").kind, "bad_request"); - }); - - t.it('ac_ack accepts a well-formed token shape but degrades to confirm_unavailable', () => { - t.assert_equal(ac.ac_ack("ac_1718900000_a1b2c3d4").kind, "confirm_unavailable"); - }); - - t.it('ac_rollback rejects a malformed token with bad_request', () => { - t.assert_equal(ac.ac_rollback("../etc/passwd").kind, "bad_request"); - }); - - t.it('ac_rollback degrades to confirm_unavailable for a valid token shape', () => { - t.assert_equal(ac.ac_rollback("ac_1718900000_a1b2c3d4").kind, "confirm_unavailable"); - }); - - t.it('ac_status rejects a malformed token with bad_request', () => { - t.assert_equal(ac.ac_status("ac_BAD").kind, "bad_request"); - }); - - t.it('ac_status degrades to confirm_unavailable for a valid token shape', () => { - t.assert_equal(ac.ac_status("ac_1718900000_a1b2c3d4").kind, "confirm_unavailable"); - }); - - t.it('ac_list degrades to confirm_unavailable when the binary is absent', () => { - t.assert_equal(ac.ac_list().kind, "confirm_unavailable"); - }); -}); diff --git a/tests/unit/handler_test.uc b/tests/unit/handler_test.uc index 2951238..0b7a92b 100644 --- a/tests/unit/handler_test.uc +++ b/tests/unit/handler_test.uc @@ -383,52 +383,6 @@ t.describe('handler.patch', () => { }); }); -t.describe('translate_tx commit-confirm 202 shaping', () => { - function jctx() { return { request_id: "01hx0000000000000000000000" }; } - let armed = { token: "ac_1718900000_a1b2c3d4", timeout: 60, deadline: 1718900060, packages: ["firewall"] }; - - t.it('a confirmed write becomes 202 with the token header and confirm body', () => { - let resp = handler.translate_tx(jctx(), - { ok: true, body: { id: "r1", target: "ACCEPT" }, confirm: armed }); - t.assert_equal(resp.status, 202); - t.assert_equal(resp.headers["X-Confirm-Token"], "ac_1718900000_a1b2c3d4"); - t.assert_equal(resp.body.confirm.token, "ac_1718900000_a1b2c3d4"); - }); - - t.it('a confirmed delete (null body) is 202 with the token header AND a synthesized confirm body', () => { - let resp = handler.translate_tx(jctx(), { ok: true, body: null, confirm: armed }); - t.assert_equal(resp.status, 202); - t.assert_equal(resp.headers["X-Confirm-Token"], "ac_1718900000_a1b2c3d4"); - // PendingConfirm schema marks body.confirm required even for DELETE. - t.assert_equal(resp.body.confirm.token, "ac_1718900000_a1b2c3d4"); - }); - - t.it('an ordinary write (no confirm) stays 200', () => { - let resp = handler.translate_tx(jctx(), { ok: true, body: { id: "r1" } }); - t.assert_equal(resp.status, 200); - }); -}); - -t.describe('handler commit-confirm threading', () => { - function cctx() { return { request_id: "01hx0000000000000000000000", confirm: 60 }; } - - t.it('a confirmed write degrades to 501 when apply-confirm is absent', () => { - // ctx.confirm threads handler -> tx_params -> transaction -> run_inner - // -> ac_stage; with no binary in the unit env, stage returns - // confirm_unavailable and translate_tx maps it to 501. - let c = with_zones(); - let r = rules.create(c, cctx(), { target: 'ACCEPT', match: { src_zone: 'lan' } }); - t.assert_equal(r.status, 501); - t.assert_equal(r.body.code, "confirm_unavailable"); - }); - - t.it('an unconfirmed write is unaffected (no confirm in ctx)', () => { - let c = with_zones(); - let r = rules.create(c, ctx(), { target: 'ACCEPT', match: { src_zone: 'lan' } }); - t.assert_equal(r.status, 200); - }); -}); - t.describe('handler.patch JSON Patch write-only secret carry-forward', () => { let wiface_mod = loadfile('src/resources/wireless.interfaces.uc')(); let wiface = handler.make(wiface_mod, { diff --git a/tests/unit/raw_test.uc b/tests/unit/raw_test.uc index c40d23c..073f848 100644 --- a/tests/unit/raw_test.uc +++ b/tests/unit/raw_test.uc @@ -172,26 +172,3 @@ t.describe('raw permission composition for writes', () => { t.assert_equal(r.status, 403); }); }); - -t.describe('raw.translate_raw_tx commit-confirm 202 shaping', () => { - function jctx() { return { request_id: "01hx0000000000000000000000" }; } - let armed = { token: "ac_1718900000_a1b2c3d4", timeout: 60, deadline: 1718900060, packages: ["firewall"] }; - - t.it('a confirmed raw write becomes 202 with the token header and confirm body', () => { - let r = raw.translate_raw_tx(jctx(), { ok: true, body: { id: "r1" }, confirm: armed }); - t.assert_equal(r.status, 202); - t.assert_equal(r.headers["X-Confirm-Token"], "ac_1718900000_a1b2c3d4"); - t.assert_equal(r.body.confirm.token, "ac_1718900000_a1b2c3d4"); - }); - - t.it('a confirmed raw delete (null body) still carries the confirm block', () => { - let r = raw.translate_raw_tx(jctx(), { ok: true, body: null, confirm: armed }); - t.assert_equal(r.status, 202); - t.assert_equal(r.body.confirm.token, "ac_1718900000_a1b2c3d4"); - }); - - t.it('confirm error kinds map to their status (confirm_unavailable -> 501)', () => { - let r = raw.translate_raw_tx(jctx(), { ok: false, kind: "confirm_unavailable", message: "x" }); - t.assert_equal(r.status, 501); - }); -});