- 运维
- 配置管理
- 后端
【免费下载链接】salt
Software to automate the management and configuration of infrastructure and applications at scale.
salt.resources.ssh.modules.state是 Salt 资源(Resources)子系统为ssh资源类型提供的状态模块覆盖层。它在管理 minion 进程内完整复刻 salt-ssh 的状态执行管线,为通过 SSH 纳管的远程主机提供state.highstate、state.sls与state.apply三个状态入口。本文以 关联文档(Sphinx autodoc 存根)所指向的 模块源码 为骨架,结合 ssh 资源模块、资源架构文档 与 单元测试,讲清它的编译—打包—执行三段式管线、每个配置参数与 CLI 用法的含义,以及它在 minion 侧运行的原理与边界条件。读完本文,你将掌握如何在 Salt 资源模型下对 SSH 纳管主机应用状态,并理解其底层调用链与容错设计。
一、定位:状态模块覆盖层与"编译—打包—执行"三段式管线
salt/resources/ssh/modules/state.py是ssh资源类型的执行模块覆盖(execution-module override),模块头部的 docstring 明确给出了它的设计目标:
Implements
state.highstate,state.sls, andstate.applyfor SSH resources by replicating the salt-ssh state-execution pipeline on the managing minion.
它不做任何远程状态求值,而是把 salt-ssh 在 master 上做的那套工作整体搬到"管理 minion"的进程里执行,共分三个环节:
- 编译(Compile):通过管理 minion 的
RemoteClient/FSClient从 master 读取 state 与 pillar 文件,以SSHHighState完成 highstate 编译。资源 ID(resource ID)被用作 top-file 的匹配目标,因此只有映射到该资源 ID 的 state 会被编译。 - 打包(Package):
prep_trans_tar把编译出的 low state、所有被引用的salt://文件以及渲染后的 pillar 打包进一个传输 tar 包(salt_state.tgz)。 - 执行(Execute):将 tar 通过 SCP 推送到远端主机的
thin_dir,再经由 salt-thin 束调用state.pkg,远端以结构化 JSON 返回执行结果。
这与 master 上直接执行salt-ssh state.highstate的效果一致,区别在于发起者:这里 salt-ssh 的发起者不是 master,而是管理 minion 自身("runs from the managing minion's process so the salt-ssh initiator is the minion, not the master")。
从资源子系统全局看(见 架构文档),Salt 资源体系由三方构成:master 持有 SRN(Salt Resource Name,形如type:id)到管理 minion 的注册表;管理 minion承载连接管线、每个资源的 grains 与 per-resource 执行/状态加载器——本模块正是管理 minion 侧针对ssh类型的状态加载器实现;资源类型则是定义"能对资源做什么操作"的 Python 包。因此本模块的价值在于:把远程主机抽象成一级"资源",让运维人员可以用统一的资源目标语法(如T@ssh:node1)对其施加状态管理,而无须关心底层是 proxy 还是 SSH 直连。
为什么放在salt/resources/ssh/modules/目录下?
模块源码的注释解释了加载门控机制:
- 文件名必须是
state.py,与 slot 名一致; - 目录位置
salt/resources/ssh/modules/决定它只会在ssh 每资源加载器(per-resource loader)中被加载,不会污染其他资源类型; __func_alias__ = {"apply_": "apply"}保证apply_以apply的公开名字暴露(因为apply是 Python 关键字,无法直接作为函数名)。
__virtual__与资源接口
ssh 资源类型本身在 salt/resources/ssh/init.py 中通过__virtual__()做门控:仅当 minion 的PATH中存在ssh二进制时才加载(if not salt.utils.path.which("ssh"))。每个 ssh 资源对应一台可 SSH 访问的远端主机;因为同类型资源共享同一个加载器,一个管理 500 台 SSH 主机的 minion 只需要一个加载器,而不是 500 个各自持有一对密钥的 proxy 进程——这是该设计在资源消耗上的显著收益。
二、配置:Pillar 声明主机与连接参数全表
ssh 资源的声明位于管理 minion 的 Pillar 中,默认顶层键为resources,可通过 minion 配置项resource_pillar_key覆盖(见 配置文档)。ssh 资源模块 docstring 给出的标准声明如下:
resources: ssh: hosts: web-01: host: 192.168.1.10 user: root priv: /etc/salt/ssh_keys/web-01 web-02: host: 192.168.1.11 user: admin passwd: secretpassword no_host_keys: true其中ssh.hosts下的每个键就是一个资源 ID,init()(salt/resources/ssh/init.py)在资源类型加载时通过salt.utils.resources.pillar_resources_tree(opts)读取这些主机配置并缓存进__context__["ssh_resource"];discover()则以这些键集合作为资源 ID 列表返回给 master 的资源注册表。
每主机连接参数全表
下表汇总了_connection_kwargs()(state.py)与_make_shell()中实际读取的字段及其默认值:
| 参数 | 默认值 | 说明 |
|---|---|---|
host | 必填 | 远端主机的主机名或 IP 地址 |
user | root | SSH 登录用户 |
port | 22 | SSH 端口 |
priv | 无 | SSH 私钥文件路径;与passwd互斥,但两者可同时指定,一旦设置priv,Salt 优先使用基于密钥的选项串 |
passwd | 无 | SSH 密码;生产环境建议优先密钥认证 |
priv_passwd | 无 | 保护私钥的 passphrase |
sudo | False | 是否通过 sudo 以 root 执行命令 |
timeout | 30(连接)/60(state 执行) | SSH 连接超时秒数;_connection_kwargs中 state 路径默认60,_make_shell中默认30 |
tty | False | 是否强制分配 TTY |
identities_only | False | 传递-o IdentitiesOnly=yes,阻止 SSH agent 提供无关密钥 |
no_host_keys | False | 完全禁用主机密钥校验:同时设置StrictHostKeyChecking=no与UserKnownHostsFile=/dev/null |
ignore_host_keys | False | 仅传-o StrictHostKeyChecking=no,不丢弃 known-hosts 数据库 |
known_hosts_file | 无 | 为该主机指定自定义known_hosts文件路径 |
ssh_options | 无 | 附加的-o Key=Value选项列表,原样传给ssh二进制 |
keepalive | True | 启用 TCP keepalive |
keepalive_interval | 60 | ServerAliveInterval(秒) |
keepalive_count_max | 3 | ServerAliveCountMax |
thin_dir | 自动生成 | 远端 salt-thin 束的工作目录;若配置则优先使用 |
注意:
timeout在两处默认值不同——_make_shell(用于ping/cmd_run等)默认 30 秒,而 state 执行路径的_connection_kwargs默认 60 秒。
主机密钥策略与 SSH 版本的处理
state 模块通过_target_opts()(state.py)构造一份适合SSHHighState与Single的 opts 副本,关键处理包括:
- 把
opts["id"]设为资源 ID,使 top file 能匹配到正确的目标主机; - 从资源配置中注入
no_host_keys、ignore_host_keys、known_hosts_file三项主机密钥策略; - 注入
_ssh_version:优先取__context__["ssh_resource"]["_ssh_version"](由init()预解析并缓存,避免 job 线程中运行子进程),否则调用salt.client.ssh.ssh_version(); - 固定
opts["relenv"] = True,让状态执行走 relenv(Python 运行时打包)路径。
三、核心函数:highstate、sls 与 apply
模块公开三个状态函数,源码中均带 CLI 示例。
state.highstate——对整个资源应用 highstate
def highstate(test=None, **kwargs):对目标 SSH 资源应用 highstate:用资源 ID 作为 top-file 目标编译 highstate,把 state 文件打包进传输 tar,SCP 到远端并用 salt-thin 执行state.pkg。
官方 CLI 示例:
salt -C 'T@ssh:node1' state.highstate salt -C 'T@ssh:node1' state.highstate test=True流程细节(state.py):
_target_opts()构造目标 opts,_seed_thin_dir()把计算出的thin_dir写回 opts,保证SSHHighState与prep_trans_tar使用一致的可写路径;_get_initial_pillar()取管理 minion 已渲染的 pillar 作为initial_pillar传入SSHHighState——这能让State.__init__跳过_gather_pillar()(否则会尝试为一个未知的 minion ID 编译 pillar,产生虚假的 pillar 编译);管理 minion 自身的 pillar 恰好已包含资源配置;- 在
with SSHHighState(...)上下文中调用st_.compile_low_chunks()编译 low chunks;SSHHighState.__exit__会调用file_client.destroy(),因此无需单独的 finally 清理; - 遍历 chunks 时若发现非 dict 元素(即错误列表)直接返回;若 chunks 为空(top file 对资源 ID 无匹配),直接返回一个与普通 minion "No Top file" 条目相同键格式的状态 dict,
result=False、comment 为"No Top file or master_tops data matches found for resource '<id>'.",从而在合并输出中干净地展示,避免空返回导致展示异常——这一行为正是 单元测试TestHighstateEmptyChunks覆盖的场景; - 用
lowstate_file_refs()收集所有被引用文件(叠加extra_filerefs),_cleanup_slsmod_low_data()清理slsmod低数据,prep_trans_tar()生成传输 tar; - 最后调用
_exec_state_pkg()执行远端状态。
state.sls——对指定 SLS 文件应用状态
def sls(mods, saltenv="base", test=None, **kwargs):对目标 SSH 资源应用一个或多个 SLS 文件。官方 CLI 示例:
salt -C 'T@ssh:node1' state.sls node1 salt -C 'T@ssh:node1' state.sls node1,common test=True实现细节(state.py):
- 若
mods是字符串,会先按逗号切分并去除空白(mods = [m.strip() for m in mods.split(",") if m.strip()]),因此支持一次性指定多个 SLS; - 与
highstate不同,这里不走 top file,而是调用st_.render_highstate({saltenv: mods})手动构造 high data,随后依次执行reconcile_extend(处理extend声明)、verify_high(校验 high data 结构)、requisite_in(处理require_in/watch_in反向依赖)、apply_exclude(应用__exclude__),最后compile_high_data得到 chunks——这条管线与 salt 常规state.sls的编译过程一一对应; - 支持
exclude参数:字符串会被切分为列表并加入__exclude__; - 任一环节返回
errors非空则立即返回错误列表,不发起远端执行。
state.apply——统一入口
def apply_(mods=None, **kwargs):官方 CLI 示例:
salt -C 'T@ssh:node1' state.apply salt -C 'T@ssh:node1' state.apply node1逻辑极简:有mods则转发给sls(),否则转发给highstate()——与常规 minion 上state.apply的行为语义一致。test=True等 kwargs 原样透传。
四、执行段:_exec_state_pkg与 salt-thin 束
编译、打包完成后,真正的远程执行落在_exec_state_pkg()(state.py)。其执行步骤:
- 计算校验和:
salt.utils.hashutils.get_hash(trans_tar, opts["hash_type"])计算传输 tar 的哈希(默认sha256),远端用pkg_sum校验包完整性; - 构造
Single:salt.client.ssh.Single是 salt-ssh 单机执行的核心类。注意代码刻意传了占位 argv"state.pkg",因为Single.__init__可能改写thin_dir(如_salt→_salt_relenv),真正的 argv 要在__init__完成后再构造:cmd = "state.pkg {thin_dir}/salt_state.tgz test={test} pkg_sum={pkg_sum} hash_type={hash_type}" - SCP 推送:
single.shell.send(trans_tar, "{}/salt_state.tgz".format(opts["thin_dir"]))把包传到远端; - 执行:
single.cmd_block()通过 salt-thin 束在远端运行state.pkg; - 清理:无论结果如何,finally 中删除本地临时 tar 文件(
os.remove(trans_tar),OSError 吞掉)。
relenv 与 thin 束的本地化
_relenv_path()(state.py)会在cachedir/relenv/linux/{x86_64,arm64}/salt-relenv.tar.xz中查找本地预构建的 relenv 压缩包;找到就把它作为thin=参数传给Single,找不到则返回None,让Single.__init__自行探测远端架构并下载对应压缩包。查找使用x86_64/arm64规范名,因为salt.utils.relenv.gen_relenv会先把架构归一化再构建缓存路径。单元测试 的TestRelenvPath覆盖了四种情形:仅 x86_64、仅 arm64、两者皆无、两者都有的优先级(优先 x86_64)。
远端 thin_dir 的确定
_thin_dir()(state.py)与 ssh 资源模块的同名函数 逻辑一致:配置了thin_dir就用配置值;否则用 UUID3(命名空间 DNS + 管理 minion FQDN)的前 6 位 hex 生成路径,且刻意放在/tmp/下(/tmp/.<user>_<hash>_salt),因为/tmp始终全局可写,而/var/tmp/在某些系统上可能仅 root 可写。_seed_thin_dir()把它写进 opts,保证编译与打包阶段路径一致。
返回结构与异常兜底
parse_ret在远端 retcode 非零时会抛出SSHCommandExecutionError——但"某些 state 失败导致 retcode 2"并不代表执行失败,远端仍产出了合法的 state 结果 dict。因此代码捕获该异常后:
- 若异常携带的
parsed["local"]["return"]是 dict,则解出 state 结果,并把local["retcode"](缺省EX_STATE_FAILURE)写入__context__["retcode"]正常返回——让运维人员看到完整 state 树而非原始 JSON; - 否则重新抛出异常。
正常路径下则解包envelope["return"](thin 束会把结果包成{"local": {"jid": ..., "return": ...}}信封),并同步远端 retcode。模块最终直接返回 state 结果 dict 本身(minion 分发器所期望的形式),而非{"local": {"return": ...}}信封。单元测试 明确覆盖了这一异常兜底分支:_exec_state_pkg()从异常中提取合法 state dict 返回、并在 parsed 无合法 state dict 时重新抛出。
为什么在 job 内新建文件客户端?
_exec_state_pkg里特意新建了一个文件客户端(_file_client()),供Single.cmd_block()调用mod_data(fsclient)扫描扩展模块。_file_client()优先使用init()缓存在__context__["ssh_resource"]["master_opts"]的 master 配置构造FSClient(本地文件系统客户端,不建立网络通道,避开 minion job 线程内 tornado IO-loop 的复杂性);无缓存时回退到RemoteClient。init()则优先从 minion 同目录的master配置文件读取完整 master opts(比RemoteClient.master_opts()更全,后者会缺fileserver_backend等键),失败时回退。
五、把模块放入资源模型:一次完整的状态执行之旅
结合 架构文档,一次对 SSH 资源的状态执行在系统中是这样流动的:
- 目标展开:运维执行
salt -C 'T@ssh:web-01' state.highstate。master 的资源注册表(by_idmmap 索引)查到 SRNssh:web-01的管理 minion ID,把 job 投递给该 minion; - 资源分发:管理 minion 的 per-resource 加载器根据
T@ssh目标找到ssh类型的 state 覆盖层,即本模块;__resource__["id"]指向web-01; - 编译与打包:
highstate()用web-01作 top-file 目标编译 low chunks,prep_trans_tar打包salt_state.tgz; - 远程执行:
_exec_state_pkg通过Single把包 SCP 到远端thin_dir,以 salt-thin 调用state.pkg,远端 JSON 结果解包后作为 state dict 返回; - 结果归并:返回的 state 结果与普通 minion 的 state 结果一样进入 Job 返回,运维看到完整 state 树。资源 ID 没有 top-file 匹配时则返回统一的 "No Top file" 状态条目。
对运维的实用意义:
- 主机的发现与下线只需增删 Pillar 中
ssh.hosts下的键并执行saltutil.refresh_resources,无需重启进程(discover()注释明确说明); - 连接细节、主机密钥策略、超时与 keepalive 均可按主机覆盖;
test=True预演、exclude排除、多 SLS 一次应用等常规 state 能力全部保留。
六、测试与可靠性保障
tests/pytests/unit/modules/test_sshresource_state.py 是围绕本模块的单元测试,覆盖两类关键行为:
TestRelenvPath:验证_relenv_path()在os.path.exists打桩下正确返回 x86_64/arm64 本地 relenv 压缩包路径、两者皆无时返回None、两者都有时优先 x86_64——保证不因路径探测逻辑在 job 线程中引入额外 SSH 往返;TestHighstateEmptyChunks:compile_low_chunks返回空列表时,highstate()必须返回result=False的 "no top file" 状态 dict(而非None/空),使合并展示干净;_exec_state_pkg异常兜底:用SSHCommandExecutionError(..., retcode=2, parsed=...)模拟远端 state 失败场景,断言能从中提取合法 state dict 正常返回,非法时重新抛出。
测试中构造的_BASE_OPTS(含id、resource_type、cachedir、hash_type: sha256、thin_dir、test、pillar)与_BASE_RESOURCE({"id": "node1", "type": "ssh"})也直观展示了模块运行所依赖的 dunder 环境:__opts__、__resource__、__context__、__salt__。
七、边界条件与注意事项
基于源码可确认的边界与约束如下:
- 依赖
ssh二进制:资源类型在ssh不在 minion PATH 上时拒绝加载(__virtual__返回False); - top file 无匹配是"可预期结果"而非异常:
highstate()返回result=False的 "No Top file" 条目,不会发起任何 SSH 往返; initial_pillar为空 dict 时的坑:State.__init__中if initial_pillar对空 dict 为假值,会重新触发_gather_pillar;因此_get_initial_pillar()对空 dict 显式返回None,让调用方明确"无缓存 pillar"而非静默走错分支;Single.__init__可能改写thin_dir:因此 argv 必须在__init__之后构造(注释明确说明);- 远端 retcode 非零并不总是失败:state 部分失败(retcode 2)时应展示完整 state 树,模块通过异常兜底实现,同时把 retcode 写入
__context__供上层判断; - 文件客户端策略:优先本地
FSClient(避免 job 线程内的网络通道问题),回退RemoteClient仅在无缓存的 master opts 时发生,并记录 warning 日志。
八、小结
salt.resources.ssh.modules.state用一个约 500 行的模块,把 salt-ssh 的"编译—打包—SCP—thin 执行"全流程下沉到管理 minion 进程内,与资源子系统无缝衔接:Pillar 声明主机、资源 ID 驱动 top-file 匹配与 SRN 路由、state.pkg承载远端执行、异常兜底保证 state 语义不被 SSH 错误码污染。对需要批量管理大量 SSH 主机的场景,它提供了比逐个 proxy 更轻量、比手写 salt-ssh 脚本更统一的资源化抽象。相关实现可继续研读 ssh 资源模块、状态执行单元测试 与 资源配置文档。
- 运维
- 配置管理
- 后端
【免费下载链接】salt
Software to automate the management and configuration of infrastructure and applications at scale.
相关推荐
Salt 内核模块管理实战:深入解析 kmod 执行模块与 kmod.present/absent 状态
Salt 内核模块管理实战:深入解析 kmod 执行模块与 kmod.present/absent 状态 本篇技术指南以 Salt 官方 API 文档 salt
运维配置管理后端Salt SSH 资源执行模块 cmd:无代理远程命令执行的源码级解析
Salt SSH 资源执行模块 cmd:无代理远程命令执行的源码级解析 导读 本文聚焦 salt.resources.ssh.modules.cmd 执行模块
运维配置管理后端Salt 资源框架中的 `dummy` 类型执行模块覆盖:深入解析 `salt.resources.dummy.modules.test`
Salt 资源框架中的 dummy 类型执行模块覆盖:深入解析 salt.resources.dummy.modules.test salt.resources
运维配置管理后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考