Skip to content

Commit 816d8e0

Browse files
committed
Implement Salt Resources foundation: targeting, loaders, dispatch, and SSH resource
Adds the complete core infrastructure for Salt Resources (SRN), allowing resources to be targeted and executed against independently from the managing minion, along with a full SSH resource implementation. Targeting layer: - T@ and M@ compound match engines (resource_match, managing_minion_match) - CkMinions._check_resource_minions and _augment_with_resources so that glob targets like '*' automatically include managed resource IDs - Master-side resource registration (_register_resources in AESFuncs) and minion-side _register_resources_with_master on connect Loader layer: - salt.loader.resource(): loads salt/resource/*.py, packs as __resource_funcs__ - salt.loader.resource_modules(opts, resource_type): isolated per-type execution-module loader with opts["resource_type"] injected; one instance per managed type per minion process (not per device) Minion dispatch: - gen_modules() initialises self.resource_funcs and self.resource_loaders - _thread_return routes resource jobs to the per-type loader, injects __resource__ and per-resource __grains__ before each call - Unknown functions for a resource type fail loudly instead of silently executing on the managing minion - _execute_job_function accepts an optional functions= loader parameter - JID deduplication bypassed for resource_job loads (shared parent JID) - _NO_RESOURCE_FUNS prevents internal Salt plumbing from dispatching to resources Dummy resource (example/test implementation): - salt/resource/dummy.py: full dummy resource (ping, grains, services, pkgs) - salt/modules/dummyresource_test.py: Pattern B execution module gating on opts["resource_type"] == "dummy", routes test.ping to dummy.ping() SSH resource: - salt/resource/ssh.py: SSH resource module using salt-ssh Shell transport - salt/modules/sshresource_cmd.py: cmd.* surface for SSH resources - salt/modules/sshresource_pkg.py: pkg.* surface for SSH resources - salt/modules/sshresource_state.py: state.* surface for SSH resources - salt/modules/sshresource_test.py: test.ping for SSH resources Also adds salt/grains/resources.py, design documents, example loader scripts, srv/ pillar configuration, and a Docker environment for testing. Made-with: Cursor
1 parent d42a3a4 commit 816d8e0

44 files changed

Lines changed: 3652 additions & 30 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

doc/ref/grains/all/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ grains modules
1919
opts
2020
package
2121
pending_reboot
22+
resources
2223
rest_sample
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
salt.grains.resources
2+
=====================
3+
4+
.. automodule:: salt.grains.resources
5+
:members:

doc/ref/modules/all/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ execution modules
6767
dpkg_lowpkg
6868
dummyproxy_pkg
6969
dummyproxy_service
70+
dummyresource_test
7071
environ
7172
etcd_mod
7273
ethtool
@@ -215,6 +216,10 @@ execution modules
215216
ssh_pkg
216217
ssh_pki
217218
ssh_service
219+
sshresource_cmd
220+
sshresource_pkg
221+
sshresource_state
222+
sshresource_test
218223
state
219224
status
220225
supervisord
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
salt.modules.dummyresource_test
2+
===============================
3+
4+
.. automodule:: salt.modules.dummyresource_test
5+
:members:
6+
:undoc-members:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
salt.modules.sshresource_cmd
2+
============================
3+
4+
.. automodule:: salt.modules.sshresource_cmd
5+
:members:
6+
:undoc-members:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
salt.modules.sshresource_pkg
2+
============================
3+
4+
.. automodule:: salt.modules.sshresource_pkg
5+
:members:
6+
:undoc-members:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
salt.modules.sshresource_state
2+
==============================
3+
4+
.. automodule:: salt.modules.sshresource_state
5+
:members:
6+
:undoc-members:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
salt.modules.sshresource_test
2+
=============================
3+
4+
.. automodule:: salt.modules.sshresource_test
5+
:members:
6+
:undoc-members:

docker/ssh-resource/Dockerfile

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
FROM debian:bookworm-slim
2+
3+
RUN apt-get update -qq && \
4+
apt-get install -y --no-install-recommends \
5+
openssh-server \
6+
procps \
7+
python3 \
8+
sudo \
9+
&& \
10+
rm -rf /var/lib/apt/lists/*
11+
12+
# sshd requires /run/sshd to exist
13+
RUN mkdir -p /run/sshd
14+
15+
# Create the salt user. An unset password is treated as a locked account
16+
# by Debian sshd; setting the hash to '*' marks it as "no password" without
17+
# locking it so that key-based auth is permitted.
18+
RUN useradd -m -s /bin/bash salt && \
19+
usermod -p '*' salt && \
20+
mkdir -p /home/salt/.ssh && \
21+
chmod 700 /home/salt/.ssh
22+
23+
# Grant the salt user passwordless sudo so salt-thin can run pkg.install
24+
RUN echo "salt ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/salt && \
25+
chmod 440 /etc/sudoers.d/salt
26+
27+
# Inject the host public key as an authorised key
28+
ARG PUBKEY
29+
RUN echo "$PUBKEY" > /home/salt/.ssh/authorized_keys && \
30+
chmod 600 /home/salt/.ssh/authorized_keys && \
31+
chown -R salt:salt /home/salt/.ssh
32+
33+
# Harden sshd: key-only auth, no root login, no PAM noise
34+
RUN sed -i \
35+
-e 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' \
36+
-e 's/^#\?PermitRootLogin.*/PermitRootLogin no/' \
37+
-e 's/^#\?UsePAM.*/UsePAM no/' \
38+
/etc/ssh/sshd_config && \
39+
echo "AllowUsers salt" >> /etc/ssh/sshd_config
40+
41+
EXPOSE 22
42+
43+
CMD ["/usr/sbin/sshd", "-D", "-e"]

