CFEngine 3.27.2 快速入门与策略编写示例

CFEngine 3.27.2 社区版快速入门:策略语言核心概念(bundle/promise/class/variable)讲解,从第一个 hello world 到 files/classes/commands 实战策略从 0 写到执行,含 cf-promises 语法校验、autorun 自动加载机制与三大踩坑实录

CFEngineCFEngineLinux自动化配置管理2026/10/17

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,IP 172.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 '...' 包裹;contain body 还能控制超时、超时是否杀进程、输出是否隐藏等属性(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 两种部署方式

写好的策略有两种进入「正式生效」的路径:

  1. 修改主配置:把 bundle 加入 /var/cfengine/masterfiles/promises.cf 的 bundlesequence 或其引用列表——侵入式,升级 Masterfiles 时可能冲突。
  2. 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> -vverbose,打印评估细节
只看不跑cf-agent -KIf <file> -ndry-run(-n),只报告将做什么
查看解析到的类`cf-agent -Kf <file> -v 2>&1grep -i "defined classes"`
强制策略同步cf-agent -KIf update.cf --define validated_updates_readymasterfiles → 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 只预报不动手。

十、参考链接