news 2026/9/25 16:11:32

Salt 资源体系中的 SSH 状态执行:`salt.resources.ssh.modules.state` 模块深入解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Salt 资源体系中的 SSH 状态执行:`salt.resources.ssh.modules.state` 模块深入解析
  • 运维
  • 配置管理
  • 后端

【免费下载链接】salt

Software to automate the management and configuration of infrastructure and applications at scale.

项目地址:https://gitcode.com/gh_mirrors/sa/salt
点击查看免费下载

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 明确给出了它的设计目标:

Implementsstate.highstate,state.sls, andstate.applyfor SSH resources by replicating the salt-ssh state-execution pipeline on the managing minion.

它不做任何远程状态求值,而是把 salt-ssh 在 master 上做的那套工作整体搬到"管理 minion"的进程里执行,共分三个环节:

  1. 编译(Compile):通过管理 minion 的RemoteClient/FSClient从 master 读取 state 与 pillar 文件,以SSHHighState完成 highstate 编译。资源 ID(resource ID)被用作 top-file 的匹配目标,因此只有映射到该资源 ID 的 state 会被编译。
  2. 打包(Package):prep_trans_tar把编译出的 low state、所有被引用的salt://文件以及渲染后的 pillar 打包进一个传输 tar 包(salt_state.tgz)。
  3. 执行(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 地址
userrootSSH 登录用户
port22SSH 端口
priv无SSH 私钥文件路径;与passwd互斥,但两者可同时指定,一旦设置priv,Salt 优先使用基于密钥的选项串
passwd无SSH 密码;生产环境建议优先密钥认证
priv_passwd无保护私钥的 passphrase
sudoFalse是否通过 sudo 以 root 执行命令
timeout30(连接)/60(state 执行)SSH 连接超时秒数;_connection_kwargs中 state 路径默认60,_make_shell中默认30
ttyFalse是否强制分配 TTY
identities_onlyFalse传递-o IdentitiesOnly=yes,阻止 SSH agent 提供无关密钥
no_host_keysFalse完全禁用主机密钥校验:同时设置StrictHostKeyChecking=no与UserKnownHostsFile=/dev/null
ignore_host_keysFalse仅传-o StrictHostKeyChecking=no,不丢弃 known-hosts 数据库
known_hosts_file无为该主机指定自定义known_hosts文件路径
ssh_options无附加的-o Key=Value选项列表,原样传给ssh二进制
keepaliveTrue启用 TCP keepalive
keepalive_interval60ServerAliveInterval(秒)
keepalive_count_max3ServerAliveCountMax
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):

  1. _target_opts()构造目标 opts,_seed_thin_dir()把计算出的thin_dir写回 opts,保证SSHHighState与prep_trans_tar使用一致的可写路径;
  2. _get_initial_pillar()取管理 minion 已渲染的 pillar 作为initial_pillar传入SSHHighState——这能让State.__init__跳过_gather_pillar()(否则会尝试为一个未知的 minion ID 编译 pillar,产生虚假的 pillar 编译);管理 minion 自身的 pillar 恰好已包含资源配置;
  3. 在with SSHHighState(...)上下文中调用st_.compile_low_chunks()编译 low chunks;SSHHighState.__exit__会调用file_client.destroy(),因此无需单独的 finally 清理;
  4. 遍历 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覆盖的场景;
  5. 用lowstate_file_refs()收集所有被引用文件(叠加extra_filerefs),_cleanup_slsmod_low_data()清理slsmod低数据,prep_trans_tar()生成传输 tar;
  6. 最后调用_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)。其执行步骤:

  1. 计算校验和:salt.utils.hashutils.get_hash(trans_tar, opts["hash_type"])计算传输 tar 的哈希(默认sha256),远端用pkg_sum校验包完整性;
  2. 构造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}"
  3. SCP 推送:single.shell.send(trans_tar, "{}/salt_state.tgz".format(opts["thin_dir"]))把包传到远端;
  4. 执行:single.cmd_block()通过 salt-thin 束在远端运行state.pkg;
  5. 清理:无论结果如何,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 资源的状态执行在系统中是这样流动的:

  1. 目标展开:运维执行salt -C 'T@ssh:web-01' state.highstate。master 的资源注册表(by_idmmap 索引)查到 SRNssh:web-01的管理 minion ID,把 job 投递给该 minion;
  2. 资源分发:管理 minion 的 per-resource 加载器根据T@ssh目标找到ssh类型的 state 覆盖层,即本模块;__resource__["id"]指向web-01;
  3. 编译与打包:highstate()用web-01作 top-file 目标编译 low chunks,prep_trans_tar打包salt_state.tgz;
  4. 远程执行:_exec_state_pkg通过Single把包 SCP 到远端thin_dir,以 salt-thin 调用state.pkg,远端 JSON 结果解包后作为 state dict 返回;
  5. 结果归并:返回的 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.

项目地址:https://gitcode.com/gh_mirrors/sa/salt
点击查看免费下载
上一篇:AnuPpuccin 彩虹文件夹一步到位:新手完整配置指南
下一篇:m4s转MP4免费合并工具:B站缓存视频无损转换,11.7GB只要38秒

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 16:10:57

学习通签到自动化技术演进与工程化实践

1. 这不是“外挂”&#xff0c;而是一次对自动化边界的技术复盘“学习通签到神器”——这六个字在高校学生群体里&#xff0c;几乎等同于“时间管理刚需”。但我要先说清楚&#xff1a;它既不是破解App的黑产工具&#xff0c;也不是绕过身份核验的越狱方案。它本质是基于公开HT…

作者头像 李华
网站建设 2026/9/25 16:07:18

37年物联网专利数据揭示技术演进与产业竞争格局

1988到2025&#xff0c;整整跨越了37年。如果把这37年间的上市公司物联网技术专利数据摊开来看&#xff0c;它记录的不只是一堆专利申请号和法律状态&#xff0c;更是一部物联网从实验室概念走向千行百业的中文产业史。我自己在梳理这份数据时&#xff0c;最感慨的不是某个企业…

作者头像 李华
网站建设 2026/9/25 16:01:40

gsd-core 命令契约校验(ADR-0002):从命令文件到 CI 的双层验证体系

【免费下载链接】gsd-core Git. Ship. Done - Core 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ge/gsd-core 点击查看 免费下载 本篇指南以 gsd-core 仓库中的 ADR-0002 决策记录为主线&#xff0c;系统讲解 commands/gsd/*.md 命令文件契约的五条结构规则与第六条…

作者头像 李华
网站建设 2026/9/25 15:59:06

专升本数据结构备考:线性表、链表、树图与排序的代码与避坑全攻略

简介&#xff1a;这份数据结构复习资料专为专升本考生设计&#xff0c;内容系统覆盖数组、链表、栈、队列、二叉树、堆、图、散列表等核心结构&#xff0c;以及排序与查找算法的应用。资源以“数据结构1800例题与答案”为主体&#xff0c;共包含三十四个文件&#xff0c;其中二…

作者头像 李华
网站建设 2026/9/25 15:58:08

TBOX信息安全系列8设计篇-SecOC车内通信安全方案

2015年&#xff0c;安全研究员通过远程接口黑进一辆切诺基的CAN总线&#xff0c;向刹车系统发送伪造指令——车在高速上被远程劫持。这件事震惊了整个汽车行业&#xff1a;CAN总线从设计之初就没考虑过"认证"&#xff0c;任何接到总线上的设备都能发任意ID的报文&…

作者头像 李华