measure_loader.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"""
2+
Measures the memory footprint of Salt loader instances.
3+
4+
Quantifies the cost of the "one loader per resource type" approach so we can
5+
make an informed decision about the loader architecture for Salt Resources.
6+
7+
Usage::
8+
9+
python measure_loader.py
10+
11+
The script measures:
12+
1. A full minion_mods loader (baseline — what every minion pays today)
13+
2. A proxy loader (salt/proxy/*.py only)
14+
3. N additional minion_mods-equivalent loaders to simulate N resource types
15+
sharing the same process (like deltaproxy or the proposed resource model)
16+
"""
17+
18+
import gc
19+
import os
20+
import sys
21+
import tracemalloc
22+
23+
import psutil
24+
25+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26+
27+
import salt.config
28+
import salt.loader
29+
import salt.loader.lazy
30+
31+
MINION_CONFIG = os.path.join(os.path.dirname(__file__), "etc", "salt", "minion")
32+
33+
_proc = psutil.Process()
34+
35+
36+
def rss_mb():
37+
return _proc.memory_info().rss / 1024 / 1024
38+
39+
40+
def _force_load(loader):
41+
"""Force the lazy loader to fully populate its _dict."""
42+
loader._load_all()
43+
return loader
44+
45+
46+
def measure(label, loader_fn):
47+
"""
48+
Build a loader, force-load all modules, and report memory cost.
49+
50+
Returns the loader so callers can keep it alive (simulating a long-running
51+
minion process that holds all loaders simultaneously).
52+
"""
53+
gc.collect()
54+
rss_before = rss_mb()
55+
56+
tracemalloc.start()
57+
snapshot_before = tracemalloc.take_snapshot()
58+
59+
loader = _force_load(loader_fn())
60+
61+
snapshot_after = tracemalloc.take_snapshot()
62+
tracemalloc.stop()
63+
64+
gc.collect()
65+
rss_after = rss_mb()
66+
67+
stats = snapshot_after.compare_to(snapshot_before, "lineno")
68+
py_heap_kb = sum(s.size_diff for s in stats) / 1024
69+
70+
n_funcs = len(loader._dict)
71+
72+
print(f"\n {label}")
73+
print(f" functions loaded : {n_funcs:>6}")
74+
print(f" python heap delta: {py_heap_kb:>8.0f} KB ({py_heap_kb/1024:.1f} MB)")
75+
print(f" RSS delta : {rss_after - rss_before:>8.1f} MB")
76+
77+
return loader
78+
79+
80+
def main():
81+
print("Loading minion config from:", MINION_CONFIG)
82+
opts = salt.config.minion_config(MINION_CONFIG)
83+
84+
# Each loader gets a unique loaded_base_name so Python's sys.modules cache
85+
# does not let them share module objects — this reflects what would happen
86+
# in a real minion process with separate per-type loaders.
87+
def make_opts(label, extra=None):
88+
o = dict(opts)
89+
o["loaded_base_name"] = f"salt.loaded.{label}"
90+
if extra:
91+
o.update(extra)
92+
return o
93+
94+
# ------------------------------------------------------------------ #
95+
print("\n" + "=" * 60)
96+
print("BASELINE — single loader, as every minion runs today")
97+
print("=" * 60)
98+
99+
gc.collect()
100+
rss_start = rss_mb()
101+
print(f"\n Process RSS at start: {rss_start:.1f} MB")
102+
103+
utils = salt.loader.utils(make_opts("utils"))
104+
105+
live_loaders = []
106+
107+
l = measure(
108+
"minion_mods (full, baseline)",
109+
lambda: salt.loader.minion_mods(make_opts("baseline"), utils=utils),
110+
)
111+
live_loaders.append(l)
112+
113+
# ------------------------------------------------------------------ #
114+
print("\n" + "=" * 60)
115+
print("PROXY LOADER — salt/proxy/*.py only")
116+
print("=" * 60)
117+
118+
l = measure(
119+
"proxy loader",
120+
lambda: salt.loader.proxy(make_opts("proxy")),
121+
)
122+
live_loaders.append(l)
123+
124+
# ------------------------------------------------------------------ #
125+
print("\n" + "=" * 60)
126+
print("RESOURCE-TYPE LOADERS — one per type, held simultaneously")
127+
print("(simulates a minion managing N distinct resource types)")
128+
print("=" * 60)
129+
130+
# Simulate resource-type-specific loaders. For now these use the same
131+
# module dirs as minion_mods but with resource_type in opts (as the final
132+
# design would, allowing __virtual__ to gate on it). This gives an upper
133+
# bound on per-type cost — real resource-type loaders would load fewer
134+
# modules once __resourceenabled__ filtering is in place.
135+
resource_types = ["dummy", "ssh", "vcf_host", "vcf_vm", "kubernetes"]
136+
cumulative_rss = rss_mb()
137+
138+
for i, rtype in enumerate(resource_types, 1):
139+
tag = f"resource_{rtype}"
140+
extra = {"resource_type": rtype}
141+
l = measure(
142+
f"resource-type loader #{i}: '{rtype}'",
143+
lambda t=tag, e=extra: salt.loader.minion_mods(
144+
make_opts(t, e), utils=utils
145+
),
146+
)
147+
live_loaders.append(l)
148+
149+
# ------------------------------------------------------------------ #
150+
print("\n" + "=" * 60)
151+
print("DELTAPROXY — one full loader per device (same type)")
152+
print("(simulates managing N devices of a single proxy type)")
153+
print("=" * 60)
154+
155+
rss_before_dp = rss_mb()
156+
delta_devices = 20
157+
158+
for i in range(1, delta_devices + 1):
159+
tag = f"deltaproxy_device_{i}"
160+
l = measure(
161+
f"deltaproxy device loader #{i}",
162+
lambda t=tag: salt.loader.minion_mods(make_opts(t), utils=utils),
163+
)
164+
live_loaders.append(l)
165+
166+
rss_after_dp = rss_mb()
167+
dp_cost = rss_after_dp - rss_before_dp
168+
print(f"\n {delta_devices} device loaders RSS cost : {dp_cost:.1f} MB")
169+
print(f" Per-device RSS cost : {dp_cost / delta_devices:.1f} MB")
170+
print(f" Projected cost at 1,000 devs : {dp_cost / delta_devices * 1000:.0f} MB")
171+
172+
# ------------------------------------------------------------------ #
173+
print("\n" + "=" * 60)
174+
print("SUMMARY")
175+
print("=" * 60)
176+
177+
gc.collect()
178+
rss_end = rss_mb()
179+
total_loaders = len(live_loaders)
180+
181+
print(f"\n Loaders alive simultaneously : {total_loaders}")
182+
print(f" Process RSS at start : {rss_start:.1f} MB")
183+
print(f" Process RSS now : {rss_end:.1f} MB")
184+
print(f" Total RSS growth : {rss_end - rss_start:.1f} MB")
185+
print()
186+
print(" Resources (per type):")
187+
print(f" 5 types : ~{5 * dp_cost / delta_devices:.0f} MB extra")
188+
print(f" 20 types : ~{20 * dp_cost / delta_devices:.0f} MB extra")
189+
print()
190+
print(" Deltaproxy (per device, same type):")
191+
print(f" 100 devs : ~{dp_cost / delta_devices * 100:.0f} MB extra")
192+
print(f" 1,000 devs: ~{dp_cost / delta_devices * 1000:.0f} MB extra")
193+
print(f" 5,000 devs: ~{dp_cost / delta_devices * 5000:.0f} MB extra")
194+
print()
195+
196+
197+
if __name__ == "__main__":
198+
main()

0 commit comments

Comments
 (0)