第3章 CFEngine 基础(CFEngine Basics)

第3章 CFEngine 基础

在本章中,我们将更详细地研究 CFEngine 背后的基本概念,包括其理论基础,其策略语言的语法和构造以及其行为的一些独特方面。
我还将为您指出许多可用于学习和提高 CFEngine 技能的在线资源。

基本原则(Basic Principles)

CFEngine 的独特之处之一是它基于预定义的,扎实的理论和行为原则。
这些原则指导所有 CFEngine 组件及其策略语言的设计和实现,并确保这些组件的行为保持一致。
这些原则是:期望状态配置,本机操作的最小基本集合,承诺理论和收敛配置。 让我们更详细地研究它们。

期望状态配置(Desired-State Configuration)

CFEngine 与许多其他自动化机制的不同之处在于,您无需告诉它要做什么。
相反,您可以指定希望系统处于的状态,CFEngine 会自动决定要达到所需状态所要采取的措施,或尽可能接近它。
用编程语言术语来说,我们说 CFEngine 策略语言是声明性的,而不是命令性的。

这些是可以作为所需状态表达给 CFEngine 的一些示例:

  • 确保文件 /etc/ssh/sshd_config 中包含 UseDNS no
  • 确保用户 mysql 存在/不存在
  • 确保进程 httpd 正在运行/没有运行

在更高的抽象水平上,您可以封装 CFEngine 操作并表达高级期望状态:

  • 确保所有 Web 服务器都安装了 Apache
  • 确保所有 root 帐户都具有相同的中央指定密码(centrally-designated password)
  • 确保在所有 sshd 配置上禁用了 UseDNSPermitRootLogin 参数,但在应启用 PermitRootLogin 的服务器 dbsrv01dbsrv02 上除外

在更高的层次上,您可以表达如下所示的顶级期望状态:

  • 将主机 dbsrv01 配置为数据库服务器
  • 创建新的 VM 群集以用作 Web 服务器

当然,实际上事情并不是那么简单。
在某个时候,CFEngine需要知道对系统进行哪些具体更改,以及如何进行更改。

CFEngine的基本操作(Basic CFEngine Operations)

这些是 CFEngine 本机知道如何执行的一些基本操作:

  • 从系统本身提取(extract)有关其当前状态和配置的信息。
  • 检查并修改文本文件的内容。
  • 检查并操纵文件权限和所有权。
  • 检查系统中是否存在正在运行的进程。
  • 检查系统中是否存在某个用户。
  • 运行程序并检查其退出状态。
  • 处理系统上安装的软件包。

CFEngine 的商业版本具有一些其他功能,包括以下功能:

  • 检查和操作 Windows 注册表,事件日志和服务。
  • 查询和操作数据库,访问 LDAP 并与虚拟机进行交互。
  • 检查和操作支持文件访问控制列表(ACL)的系统。
  • 在多个不同平台上操纵虚拟机。

这些操作足以在系统上执行大多数配置任务。
在最低的级别上,CFEngine 包含有关如何对系统进行更改的功能规范。但是,在最高级别上,您要声明所需内容,并将详细信息留给 CFEngine。
CFEngine 附带了一些内置库,这些库使用这些基本功能执行更高级的操作,您还可以构建自己的库来执行自定义检查和活动。

承诺理论(promise theory)

CFEngine 3 在一个名为 Promise Theory 的理论模型的基础上工作。
该理论仅基于每个代理做出的行为承诺,在没有中央权限的环境中对自治代理的行为进行建模,并表明即使没有中央控制,系统也可以收敛到稳定状态。

承诺理论奠定了 CFEngine 的基本宗旨之一:自愿合作(voluntary cooperation)。

在 CFEngine 中,每个系统都是自愿参与的,仅对自己的行为做出承诺(如果您考虑一下,系统对别人的行为做出承诺是没有意义的),并且不能被迫接受任何外部的命令或信息实体。
这给 CFEngine 提供了非常强的安全性,因为这意味着不能强制主机上运行的 CFEngine 根据某些外部影响来强制更改其行为。
它可以选择这样做(例如,通过从中央服务器获取策略),但是与许多其他配置管理系统不同,CFEngine 不需要您打开一个命令通道即可通过该通道为每个主机提供指令(您可以执行此操作,这可以使服务器 ping 客户端,以便客户端在计划的时间之前运行其策略,或查询它们的策略信息,但绝不执行任意操作或命令)。

承诺只是意图的声明,是承诺者期望状态的模型。

一个承诺并不意味着在下一个迭代中(或曾经)将达到所需的状态,而是暗示了一种验证该承诺是否已得到满足的能力。
通过这种看似简单的特性,承诺理论使 CFEngine 可以处理系统管理的关键方面:操作不确定性。
系统处于不断变化中,无论是有意的(更改需求,更改软件,更改用户行为)还是意外的(断开网络连接,资源消失,软件崩溃),并且必须对它们做出反应,通常带有不完整的信息。
承诺理论允许 CFEngine 以弹性方式处理这些情况。

承诺理论最初是作为 CFEngine 行为的基础开发的(实际上,CFEngine 3 中的策略语言经过重新设计以反映该理论),但是它在计算机科学以及其他学科(例如经济学和组织学)中发现了更多的通用应用。

根据承诺理论,CFEngine 3 中的所有内容都是一个承诺,其中规定了如果承诺已被满足,承诺没有得到满足但可以被修复,承诺没有得到满足且无法被修复时该怎么做等。

表3-1显示了您可以在 CFEngine 策略中找到的承诺的一些示例,以及在不遵守承诺的情况下 CFEngine 可以自动采取的可能措施。

Promiser(承诺者) Promise to...(承诺) If not currently kept, CFEngine will...(修复操作)
A variable ...hold a certain value of a certain type.
持有特定类型的特定值
...store the appropriate value in the variable.
将适当的值存储在变量中
A file ...have certain characteristics (permissions,ownership, ACLs, etc.)
具有特定特征(权限,所有权,ACL等)
...set the desired properties on the file.
在文件上设置所需的属性
A file ...exist and to have certain content.
存在文件且拥有特定条件
...create the file if needed, modify its content (add, remove or edit lines) to match the desired state.
根据需要创建文件,修改其内容(添加,删除或编辑行)以匹配所需状态
A user account ...exist and have certain characteristics(home directory , group, etc.)
存在并具有特定特征(主目录,组等)
...create the user account with the desired characteristics.
创建具有所需特征的用户帐户
A process ...be running on the system.
正在系统上运行
...run the appropriate command to create the process.
运行适当的命令以创建 process
A shell command ...have been executed.
已经被执行
...execute the command and collect its output and exit status.
执行命令并收集其输出和退出状态
A directory on the policy hub ...provide access to its content to certain clients.
允许特定的客户访问其内容
...reconfigure its access rules to permit or block the access as desired.
重新配置其访问规则,以根据需要允许或阻止访问
A output message ...be generated when certain conditions arise, with a certain frequency and in a certain format.
当特定条件出现时,以特定频率和特定格式生成
...produce the appropriate message.
产生适当的信息

当尚未满足承诺时(例如,文件在应该存在时不存在),CFEngine 将根据其内置规则和策略中声明的任何其他承诺采取必要的措施对其进行修复。
根据系统针对给定诺言的当前状态,CFEngine 在评估诺言(evaluating a promise)时采取的操作以及这些操作的结果。

