# Operational Flow
How the extension orchestrates a configuration run, from pillar input
to applied config. The flow varies depending on the agent mode set in
`/etc/config/salt-openwrt` on the device.
## Full sequence -- oneshot mode
The happy path when `mode = oneshot`. Salt reads, diffs, stages, applies
with rollback protection, verifies, and confirms.
```{mermaid}
sequenceDiagram
autonumber
participant Master as Salt Master
participant State as state.managed()
participant Agent as /etc/config/
salt-openwrt
participant UCI as UCI (ubus/SSH)
participant Device as OpenWrt Device
Master->>State: managed(config, sections)
State->>UCI: get("salt-openwrt", "global")
UCI-->>State: {enabled: "1", mode: "oneshot"}
Note over State: Mode = oneshot, proceed
State->>UCI: changes(config)
UCI-->>State: {} (no pending deltas)
State->>UCI: get(config)
UCI-->>State: current config state
Note over State: Resolve sections
(singleton & multi-instance anonymous)
Note over State: Diff desired vs current
(partial semantics)
alt No drift detected
State-->>Master: result=True, "already in desired state"
end
Note over State: Drift detected
alt test=True (dry run)
State-->>Master: result=None, changes preview
end
rect rgb(230, 245, 230)
Note over State,Device: Stage phase
loop Each changed section
opt Section does not exist yet
State->>UCI: add(config, type, name)
end
State->>UCI: set(config, section, values)
end
end
rect rgb(245, 240, 250)
Note over State,Device: Service snapshot
State->>UCI: service_list()
UCI-->>State: {svc: {inst: pid}, ...}
Note over State: Record running services + PIDs
end
rect rgb(230, 235, 250)
Note over State,Device: Apply phase (rollback=90s)
State->>UCI: apply(rollback=90)
UCI->>Device: uci commit + reload services
Device-->>UCI: OK
end
rect rgb(250, 245, 230)
Note over State,Device: Verify phase -- UCI values
State->>UCI: get(config)
UCI-->>State: new config state
Note over State: Compare each changed option
against expected value
alt Verification failed
Note over State: Rollback will revert
in 90 seconds
State-->>Master: result=False, verification error
end
end
rect rgb(250, 240, 245)
Note over State,Device: Verify phase -- service health
loop Poll until all services running or deadline
State->>UCI: service_list()
UCI-->>State: {svc: {inst: running, pid}, ...}
end
alt Services not recovered
Note over State: Do NOT confirm
Rollback reverts automatically
State-->>Master: result=False, "services not recovered"
end
end
rect rgb(235, 250, 235)
Note over State,Device: Confirm phase
State->>UCI: confirm()
UCI->>Device: Cancel rollback timer
Device-->>UCI: OK
end
State-->>Master: result=True, "applied and confirmed
(N service(s) verified running)"
```
## Audit mode
When `mode = audit`, Salt reads and diffs but never writes. The device
is protected from any configuration changes.
```{mermaid}
sequenceDiagram
autonumber
participant Master as Salt Master
participant State as state.managed()
participant UCI as UCI (ubus/SSH)
Master->>State: managed(config, sections)
State->>UCI: get("salt-openwrt", "global")
UCI-->>State: {enabled: "1", mode: "audit"}
Note over State: Mode = audit
State->>UCI: changes(config)
UCI-->>State: pending deltas (if any)
State->>UCI: get(config)
UCI-->>State: current config state
Note over State: Resolve sections
Note over State: Diff desired vs current
alt No drift
State-->>Master: result=True,
"audit mode -- no drift detected"
else Drift detected
Note over State: Report drift in changes dict
but do NOT call set/apply/confirm
State-->>Master: result=True, changes={...},
"audit mode -- N section(s) drifted,
no changes applied"
end
```
## Autoverified mode
When `mode = autoverified`, Salt stages UCI changes (`uci set`) but does
not call `uci apply` or `uci confirm`. The `applied()` state activates
staged changes with rollback protection. The staging behavior differs by
transport because rpcd routes changes differently depending on whether
a session ID is present:
- **SSH transport** runs `ubus call uci set` without a session. Changes
stage to `/tmp/.uci/`, visible to `uci changes` from CLI and LuCI.
- **JSON-RPC transport** passes the rpcd session token with every call.
Changes stage in the rpcd session, kept alive by the proxy minion.
The `applied()` state applies all staged changes globally with rollback
protection, verifies that all previously-running services are healthy, and
confirms. It can be called without a `config` parameter for session-global
apply.
### Autoverified mode -- SSH transport
```{mermaid}
sequenceDiagram
autonumber
participant Master as Salt Master
participant State as state.managed()
participant UCI as UCI (SSH)
participant Device as /tmp/.uci/
participant Human as Operator
(LuCI / CLI)
Master->>State: managed(config, sections)
State->>UCI: get("salt-openwrt", "global")
UCI-->>State: {enabled: "1", mode: "autoverified"}
Note over State: Mode = autoverified
Override: apply_rollback = None
State->>UCI: changes(config)
UCI-->>State: {} (no pending)
State->>UCI: get(config)
UCI-->>State: current config state
Note over State: Resolve + Diff
rect rgb(230, 245, 230)
Note over State,Device: Stage phase
loop Each changed section
opt New section
State->>UCI: add(config, type, name)
end
State->>UCI: set(config, section, values)
UCI->>Device: staged in /tmp/.uci/
end
end
Note over State: SSH transport detected
Skip commit -- changes are
in /tmp/.uci/ for review
State-->>Master: result=True, changes={...},
"staged (review with 'uci changes')"
Note over Human: Later...
Human->>Device: uci changes (review staged)
rect rgb(230, 235, 250)
Note over Human,Device: applied() state
Human->>UCI: saltext_ubus.applied()
Note over UCI: snapshot services
→ apply(rollback)
→ poll services until healthy
→ confirm
UCI-->>Human: Applied and confirmed
(N service(s) verified running)
end
```
### Autoverified mode -- JSON-RPC transport
```{mermaid}
sequenceDiagram
autonumber
participant Master as Salt Master
participant State as state.managed()
participant UCI as UCI (JSON-RPC)
participant Session as /var/run/rpcd/
uci-/
participant Human as Operator
(LuCI / CLI)
Master->>State: managed(config, sections)
State->>UCI: get("salt-openwrt", "global")
UCI-->>State: {enabled: "1", mode: "autoverified"}
Note over State: Mode = autoverified
Override: apply_rollback = None
State->>UCI: changes(config)
UCI-->>State: {} (no pending)
State->>UCI: get(config)
UCI-->>State: current config state
Note over State: Resolve + Diff
rect rgb(230, 245, 230)
Note over State,Session: Stage phase
loop Each changed section
opt New section
State->>UCI: add(config, type, name)
end
State->>UCI: set(config, section, values)
UCI->>Session: staged in rpcd session
(kept alive by proxy minion)
end
end
Note over State: No commit, no apply
Changes live in rpcd session
State-->>Master: result=True, changes={...},
"staged in rpcd session"
Note over Human: Later...
rect rgb(230, 235, 250)
Note over Human,Session: applied() state
Human->>UCI: saltext_ubus.applied()
Note over UCI: snapshot services
→ apply(rollback)
→ poll services until healthy
→ confirm
UCI-->>Human: Applied and confirmed
(N service(s) verified running)
end
```
## Humanreviewed mode
When `mode = humanreviewed`, Salt behaves identically to autoverified
mode. This mode is reserved for a future LuCI approval gate where an
operator must explicitly approve staged changes through the web
interface before `applied()` activates them.
## Disabled device
When `enabled = 0`, Salt returns immediately without reading any config.
```{mermaid}
sequenceDiagram
autonumber
participant Master as Salt Master
participant State as state.managed()
participant UCI as UCI (ubus/SSH)
Master->>State: managed(config, sections)
State->>UCI: get("salt-openwrt", "global")
UCI-->>State: {enabled: "0", mode: "oneshot"}
Note over State: Device disabled, skip entirely
State-->>Master: result=True,
"salt-openwrt disabled on device, skipping"
```
## Graceful fallback (package not installed)
When `/etc/config/salt-openwrt` does not exist, the `get()` call raises
an exception. The state module catches it and defaults to `oneshot` mode,
preserving backward compatibility.
```{mermaid}
sequenceDiagram
autonumber
participant Master as Salt Master
participant State as state.managed()
participant UCI as UCI (ubus/SSH)
Master->>State: managed(config, sections)
State->>UCI: get("salt-openwrt", "global")
UCI--xState: Exception (config not found)
Note over State: Catch exception
Default: enabled=True, mode="oneshot"
Note over State: Continue with full oneshot flow...
```
## Pending deltas handling
When uncommitted changes exist for the target config package, the
behavior depends on the `revert_pending` parameter.
```{mermaid}
sequenceDiagram
autonumber
participant Master as Salt Master
participant State as state.managed()
participant UCI as UCI (ubus/SSH)
Master->>State: managed(config, sections)
State->>UCI: get("salt-openwrt", "global")
UCI-->>State: {enabled: "1", mode: "oneshot"}
State->>UCI: changes(config)
UCI-->>State: {section: {option: delta}}
Note over State: Pending deltas detected!
alt revert_pending = False (default)
State-->>Master: result=False,
"Uncommitted changes exist..."
else revert_pending = True
State->>UCI: revert(config)
UCI-->>State: OK
Note over State: Continue with normal flow...
end
```