Architecture#
New in version 3008.0.
How Salt Resources actually work: where the data lives on the master, how a publish reaches a resource, what runs where, and the seams an operator or extension author can hook into.
Three sides, three responsibilities#
The master owns the system-of-record for which minion manages which resource. It uses that record to expand targeting expressions and to populate the job's wait list.
The managing minion carries the connection plumbing, the per-resource grain dicts, and the per-resource execution and state loaders. It is the process that actually talks to the resource.
The resource type is a Python package — in core Salt under
salt.resourcesor in any Salt extension undersaltext.<ext>.resources.<rtype>— that defines what type of thing the resource is and what operations it understands.
Master side: the resource registry#
Source of record: salt.utils.resource_registry#
The master keeps an mmap-backed index of every resource any minion
currently manages. Each entry is a composite key ("type:id", written
SRN for Salt Resource Name) mapping to a small JSON payload:
{"m": "<managing-minion-id>", "t": "<resource_type>"}
The primary index (
by_id) is aMmapCachefile on disk. Lookups and inserts are O(1) linear-probing hash operations.Two derived secondaries (
by_type,by_minion) are materialised in-process on first access and rebuilt when the master observes the primary file has been compacted.A separate
resource_grainscache bank stores each resource's per-resource grain dict (one msgpack blob per SRN).
The registry is reused for three jobs:
Expanding compound targets.
T@sshwalksby_type["ssh"];T@ssh:web-01readsby_id["ssh:web-01"]and gets back the managing minion id.Augmenting grain matches.
_augment_grain_match_with_resource_grains()walks theresource_grainsbank to find resources whose grain dict satisfies the operator's-G/G@expression and adds them to the response wait list.Picking the managing minion for merge-mode functions. When the command is
state.applyor another merge fun, the master returns the managing minion's id in the wait list instead of the resource id — the managing minion runs the apply inline and returns one combined block. See States against Salt Resources.
Writes#
The only writer is the master worker's AESFuncs._register_resources
handler (called by the minion's _register_resources_with_master).
Every register call:
Diffs the minion's previous registration against the new payload.
Drops entries for resources the minion no longer manages.
Inserts or refreshes entries for resources the minion does manage.
Updates
resource_grainsfor any resource whose grain dict changed.
Re-registration triggers — what causes the master's view to refresh — are documented in Operations.
Minion side: per-resource loaders#
Discovery#
On startup and on every pillar refresh, the managing minion reads its
pillar subtree at resource_pillar_key (default
resources). For each resource type <rtype> listed there, it:
Imports the resource module —
salt.resources.<rtype>for in-tree types, orsaltext.<ext>.resources.<rtype>for extension-shipped types.Calls the module's
init(opts)for each declared resource id — establishing the connection or doing whatever per-resource setup the type requires.Calls the module's
grains()for each resource and caches the returned dict.Reports the full set of
(type, id, grains)triples back to the master via_register_resources_with_master.
Per-type loader#
When a publish arrives that targets a managed resource, the minion selects (or builds) a per-resource execution loader keyed by the resource type. This loader is constructed exactly like the standard minion loader, except:
Loader dir search order is rewritten so that
salt/resources/<rtype>/modules/(and the equivalent path inside any Salt extension that ships a resource type) sits ahead of the standardsalt/modules/. A file at the per-type path wins its slot; standard modules fill any slot the resource type doesn't override. Seesalt.loader._module_dirs().A few dunders are packed specifically for resource context.
__grains__is the resource's grain dict (not the managing minion's).__resource__is{"type": ..., "id": ...}.__minion__is the managing minion's regular execution-module loader, available as an explicit escape hatch when a resource module needs to reach back to the host. See States against Salt Resources.
The state loader (salt.loader.states) is built the same way: it
discovers state modules from per-type states/ directories first,
then falls back to salt/states/.
Dispatch: how a publish becomes a resource job#
This is the path a publish takes from the master's wire to a return.
Master publishes.
CkMinionsproduces a wait list combining per-minion matches with per-resource matches read from theresource_grainsbank and the SRN registry. ForT@ssh:web-01the wait list is{"web-01"}; forstate.applyagainstT@ssh:web-01it is{<managing-minion-id>}(the merge-mode remap — see _MERGE_RESOURCE_FUNS).Minion accepts the load.
_target_load()runs_resolve_resource_targets()against the target expression. The result is a list of{"type": ..., "id": ...}dicts.Minion fans out. For non-merge functions the minion calls
_handle_decoded_payload()once per matched resource, copying the load and settingload["resource_target"]to that resource. Each copy runs in its own subprocess like any other job.The job picks the per-resource loader.
_thread_return()readsdata["resource_target"]and selects the correspondingresource_loadersentry. The function is executed against that loader, so__salt__["cmd.run"](etc.) dispatches to per-resource overrides when they exist and to standard Salt modules otherwise.Return. The result is published to the master with
ret["resource_id"]set; the master's_returnhandler remapsload["id"] = load["resource_id"]so the CLI sees a return keyed by the resource id.
Special function classes#
Two sets of functions are treated specially:
_NO_RESOURCE_FUNSInternal minion housekeeping (job-status queries, module reloads,
saltutil.sync_*, …). Never dispatched to resources — they always run on the managing minion alone.
_MERGE_RESOURCE_FUNSstate.apply,state.highstate,state.sls,state.sls_id,state.single. The managing minion runs the per-resource state apply inline and folds each resource's state IDs into a single response, prefixed with the resource id. The master's wait list contains the managing minion's id, not the resource ids; one combined block goes back to the operator. See States against Salt Resources for the prefixing rules and the__minion__escape hatch.
Per-type directory layout#
A resource type is a Python package whose tree mirrors Salt's own
loader trees. Every directory is optional except __init__.py:
salt/resources/<rtype>/
__init__.py # connection module — init, grains, helpers
modules/ # execution-module overrides (filename = slot)
cmd.py
pkg.py
state.py
...
states/ # state-module overrides (filename = slot)
...
grains/ # grain modules (per-resource grains, optional)
...
Salt extensions follow the same layout under their package path —
e.g. saltext/<ext>/resources/<rtype>/modules/<slot>.py — and Salt's
loader picks them up automatically via setuptools entry-point
discovery.
For an authoring guide see Authoring a Salt resource type.
Why directory order instead of __virtual__?#
Earlier iterations of this design used __virtualname__ collisions
and __virtual__ guards keyed on opts["resource_type"] to decide
which module won a slot. That approach had two failure modes:
It was easy for the override to opt out correctly while the standard module was unaware of the resource context, leaving the slot empty in the per-resource loader (the original "Gap 4" / "Gap 5" bug class).
It coupled every standard module to the resource framework — any new resource type required edits to
salt/modules/state.py,salt/modules/cmd.py, etc.
The directory-order approach inverts that. Standard modules know nothing about resources. The loader picks the per-type version when one exists and the standard version otherwise. Adding a resource type requires no edits to core Salt — drop a directory in your extension and you're done.
Cross-references#
Targeting reference: Targeting Salt Resources
State authoring (merge mode,
__minion__): States against Salt ResourcesOperator commands: Operations
Configuration options: Configuration
Registry API:
salt.utils.resource_registry