CFEngine 定义以下诺言状态:

  • 承诺保持(Promise kept
    • 系统的状态已经如承诺中所述,因此无需采取任何措施。
  • 承诺修复(Promise repaired
    • 系统状态不符合承诺的要求,因此 CFEngine 采取了适当的措施,并修复了系统状态以符合承诺的要求。
  • 修复失败(Repair failed
    • CFEngine 尝试了修复操作,但是由于某些原因(例如,缺少编辑文件的权限)而失败。
  • 拒绝修复(Repair denied
    • CFEngine 尝试了修复操作,但是由于无法访问某些资源而失败。(在当前版本的 CFEngine 中,仅当 CFEngine 无法更改文件的所有者或 BSD 标志或无法触摸文件时,才设置此状态。
  • 修复超时(Repair timeout
    • CFEngine 尝试了修复操作,但执行时间太长,CFEngine 取消了该操作。

CFEngine 策略是按照确定的顺序执行并可以与其他承诺交互的单个承诺构成的。
评估(执行)承诺后(After a promise is evaluated (executed)),您可以确定其状态并根据其采取行动,从而触发进一步的操作,例如报告(reporting),命令执行(command execution)或执行其他承诺(evaluation of other promises)。

收敛配置(convergent configuration)

CFEngine 的基本原则之一是收敛配置。
这意味着您不必在第一遍就将系统保持在所需状态。换言之,您进行增量更改,每次都更接近目标,而与系统的启动状态无关。
CFEngine 策略可能不会在第一次通过时就使系统完全配置,但至少会进行一些更改。在随后的遍历中,它将继续进行更改,最终使其尽可能接近所需状态。

收敛配置和 CFEngine 声明性的优点之一是,您无需知道系统的当前状态即可对其进行更正。
如果系统已经处于所需状态,则正确编写的 CFEngine 策略将不起作用。如果不是这样,CFEngine 将迭代进行不连续(discrete)变更以使其更接近理想状态,仅采取必要的措施来纠正现有偏差(deviation)。

为了执行收敛配置,CFEngine 对它的策略执行了三遍。 在每次遍历(pass)期间,都会执行策略中的所有承诺。
由于策略(policy)的不同组成部分之间的依赖性,可能会有一些承诺在第二或第三次通过之前无法执行,因此,多次遍历(pass)可以帮助 CFEngine 尽快将其变为收敛状态(convergent state)。

CFEngine 组件(CFEngine Components

CFEngine 安装包含多个执行不同特定功能的组件,如图 3-1 所示。

图3-1. CFEngine 组件及其关系

虚线(Dotted lines)表示执行其他组件的组件,实线(solid lines)表示组件之间的通信,粗线(bold lines)表示数据流。

让我们更详细地研究这些组件中每个组件的功能和角色。

cf-agent

正如 CFEngine 文档中所述,这是 “变更的推动者”(instigator of change)。

cf-agent 是评估策略并对其执行以对系统进行任何必要更改的程序。
cf-agent 通常是直接启动的(例如,我们已经从命令行直接运行它以测试本书中的策略),或者通过以下两个高级命令之一启动:cf-execd(作为定期启动它的机制)或 cf-serverd(以响应从其他主机执行的 cf-runagent 命令)。
请注意,根据其策略,cf-agent 可以负责(in turn,反过来)重新启动 cf-execdcf-serverdcf-monitord——如果它们因任何原因而停止(此外,cf-agent 使用 cf-promises 来验证其策略)在尝试运行它们之前)。

默认情况下,cf-agent 在被调用时将尝试运行 /var/cfengine/inputs/promises.cf(更确切地说,是通过在 $(sys.workdir)/inputs/promises.cf 中展开变量而找到的文件),除非使用 -f 命令行选项指定了不同的文件。
如果执行的文件名产生错误,则 cf-agent 将尝试运行 /var/cfengine/inputs/failsafe.cf。这个想法是使 failsafe.cf 成为准系统策略,其作用仅仅是尝试将 CFEngine 策略还原到工作状态。
通常,failsafe.cf 将尝试从策略中心更新本地策略,并且它也可能尝试启动 cf-execdcf-monitord 以至少运行最小的 CFEngine 基础结构。

cf-execd

此过程定期执行 cf-agent,收集其输出,并可能通过电子邮件将其发送到某个地方。
默认情况下,cf-execd 每五分钟运行一次 cf-agent,但是您可以使用 executor control body 来修改其行为。

例如:

body executor control
{
    any::

        splaytime  => "10";
        mailto     => "[email protected]";
        mailfrom   => "cfengine@$(sys.host).example.org";
        smtpserver => "mail.example.org";

        schedule => { "Min00_05", "Min30_35" };

}

上述代码的逻辑简述:

  • schedule 属性告诉 cf-execd 仅每 30 分钟运行一次 cf-agent(更准确地说,每当启用 Min00_05Min30_35 类时,每小时的 00-0530-35 分钟之间就是这种情况)。
  • splaytime 参数告诉 cf-execd 执行最多可能延迟 10 分钟(这在大型安装中非常有用,可以防止所有客户端一次连接到服务器)。
  • mailtomailfromsmtpserver 属性确定如何发送电子邮件报告。

cf-execd 通常是系统启动时由操作系统启动的一个进程(例如,通过 cron job)。
cf-execd 然后运行 cf-agent,这样可以通过适当的策略确保 cf-execdcf-monitordcf-serverd 在后台运行(如果需要)。

cf-serverd

该组件在 CFEngine 中实现服务器功能——监听来自客户端的连接并将文件提供给客户端的能力。
cf-serverd 还具有侦听其他主机中 cf-runagent 进程的连接的能力,并根据其配置通过在本地执行 cf-agent 进行响应。

这是您可能要在 CFEngine 客户端上运行 cf-serverd 的原因之一:如果希望策略中心能够远程指示客户端运行 cf-agent

cf-serverd 侦听端口 TCP/5308,需要打开这个端口,客户端才能与服务器进行通信。

cf-runagent

在远程主机上调用 cf-agent,以便它们执行其策略。
这是远程计算机可以在 CFEngine 中对另一计算机执行的唯一控制形式。

cf-key

这是在新主机上安装 CFEngine 时运行的第一批命令之一。
它为当前主机创建一个加密密钥对,用于与策略中心或任何其他 CFEngine 服务器进行通信时进行身份验证。

cf-monitord

cf-monitord 进程旨在在后台连续运行。它收集有关系统不同方面的统计信息,并通过特殊的 mon 变量上下文将其提供给 cf-agent

cf-monitord 收集的信息的一些示例:

  • 系统中具有活动进程的用户数:mon.value_users
  • 根磁盘分区中的可用空间:mon.value_disk free
  • 内核平均负载:mon.value_loadavg

对于大多数值,cf-monitord 还会保留移动平均值标准偏差(例如,mon.av_loadvgmon.dev_loadavg)。

cf-report

提取并报告有关 CFEngine 行为的信息,包括承诺处理统计信息(promises kept, repaired, and failed),承诺类型等。

cf-know

允许处理策略中的知识管理承诺,并从中生成知识图谱。 这是一个高级主题,我们将不在本书中介绍。

第一个例子(A First Example)

让我们考虑修改 ssh server配置的简单情况。在顶层(at the top level),我们需要确保 sshd 服务已启动并正在运行。

使用 CFEngine,我们可以简单地编写以下内容:

services:
  "ssh";

默认情况下,这将在任何操作系统上启用并确保该服务正在运行。

如果我们要确保服务未运行,我们只需要编写:

services:
  "ssh" service_policy => "stop";

【提示】
CFEngine 3.3.0 中引入了一种新的 services: promise 模型,从而可以大大简化服务管理,如上所示。
在以前的版本中,您需要通过以下两个过程的组合来操作服务:processes:commands: promise。


现在,让我们进入下一个层次(go down a level)并更改 ssh 守护程序的配置。
按照惯例,我们将使用 Shell script 执行此类任务。

Shell脚本中的以下代码段旨在向 /etc/ssh/sshd_config 中添加一行,以防止 root 用户登录:

echo "PermitRootLogin no" >> /etc/ssh/sshd_config

照原来的样子,此代码将在每次运行时向文件添加新行。它假定文件不包含该行。
当然,您可以为此添加检查,但是代码很快变得不可读:

(grep -iq 'PermitRootLogin' /etc/ssh/sshd_config ||
  echo "PermitRootLogin no" >> /etc/ssh/sshd_config) &&
  sed -i 's/^.*PermitRootLogin.*$/PermitRootLogin no/;' /etc/ssh/sshd_config

上面的代码片段(snippet)使用 grep 命令确定文件是否已包含 PermitRootLogin 字符串,并根据结果添加相应的行或使用 sed 命令编辑现有行。

与上述代码片段等效的 CFEngine 声明如下所示:

files:
  "/etc/ssh/sshd_config"
    comment => "Ensure root login is disallowed",
    edit_line => replace_or_add(".*PermitRootLogin.*", "PermitRootLogin no");

CFEngine 策略仅在该行不存在的情况下才添加该行。
此外,您可以看到 CFEngine 规则允许注释(comment)作为规则属性。可以在策略执行时使这些注释可用,从而使管理员可以更好地理解和调试 CFEngine 采取的操作。
CFEngine 中的规则可以按您希望的一样详细或高层次(high-level)。例如,您可以概括(generalize)SSH 配置文件机制并像这样表达它。

vars:
  # SSHD configuration to set
  "sshd[Protocol]"        string => "2";
  "sshd[X11Forwarding]"   string => "yes";
  "sshd[UseDNS]"          string => "no";
  "sshd[PermitRootLogin]" string => "no";

files:
  "/etc/ssh/sshd_config"
    handle    => "sshd_config",
    comment   => "Set sshd configuration",
    edit_line => set_config_values("sshd"),
    classes   => if_repaired("restart_sshd");

commands:
  restart_sshd::
    "/etc/init.d/sshd reload"
      handle  => "sshd_restart",
      comment => "Restart sshd if the configuration file was modified";

通过此 CFEngine 策略,您可以在顶部定义的 sshd 数组中(在 vars: 部分中)定义任意配置参数,通过修改或仅添加那些需要修复的参数,将由 files: 部分应用。最后,只有在进行了任何更改后,sshd 才会重新启动。
换句话说,只要通过检查它们是否已正确设置,就可以满足在文件中具有正确参数的承诺(前面所述的 Promise kept 状态),因此重新启动将仅在 Promise repaired 状态下进行。

让我们回到对承诺(promise)的描述,开始弄清楚这项政策(policy)中正在发生的事情。

  • vars: 部分只是变量声明。在这个例子中,该数组是一个由配置参数名称索引的数组,其中包含每个参数的值。
  • files: 部分中,/etc/ssh/sshd_config 文件承诺根据 set_config_values() 函数(在 CFEngine 术语中称为 bundles)中包含的规范对其内容进行编辑,并在需要修复文件时设置 restart_sshd 类(也就是说,为满足诺言而进行了修改)。
  • 最后,在 commands: 部分中,重新启动 sshd 的命令承诺仅在设置了 restart_sshd class 时才运行(如果设置了此类,则意味着文件已被修改,因此需要重新启动守护程序以使更改生效)。

此示例中未显示的是 set_config_values() bundle,它是 CFEngine 标准库的一部分,它使用 CFEngine 中的内置文件编辑原语(built-in file-editing primitives)来实际编辑文件以设置所需的参数。

CFEngine 允许您在所需的抽象级别(the level of abstraction)上表达配置策略(configuration policy),使较低级别(lower-level)的细节不可见,但在你需要它们时可用。
现在,让我们更详细地了解 CFEngine 策略的语法。

CFEngine 策略结构(CFEngine Policy Structure)

CFEngine 3 配置文件的语法非常统一,因为一切都是承诺(everything is a promise)。

通常,CFEngine 策略中的每个元素都具有以下结构:

promise_type:
  class_expression::
    "promiser" -> { "promisee1", "promiseeX" }
      attribute1 => value1,
      attributeX => valueX;
      ...
  • promise_type 可以具有的值取决于(depend on)存储 promise 的容器的类型。
    • promise_type 的值确定如何解释 promiser,哪些属性有效,以及如何使用它们的相应值。
    • 属性值可以是常量值,每个属性的允许值的类型是固定的。
  • promisee 是可选的。
    • 如果指定,则包含对依赖于当前承诺的其他承诺的引用,并用于记录。
    • 这样,我们可以指定哪些承诺会影响其他承诺。
    • CFEngine 可以生成包含此信息的报告。
    • 请注意,指定承诺不会影响策略的执行顺序,它们仅用于提供信息。
  • 如果指定了 class_expression,则可以根据表达式的值有条件地执行 promise

根据 promise 的类型,此语法的几乎所有元素(promise_typepromiser 除外)都是可选的。

例如,要无条件执行一个命令,我们只需将其声明如下:

commands:
  "/bin/ls /";

在这个例子中,commands: 是承诺类型,并指定将以下部分中的承诺者(promiser)解释为要执行的命令。
承诺者 "/bin/ls /" 表示要执行的命令。
由于未指定任何属性,因此将始终执行该命令,并且 CFEngine 将报告其输出。

CFEngine 中的数据类型和变量(Data Types and Variables in CFEngine)

CFEngine 支持不同的数据类型:

  • 标量(scalar)可以是字符串,整数或浮点数。
  • 列表(list)包含一组有序的标量。
  • 数组(array)包含由任意字符串索引的值的集合。

这些数据类型可以用作常量值,也可以存储在变量中。

变量声明(Variable declarations)

CFEngine 中的变量在 bundlevars: 部分声明(它们是 vars: 类型的承诺)。
vars: 是可以包含在任何类型的 bundle 中的公共 promise 类型之一。(还有 classes:reports: )。

vars: 承诺符合前文描述的通用结构,在这种情况下,其解释如下:

vars:
  "variable"
    type => value;

承诺者(promiser)是用引号(in quotes)引起来的变量的名称。变量的类型作为属性给出,其值指示要存储在变量中的值。

通常,为了简洁起见,您将整个声明写在一行中,如下所示:

vars:
  "name" string => "Diego";
  "year" int => "2011";
  "colors" slist => { "red", "green", "blue" };

除了类型,变量声明中唯一有效的属性是 policy,它定义是否可以修改变量。
默认情况下,CFEngine 中的所有变量都定义为常量,如果尝试将新值分配给先前定义的变量,则会收到错误消息。
通常,您无需更改变量。但是,如果将 policy 设置为 "overridable""free"(它们是同义词)后就可以这样做。

现在让我们看一下 CFEngine 中可用的不同数据类型的细节。

字符串(Strings)

CFEngine 中的字符串是使用 string 类型声明的。
字符串值必须始终用单引号或双引号(single or double quotes)引起来(它们的行为没有区别)。
如果需要在双引号字符串中包含双引号,则需要在其前面加上反斜杠(backslash)(对于单引号字符串也是一样)。
您只需将多行字符串拆分为多行即可创建多行字符串。
要引用字符串变量(和任何标量变量(scalar variable)),需要将变量名称括在括号(parentheses)或花括号(curly braces)中,并在其前面加一个美元符号(dollar sign)。

您可以简单地通过在字符串中引用变量来将变量插值到字符串中。
以下示例显示了一些字符串示例:

body common control
{
        bundlesequence => { "test" };
}

bundle agent test
{
  vars:
      "s1" string => "one";
      "s2" string => "this
is a
multine string";
      "s3" string => "with \"quotes\"";

  reports:
    cfengine::
      "s1 = $(s1)";
      "s2 = $(s2)";
      "s3 = $(s3)";
}

如果将此简短策略保存到文件中并运行它,将得到以下输出:

$ cf-agent -KI -f ./vars_string_examples.cf
R: s1 = one
R: s2 = this
is a
multine string
R: s3 = with "quotes"

注意:reports: 部分中的字符串遵循相同的规则,并且包含声明的变量的内插值(interpolated values)。

数字(Numbers)

CFEngine 支持整数和浮点数,由 intreal 类型表示。

注意:数字值(numeric values)在 CFEngine 中也以字符串形式给出,但是在将它们存储在变量中之前会对其进行有效性检查。

对于整数,CFEngine 支持后缀 kmg 代表 10 的幂(即 1000 等),前缀 KMG 代表 2 的幂(即 1024 等)。
浮点数可以通过十进制或指数形式指定。

例如:

body common control
{
        bundlesequence => { "test" };
}

bundle agent test
{
  vars:
      "i1" int => "25";
      "i2" int => "10k";
      "i3" int => "10K";
      "r1" real => "1.2";
      "r2" real => "10e-5";

  reports:
    cfengine::
      "i1 = $(i1)";
      "i2 = $(i2)";
      "i3 = $(i3)";
      "r1 = $(r1)";
      "r2 = $(r2)";
}

上述代码片段将产生以下输出:

$ cf-agent -KI -f ./vars_num_examples.cf
R: i1 = 25
R: i2 = 10000
R: i3 = 10240
R: r1 = 1.200000
R: r2 = 0.000100

列表(Lists)

CFEngine 支持任何标量类型的有序列表:字符串列表(slist),整数列表(ilist)和实数列表(rlist)。
在所有情况下,必须将值指定为字符串,但是将根据声明的类型来解释和验证它们。

您可以在不同类型的变量之间分配和存储列表,只要这些值兼容即可。这意味着您始终可以将一个 ilist 或一个 rlist 分配到一个 slist 中,但是只有在根据目标变量的类型包含有效值的情况下,才可以将 slist 分配到 ilistrlist 中。

您可以通过在变量名称之前使用符号 @at-sign)来引用列表变量。通过这样做,您可以将整个列表传递给需要列表参数的函数。您还可以将一个列表指定为另一个列表值的一部分,它将在适当位置展开。

下面的示例说明了这些要点:

body common control
{
        bundlesequence => { "test" };
}

bundle agent test
{
  vars:
      "l1" ilist => { "1", "2", "3" };
      "l2" rlist => { "1.0", "2.0", "3.0" };
      "l3" slist => { "one", "two", "three", @(l1), @(l2) };

  reports:
    cfengine::
      "l3 = $(l3)";
}

运行它时,将得到以下输出:

$ cf-agent -KI -f ./vars_list_examples.cf
R: l3 = one
R: l3 = two
R: l3 = three
R: l3 = 1
R: l3 = 2
R: l3 = 3
R: l3 = 1.0
R: l3 = 2.0
R: l3 = 3.0

@(l1)@(l2) 都在 @(l3) 内部展开,因此其最终值为:

{ "one", "two", "three", "1", "2", "3", "1.0", "2.0", "3.0" }

在此示例中,我们还通过将 @(l3) 数组称为标量 $(l3) 来使用 CFEngine 隐式循环(implicit looping)。

数组(Arrays)

数组是由字符串索引的值集(在其他编程语言中,它们通常称为哈希)。数组元素甚至可以在同一数组内包含标量,列表或其他数组。

在 CFEngine 中,将数组逐个元素声明为数组,就像它们是常规变量一样,不同之处在于数组的名称包含用方括号(brackets)括起来的索引。
没有一个步骤(single step)就可以声明整个数组的快捷方式(shortcut)。
有一些对数组进行操作的函数,例如 getindices()getvalues(),它们以字符串形式接收数组的名称作为参数。

例如,我们可以使用一个数组来存储用户帐户信息:

body common control
{
        bundlesequence => { "test" };
}

bundle agent test
{
  vars:
      "user[name]"              string => "zamboni";
      "user[fullname][first]"   string => "Diego";
      "user[fullname][last]"    string => "Zamboni";
      "user[dirs]"              slist => { "/home/zamboni",
                                           "/tmp/zamboni",
                                           "/export/home/zamboni" };

      "fields"     slist => getindices("user");
      "userfields" slist => getindices("user[fullname]");

  reports:
    cfengine::
      "user fields = $(fields)";
      "account name = $(user[name])";
      "$(userfields) name = $(user[fullname][$(userfields)])";
      "user dir = $(user[dirs])";
}

特意设计此示例以说明如何在数组中存储不同的数据类型。

注意:getindices() 将基于为用户数组声明的索引自动填充 @(fields)。 换句话说,下述语句

"fields" slist => getindices("user");

创建一个名为 fields 的变量,该变量引用从索引中提取的三个字符串的列表:namefullnamedirs
然后可以像以前一样使用该列表循环遍历用户数组。

此外,使用存储在 user[fullname] 中的数组的索引填充 @(userfields)

"userfields" slist => getindices("user[fullname]");

最后,观察到 user[dirs] 包含一个字符串列表,并且像遍历常规列表变量(例如本例中的 @(fields) 或@(userfields)`)一样。
通过遍历该列表,将其视为标量:

"user dir = $(user[dirs])";

下面是上面代码的输出:

$ cf-agent -KI -f ./vars_array_examples2.cf
R: user fields = dirs
R: user fields = name
R: user fields = fullname
R: account name = zamboni
R: last name = Zamboni
R: first name = Diego
R: user dir = /home/zamboni
R: user dir = /tmp/zamboni
R: user dir = /export/home/zamboni

类和决策(Classes and Decision Making)

类是控制流和在 CFEngine 策略中做出决策的关键。

在 CFEngine 中,类是命名属性(named attributes),可以为 true(定义了该类)或 false(未定义的类)。类可以表示有关系统的任何属性,已知(真)或未知(假)的信息,或您要在策略中指示的任何条件。

在您定义的一段时间内,它们可能是易失性的(volatile,当前 CFEngine 运行一结束,它们就会停止存在)或持久的(persistent)。
尽管 CFEngine 预定义了许多重要的类(hard classes),但是您可以根据自己的特殊需要定义其他类(soft classes)。

Hard classes

这些由 CFEngine 在运行时自动定义,主要代表关于 CFEngine 发现的系统或当前环境的信息。

hard classes 的示例包括:

  • 主机信息(Host information
    • 例如,如果正在运行 cf-agent 的计算机的主机名是 "doomsday",则将定义 class doomsday
    • 如果主机的 IP 地址是 192.168.1.2,则将定义 class IPV4_192_168_1_2
  • 时间信息(Time information
    • 例如,如果 CFEngine 在凌晨 5 点到 6 点之间运行,则将设置 class Hr5
    • 如果当前时间是每小时的 15 分钟到 20 分钟之间,则定义 class Min15_20
    • 如果是星期一,则定义 class Mon
  • 操作系统信息(Operating system information
    • 例如,将在任何 Linux 系统上设置 linux,如果 Linux 发行版为 SuSE 9,则将设置 suse_9

要查看在特定系统上预定义的 class 的完整列表,请运行以下命令:

$ cf-promises -V
CFEngine Core 3.12.1
$ cf-promises -v | grep class
 verbose: Additional hard class defined as: 64_bit
...
 verbose: BEGIN Discovered hard classes:
 verbose: C: discovered hard class 10_1_16_23
 verbose: C: discovered hard class 127_0_0_1
 verbose: C: discovered hard class 172_17_0_1
···
 verbose: C: discovered hard class Afternoon
 verbose: C: discovered hard class April
 verbose: C: discovered hard class Day23
 verbose: C: discovered hard class Friday
 verbose: C: discovered hard class Hr16
 verbose: C: discovered hard class Min45_50
 verbose: C: discovered hard class Min48
 verbose: C: discovered hard class Yr2021
 verbose: C: discovered hard class any
 verbose: C: discovered hard class cfengine
 verbose: C: discovered hard class cfengine_3
 verbose: C: discovered hard class debian
 verbose: C: discovered hard class debian_10
 verbose: C: discovered hard class debian_10_9
 verbose: C: discovered hard class dev
 verbose: C: discovered hard class dev_wumii_net
 verbose: C: discovered hard class ipv4_10
 verbose: C: discovered hard class ipv4_10_1
 verbose: C: discovered hard class ipv4_10_1_16
 verbose: C: discovered hard class ipv4_10_1_16_23
 verbose: C: discovered hard class ipv4_127
 verbose: C: discovered hard class ipv4_127_0
 verbose: C: discovered hard class ipv4_127_0_0
 verbose: C: discovered hard class ipv4_127_0_0_1
 verbose: C: discovered hard class wumii_net
 verbose: C: discovered hard class x86_64
...
 verbose: END Discovered hard classes
 verbose: BEGIN initial soft classes:
 verbose: C: added soft class specific_linux_os
 verbose: END initial soft classes

Soft classes

Soft classes 是在策略执行期间定义的。

例如,在以下情况下可以定义一个 class

  • 取决于是否存在某个文件。

在此示例中,如果内置了 /var/sitedata/devel_host.flag 文件,则使用内置的 fileexists() 函数设置 devel_host class,以执行检查:

classes:
  "devel_host" expression => fileexists("/var/sitedata/devel_host.flag");

像这样设置一个类可能会很有用。
例如,在调用某些可执行程序或其他功能之前,先确定它们是否存在于系统上,或者将不同的配置应用于系统。

  • 作为其他 class 的布尔表达式(Boolean expression)。

在此示例中,如果定义了 testhost1testhost2testhost3 中的任何一个 class,则将定义 test_host
这些可能是要在其中定义 test_host 类的计算机的主机名:

classes:
  "test_host" or => { "testhost1", "testhost2", "testhost3"};

设置这样的类可能对在某些系统上运行某些操作很有用——在这种情况下进行测试。

  • 表示(indication)已对文件进行了更改。

在此示例中,如果通过 edit_line 属性对 /etc/ssh/sshd_config 文件进行了任何更改,则 CFEngine 将认为已修复承诺,在这种情况下,将定义restart_sshd class

files:
  "/etc/ssh/sshd_config"
    edit_line => set_config_values("sshd"),
    classes => if_repaired("restart_sshd");

设置这样的类可能对跟踪系统状态很有用,并确保 CFEngine 在 next pass 时进行后续操作。

请注意,可以在 classes: 部分中明确定义类,但是如您在第三个示例中所见,它们也可以由公共 classes 属性(attribute)定义,您可以在所有promise 中使用它们,以基于 promise 的结果来设置或取消设置类。
CFEngine 标准库中预定义了几个 classes 主体,包括 if_repaired()if_ok()if_notkept()if_else()always()


类和上下文(Classes and Contexts)

术语(termclass 最常与面向对象的编程相关联。为了避免混淆,CFEngine 开发人员决定将 classes 重命名为 contexts
CFEngine 代码中尚未发生此更改,但是在阅读本文时,您可能会遇到该术语的使用。
无论如何,CFEngine 3 策略语言很可能会在很长一段时间内保持与 class 的兼容性,以避免破坏现有策略。


除了定义 class 之外,您还需要一种对它们执行操作的方法。这就是前文 “CFEngine Policy Structure” 中显示的 class_expression 的目的。
CFEngine 中的类表达式(class expression)是由类名(class names)和布尔运算符 AND(& or .),OR(|)和 NOT(!)构成的布尔表达式(boolean expression)。
括号(parenthesis)可用于对表达式的各个部分进行分组。
当一行以双冒号(double colon)结尾时,它将被评估为类表达式。 仅当类表达式为 true 时,才执行(evaluate)后面的行。

以下是有效的类表达式的示例:

linux:: # True if the linux class is defined
reboot_needed.linux:: # True if both reboot_needed and linux are defined
reboot_needed.!(linux|windows)::  # True if reboot_needed is defined and neither linux nor windows are defined
any:: # The any class is always defined, so whatever follows will always be evaluated

此外,您可以在大多数 promise 类型中使用 ifvarclass 属性,来限制对一个包含的类表达式(class expression)的 promise 的求值。
例如,以下两个承诺是等效的:

commands:
  # First command is conditioned by the ifvarclass attribute
  "/usr/sbin/shutdown -r now"
    ifvarclass => "linux";
  # Second command is conditioned by the class expression before it
  linux::
    "/usr/sbin/shutdown -r now";

ifvarclass 属性允许您在单个 promise 周围放置条件。
它具有将类表达式指定为字符串的优点,这意味着您可以在类表达式中使用变量,并且变量将在评估表达式之前进行扩展。这使您可以使用的条件类型具有很大的灵活性。

例如,您可以使用变量动态构造类名称:

body common control
{
        bundlesequence => { "test" };
}

bundle agent test
{
  vars:
      "words" slist => { "apple", "darwin", "table", "linux" };
  reports:
    cfengine::
      "Class $(words) is defined"
        ifvarclass => "$(words)";
      "Class $(words) is not defined"
        ifvarclass => "!$(words)";
}

在此示例中,report: 部分循环遍历 @(words) 列表中的所有字符串,并根据是否定义了以当前值命名的类来打印相应的消息。
请注意,第二个报告的类表达式(“未定义”)如何在开头包含 NOT 字符。

这是在 Linux 上的输出:

$ cf-agent -KI -f ./ifvarclass_examples.cf
R: Class linux is defined
R: Class apple is not defined
R: Class darwin is not defined
R: Class table is not defined

类表达式还可以用于在 classes: promise 中使用 expression 属性来定义其他 class

例如,上面显示的 test_host 类也可以这样定义:

classes:
  "test_host" expression => "testhost1|testhost2|testhost3";

类:
“ test_host”表达式=>“ testhost1 | testhost2 | testhost3”;

最后,通过使用类声明(class declaration)中的 persistence 属性,即使在 cf-agent 调用之间,也可以使类具有持久性(persistent)。它的值应为时间长度,以分钟为单位,在评估后该类应保留其值。
如果类值(class value)是耗时(time-consuming)或其他昂贵操作的结果,这对于避免每次 cf-agent 运行时都需要重新计算很有用。
请注意,将一个类设置为持久性并不意味着每次 cf-agent 运行时都不会重新评估(reevaluated)它,只是在持久性期间(persistence period)它将使用其先前的值。为了避免不必要的重新评估(reevaluation),通常的做法是使用具有相同持续时间的 “标志类”(flag class)。

例如:

body common control
{
        bundlesequence => { "test" };
}

bundle agent test
{
  classes:
    !cache_is_active::
      "line_exists"     expression => regline(".*foo.*", "/tmp/test_data.txt"),
        persistence => "1";
      "cache_is_active" expression => "any",
        persistence => "1";
  reports:
    line_exists::
      "Line exists in file";
    !line_exists::
      "Line does not exist in file";
}

在这种例子中,我们使用 cache_is_active 作为“标志类”来指示是否应重新计算 line_class 的值(可以说,这是一个计算非常昂贵的 class。我们使用 regline() 在文件 /tmp/test_data.txt 中查找包含 "foo" 的行)。
classes: 部分,仅当未定义 cache_is_active 时,我们才评估这些类。我们无条件地设置 cache_is_active(使用特殊表达式 "any"),并根据函数的结果设置 line_exists,并且两者都具有相同的持久化周期(persistence period)。这意味着在一分钟之内,无论 cf-agent 执行多少次,都不会重新评估这些类,并且将报告其缓存的值。
您可以在上一个示例中测试此行为,方法是运行它,然后编辑文件,观察直到上次执行一分钟后才检测到更改。
在部署中,这对于限制值缓慢或不频繁变化的复杂或昂贵的类表达式(complex or costly class expressions)的重新评估非常有用。

容器(Containers)

CFEngine 的 promise 可能会变得非常复杂,因此仅将它们依次(back to back)列出就不会具有很大的可扩展性。
因此,为了提高可重用性,CFEngine 将其语法元素分为两种类型的容器:bundlebody

捆绑包(Bundles)

捆绑包是最通用,最强大的分组机制(grouping mechanism)。它们是可以包含承诺的唯一元素。
一个捆绑包可以包含许多 promise,可能分成多个部分。 前文 “CFEngine 策略结构” 中描述的结构只能包含在一个 bundle 中。

捆绑包的定义如下:

bundle type name(arguments)
{
  promise_type:
    class_expression:
      promise
      ...
}

bundle 的名称是可以用来标识它的任意字符串。
bundle 的类型必须是 CFEngine 认可(CFEngine-recognized)的类型之一,并且它定义 bundle 的语义(semantics,即,如何解释其中的 promises)以及它可以包含的 promise 类型部分。
所有的 bundle 均可接收任意数量(arbitrary number)的参数。 如果不需要参数,则括号是可选的。

CFEngine 定义的 bundle 类型为:

agent

类型 agent 的捆绑包是 “可执行(executable)” 的捆绑包,可以从主 bundlesequence 声明中调用,也可以作为另一个 agent bundlemethods: 的方法调用。在这方面,可以将它们与其他编程语言中的子例程(subroutines)进行对照。
它们是捆绑软件中功能最广泛,功能最强大的一种,并且是实际上实现我们想要在系统中进行的任何更改的 bundle

agent bundle 可以包含以下 promise 类型:

  • commands::指定要执行的命令
  • files::编辑和操作文件
  • methods::调用其他代理捆绑
  • packages::查询和操作系统中的软件包
  • processes::查询和操纵正在运行的进程
  • storage::查询和配置文件系统
  • services::在类 Unix 的系统中配置系统服务

此外,CFEngine 的商业版本支持代理捆绑中的以下类型的 promise:

  • databases::操作和配置数据库
  • environments::操纵和配置虚拟环境
  • outputs::更方便地配置不同捆绑软件的日志记录级别
  • services::配置 Windows 系统服务
common

这种类型的捆绑包与 agent 捆绑包一样,但有一个特殊之处,即它们中定义的变量(variables)和类(classes)可自动用于策略中的所有其他 bundle
因此,它们是定义全局有用的变量和类的好地方

例如:

bundle common g
{
  vars:
      "localdir"    string => "/usr/local";
      "confdir"     string => "/etc";
  classes:
      "testhost"    or => { "testhost1", "testhost2" };
}

此示例使用字符串定义了两个变量,这些变量在策略的其他部分也是有效的,我们可以通过 $(g.localdir)$(g.confdir) 来引用这两个变量。
通常,任何变量都可以通过在其他任何地方添加前缀(定义所在 bundle 的名称)来访问。

还根据是否定义了 testhost1testhost2 类来定义一个类。这是为特定主机组定义类的常用方法。
此类自动设置为全局(global),这意味着它可以在任何其他包中使用。


类和变量作用域(Class and Variable Scoping

CFEngine 中的所有变量对于定义它们的包都是本地的(local)。
但是,可以通过在任何其他 bundle 中添加 bundle name (其定义的名称)前缀来访问它们,并以点(dot)分隔,例如 `$(g.localdir)。

CFEngine 中的大多数类( class)对于定义它们的包(bundle)都是本地的,并且不能从其他任何地方访问它们(没有用于指定一个类(class)的包(bundle)的机制)。有如下几个例外:

  • common bundle 中定义的类(class)是自动全局的。
  • promise 中的 classes 属性定义的类(作为其状态的结果)是自动全局的。这很有用,因为这些类通常用作跨 promisebundle 的信号机制(signaling mechanism)。

请注意,尽管这是一个普遍的误解(misconception),但 common bundle 不一定要在常规 agent bundle 之前进行评估。
您可以(并且应该)将它们放在 bundlesequence 中,以确保在正确的时刻对其进行评估(通常,您应将它们放在执行序列(execution sequence)的开头,以确保其中定义的值可用于所有其他 bundle)。


edit_line

类型为 edit_line 的捆绑包(bundle)可用于更改文件,这是 CFEngine 执行的最常见和最复杂的操作之一。
这些捆绑包必须在文件编辑(file-editing)约定(即 files: promise)中作为 edit_line 属性的值被指定。

edit_line 捆绑包本身可能非常复杂,并且包含它们自己的一组允许的诺言类型,其中包括:

  • insert_lines::向文件中添加行
  • delete_lines::从文件中删除行
  • field_edits::在文件中进行面向字段(field-oriented)的更改
  • replace_patterns::在文件服务器中进行正则表达式替换

server

server 捆绑类型控制 cf-serverd 进程的行为,该进程负责将文件提供给其他请求它们的CFEngine机器。
cf-serverd 通常在 CFEngine 策略中心(CFEngine policy hub)上运行。

这种类型的 bundle 可以包含两种承诺类型(promise types):

  • access::定义对服务器上不同资源的访问权限。
  • roles::定义哪些用户可以在服务器进程中显示(indicate)的类(以及他们可以定义哪些类),以更改 cf-serverd 守护程序(daemon)的行为。

CFEngine 强大的安全功能(strong security features)之一是远程计算机永远无法执行任意命令(arbitrary commands)。相反,他们可以执行某些 bundles
根据 roles: 诺言定义的用户,在调用这些远程 promise 时可能具有设置自定义类(custom classes)的能力,从而允许他们修改 promise 的行为,但仅在远程 bundle 及其对已定义类(the defined classes)的处理允许的情况下。

Bodies

实体,也称为复合实体(compound bodies),是属性和值的集合,可作为其他属性的值。
实体虽然可以接收参数,但不能包含 promise 也不能包含 sections
实体可以包含用于为某些属性指定不同值的类表达式(class expressions)。

实体(body)与包(bundle)一样,具有类型,该类型指示它们可以传递到的属性以及它们可以包含的属性。

复合体(compound body)的通用结构(generic structure)为:

body type name(arguments)
{
  attribute1 => value1;
  attribute2 => value2;
  …
[class_expression::]
  attributeN => valueN;
}

您将使用的一些常见的复合体包括:

control

control bodies 是特殊的容器,在任何 promise 中均未被提及,但它们控制 CFEngine 本身不同方面的行为。
控件主体(control bodies)有不同类型,具体取决于它们控制其行为的组件(component)。

您必须使用的是 common control body
此外(Among other things),在这个 common control body中,您可以使用 bundlesequence 属性指定将在策略中执行的捆绑包,并以什么顺序执行该捆绑包,并使用 inputs 属性指定要读取的其他文件:

body common control
{
  inputs => { "cfengine_stdlib.cf" };
  bundlesequence => { "test" };
}

该块(block)告诉 CFEngine 加载 cfengine_stdlib.cf 文件,并执行 test bundle

每个 CFEngine 策略都需要有一个 bundlesequence 定义,这通常是通过一个 common control body来完成的。
您也可以使用 cf-agent-b 选项来指定它,但通常仅在测试策略组件时才执行此操作。
您可能会想到,common control 支持许多其他指定 CFEngine 全局行为(global behavior)的属性。

还有一些用于特定 CFEngine 组件的 control body,包括:

  • agent control,用于指定承诺评估行为(promise-evaluation behavior)。例如,对同一承诺的连续评估(consecutive evaluations)之间的最短时间(ifelapsed),告诉 CFEngine 中止的类(abortclasses),以及许多其他方法。
  • server control,用于指定服务器行为,例如将允许连接的地址和用户(allowconnectsallowusers)以及 cf-serverd 进程应绑定到的接口(bindtointerface)。
  • 其他诸如 monitor controlrunagent controlexecutor controlknowledge controlreporter controlhub control

classes

类复合主体(classes compound bodies)根据承诺的结果(outcome)指定哪些类将被定义。
这些主体(body)是所有承诺(promise)类型的有效属性。

例如,考虑以下文件 promise

"/var/run/somefile"
  create => "true",
  classes => passfail;

在这个例子中,passfail 是类型为 classes 的复合体(compound body)的名称,需要在其他地方定义。例如:

body classes passfail
{
  promise_kept => { "fileexisted" };
  promise_repaired => { "filecreated" };
  repair_failed => { "fileerror" };
}

这里有几件事需要注意。

首先,主体部分的类型是 classes,这意味着它只能用作 promise 中的 classes 属性的值。名称 passfail 是任意标识符。
类主体类型(classes body type)的文档列出了它可以包含的属性。

在这个例子中,

  • 如果 /var/run/somefile 文件已经存在(the promise was kept),则在 promise 运行(run)后将定义 fileexisted
  • 如果 /var/run/somefile 文件不存在,并且 CFEngine 能够创建它(the promise was repaired),则将定义 filecreated
  • 如果文件由于某种原因而无法创建((the repair failed),将定义 fileerror class

这些类可以在以后用于控制其他 promise。更重要的是,passfail body可以用于许多不同的 promise 中,从而可以进行封装(encapsulation)和代码重用(code reutilization)。
需要重点注意的是,body 部分也可以具有参数(parameters),从而可以进一步自定义其行为。
例如,假设我们希望修复/保留/失败(repaired/kept/failed)的类包含任意标识符(arbitrary identifier),以帮助我们区分(differentiate)多个文件(multiple files)检查。

我们可以定义 passfail 如下所示:

body classes passfail(id)
{
  promise_kept => { "$(id)_existed" };
  promise_repaired => { "$(id)_created" };
  repair_failed => { "$(id)_error" };
}

然后,我们将文件 promise 修改为以下内容:

"/var/run/somefile"
  create => "true",
  classes => passfail("somefile");

现在,"somefile" 作为参数传递给 passfail 主体部分(body part),并用作定义的类名称(class names)的一部分。
这意味着根据承诺的结果,将定义 somefile_existedsomefile_createdsomefile_error 类,而不是我们之前使用的通用名称。

action

action 是可以在任何承诺中使用的另一个属性,它定义了 promise 应如何评估(evaluated)和修复(fixed)。

使用它,我们可以

  • 定义仅检查(checked)而不是修复(fixed)的承诺,
  • 与承诺相关(related)的动作是否应在后台发生(occur in the background),
  • 检查承诺的频率(how often promises should be checked),
  • 记录(logging)承诺的行为以及其他属性。

例如,如果 /etc/motd 中不存在某行,则以下 promise 将发出警告,并且即使 CFEngine 进行更频繁的检查,它也只会每小时发出(issue)一次警告:

bundle agent test
{
  files:
    "/etc/motd"
      edit_lines => insert_lines("Unauthorized access will be prosecuted."),
      action => warn_hourly;
}

body action warn_hourly
{
  action_policy => "warn"; # Produce warning only, don't fix anything
  ifelapsed => "60";
}

copy_from

copy_from 是只能在 files: promises 中使用的属性,它表示(indicate)从何处以及如何复制文件。

这是一个非常灵活(extremely flexible)的属性,因为它允许我们请求本地或远程文件副本(copies),如何比较文件,是否在传输过程(transit)中对文件进行加密(encrypted)以及许多其他参数。

例如,以下两个主体来自 CFEngine 的标准库(standard library):

body copy_from secure_cp(from,server)
{
  source => "$(from)";
  servers => { "$(server)" };
  compare => "digest";
  encrypt => "true";
  verify => "true";
}

body copy_from remote_cp(from,server)
{
  servers => { "$(server)" };
  source => "$(from)";
  compare => "mtime";
}

两者都处理从远程服务器(remote server)复制文件,并同时使用(take)服务器地址(a server address)和源文件(a source file)作为参数。

第一个指定将对连接(connection)进行加密(使用内部 CFEngine 机制,using an internal CFEngine mechanism),将在复制文件后对其进行验证,并通过计算其内容的加密哈希(cryptographic hash)来比较文件。
第二个比较简单,表示不需要加密或验证,并且仅使用两个文件的最后修改时间(mtimethe time of last modification)进行比较。

前一种(The former)更昂贵的验证机制(verification mechanism)使我们能够在文件的修改日期可能不是可靠的指标(reliable indicator)时,可靠地检测(reliably detect)文件中的更改。

在这两种情况下,如果文件已经匹配,则比较机制(comparison mechanism)允许 CFEngine 跳过昂贵的复制操作(expensive copy operation)。

depth_search

depth_searchfiles: promises 的另一个属性,它使我们能够控制递归操作(recursive operations)。

它指定(specify)遍历的深度(how deep to traverse),要跳过的目录(which directories to skip)以及其他参数。例如:

body depth_search recurse_ignore(d,list)
{
  depth => "$(d)";
  exclude_dirs => { @(list) };
}

此定义指定仅遍历最多(up to$(d) 层级的目录(特殊字符串 "inf" 可用于指定无限递归),并允许调用方指定要排除的目录列表。

copy_fromdepth_search 放在一起,我们已经可以创建一个功能齐全的文件复制承诺(a functional file-copy promise):

bundle agent update_inputs
{
  vars:
    "server" string => "10.1.1.1";
    "inputs" string => "/var/cfengine/masterfiles/inputs";
  files:
    "$(sys.workdir)/inputs"
      copy_from => remote_cp("$(server)", "$(inputs)"),
      depth_search => recurse_ignore("inf", { "_.*" });
}

在这里,我们将从服务器 10.1.1.1/var/cfengine/masterfiles/inputs 目录中的所有文件复制到本地 /var/cfengine/inputs 目录( $(sys.workdir) 是 CFEngine 定义为其工作目录的内部变量,通常为 /var/cfengine)。

无限深度的递归拷贝(A recursive copy of infinite depth)将被处理,但是所有以 _ 开头的目录都将被忽略。

提供的模式(the patterns provided)必须是正则表达式(regular expressions),而不是 shell 元字符表达式(metacharacter expressions),因此是 _.* 而不是 _ *

注意,不能将 depth_searchedit_line 结合使用。对于编辑文件,需要指定要编辑的明确文件(precise file)。

edit_defaults

edit_defaultsfiles: promises 的属性,控制文件编辑过程(file-editing process)的参数。您可以指定(specify):

  • 是否应对原始文件进行备份(whether backups should be made of the original file),
  • 可以编辑的合理文件的最大大小,
  • 以及是否应每次清空(emptied)并重新创建(recreated)文件。

在以下示例中,每次更改文件时都会保留带有时间戳的文件副本(timestamped copies):

bundle agent editexample
{
  files:
    "/etc/motd"
      create => "true",
      edit_line => insert_lines("Unauthorized use will be prosecuted"),
      edit_defaults => backup_timestamp;
}

body edit_defaults backup_timestamp
{
  empty_file_before_editing => "false";
  edit_backup => "timestamp";
  max_file_size => "300000";
}

edit_field

edit_fieldfield_edits: promises 的属性,在文件中执行基于字段(field-based)的编辑(必须将其指定为 field_edits: 类型的 promise 中的属性,而该类型又只能在 edit_line bundle 内部使用)。
它指定要用作分隔符的字符(what characters to use as delimiters)以及将对哪些字段执行哪些操作(which actions will be taken on which fields)。

例如,cfengine_stdlib.cf 中的以下定义使用用户提供(user-provided)的信息执行通用的字段编辑操作(performs generic field-editing operations):

body edit_field col(split,col,newval,method)
{
  field_separator => "$(split)";
  select_field => "$(col)";
  value_separator => ",";
  field_value => "$(newval)";
  field_operation => "$(method)";
  extend_fields => "true";
  allow_blank_fields => "true";
}
  • split 参数指定一个正则表达式用作分隔符(separator),因此将其分配给 field_separator 属性。
  • col 指示要在其上进行操作的列,并将其分配给 select_field
    • 默认情况下,CFEngine 从 1 开始计数。
    • 可以使用 edit_field body中的 start_fields_from_zero 属性更改此行为。
  • newval 指示要从该字段插入或删除的值。它用于 field_value,每个字段可以包含由 value_separator 分隔的多个值,在这里配置为逗号(a comma
  • method 指示要执行的操作(设置,删除,追加,前置等,set, delete, append, prepend, etc.)。

cfengine_stdlib.cf 中使用 col() body 定义来编辑用冒号分隔的文件,例如 Unix 系统中的 /etc/passwd

bundle edit_line set_user_field(user,field,val)

# Set the value of field number "field" in
# a :-field formatted file like /etc/passwd

{
  field_edits:
    "$(user):.*"
      comment => "Edit a user attribute in the password file",
      edit_field => col(":","$(field)","$(val)","set");
}

https://github.com/cfengine/masterfiles/blob/master/lib/files.cf

bundle 包含三个参数:要编辑的用户($(user)),要编辑的字段编号($(field))和要在该字段中设置的值($(val))。

field_edits: 中,promiser 被解释为与文件中所有行匹配的正则表达式,以选择要编辑的行。
在这个例子中(In this case),$(user) 参数的值用于选择以该字符串开头的行,后面跟一个冒号(a colon)和其他任何文本("$(user):.*"——模式由 CFEngine 自动锚定(anchored)到行的开头和结尾,因此不需要 ^ 字符来指定该模式在行的开头)。

选择一行后,edit_field 属性使用 col() 执行实际的字段更改操作(field-change operation),将分隔符指定为冒号,直接通过参数 $(field)$(val) 传递(passed)字段编号和新值,并且要执行的操作是 "set",它告诉 CFEngine 将字段的旧值替换为新的值。
回到上下文(To put this in context),请注意 set_user_fieldedit_line bundle,这意味着它必须用作 files: promiseedit_line属性的参数。

例如:

files:
  # Set the 7th field (shell) of user "nobody" to "/bin/false"
  "/etc/passwd"
    edit_line => set_user_field("nobody", "7", "/bin/false");

CFEngine 中还允许许多其他复合体(compound-body)属性被用于其他 promise 类型。
我在这里展示了一些最常见的示例,但是您可以在CFEngine 参考手册中找到完整的列表和详细信息。


捆绑包和实体总结(Bundles and Bodies Summary

首先,bundlebody 之间的区别可能会令人困惑。

记住以下几点可能会有所帮助(Remembering these points may help):

  • body 是被命名的属性(attributes)集合,而 bundle 则是承诺(promise)的集合。
    • promise 是在 CFEngine 中实际执行某项操作(do something)的单元。例如,
      • 运行命令(run a command
      • 或向文件中添加一行(or add a line to a file
    • 属性指定事物处理方式(how things are done)的特征。例如,
      • 是否在外壳中运行命令(whether to run the command in a shell
      • 或在文件中的什么位置添加行(or where in the file to add the line.
  • 属性的值可以是基本数据类型(string, integer, list, etc.),可以是 body 的名称,也可以是 bundle 的名称。
  • 属性值的类型是固定的,并由属性本身确定。
    • 例如, files: promisedepth_search 属性的值始终是 bodyedit_line 属性的值始终是 bundle
  • 对于 bodybundle,它们的类型始终是它们所对应的属性的名称。
    • 例如,与 depth_search 属性一起使用的 body 始终声明为 body depth_search xyz,其中 xyz 是您选择的任意名称。包也是如此(The same goes for bundles):与 edit_line 属性一起使用的 bundle 始终声明为 bundle edit_line xyz
    • 没有用作属性参数(as arguments to attributes)的 “顶级(top level)” 捆绑包只有四种类型:agentserverknowledgemonitor
  • bundle 中可能出现(appear)的承诺类型(部分)由 bundle 类型决定。
    • 例如,commands: 承诺只能出现在类型为 agentbundle 中。

常规排序(Normal Ordering)

CFEngine 没有任何流控制语句(flow control statements),至少和您熟悉(familiar)的命令式编程语言(imperative programming languages)不一样。
如果您之前进行过任何声明式编程(declarative programming,如 Prolog 语言),隐式流控制的概念(the concept of implicit flow control)可能对您来说是熟悉的) 。

CFEngine的许多行为都是硬编码的,其中包括事物评估的顺序。这称为常规排序,并且是根据对不同类型的 bundlepromise 有意义(makes sense)的内容来确定的。
例如,先创建一个文件然后删除它是没有意义的(makes no sense),而先删除它然后再创建它是没有意义的(makes sense)。
如果需要,可以通过在操作完成后定义 classes,然后基于该类定义其他操作来覆盖常规顺序。

common bundle 是定义变量和类的好地方,这些变量和类可供策略中的所有其他 bundle 访问(accessible)。common bundle 中的所有类都是全局的,并且它们中的所有变量都可以通过在其他包中添加包名称作为前缀来访问,例如 $(bundle.variable)


【提示】
CFEngine 具有检测变量/类依赖关系(variable/class dependencies)的机制以及尽力而为算法(best-effort algorithm),以确保在对表达式或promise求值之前,所有必需的值均可用。
您可以通过在 bundlesequence 声明中包含 common 类型的 bundle 来帮助其确保一致性(consistency)和收敛性(convergence),即使这不是严格需要的。


对于 agent bundle,CFEngine 将最多执行三次,以尝试达到收敛状态。
在每次迭代(iteration)中,bundle 中的部分将按以下顺序执行(标有 * 的部分仅在 CFEngine 的商业版本中可用):

1. vars
2. classes
3. outputs *
4. interfaces
5. files
6. packages
7. environments *
8. methods
9. processes
10. services
11. commands
12. storage
13. databases *
14. reports

edit_line bundle 中,将保持以下顺序:

1. vars
2. classes
3. delete_lines
4. field_edits
5. insert_lines
6. replace_patterns
7. reports

在每个部分中,承诺将按照它们在策略中出现的顺序执行。
每个 bundle 可以多次执行意味着——您可以定义一个变量(variable),然后根据该变量定义一个类(class),然后根据该类定义其他变量。

可能不会在所有情况下都执行三个迭代——如果在迭代过程中没有修复任何承诺(there are no promises repaired),则 CFEngine 会认为 bundle 已收敛(has converged),并停止进一步(further)的迭代。

server bundle 中,常规排序如下所示:

1. vars
2. classes
3. access
4. roles

常规排序为 CFEngine 策略的执行提供了相当严格的结构(fairly rigid structure)。
当您首次开始编写CFEngine策略时,特别是如果您熟悉命令式编程,通常会尝试 “反抗(fight)” 常规排序以适配(fit)您的需求。
当您确定需要更改常规顺序时,建议(encourage)您备份并在更高级别上重新考虑要完成的任务(rethink at a higher level the task you want to accomplish)。大多数时候,您会发现以其他方式构造任务将使重新排序操作的需求将会消失(go away),并且实际上对 CFEngine 的 “思考(thinks)” 方式更有意义(make more sense)。

CFEngine 的循环(Looping in CFEngine)

“在 CFEngine 中进行思考(thinking in CFEngine)” 最明显(evident)的例子之一就是隐式循环的概念(the concept of implicit looping)。

这是最基本(most basic)的行为之一,对于 CFEngine 初学者(beginner)来说是最令人困惑(most confusing)的行为之一,而一旦使用(harness)它,它便是最强大(most powerful)的行为之一。

首先,让我们定义它:
在 CFEngine 3 中,如果您将列表变量(list variable@(var))通过标量(scalar$(var))引用,则 CFEngine 会将其解释为 “迭代列表中的所有值(iterate over all the values in the list)”。

尝试一下(Let’s try it),输入(type in)下面的 policy

body common control
{
  bundlesequence => { "test" };
}

bundle agent test
{
  vars:
    "colors" slist => { "red", "green", "blue" };
  reports:
    cfengine::
      "$(colors)";
}

运行结果如下所示:

$ cf-agent -KI -f ./looping1.cf
R: red
R: green
R: blue

"R:" 开头的行表示 reports: promise 产生的消息。
您可以看到 reports: 部分中的单个承诺(single promise)已针对列表中的每个值重复执行,因此将打印所有值。

您也可以尝试嵌套循环(nested looping):

bundle agent test
{
  vars:
    "colors" slist => { "red", "green", "blue" };
    "tone" slist => { "dark", "light" };
  reports:
    cfengine_3::
      "$(tone) $(colors)";
}

这将返回以下内容:

$ cf-agent -K -f ./looping2.cf
R: dark red
R: light red
R: dark green
R: light green
R: dark blue
R: light blue

很简单(simple enough),不是吗?在这个明确的(explicit)示例中,行为很明显(clear)。

当您意识到隐式循环可以用于任何类型的 promise 时,其真正的威力就将显现出来,这意味着整个承诺将被执行与列表中的项目一样多的次数。
另外,循环变量可以在任何地方使用——在定义变量或类(defining variables or classes),执行命令(executing commands)或使用类做出决策(making decisions with classes)时。

让我们看一个真实的例子(look at a real example),其中隐式循环节省了一天的时间(saved the day)。
顺便说一句(incidentally),这是我开始使用 CFEngine 3 时,在我脑海中真正 “被触动”(really “clicked” in my head)的时间。

我需要确定(determine)在某个网段中配置了系统中的哪个网络接口,才能应用一些配置命令。
CFEngine 具有一个名为 sys.ipv4 的内置数组变量(built-in array variable),该变量包含系统中所有网络接口的 IP 地址,并按接口名称索引。
我的第一个想法(first thought)是我需要一个函数,该函数可以给我存储在该数组中的所有值,因此我可以将它们与所需的 IP 地址范围进行比较,以找到所需的值。
令我惊讶的是(To my surprise),我意识到 CFEngine 具有 getindices() 函数,但是没有等效的 getvalues() 函数(实际上,此函数是从 3.1.5 版本开始添加的,但是当我提出此解决方案时,该函数不可用)。
在解决了很多问题之后,我意识到在这种情况下不需要使用 getvalues() 函数。

这是我想出的代码:

body common control
{
  bundlesequence => { "find_netif" };
}

bundle agent find_netif
{
  vars:
    "nics" slist => getindices("sys.ipv4"); #(1)
    # Regex we want to match on the IP address
    "ipregex" string => "192\.168\.1\..*";

  classes:
    "ismatch_$(nics)" expression => regcmp("$(ipregex)", "$(sys.ipv4[$(nics)])"); #(2)

  reports:
    cfengine::
      "NICs found: $(nics) ($(sys.ipv4[$(nics)]))"; #(3)

      "Matched NIC: $(nics) ($(sys.ipv4[$(nics)]))"
        ifvarclass => "ismatch_$(nics)";
}

让我们详细看一下(Let us look at this in detail)。

(1)首先,我们获得系统中所有网络接口的列表(使用 getindices()),并将其存储在 nics 变量中。我们还将要匹配的 IP 地址范围的正则表达式(192.168.1.*)分配给 ipregex 变量。

(2)然后,我们在 classes: promise 中通过标量引用(referenced as a scalar)使用此列表,通过在类名称本身中使用 $(nics) 来定义以每个接口命名的多个类。该类的定义取决于该网络接口的 IP 地址(在对 regcmp() 方法 的调用中再次使用 $(nics) )是否与我要查找的 IP 地址的正则表达式匹配。结果是,对于系统上的每个 NIC,如果其 IP 地址匹配,则定义相应的类;如果不匹配,则定义未定义的类。。

(3)最后,我们在报告消息(report message)中使用 $(nics) 打印所有接口,并且我们还通过使用属性 ifvarclass => "ismatch_$(nics)" 调节第二条消息来仅打印匹配的消息。ifvar class 属性中对 $(nics) 的引用也依次扩展到每个值,因此,仅针对定义了相应类别的那些 NIC 打印第二条消息。

如此看来(as you see),我们根本不需要 getvalues() 函数。
在此示例中(In this example),我使用了已定义的类来打印消息,但是在我的实际示例(real example)中,我使用它们将适当的配置语句附加到文件中,但仅用于那些与我想要的IP范围匹配的接口。

我鼓励(encourage)您再次查看该示例,并确保您理解它。

任何地方都没有循环结构(looping constructs)——实际上,它们在 CFEngine 语法中根本(at all)不存在。
可能需要一段时间才能习惯(It may take a while getting used to this.)。

每当您构建策略时,您都会认为 “我绝对需要一个 while 循环(a while loop)来执行此操作”,请退后一步(take a step back),看看是否可以使用隐式循环来重现问题。
使用隐式循环基于条件(based on a condition)的类的定义是一项强大的技术(a powerful technique),您将在本书的许多示例中看到它的使用。

在 CFEngine 中思考(Thinking in CFEngine)

如我们所见(As we have seen),CFEngine 在其操作的许多方面都采用了刚性结构(rigid structure)。
两个主要的例子(prime example)是常规排序(normal ordering)隐式循环(implicit looping,这有助于摆脱(get rid)对显式控制流语句(explicit control flow statements)的需求。

在大多数情况下(For the most part),您不会告诉 CFEngine 如何做事。
相反(rather),您告诉它要实现的目标,并写出(write out)如何实现某些承诺的低级构建块(low-level building blocks),CFEngine 会将它们组合在一起,以使系统达到所需的状态(to bring the system to the desired state)。

如果您像我一样,那么您在遇到CFEngine之前已经进行了一段时间的编程,并且您的大脑以某种方式思考问题和任务(your brain is wired to think about problems and tasks in a certain way)。
当您必须 “放开(let go)” 控制权并将其让给 CFEngine 时,这几乎不可避免(inevitably)地会导致冲突(cause a clash)。
我个人发现对我有用的是从当前任务(the task at hand)的细节中 “退后一步(step back)”,并在更高层次上思考:“我要实现什么目标?”(think at a higher level: “what am I trying to achieve?”

通常,这会为您为什么要执行某些操作以及如何实现这些目标提供不同的观点(a different perspective)。
我的主要建议(main advice)是持续练习(keep practicing),并使用可用的社区资源来研究示例,并从更有经验的用户那里获得有关您的诺言的反馈(get feedback)。

客户端和服务器(Clients and Servers)

自治(autonomy)是 CFEngine 的主要优势(key strengths)之一。

安装并配置了 CFEngine 的计算机不需要网络连接即可运行(operate),并且只要(as long as)其策略定义正确,它将继续遵守(obey)这些策略并按照配置维护系统(maintain the system as configured)。
例如(For instance),有时连接到公司网络但又经常不在的便携式计算机将继续从其上运行的 CFEngine 中受益。

然而,CFEngine 的真正威力(the true power)在于(lie in)它可以轻松管理数千台计算机的能力,为此,您需要将相应的策略分配(distribute)给所有这些主机。
幸运的是,CFEngine 让设置客户端服务器环境(set up a clientserver environment)变得非常容易,其中一个或多个主机充当(act)策略中心(policy hubs),将策略和数据分发给其他主机。

只需要一个命令即可配置 CFEngine 并告诉它将哪台机器用作其策略中心(policy hub):

cf-agent --bootstrap --policy-server x.y.z.w

此命令在策略中心(policy hub)本身及其客户端上均有效。在策略中心中,cf-agent 将识别其自己的 IP 地址,并将主机配置为策略中心。

以最简单的形式,并且非常适合除规模最大的组织之外的所有组织,您可以有一个策略中心(a single policy hub),其中有多个客户端从中提取策略和文件,如图 3-2 所示。

图3-2. CFEngine 分布式部署的最简单形式,具有单个策略中心和多个客户端

在更大,更复杂的环境中,您可以拥有更复杂的结构。
CFEngine 策略中心本身可以成为某些其他中心的客户端,从而创建CFEngine策略分发点的层次(hierarchy)结构,如图 3-3 所示。


图3-3. 更复杂的 CFEngine 分布式部署,在层次结构中具有多个策略中心

对层次结构的需求可以由技术需求(technical requirements)或管理需求(administrative needs)决定。

  • 技术需求:例如,地理位置分散的网站,它们之间的低带宽链接,流量阻塞。
  • 管理需求:例如,负责(in charge of)不同位置(different locations)的不同团队需要对顶级继承策略(top-level inherited policies)进行自定义。

CFEngine 足够灵活(flexible),可以容纳(accomodate)其中的任何一个。
理想情况下(Ideally),所有策略文件都应从顶层传播(propagate),并在主存储库(a master repository)中进行维护(maintained),但是您的组织(organization)中也可能有几棵不相交的树(several disjoint trees)。

CFEngine 遵循(follows)严格的 “仅拉” 原则(strict pull-only philosophy):只有客户端可以向服务器发出请求,要求其提供所需的信息和文件;服务器无法将任何内容推送到客户端。

only the client can make requests to the server, asking for the information and files it needs.
The server cannot push anything onto the clients.

该约定(convention)使配置网络以允许CFEngine客户端和服务器之间的通信(communication)非常简单。
客户端连接到服务器,仅需要(is necessary)一个端口(TCP/5308)即可连接到服务器。所有通信,包括文件传输(file transfers),都通过此端口进行(take place)。


【提示】
可以在服务器上使用 cf-runagent 命令来联系(contact)客户端。

在这种情况下,还需要从服务器到客户端打开 TCP/5308 端口(客户端监听此端口),并且需要在客户端上运行 cf-serverd 来处理这些连接(connection)。

但是请注意,cf-runagent 不允许服务器在客户端上执行任意命令(arbitrary commands)。

它所做的(All that it does)只是通知(instruct)客户端上的 cf-agent “唤醒(wake up)” 并立即处理其策略(process its policies),而不是等待下一次定时运行(scheduled run)。

我们将在后文的 “使用 cf-runagent 进行 CFEngine 远程执行”中 看到此配置的详细信息。


只允许客户端从服务器中 pull(而不是服务器将事物 push 到客户端上)的决定也植根于诺言理论(rooted in promise theory)。

实体(entity)不能(cannot)对自己以外的任何人作出承诺(make promises)。因此,分布式系统的操作(the operation of a distributed system)不能依赖于(depend on)一个实体强迫(forcing)其他实体执行某项操作。

实体可能会要求其他人提供信息(request information),但它们可能仅对自己的行为做出承诺(make promises)。

自愿合作(Voluntary cooperation)是 CFEngine 的核心原则(core principles)之一。

另一方面(On the other hand),CFEngine 在设计时考虑(in mind)了弹性(resilience)和优雅降级(graceful degradation)。
如果客户端与网络断开连接(becomes disconnected),CFEngine 会继续使用最新的本地存储策略版本(the latest locally-stored version)来管理它,直到恢复连接(restores connectivity)为止。

这样,客户端可以在网络中断(network outages),网络拥塞(network congestion),安全事件(security incidents)或其他可能阻止与主策略中心(the master policy hub)连接的情况(circumstance)下继续工作。

CFEngine 服务器配置(CFEngine Server Configuration)

CFEngine 服务器功能由 cf-serverd 进程提供。

使用服务器控制主体块(server control body block)进行配置,如以下示例所示。
该示例取自(taken from)CFEngine 的默认配置(default configuration)——这是引导(bootstrap) CFEngine 时生成的 /var/cfengine/inputs/promises.cf 文件的一部分):

body server control
{
  denybadclocks => "false";
  allowconnects => { "127.0.0.1" , "::1", @(def.acl) };
  allowallconnects => { "127.0.0.1" , "::1", @(def.acl) };
  trustkeysfrom => { "127.0.0.1" , "::1", @(def.acl) };

  skipverify => { ".*$(def.domain)", "127.0.0.1" , "::1", @(def.acl) };

  allowusers => { "root" };
  maxconnections => "100";

  # Uncomment the line below to allow remote users to run
  # cf-agent through cf-runagent
  # cfruncommand => "$(sys.cf_agent)";
}

【提示】
引导客户端(bootstrap a client)时,CFEngine 生成的默认策略文件是一个很好的起点(an excellent starting point),并提供了许多基本功能(provide a lot of basic functionality)。
强烈建议(highly recommended)您仔细阅读它们(go over them),并根据自己的特定需求(particular needs)和基础结构(infrastructure)进行自定义(customize)。


该主体(body server control)定义了:

  • allowconnects:允许进行连接的机器组(the set of machines
  • allowallconnects:允许同时连接多次的机器组。(通常,每个主机一次仅允许一个连接,Normally each host is allowed only one connection at a time
  • trustkeysfrom:公钥(public keys)将受到信任的机器组
  • skipverify:DNS 记录不会被检查一致性(not be checked for consistency)的机器组
  • maxconnections:指定最大同时连接数(the maximum number of simultaneous connections
  • allowusers:被允许连接的用户
  • denybadclocks:时钟不同步(out-of-sync clocks)的机器是否将被阻止(`blocked)。

此外,可以在 access_rules() bundle 中定义每个目录(per-directory)和每个文件(per-file)的 ACL。
其默认版本包含以下内容:

bundle server access_rules()
{
  access:
    any::
      "$(def.dir_masterfiles)"
        handle => "server_access_rule_grant_access_policy",
        comment => "Grant access to the policy updates",
        admit => { ".*$(def.domain)", @(def.acl) };
  roles:
}

bundle 包含类型为 access:promise,用于定义要应用的 ACL。
在这种情况下,目录 $(def.dir_masterfiles)(默认扩展为 /var/cfengine/masterfiles)将由 $(def.domain) 域(domain)以及在 @(def.acl) 列表中明确定义的所有计算机都可以访问(will be accessible)。

请注意,这些参数大多数都包含 @(def.acl)$(def.domain) 变量。这是对 def() bundle 中定义的 acldomain 变量的引用。
它们包含在同一个文件中:

bundle common def
{
vars:
  "domain" string => "example.com",  # (1)
    comment => "Define a global domain for all hosts",
    handle => "common_def_vars_domain";

  "acl" slist => {
                          "$(sys.policy_hub)/16" # (2)
  },
    comment => "Define an acl for the machines to be granted accesses",
    handle => "common_def_vars_acl";

  "dir_masterfiles" string => translatepath("$(sys.workdir)/masterfiles"),
    comment => "Define masterfiles path",
    handle => "common_def_vars_dir_masterfiles";
}

您应该在将 CFEngine 投入生产(putting CFEngine into production)之前编辑 def() bundle,尤其是两个值:

  • (1)$(domain) 变量必须包含当前环境的域名(domain name)。
    • 在前面显示的代码中使用了该域名来限制对该域中计算机的访问。
    • 还用于默认的 promises.cf 文件中的其他上下文(some other contexts)中。
  • (2)@(acl) 变量是一个列表,其中包含应该可以访问服务器的所有 IP 地址。
    • 此承诺使用 $(sys.policy_hub) 的值(一个自动设置的变量,其中包含引导主机时 hub 的 IP 地址)并确定其本地 B 类网络(/16local class-B network)。
    • 假设在大多数情况下,策略中心(policy hub)将与客户端位于同一网络中。
    • 当然,根据您的需要,该范围可能太宽(too broad)或太窄(too broad),因此您必须进行相应的编辑。

从服务器更新客户端文件(Updating Client Files from the Server)

策略中心的主要任务(main tasks)之一是向其客户端分发策略文件(distribute policy files)以及任何其他必要的文件。
这是至关重要的操作(a crucial operation),因为它可以通过更新 policy hub 上的文件并使它们自动传播到所有客户端。
为此,CFEngine提供了足够的功能(ample capabilities)来进行有效且安全的文件传输(efficient and secure file transfer)。

与 CFEngine 一起安装的默认策略包含一个名为 update()bundle,该 bundle 在客户端上执行(请记住,CFEngine 策略中心无法指示客户端执行任何操作)。它会自动处理所有这些任务,但是您可能需要根据需要对其进行修改。它包含在文件 /var/cfengine/inputs/ failsafe.cf 中。

简而言之(in a nutshell),这些是它执行的任务:

  • 检查主机密钥文件是否存在(在 /var/cfengine/ppkeys/ 目录中),如果不存在,则运行 cf-key 创建它们。
  • 如果 cf-serverdcf-monitordcf-execd 进程未运行,则启动它们。
  • 从策略中心上的 /var/cfengine/masterfiles/ 复制更新后的文件到所有机器(客户端和策略中心)上的 /var/cfengine/inputs/ 上,以使其投入生产(put them in production)。
  • 将更新后的 CFEngine 二进制文件从 /var/cfengine/bin/ 复制到 /usr/local/sbin/
  • 确保所有关键目录和文件(all critical directories and files)都具有正确的权限(correct permissions)。

目前,我们仅关注文件复制操作(file-copying operations),但我建议您通读整个 bundle 以了解其功能(get an idea of what it does)。
这些是关键部分(crucial parts):

bundle agent update
{
  vars:
    "inputs_dir" string => translatepath("$(sys.workdir)/inputs"), # (1)
      comment => "Directory containing Cfengine policies",
      handle => "update_vars_inputs_dir";
    "master_location" string => "/var/cfengine/masterfiles",  # (2)
      comment => "The master cfengine policy directory on the policy host",
      handle => "update_vars_master_location";

  files:
    "$(inputs_dir)"  # (3)
      comment => "Copy policy updates from master source on policy server",
      handle => "update_files_inputs_dir",
      copy_from => u_rcp("$(master_location)","$(sys.policy_hub)"),  # (4)
      depth_search => u_recurse("inf"),  # (5)
      file_select => u_input_files,  # (6)
      classes => u_if_repaired("update_report");  # (7)
}
  • (1)$(inputs_dir) 变量包含本地安装的 CFEngine 寻找策略文件的目录。
    • Unix/Linux 主机上,这通常是 /var/cfengine/inputs,但是不同平台上可能是不同的路径。
    • 因此,我们使用 $(sys.workdir) 变量,该变量自动定义为本地 CFEngine 安装的基本目录(base directory)。
    • 我们还使用了 translatepath() 函数将 Unix 样式的路径转换为本地样式(例如,在 Windows 上使用反斜杠(backslashes)而不是正斜杠(slashes))。
  • (2)$(master_location) 变量包含策略中心上 “主文件(master files)” 所在的目录,并将它们从该目录复制到本地主机。
    • 策略中心必须是 Unix 风格的主机,因此在这种情况下,我们不需要执行任何路径转换。
  • (3)files: promise 是完成实际工作的那个。
    • 承诺者(promiser)是目标目录 $(inputs_dir),文件将根据随后的 promise 属性指定的参数进行复制。
  • (4)copy_from 属性指示文件的来源。
    • 此属性的值是复合主体(compound body),也是在 failsafe.cf 文件中定义,如下所示:
body copy_from u_rcp(from,server)
{
    source => "$(from)";
    compare => "digest";
    trustkey => "true";

  !am_policy_hub::
    servers => { "$(server)" };
}

body 作为参数(arguments)接收应从中复制文件的目录和主机。$(master_location) 是之前定义的变量,$(sys.policy_hub) 是特殊的 CFEngine 变量,该变量在引导客户端时设置。
另外,它指示应使用加密摘要(compare => "digest")对文件进行比较,并且客户端应信任服务器提供的加密密钥(trustkey => "true")。
仅当未设置 am_policy_hub 类时才设置 servers 属性,并且 am_policy_hub是仅在策略中心上设置的硬类。因此,在策略中心上,文件复制操作将在本地进行(the file copy operation will be done locally),从 /var/cfengine/masterfiles//var/cfengine/inputs/

  • (5)depth_search 属性用于指示无限深度的递归文件复制操作。它的值是另一个复合体(compound body):
body depth_search u_recurse(d)
{
  depth => "$(d)";
  exclude_dirs => { "\.svn" };
}

depth 属性设置为传递的参数(可以是数字,也可以是无限递归的特殊值 "inf")。
exclude_dirs 属性还用于跳过服务器(假设使用 Subversion 完成版本控制)中可能存在的版本控制目录。

  • (6)file_select 属性用于控制要复制的文件类型。这是另一个复合体:
body file_select u_input_files
{
  leaf_name => { ".*.cf",".*.dat",".*.txt" };
  file_result => "leaf_name";
}

在这个例子中,我们要求 CFEngine 仅复制名称以 .cf.dat.txt 结尾的文件。
file_select 主体中可以指定许多条件,因此,我们需要使用 file_result 属性来告诉 CFEngine 我们要匹配哪些条件。

  • (7)classes 属性指示如果复制了任何文件(which flags the promise as “repaired”,将 promise 标记为“repaired”),则应设置 update_report 类。

可以在策略的其他部分中使用该类,以在更新文件时执行任何必要的操作(例如,生成报告)。
这是设置合适的类的另一个复合体:

body classes u_if_repaired(x)
{
  promise_repaired => { "$(x)" };
}

文件复制承诺(File-copy promises)非常灵活和强大。
策略和二进制文件更新由内置的 CFEngine 策略自动处理,但是我建议您阅读 files: promise 的文档,从而很好地了解他们可以执行的各种任务。


为什么将 bundle agent update 放在 failsafe.cf 文件而不是 promises.cf 文件中?

默认的 CFEngine 策略指示 cf-execd(在 promises.cf 中的 body executor control 中定义)在运行 promises.cf 之前始终评估 failsafe.cf,以确保正确更新所有文件。
此外,如果策略评估失败,则 cf-agent 会自动尝试加载 failsafe.cf,该文件必须设计为独立运行,并执行使 CFEngine 重新投入运行所需的任何修复任务(reparation tasks)。


使用 cf-runagent 进行 CFEngine 远程执行(CFEngine Remote Execution Using cf-runagent)

CFEngine 的基本前提(basic premises)之一是客户自主运行。

如果有一个中央协调点(central coordination point)——如策略中心(``),则由客户端决定连接到中心并获取策略或文件。但是,实际上,服务器(或其他计算机)有时需要 ping 客户端并要求他们做一些事情。
这就是 cf-runagent 出现的地方。它不允许执行任意操作(arbitrary actions),而只是要求远程计算机运行 cf-agent 并评估其策略。

远程主机(remote host)——在大多数情况下,它是 CFEngine 客户端,需要运行 cf-serverd 并配置为侦听 cf-runagent 的连接。

请允许我强调这一点(Allow me to emphasize this point):cf-runagent 不允许在远程主机上执行任意命令或任意操作。它只是指示主机运行 cf-agent 并开始评估其策略。

当您不想等到下一次常规执行 cf-agent(例如关键策略(critical policy)或操作系统更新)时,此功能很有用。

cf-runagent 命令的行为可以在 runagent control body 中配置为 CFEngine 策略的一部分,除了其他功能外,它还可以指定运行命令时默认情况下将联系的主机列表。
在客户端(cf-runagent 命令将连接到的那一侧)上,server control body 指定是否允许 cf-runagent 连接以及他们将被允许做的事情。
它还可以指定运行 cf-runagent 时将允许设置自定义类的远程用户列表。这样可以对策略行为进行更细粒度的控制。

cf-runagent 连接由 cf-serverd 处理,因此,如果需要此功能,则还需要打开从服务器到客户端的端口 TCP/5308 通信(traffic)。

由于其潜在的安全隐患(potential security implications),因此 CFEngine 默认策略中禁用了 cf-runagent 功能。
要启用它(To enable it),您需要在 server control body 中取消注释 cfruncommand属性,如前文的 “CFEngine 服务器配置” 所示:

cfruncommand => "$(sys.cf_agent)";

这指示 cf-serverd 侦听来自 cf-runagent 的连接,并响应它们执行 cfagent(请记住,这是 cf-runagent 可以做的所有事情:唤醒 cf-agent)。

我们仍然需要指示 cf-serverd 允许从策略中心访问 cf-agent 二进制文件(其路径存储在特殊变量 $(sys.cf_agent) 中,通常为 /var/cfengine/bin/cf-agent)。
我们需要在 access_rules() bundle 中执行此操作:

bundle server access_rules()
{
  access:
    any::
...
  "$(sys.cf_agent)"
    handle => "grant_access_policy_agent",
    comment => "Grant access to the agent (for cf-runagent)",
    admit => { "$(sys.policy_hub)" };
}

在这种情况下,我们告诉 cf-serverd 仅允许访问策略中心的 cf-agent 二进制文件,这由特殊变量 $(sys.policy_hub) 定义。

最后,我们需要告诉策略中心在执行 cf-runagent 时默认与哪些主机进行联系。
我们需要在 runagent control bod 中明确地执行此操作:

body runagent control
{
    # A list of hosts to contact when using cf-runagent
  any::
    hosts => { "127.0.0.1" };

    # , "myhost.example.com:5308", ...
}

请注意,定义此列表并不是绝对必要的,因为可以在运行 cf-runagent 时使用 --hail 选项在命令行中指定主机列表。

参考

  • https://cf-learn.info/home/
  • The Code of Learning CFEngine 3
  • CFEngine Standard Library
  • All Promise and Body Types
  • Bodies
  • ifelapsed
  • Normal Ordering
  • Functions

你可能感兴趣的:(第3章 CFEngine 基础(CFEngine Basics))