- 运维
- 配置管理
- 后端
【免费下载链接】salt
Software to automate the management and configuration of infrastructure and applications at scale.
本文以 Salt 官方 API 文档 doc/ref/modules/all/salt.modules.slsutil.rst 为核心骨架,结合 salt/modules/slsutil.py 源码及其单元测试 tests/pytests/unit/modules/test_slsutil.py,系统讲解slsutil执行模块的每一个函数。读完本文,你将掌握:在 Jinja 模板与 SLS 状态文件中递归合并/更新数据结构、选用不同合并策略、调用渲染器管道渲染文件或字符串、跨格式序列化与反序列化、生成标准化的“本文件由 Salt 管理”注释横幅、把布尔值转换为任意字符串,以及在 state tree 中探测文件/目录是否存在并向上逐级查找文件——这些都是编写复杂 Salt 公式(Formula)时高频使用的实战能力。
slsutil(SLS utility)是 Salt 中一组“供 SLS 文件内部使用”的通用工具函数。它不直接管理系统资源,而是为状态文件和 Jinja 模板提供数据处理能力,是 Salt 公式开发者的“瑞士军刀”。
一、模块概览:slsutil能做什么
从 salt/modules/slsutil.py 的源码看,slsutil一共提供了 10 个公开函数,可以归为四类:
| 分类 | 函数 | 核心用途 |
|---|---|---|
| 数据合并 | update、merge、merge_all | 递归合并/更新字典,支持多种合并策略 |
| 渲染 | renderer | 将文件或字符串送入 Salt 渲染器管道(renderer pipe)处理 |
| 序列化 | serialize、deserialize | 调用 Salt 的序列化器在 Python 对象与字符串/流之间转换 |
| 模板辅助 | banner、boolstr | 生成注释横幅、把布尔值输出为指定字符串 |
| 文件探测 | file_exists、dir_exists、path_exists、findup | 在 state tree 中检查文件/目录存在性并沿祖先目录查找 |
其中_set_context(版本 3004 起)与_get_serialize_fn是内部辅助函数,公开 API 文档不会单独列出,但它们是file_exists/dir_exists缓存机制与序列化函数查找的实现基础。
二、数据合并:update、merge与merge_all
2.1update:递归版本的dict.update
slsutil.update(dest, upd, recursive_update=True, merge_lists=False)将upd递归合并进dest:
salt '*' slsutil.update '{foo: Foo}' '{bar: Bar}' # 结果: {'foo': 'Foo', 'bar': 'Bar'}关键参数:
recursive_update:默认True,表示递归合并嵌套字典;设为False则退化为经典dict.update行为(整体覆盖)。merge_lists:默认False。仅在recursive_update=True时生效;设为True后,两个列表将按“追加”方式聚合(dest[key] + upd[key]),且自 2016.11.6 起重复值会被去重。
从 salt/utils/dictupdate.py 的底层实现可见其精确语义:
- 当
dest与upd的键完全不相交时,会自动降级为非递归更新(recursive_update=False),提升性能; - 双方均为
Mapping时递归进入; - 双方均为
list且merge_lists=True时,upd中不存在的元素才被追加进深拷贝后的dest列表; - 自 3008.0 起,当
merge_lists=True且试图用映射覆盖列表时(strict=True场景)会抛出TypeError,避免隐式数据丢失。
单元测试 test_update 验证了键合并与merge_lists=False时的覆盖行为。
2.2merge:按策略合并
slsutil.merge(obj_a, obj_b, strategy="smart", renderer="yaml", merge_lists=False)允许通过strategy显式选择合并方式:
salt '*' slsutil.merge '{foo: Foo}' '{bar: Bar}'支持的合并策略(定义于 salt/utils/dictupdate.py):
| 策略 | 行为 |
|---|---|
smart(默认) | 依据renderer参数自动选择:若渲染器以yamlex结尾或以yamlex_开头则用aggregate,否则用recurse |
recurse | 深拷贝obj_a后递归合并obj_b |
aggregate | 使用 yamlex 序列化器的merge_recursive进行“聚合”式合并(层级合并,可处理列表语义) |
list | 相同键的值组合成列表[obj_a[key], obj_b[key]] |
overwrite | 仅当键存在于obj_a时用obj_b的值覆盖,再递归合并 |
none | 不真正合并(用于单 pillar 场景),行为等同 recurse |
| 其他未知值 | 记录 warning 日志后回退为recurse |
测试用例 test_merge 展示了各种策略的典型差异,例如strategy="list"时{"foo": "Foo"}与{"foo": "Bar"}合并为{"foo": ["Foo", "Bar"]}。
2.3merge_all:按顺序合并一组对象
slsutil.merge_all(lst, strategy="smart", renderer="yaml", merge_lists=False)(版本 2019.2.0 起)依次将列表中的每个对象合并进结果字典,后合并的值覆盖先合并的值:
salt-call --output=txt slsutil.merge_all '[{foo: Foo}, {foo: Bar}]' local: {u'foo': u'Bar'}其实现就是循环调用salt.utils.dictupdate.merge(ret, obj, ...),strategy、renderer、merge_lists三个参数与merge完全一致。这在合并多层 pillar 数据、或把多个 map 文件的结果叠加时非常有用。
三、renderer:把文件/字符串送入渲染器管道
slsutil.renderer(path=None, string=None, default_renderer="jinja|yaml", **kwargs)是模块中功能最开放的函数。它利用 Salt 的“渲染器管道”(renderer pipes)机制,将文件或内联字符串依次通过多个渲染器处理,最终返回处理结果。自 2018.3.0 起支持 Salt fileserver URI(如salt://path/to/file)。
salt '*' slsutil.renderer salt://path/to/file salt '*' slsutil.renderer /path/to/file salt '*' slsutil.renderer /path/to/file.jinja default_renderer='jinja' salt '*' slsutil.renderer /path/to/file.sls default_renderer='jinja|yaml' salt '*' slsutil.renderer string='Inline template! {{ saltenv }}' salt '*' slsutil.renderer string='Hello, {{ name }}.' name='world'参数语义:
path:可以是 Salt fileserver 上的任意 URI(支持cp.get_url支持的所有 URI 形式),也可以是本地文件系统路径;string:内联字符串。注意并非所有渲染器都支持字符串输入——例如py渲染器就要求文件;default_renderer:默认渲染管道,会被文件开头的 shebang(如#!jinja|yaml)覆盖;kwargs:透传给compile_template()的关键字参数,如name='world'会作为模板变量注入。
典型实战场景:解耦 map 文件的渲染器。Salt 公式中常见的 map 文件通常与使用它的 SLS 采用相同渲染器,但通过slsutil.renderer可以打破这一限制——map 文件用 Python 渲染器编写,而引用它的 SLS 依然使用默认的jinja|yaml。官方文档给出了两个功能等价的 map 文件示例:
jinja|yaml版本(#!jinja|yaml头):
#!jinja|yaml {% set apache = salt'grains.filter_by') %} {{ apache | yaml() }}py版本(#!py头):
#!py def run(): apache = __salt__'grains.filter_by') return apache无论使用哪个版本,其他任何 SLS 文件都能用同一行 Jinja 调用它:
{% set apache = salt'slsutil.renderer' %}实现原理(salt/modules/slsutil.py):
- 只传
path或string之一,同时传或不传都会抛出SaltInvocationError; - 通过
salt.loader.render(__opts__, __salt__)加载全部渲染器; - 对
path使用__salt__"cp.get_url")获取本地路径; - 对
string使用特殊占位:string:并把字符串放入kwargs["input_data"]; - 调用
salt.template.compile_template(path_or_string, renderers, default_renderer, __opts__["renderer_blacklist"], __opts__["renderer_whitelist"], **kwargs); - 最后根据
stringio.is_readable判断返回 StringIO 内容还是原始返回值。
注意其中会遵守__opts__中的renderer_blacklist/renderer_whitelist配置,即 master/minion 配置里对渲染器的黑白名单在此同样生效。
选择渲染管道时的一个关键提醒:不同渲染器的产出类型不同——Jinja 处理文本并产生字符串,而 YAML 渲染器处理文本后产生的是数据结构。因此在管道中选择渲染器时,要始终清楚自己期望得到字符串还是字典/列表。
单元测试 test_renderer 覆盖了字符串渲染、缺参报错、path/string 同时传入报错以及文件渲染四种情形。
四、序列化与反序列化:serialize与deserialize
slsutil.serialize(serializer, obj, **mod_kwargs)使用 Salt 已加载的序列化器将 Python 对象序列化为字符串;slsutil.deserialize(serializer, stream_or_string, **mod_kwargs)则相反。
# 注意 --no-parse=obj 防止命令行参数被提前解析 salt '*' --no-parse=obj slsutil.serialize 'json' obj="{'foo': 'Foo!'}" salt '*' slsutil.deserialize 'json' '{"foo": "Foo!"}' salt '*' --no-parse=stream_or_string slsutil.deserialize 'json' \ stream_or_string='{"foo": "Foo!"}'Jinja 中的用法:
{% set json_string = salt'slsutil.serialize' %} {% set python_object = salt'slsutil.deserialize' %}从 salt/serializers/ 目录可以看到当前仓库内置的序列化器:configparser、json、msgpack、tomlmod、yaml、yamlex。serializer参数即取其中的名称,例如slsutil.serialize('yaml', obj)。
实现细节:_get_serialize_fn(salt/modules/slsutil.py)通过salt.loader.serializers(__opts__)加载序列化器,若序列化器不存在抛出CommandExecutionError("Serializer '<name>' not found."),若对应函数未实现则抛出CommandExecutionError("Serializer '<name>' does not implement <fn>.")。测试 test__get_serializer_fn 对这两种报错路径都有验证。
五、模板辅助:banner与boolstr
5.1banner:标准化注释横幅
配置管理的一个常见做法是在被管理的文件中插入“此文件由 Salt 管理,请勿手动修改”的注释块。slsutil.banner()让这一操作标准化、可定制:
{{ salt['slsutil.banner']() }}输出(默认width=72,commentchar='#',borderchar='#'):
######################################################################## # # # THIS FILE IS MANAGED BY SALT - DO NOT EDIT # # # # The contents of this file are managed by Salt. Any changes to this # # file may be overwritten automatically and without warning. # ########################################################################参数说明:
| 参数 | 默认值 | 说明 |
|---|---|---|
width | 72 | 横幅宽度(字符数) |
commentchar | # | 每行行首的注释字符,支持//等多字符序列;若文件语法不支持行注释(如 XML),改用blockstart/blockend |
borderchar | # | 上下边框字符,必须是单个字符 |
blockstart | None | 块注释起始序列,需与blockend配合(如/*) |
blockend | None | 块注释结束序列(如*/) |
title | THIS FILE IS MANAGED BY SALT - DO NOT EDIT | 居中显示在方框顶部 |
text | 固定的警告文本 | 左对齐显示在方框底部 |
newline | False | 是否在横幅末尾追加换行符 |
Javadoc 风格示例:
{{ salt'slsutil.banner' }}/** *********************************************************************** * * * THIS FILE IS MANAGED BY SALT - DO NOT EDIT * * * * The contents of this file are managed by Salt. Any changes to this * * file may be overwritten automatically and without warning. * *********************************************************************** */自定义标题与正文示例:
{{ set copyright='This file may not be copied or distributed without permission of VMware, Inc.' }} {{ salt'slsutil.banner' }}实现上,banner使用textwrap.TextWrapper对标题与正文按width自动折行,并通过os.linesep拼接各行;若width过小导致内容宽度为负,会抛出ArgumentValueError。单元测试 test_banner 断言了每一行的长度恰好等于width、以commentchar开头并以commentchar.strip()结尾,以及 blockstart/blockend 出现在首尾行。
5.2boolstr:布尔值转任意字符串
slsutil.boolstr(value, true="true", false="false")把布尔值映射为模板所需的字符串,常用于把 Pillar/Grains 中的布尔配置输出为特定文件语法要求的取值(如yes/no):
{% set encrypted = salt'pillar.get' %} use_tls: {{ salt'slsutil.boolstr' }}当 pillar 中smtp:encrypted为True时输出use_tls: yes。默认true/false参数值分别是字符串"true"与"false",也可以传任意其他取值。测试 test_boolstr 验证了yes/no映射。
六、state tree 文件探测:file_exists、dir_exists、path_exists与findup
这四个函数(自版本 3004 起)用于在 Salt fileserver 的 state tree 中探测路径,适合在状态文件中根据文件是否存在做条件渲染。
6.1 存在性检查
salt '*' slsutil.file_exists nginx/defaults.yaml salt '*' slsutil.dir_exists nginx/files salt '*' slsutil.path_exists nginx/defaults.yamlfile_exists(path, saltenv="base"):文件是否存在;dir_exists(path, saltenv="base"):目录是否存在;path_exists(path, saltenv="base"):文件或目录是否存在(即前两者的或)。
三个函数都接受saltenv参数指定 fileserver 环境,默认base。path_exists的实现就是file_exists(...) or dir_exists(...)。
性能设计:file_exists与dir_exists通过_set_context把cp.list_master(saltenv)/cp.list_master_dirs(saltenv)的结果缓存在__context__["slsutil"][saltenv]下(CONTEXT_BASE = "slsutil"),同一次 minion 运行内重复探测不会反复拉取文件列表。_set_context会按需逐级创建字典路径,并支持force参数强制刷新缓存。
6.2findup:沿祖先目录向上查找
slsutil.findup(startpath, filenames, saltenv="base")从指定目录开始,逐级向上(到 state tree 根)查找文件名/目录名,返回第一个匹配的完整路径。
salt '*' slsutil.findup formulas/shared/nginx map.jinjaJinja 中最典型的用法是配合tplfile(当前正在处理的模板文件路径),从当前状态文件所在目录向上找到最近的defaults.yaml:
{{ salt"slsutil.findup" }}行为细节(salt/modules/slsutil.py):
startpath为空字符串或None时从 state tree 根开始查找;filenames接受单个字符串或字符串列表,也支持目录名;- 起始路径本身必须存在于 state tree,否则抛出
SaltInvocationError; - 一路向上到根仍未找到,抛出
CommandExecutionError("File pattern(s) not found in path ancestry")。
单元测试 test_findup 验证了向上查找、多文件名优先级、根目录查找以及各类异常路径。
七、在 SLS 与 Jinja 中的综合实战示例
将以上能力组合,可以实现非常灵活的公式编写模式。例如,一个 nginx 公式的状态文件可以这样写:
{%- set tplroot = tpldir.split('/')[0] %} {%- set sls_pillar = salt'pillar.get' %} {%- set defaults = salt'slsutil.renderer', default_renderer='jinja|yaml' ) %} {%- set settings = salt'slsutil.merge' %} # 合并后的配置通过 serialize 输出为 JSON,供其他工具消费 {%- set json_conf = salt'slsutil.serialize' %} nginx: pkg.installed: [] service.running: [] file.managed: - name: /etc/nginx/nginx.conf - source: salt://{{ tplroot }}/files/nginx.conf.jinja - template: jinja - context: settings: {{ settings | json }}其中findup负责“找到离当前状态文件最近的 defaults.yaml”,renderer负责以指定渲染管道读取它,merge负责把 pillar 覆盖值合并进去,serialize负责把最终结果导出为其他格式——这正是四个核心函数在同一场景下的协同工作。
八、API 文档与测试的对应关系
- API 文档:
slsutil的官方文档入口位于 doc/ref/modules/all/salt.modules.slsutil.rst,通过automodule指令从源码 docstring 自动生成,因此在 salt/modules/slsutil.py 中各函数的 docstring 即文档本体; - 单元测试:tests/pytests/unit/modules/test_slsutil.py 覆盖
update/merge/merge_all/renderer/serialize/deserialize/banner/boolstr/file_exists/dir_exists/path_exists/findup及两个内部辅助函数,是理解各函数精确行为的最佳补充材料; - 底层依赖:合并逻辑的核心实现在 salt/utils/dictupdate.py,序列化依赖 salt/serializers/ 下各序列化器;
- 集成测试:
slsutil在 SSH 模式(salt-ssh)下同样可用,相关测试见 tests/pytests/integration/ssh/test_slsutil.py 与 tests/pytests/unit/client/ssh/wrapper/test_slsutil.py。
结语
slsutil是一组“小而精”的工具:update/merge/merge_all解决了 SLS 开发中最常见的数据合并问题,renderer打破了 map 文件与 SLS 渲染器之间的耦合,serialize/deserialize打通了对象与文本格式之间的转换,banner/boolstr简化了模板输出,而file_exists/dir_exists/path_exists/findup让状态文件具备基于 state tree 实际内容的感知能力。掌握这组函数,可以让你的 Salt 公式更健壮、更灵活,也更易于维护。
- 运维
- 配置管理
- 后端
【免费下载链接】salt
Software to automate the management and configuration of infrastructure and applications at scale.
相关推荐
Salt locate 执行模块完全指南:用 Salt 远程调用 locate/updatedb 进行文件检索与数据库管理
Salt locate 执行模块完全指南:用 Salt 远程调用 locate/updatedb 进行文件检索与数据库管理 导读 本文围绕 Salt 执行模块
运维配置管理后端Salt hosts 执行模块全指南:用 `salt '*' hosts.*` 管理 hosts 文件中的 IP 与主机名映射
Salt hosts 执行模块全指南:用 salt ' ' hosts. 管理 hosts 文件中的 IP 与主机名映射 本篇技术指南围绕 Salt 内置的 h
运维配置管理后端Salt 中 FreeBSD pkgng 软件包管理执行模块完全指南
Salt 中 FreeBSD pkgng 软件包管理执行模块完全指南 本文档基于 Salt 开源仓库中 salt/modules/pkgng.py https:
运维配置管理后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考