States against Salt Resources#
New in version 3008.0.
How state.apply, state.highstate, and state.sls behave when
they target resources. This page is the runtime counterpart to the
state-module authoring guide — read
that one if you're writing state modules, this one if you're applying
states or trying to debug what's going on.
The merge-mode contract#
These functions are special:
state.applystate.highstatestate.slsstate.sls_idstate.single
(The full list lives in
_MERGE_RESOURCE_FUNS.)
When the operator runs one of them against a target that includes
resources — salt -C 'T@ssh' state.apply mysls, say — Salt does
not dispatch one independent return per resource. Instead:
The master's wait list contains the managing minion id, not the resource ids.
The managing minion runs the per-resource apply inline — building one
HighStateper matched resource using that resource's per-type loader.The managing minion folds the per-resource state IDs into a single
ret["return"]dict and publishes one combined return.The CLI prints one block + one Summary, with each state ID prefixed by its resource id so provenance is visible.
This matches how any other minion looks to the master: one publish,
one return, one block of output. The difference is invisible to
state.show_lowstate and friends.
State-ID prefixing#
Salt's state low keys are {module}_|-{id}_|-{name}_|-{function}.
When the managing minion folds per-resource results into the parent
dict, it rewrites positions 1 and 2 (id and name) with the resource id
prepended. So a state declared as
install_curl:
pkg.installed:
- name: curl
apply'd against T@ssh:web-01 and T@ssh:web-02 produces two
keys:
pkg_|-web-01 install_curl_|-web-01 curl_|-installed
pkg_|-web-02 install_curl_|-web-02 curl_|-installed
The {module} (pkg) and {function} (installed)
positions are left alone so the highstate formatter still shows
Function: pkg.installed correctly; only the ID and Name are
relabelled to surface the resource.
If a resource fails before the per-resource HighState produced any
chunks (e.g. the resource type couldn't fulfil the operation at all),
the framework inserts a synthetic chunk under
no_|-{rid}_|-{rid}_|-None so the result is still visible in the
combined dict and still contributes to the overall pass/fail.
You don't have to think about prefixing inside your states. Just keep state IDs stable across resources and the output will be readable.
The managing minion is NOT a target (usually)#
For T@ and M@ compound expressions that only address
resources (a "pure resource target"), the managing minion is not a
target for the function itself — its job is to run the resources
inline, not to apply the state to its own filesystem.
The framework detects this case via data["pure_resource_target"]
in _thread_return() and skips the
regular function execution on the managing minion. Without that skip
you'd see a spurious "state.apply not found" block from the
managing minion alongside the real per-resource results.
If the target expression also matches the managing minion (a wildcard
glob like salt '*' state.apply, or a grain match the host also
satisfies), the managing minion runs the apply against itself too —
its results appear in the combined dict alongside the per-resource
results.
The __minion__ escape hatch#
Inside per-resource state code, __salt__ is the per-resource
execution loader. That's almost always what you want — it gives you
both your overrides (where they exist) and the standard module set
(everywhere else).
Occasionally a state needs to do something on the managing minion
itself — write a checkpoint to /var/lib/salt/ after each resource
finishes, look up a credential from the host's keychain, etc. That's
what __minion__ is for. It's the managing minion's regular
execution-module loader, packed into per-resource state and execution
modules as an explicit escape hatch:
def post_widget_apply(name, **kwargs):
ret = {"name": name, "result": True, "changes": {}, "comment": ""}
widget_status = __salt__["widget.status"](name)
if widget_status.get("ok"):
__minion__["file.append"](
"/var/lib/salt/widget-applied.log",
f"{name} applied successfully\n",
)
ret["comment"] = "Recorded apply on managing minion"
else:
ret["result"] = False
ret["comment"] = "Widget not in OK state"
return ret
Reach for __minion__ deliberately. It is a deliberate cross-context
call: the state is running in resource context but reaches back to the
host. State module authors should think of it the way they'd think of
running subprocess.run from a state function — fine when it's the
right answer, a smell when used by reflex.
When state.apply falls through to the standard module#
For resource types without a per-type override of the state
slot (no salt/resources/<rtype>/modules/state.py), the standard
salt.modules.state module runs in the per-resource loader. That
means:
__salt__["state.apply"]for that resource compiles the high state on the managing minion, using the per-resource execution loader for__salt__inside the rendered states.The states themselves run on the managing minion (because that's where the state engine is) but every module call inside them dispatches via the per-resource loader.
For resource types that ship a per-type state.py (today: the
ssh resource), state.apply runs on the resource itself via
whatever transport the override implements.
Both shapes converge at the same return contract — per-resource state results folded into the managing minion's combined return.
Debugging tips#
Where is my state running? Look at the
minionfield in the highstate output. For resource types without a state override it's the managing minion (with per-resource__salt__). For types with a state override it's "wherever the override sends it" — for the SSH resource type, that's the remote host over SSH.State ID collisions in output. If two different resources produce the same state ID after the rid prefix, you have a prefixing bug — usually a state ID that contains characters Salt treats specially in the low-key encoding. File a bug.
"State X is not available." Means the per-resource loader has no module providing slot
X. Either ship an override at<rtype>/modules/X.pyor use a different state.