CFEngine 3.27.2 快速入门与策略编写示例
CFEngine 3.27.2 社区版快速入门:策略语言核心概念(bundle/promise/class/variable)讲解,从第一个 hello world 到 files/classes/commands 实战策略从 0 写到执行,含 cf-promises 语法校验、autorun 自动加载机制与三大踩坑实录
CFEngine 3.27.2 快速入门与策略编写示例
目标:在已安装 CFEngine 3.27.2 的 Ubuntu 22.04 机器上(安装见《CFEngine 3.27.2 Ubuntu 22.04 安装指南》),从第一个 hello world 策略写到 files/classes/commands 综合策略,理解 promise 策略语言的核心概念,掌握 cf-promises 语法校验与 autorun 自动加载机制。全部策略与回显均在真实环境实测通过。
一、环境信息
本文所有策略与回显在以下环境实测通过,读者可按同样配置复现:
- 操作系统与内核:Ubuntu 22.04(主机名
ed7ec493a76a,IP172.17.0.25),内核 6.8.0-138-generic - 架构:x86_64(amd64)
- CFEngine 版本:3.27.2(社区版 / Community Edition)
- 常驻组件:cf-execd(每 5 分钟定时调度执行策略)、cf-serverd(策略分发服务)、cf-monitord(指标采集)
- 通信端口:TCP 5308(cf-serverd 监听)
- 工作目录:
/var/cfengine/(masterfiles/为权威源,inputs/为生效副本) - 策略文件路径:演示策略统一放在
/tmp(如/tmp/hello_policy.cf);正式策略放/var/cfengine/masterfiles/services/autorun/,主配置/var/cfengine/masterfiles/promises.cf与def.json
二、核心概念速览
写策略前先建立五个概念,CFEngine 的一切都围绕它们展开:
| 概念 | 说明 |
|---|---|
| promise(承诺) | 策略的最小单元:「我承诺让某个对象处于某种状态」。类型有 files、commands、packages、services、reports 等 |
| bundle(束) | promise 的组织与命名单元,类比「函数」:bundle agent <name> 定义,bundlesequence 决定执行顺序 |
| vars(变量) | bundle 内的变量定义区,支持 string/int/list,用 $(name) 或 ${name} 引用 |
| classes(类) | 布尔标签:表示「系统当前处于某种状态」或「某事刚发生」,决定 promise 是否执行(class:: 前缀守卫) |
| body(体) | promise 的属性修饰器,类比「参数模板」:如 create => "true"、contain => in_dir |
💡 技术拓展:CFEngine 的策略是「收敛式」的——同一个 bundle 重复执行,只有系统状态与策略期望不一致时才会产生动作,天然幂等;而 Ansible playbook 虽然模块本身幂等,但 YAML 里任何 command/shell 步骤都会无条件执行。CFEngine 的 classes(尤其 restart 之类的
statechange类)就是「仅当本承诺造成变更时才为真」的机制,为有副作用的后续动作提供了精确的触发条件。
约定:本文示例策略统一放在 /tmp,用 cf-agent -KIf <文件> 执行(-K 忽略运行锁、-I 只输出变更信息、-f 指定策略文件),与 inputs 主策略互不干扰;「部署为正式策略」见第八章。
三、第一个策略:hello world
3.1 编写
创建 /tmp/hello_policy.cf:
cat > /tmp/hello_policy.cf <<'EOF'
bundle agent main
{
reports:
"Hello, CFEngine!";
}
EOF
逐行解读:
bundle agent main:定义一个 agent 类型的 bundle,名为main。main是约定入口——不指定 bundlesequence 时 CFEngine 先执行它。reports::promise 类型区,reports 承诺「输出一条消息」,多用于调试与演示。"Hello, CFEngine!";:一条 promise,承诺语是字符串,以分号结尾。
3.2 执行
/var/cfengine/bin/cf-agent -KI -f /tmp/hello_policy.cf
实测回显:
R: Hello, CFEngine!
R: 前缀表示 report 输出。再执行一次回显完全相同、且不做任何多余动作——这就是收敛式的直接体现。
3.3 变量与内建变量
CFEngine 提供大量 sys. 内建变量(主机名、IP、内核等)。写一个使用 vars 与内建变量的策略:
cat > /tmp/valid_policy.cf <<'EOF'
bundle agent main
{
vars:
"app_port" int => "8080";
"app_name" string => "demo-app";
reports:
"policy check OK: $(app_name) listens on port $(app_port)";
}
EOF
/var/cfengine/bin/cf-agent -KI -f /tmp/valid_policy.cf
实测回显:
R: policy check OK: demo-app listens on port 8080
解读:vars: 区每行一个赋值,int => "8080" 声明整型(值仍要加引号,类型由 => 左侧的约束名决定);reports: 里用 $(app_port) 展开变量。
💡 说明:CFEngine 变量既可用
$(name)也可用${name}(两种写法等价),本文统一用$(name)。字符串内嵌变量属于「惰性求值」——引用的是运行时的最终值,嵌套引用也合法(如$($(mapvar)))。
四、语法校验:cf-promises
策略在执行前应该先做语法与语义校验,cf-promises 只检查不执行:
/var/cfengine/bin/cf-promises -f /tmp/valid_policy.cf
实测回显(校验通过时无输出):
(无输出)
echo $?
实测回显:
0
故意写一个语法错误的策略(字符串未闭合、括号不配对):
cat > /tmp/bad_policy.cf <<'EOF'
bundle agent main
{
vars:
"undefined_var_test" string => "$(does.not.exist)";
reports:
"this policy has a syntax error: unbalanced quote;
}
EOF
/var/cfengine/bin/cf-promises -f /tmp/bad_policy.cf
实测回显(输出节选,保留首尾与关键行):
/tmp/bad_policy.cf:7:6: error: syntax error
"this policy has a syntax error: unbalanced quote;
^
/tmp/bad_policy.cf:7:6: error: Expected promiser string, got '"'
"this policy has a syntax error: unbalanced quote;
^
/tmp/bad_policy.cf:7:17: error: Expected '=>', got 'policy'
"this policy has a syntax error: unbalanced quote;
^
/tmp/bad_policy.cf:8:2: error: Invalid r-value type '}'
}
^
error: There are syntax errors in policy files
echo $?
实测回显:
1
解读:现代解析器按 文件:行:列: error: 格式逐个给出错误点(实测共 10 条,上文节选 4 条),根因是第 7 行字符串未闭合导致的连锁解析错误,最后汇总 There are syntax errors in policy files,退出码 1。生产上把 cf-promises -f 挂进提交检查(pre-commit/CI),能拦住绝大多数低级语法错误。
五、files 承诺:从创建到漂移修复
files 承诺是配置管理最常用的能力。写一个「保证 /tmp/demo/marker.txt 存在且内容正确」的策略:
mkdir -p /tmp/demo
cat > /tmp/files_policy.cf <<'EOF'
bundle agent main
{
files:
"/tmp/demo/marker.txt"
create => "true",
content => "managed by CFEngine, do not edit";
}
EOF
解读:files 承诺的 promisee 对象是文件路径;create => "true" 允许创建不存在的文件,content 指定期望内容(内容不一致时整体覆盖)。
5.1 首次执行:创建文件
rm -f /tmp/demo/marker.txt
/var/cfengine/bin/cf-agent -KI -f /tmp/files_policy.cf
实测回显:
info: Created file '/tmp/demo/marker.txt', mode 0600
info: Updated file '/tmp/demo/marker.txt' with content 'managed by CFEngine, do not edit'
cat /tmp/demo/marker.txt
实测回显:
managed by CFEngine, do not edit
5.2 幂等复跑:无变更
/var/cfengine/bin/cf-agent -KI -f /tmp/files_policy.cf
实测回显(已收敛,无任何输出):
(无输出)
5.3 漂移修复:手工篡改后被自动纠正
这是配置管理的核心价值演示——人为把文件改成别的样子:
echo tampered-by-hand > /tmp/demo/marker.txt
/var/cfengine/bin/cf-agent -KI -f /tmp/files_policy.cf
实测回显:
info: Updated file '/tmp/demo/marker.txt' with content 'managed by CFEngine, do not edit'
cat /tmp/demo/marker.txt
实测回显:
managed by CFEngine, do not edit
篡改的文件在下次 agent 执行时被修复回策略期望的状态——只要策略生效,这个系统就会持续向期望状态收敛。
六、classes 与 commands:条件执行
写一个综合策略:用 classes 判断操作系统、用 commands 执行 shell 命令(demo3.cf,完整文件同时保存为实验产物):
mkdir -p /tmp/demo
cat > /tmp/demo3.cf <<'EOF'
body contain in_dir_tmp
{
chdir => "/tmp";
}
bundle agent main
{
classes:
"is_ubuntu" expression => strcmp("$(sys.os_release[ID])", "Ubuntu");
commands:
"/bin/sh -c 'date +%Y-%m-%d_%H:%M:%S > /tmp/demo/last_run.txt'"
contain => in_dir_tmp;
reports:
is_ubuntu::
"system is Ubuntu, kernel $(sys.release)";
!is_ubuntu::
"system is $(sys.os_release[NAME]), kernel $(sys.release)";
}
EOF
/var/cfengine/bin/cf-agent -KI -f /tmp/demo3.cf
实测回显:
info: Executing 'no timeout' ... '/bin/sh -c 'date +%Y-%m-%d_%H:%M:%S > /tmp/demo/last_run.txt''
info: Completed execution of '/bin/sh -c 'date +%Y-%m-%d_%H:%M:%S > /tmp/demo/last_run.txt''
R: system is Ubuntu, kernel 6.8.0-138-generic
cat /tmp/demo/last_run.txt
实测回显:
2026-10-17_00:48:57
逐段解读:
body contain in_dir_tmp:定义 body,chdir => "/tmp"让 commands 在该目录下执行——body 复用的典型场景。classes:区:expression => strcmp(...)用字符串比较函数求值,相等则is_ubuntu类成立。sys.os_release[ID]是 os-release 文件解析出的数组变量。commands:区:promise 前缀是命令路径;contain => in_dir_tmp挂上自定义 body。reports:区:is_ubuntu::是类守卫——只有该类成立时这条 promise 才参与评估;!is_ubuntu::取反(本机是 Ubuntu,所以只有第一行输出,!is_ubuntu分支静默跳过)。
💡 说明:commands 承诺默认要求命令以绝对路径开头(安全约束)。较复杂的命令行用
/bin/sh -c '...'包裹;containbody 还能控制超时、超时是否杀进程、输出是否隐藏等属性(cf-promises --body-copy可查看内置 body 定义)。
七、inventory 速览:常用内建变量
排查策略问题时最常查的是 sys. 内建变量。用一个小策略把它们打印出来(即查即用):
cat > /tmp/inv_demo.cf <<'EOF'
bundle agent main
{
reports:
"host=$(sys.host) ip=$(sys.ipv4) kernel=$(sys.release)";
"os=$(sys.os_release[NAME]) $(sys.os_release[VERSION_ID]) id=$(sys.os_release[ID])";
}
EOF
/var/cfengine/bin/cf-agent -KI -f /tmp/inv_demo.cf
实测回显:
R: host=ed7ec493a76a ip=172.17.0.25 kernel=6.8.0-138-generic
R: os=Ubuntu 22.04 id=ubuntu
解读:sys.host 主机名、sys.ipv4 主 IP、sys.release 内核版本、sys.os_release[...] 是 /etc/os-release 解析出的数组(NAME/VERSION_ID/ID 等键与文件一致)。
💡 说明:同族还有
const.(换行等常量)、mon.(cf-monitord 采集的实时指标,如mon.value_loadavg)。每个内建事实同时生成一个 hard class(硬类):把值中的特殊字符换成下划线作为类名(如sys.ipv4对应ipv4_172_17_0_25、sys.os_release[ID]=ubuntu对应ubuntu_22_04),可直接当守卫用——ubuntu_22_04::、x86_64::这类条件就来自这里。需要更系统的指标清单时,cf-agent -v的详细输出里有完整的 discovered classes 信息(用grep "hard class"过滤)。
八、部署为正式策略:inputs 与 autorun
8.1 两种部署方式
写好的策略有两种进入「正式生效」的路径:
- 修改主配置:把 bundle 加入
/var/cfengine/masterfiles/promises.cf的bundlesequence或其引用列表——侵入式,升级 Masterfiles 时可能冲突。 - autorun 机制(推荐):把策略文件放进
/var/cfengine/masterfiles/services/autorun/,MPF(Masterfiles Policy Framework)自动扫描执行该目录下的 bundle,无需改动任何主配置。
autorun 有两个前置条件(缺一不可,实测验证):
- 在
/var/cfengine/masterfiles/def.json的classes中启用services_autorun——MPF 的 bundlesequence 只有在该类成立时才合并 autorun 目录,全新安装的机器默认没有 def.json,autorun 目录整体不生效; - bundle 需要带
meta: "tags" slist => { "autorun" };声明——MPF 以此过滤要执行的 bundle。
先建 def.json(已有则合并 classes):
cat > /var/cfengine/masterfiles/def.json <<'EOF'
{
"classes": {
"services_autorun": ["any"]
}
}
EOF
chmod 600 /var/cfengine/masterfiles/def.json
再放一个 autorun bundle:
cat > /var/cfengine/masterfiles/services/autorun/hello.cf <<'EOF'
bundle agent hello_world
{
meta:
"tags" slist => { "autorun" };
reports:
"hello from autorun bundle on $(sys.host)";
}
EOF
同步到 inputs 并验证(强制同步的原理见 8.2 坑三):
/var/cfengine/bin/cf-agent -KIf /var/cfengine/inputs/update.cf --define validated_updates_ready
/var/cfengine/bin/cf-agent -KI
实测回显:
R: hello from autorun bundle on 3b46438779b5
此后每次 cf-execd 定时调度(每 5 分钟)都会执行该 bundle,输出进入 journal:
journalctl -u cf-execd --no-pager | grep hello | tail -3
实测回显:
Oct 17 00:30:14 3b46438779b5 cf-agent[1030]: CFEngine(agent) R: hello from autorun bundle on 3b46438779b5
8.2 三个实测踩坑
坑一:没有 meta tags 的 bundle 被 autorun 静默忽略。 实测 A/B 验证:在 autorun 激活的机器上放入无 meta tags 的 bundle(bundle agent tagab),强制同步后全量运行 agent,该 bundle 输出 0 次(grep 计数 0),agent 也不报任何错误;给同一 bundle 补上 "tags" slist => { "autorun" }; 再同步,立即输出 R: tag A/B test bundle。排障口诀:autorun 不生效,先查 meta tags。
坑二:def.json 未启用 services_autorun 时,autorun 目录整体失效。 在全新安装(无 def.json)的机器上实测:往 services/autorun/ 放入带 meta tags 的 bundle 并强制同步——文件确实被同步进了 inputs/services/autorun/(同步日志出现 info: Copied file ... services/autorun/dup_test.cf 行),但 agent 运行零输出、零报错,bundle 从未被执行。原因是 MPF 的 bundlesequence 以 services_autorun 类为开关(promises.cf 中 services_autorun, 与 @(services_autorun.bundles) 条目),类不存在则整个目录不进入序列。处置:按 8.1 建 def.json 即可。
坑三:masterfiles 的修改不会立即同步到 inputs。 CFEngine 用 cf_promises_validated 哨兵文件做缓存:只有当 masterfiles 的哈希变化经 update.cf 同步确认后,inputs 才会更新。手工改了 masterfiles 里的文件而没触发同步时,cf-agent -KI 跑的还是旧策略。实测确认:改完 autorun 文件先跑一次
/var/cfengine/bin/cf-agent -KIf /var/cfengine/inputs/update.cf --define validated_updates_ready
(同步阶段的回显含 info: Copied file '/var/cfengine/masterfiles/services/autorun/dup_test.cf' to '/var/cfengine/inputs/services/autorun/dup_test.cf.cfnew' (mode '600') 一类的行),再跑正式 agent 即可看到新 bundle 输出。日常不用手动干预:cf-execd 的每 5 分钟调度会自动完成这一同步,本节命令用于开发调试时即时验证。
💡 说明:
-f指定单文件运行时(本文三至七章的方式),只有该文件自己被解析,inputs 主策略与 autorun 不会介入——适合快速验证;/var/cfengine/masterfiles/def.json中定义的变量与类也只在完整上下文中可见(对照《CFEngine 3.27.2 配置参数详解》)。
九、调试速查表
| 需求 | 命令 | 说明 |
|---|---|---|
| 语法校验 | cf-promises -f <file> | 退出码 0/1 |
| 手动单次执行 | cf-agent -KIf <file> | -K 忽略锁、-I 显示变更 |
| 查看全部输出 | cf-agent -Kf <file> -v | verbose,打印评估细节 |
| 只看不跑 | cf-agent -KIf <file> -n | dry-run(-n),只报告将做什么 |
| 查看解析到的类 | `cf-agent -Kf <file> -v 2>&1 | grep -i "defined classes"` |
| 强制策略同步 | cf-agent -KIf update.cf --define validated_updates_ready | masterfiles → inputs |
| 查看执行历史 | journalctl -u cf-execd | 含全部 R: 输出与错误 |
-n(dry-run)示例——对 5.3 的篡改现场做一次只看不跑:
echo tampered-again > /tmp/demo/marker.txt
/var/cfengine/bin/cf-agent -KIf /tmp/files_policy.cf -n
实测回显:
warning: Should update file '/tmp/demo/marker.txt' with content 'managed by CFEngine, do not edit'
warning: Warnings encountered when actuating files promise '/tmp/demo/marker.txt'
文件内容保持被篡改状态,dry-run 只预报不动手。
十、参考链接
- CFEngine 策略语言官方文档(Policy Language):https://docs.cfengine.com/latest/language-promise-types.html
- Masterfiles Policy Framework(autorun 机制):https://docs.cfengine.com/latest/guide-writing-and-serving-policy.html
- 内建变量清单(Special Variables):https://docs.cfengine.com/latest/reference-special-variables.html
- 同系列文档:《CFEngine 3.27.2 Ubuntu 22.04 安装指南》《CFEngine 3.27.2 CentOS 8 安装指南》《CFEngine 3.27.2 配置参数详解》《CFEngine 3.27.2 进阶实战与运维》