第4章 使用 CFEngine(Using CFEngine)

现在,我们将探索如何使用 CFEngine 执行一些常见的配置任务。
在此过程中,我们将遇到 CFEngine 语言的更高级的概念和结构。

初始系统配置(Initial System Configuration)

安装系统后,在宣布可以使用之前,需要执行许多例行任务(routine tasks)。

这些包括基本软件包的安装(base software packages),网络配置(network configuration),文件系统配置(file system configuration),用户创建(user creation),身份验证配置(authentication configuration)以及系统组件的配置(configuration of system components)。

CFEngine 可以一致且可预测地完成所有这些任务。

在本节中,我们将逐步构建一个 CFEngine 策略,该策略从一个入口点开始编辑许多配置文件。 在此过程中,我将向您展示一些用于传递和处理参数(passing and processing parameters)的常用技术,以及一些新的 CFEngine 构造和概念(constructs and concepts)。

编辑 /etc/sysctl.conf(Editing /etc/sysctl.conf)

在新的 Linux 系统中,通常需要配置的一个文件是 /etc/sysctl.conf。该文件包含一些用于控制系统行为(system behavior)的不同方面(different aspects)的内核参数(kernel parameters)的配置值。
例如,它可能包含以下几行:

net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.log_martians = 1

这些特定参数控制内核(net.ipv4)中网络堆栈(networking stack)的行为。

我们可以使用 CFEngine 确保 /etc/sysctl.conf 文件中存在这些参数。
我们将通过(walk through)一个示例来演示此功能(demonstrates this ability,),同时还显示 CFEngine 策略可以操作的不同级别(different levels):

  • (1)在最高级别(At the highest level),策略只是说 “配置 /etc/sysctl.conf 文件(configure the /etc/sysctl.conf file)”。该策略的这一部分是一个构建基块(building block),可以在管理级别(management level)将其添加或删除到安装(installation)中,而不必担心其实现方式。
  • (2)下一级别(The next level down)说 “在 /etc/sysctl.conf 文件中设置这些值”。可以在系统管理员(sysadmin)决定需要启用哪些选项的情况下更改此设置,而无需考虑文件的语法。
  • (3)下一级别(The next level)说明(explains)文件的结构以及应如何设置参数。它从本质上(essentially)提取实现细节,而这些细节与您选择的选项无关。
  • (4)最低级别(The lowest level)说明(explains)了如何在文件中执行字段编辑,应如何处理类以及其他实现细节。

具体代码如下所示:

bundle agent configfiles # (1)
{
  vars:
    # Files to edit
    "files[sysctl]" string => "/etc/sysctl.conf"; # (2)

    # Sysctl variables to set
    "sysctl[net.ipv4.tcp_syncookies]" string => "1"; # (3)
    "sysctl[net.ipv4.conf.all.accept_source_route]" string => "0";
    "sysctl[net.ipv4.conf.all.accept_redirects]" string => "0";
    "sysctl[net.ipv4.conf.all.rp_filter]" string => "1";
    "sysctl[net.ipv4.conf.all.log_martians]" string => "1";

  methods: # (4)
    "sysctl" usebundle => edit_sysctl,
      comment => "Configure $(files[sysctl])";
}

bundle agent edit_sysctl
{
  files: # (5)
    "$(configfiles.files[sysctl])"
      handle => "edit_sysctl",
      comment => "Make sure sysctl.conf contains desired configuration settings",
      create => "true",
      edit_line => set_variable_values("configfiles.sysctl"), # (6)
      classes => if_repaired("sysctl_modified"); # (7)

  commands: # (8)
    sysctl_modified.!no_restarts::
      "/sbin/sysctl -p"
        handle => "reload_sysctl",
        comment => "Make sure new sysctl settings are loaded";
}

简短的 CFEngine 策略确保 /etc/sysctl.conf 文件中存在适当的行,以设置所需的参数。
如果参数已经存在但具有不同的值,则策略将对其进行修复。如果参数未出现在文件中,则策略将添加它。
让我们按部分剖析该示例。

  • (1)configfiles() agent bundle 是一个 “driver” bundle,它调用其他人来实际执行工作(请参见后面的 methods: 部分)。这使我们可以添加从同一驱动程序包(the same driver bundle)中调用的更多任务,就像我们在后面的部分中所做的一样。
  • (2)在 configfiles() 中,我们首先在 vars: 部分中定义一些变量。
    • 在其中,我们定义了一个称为文件的数组,还指定一个由字符串 sysctl 索引的元素,其中包含要编辑的文件的路径。
    • 我们将在本策略的后面部分引用该数组,并在本章中将向其添加更多元素,以保存要编辑的不同文件的文件名。
    • 为了引用该数组,我们将使用名称 configfiles.files 来指示 configfiles() bundle 中的文件数组。

请记住,CFEngine 中的数组是由任意字符串索引的(indexed by arbitrary strings)。

  • (3)我们还定义了一个名为 sysctl 的数组。

    • 我使用与上面定义 的 files中元素相同的名称,只是因为它们都引用相同的文件,但是我可以使用任何名称,
    • 该数组按参数名称索引,并包含为每个参数设置的值。
    • 这是我们将用于在 CFEngine 中传递键/值对(passing key/value pairs)的一种常用技术(common technique),因为它使我们能够简洁地定义配置文件,用户和许多其他参数的值。
    • 请注意,我们在自己的行上定义了数组的每个元素,每个元素均由要在 /etc/sysctl.conf 中设置的参数的名称索引,并包含为其设置的值作为其值。
    • 我们将元素定义为字符串,以使其具有通用性并能够包含任何类型的值。
    • 要从其他包中引用此数组(To refer to this array from other bundles),我们将使用其全名 configfiles.sysctl 来标识在其定义位置的 bundle
  • (4)在 configfiles() 中设置变量后,我们包括一个 methods: 部分,该部分允许我们指定要依次(in sequence)调用的多个 bundle

    • 在此示例中,我们仅具有对 edit_sysctl() 的调用,该调用完成了编辑文件的工作。 (我们稍后将对 edit_sysctl() 进行说明)
    • 每个方法调用(method call)都有一个任意标识符(an arbitrary identifier)。
    • 在此示例中,我们使用标识符 "sysctl" 将其作为在 /etc/sysctl.conf 上执行编辑的顺序(sequence)的一部分。
    • 在本章的后面,我们会将调用添加到执行不同配置任务(perform different configuration tasks)的其他 bundle 中。
    • 我们还指定了注释属性来表达此承诺的更高层次的意图。

【提示】
使用 methods: promises 抽象出较低级别的包(to abstract lower-level bundles)是在 CFEngine 策略中传达较高级别意图(communicating higher-level intentions)的一种好方法。
无需分心(without distraction)实际的实现细节(actual implementation details)。


  • (5)edit_sysctl() bundle 是从 methods: 部分调用的,它包含指定系统所需状态的 promise

    • 这个 bundlefiles: 部分开头,其中 promiser 是要编辑的文件。
    • 我们使用 configfiles.files 数组的 sysctl 元素作为文件名,这是 configfiles() bundle 中定义的文件数组,为我们提供了值 "/etc/sysctl.conf"
    • 我们提供的 handlecomment 属性对配置活动没有任何帮助,但在所有承诺中均建议使用,因为它们在观察日志输出或使用 CFEngine 的知识管理工具生成文档时会为您提供极大的帮助。
    • create 属性指定如果文件不存在(如果未设置自定义参数,则安装后可能会立即出现这种情况),则应创建该文件。
  • (6)接下来是实际完成工作的部分,它非常简单。

    • edit_line 属性使用包含我们要设置的值的数组名称调用 set_variable_values() bundle
    • 我们不传递数组本身,而是传递其名称,并且将在 set_variable_values() 中解引用(dereferenced)此名称以查找实际的数组。
    • 您可能已经意识到set_variable_values()捆绑包非常重要,因为它实际上是执行文件编辑工作的捆绑包。这不是内置命令,而是包含在 CFEngine 标准库中,该库存储在 cfengine_stdlib.cf 中。我们待会儿再讲。

set_variable_values in lib/files.cf

  • (7)classes 属性告诉 CFEngine,如果诺言已得到修复(if the promise is repaired),则应设置 sysctl_modified 类。
    • if_repaired body 也在 cfengine_stdlib.cf 中定义。

等一会儿。我所说的修复是什么意思?(What do I mean by repaired

对于 CFEngine,当评估承诺时需要采取任何行动时,这些行动会导致达到承诺的期望状态。此时可以说 a promise is repaired
例如,如果文件已具有所需的配置值,则 CFEngine 将不需要对其进行编辑,并且不会将其标记为已修复(repaired)。在这种情况下,CFEngine 会将承诺视为 “保留(kept)”。

另一方面,如果不存在任何参数,但诺言将其添加,则诺言将被标记为 “已修复(repaired)”。promises 的所有可能的结束状态(end states)在 classes 属性的文档中进行了描述。

我们可以根据需要自由执行此捆绑包多次,CFEngine 只会在需要时进行更改。
这就是 CFEngine 允许我们执行配置收敛(convergent configuration)的本质(nature)。


  • (8)如果文件不包含所有配置参数,并且 CFEngine 添加了其中的任何一个(从而 “修复(repairing)” 文件的状态),则将设置 sysctl_modified 类,这要归功于我们在配置中看到的 if_repaired body部分。
    • 这很有用,因为修改文件后,我们必须发出 /sbin/sysctl -p 命令来指示系统重新加载值并使它们立即生效。
    • 因此,在 commands: 部分中,您可以看到我们正在发出此命令。
    • 该命令前面有一个类表达式:
sysctl_modified.!no_restarts::

这是一个布尔表达式(boolean expression),其中的点(dot)表示 AND(也可以使用 &),感叹号(exclamation mark)表示 NOT。(在此示例中未使用的竖线或管道字符(avertical bar pipe character)表示 OR

在这种特殊情况下,仅当设置了 sysctl_modified 类(也就是说,修改了 /etc/sysctl.conf 文件)并且未设置 no_restarts 类时,才会执行 /sbin/sysctl -p 命令。
这种结构允许我们通过定义 no_restarts 类(例如,我们可以通过在执行策略时为 cf-agent 提供 -Dno_restarts 命令行选项来做到这一点)来更改配置文件,而无需执行任何重新启动或重新配置命令。

至此结束了对策略的高级描述(high-level description of the policy),如您所见,该描述以相当容易理解的方式描述了我们想要实现的目标。

总的来说,我们的配置文件在顶层(at the top level)定义了两个 bundleconfigfiles()edit_sysctl()

  • configfiles() bundle 提供了策略的入口点,定义了我们要编辑的文件和想要它们具有的内容,并调用了 edit_sysctl() bundle
  • edit_sysctl() bundle 反过来执行我们要在 /etc/sysctl.conf 文件中执行的编辑操作。

现在,我们将更深入地研究(delve deeper into)实现细节。
首先,让我们回到 set_variable_values() bundle,因为它是如此重要。如果打开 cfengine_stdlib.cf,则将找到其定义:

bundle edit_line set_variable_values(v) # (1)
{
  vars:
    "index" slist => getindices("$(v)"); # (2)
    "cindex[$(index)]" string => canonify("$(index)"); # (3)

  field_edits: # (4)
    "\s*$(index)\s*=.*" # (5)
      edit_field => col("=","2","$($(v)[$(index)])","set"), # (6)
      classes => if_ok("$(cindex[$(index)])_in_file"), # (7)
      comment => "Match a line starting like key = something";

  insert_lines: # (8)
    "$(index)=$($(v)[$(index)])",
      comment => "Insert a variable definition",
      ifvarclass => "!$(cindex[$(index)])_in_file";
}

请记住,CFEngine 中的 bundle 等同于其他编程语言中的函数(subroutines)——它们是独立的单元,可以包含大多数不同的 promise 类型,因此使我们能够封装功能(encapsulate functionality)。

现在,我们将对此 set_variable_values() bundle 进行剖析,以了解其如何发挥其魔力(to see how it performs its magic)。

  • (1)该捆绑软件将数组的名称 v 作为其参数。

    • 在我们当前的示例中,将使用带有值 configfiles.sysctlv 来调用该捆绑包。其中索引是参数名称,数组中的值是参数需要设置的值。
    • 因此,v 提供了以下指令:编辑格式为 name = value 的文件,修改已存在的参数的值,并添加尚不存在的参数的值。
  • (2)首先,我们获得所有参数的列表,并将其存储在名为 index 的列表变量中。

    • 使用传递的数组上的内置 CFEngine 函数 getindices() 完成此操作,该函数返回数组中索引的列表。
    • 请注意,getindices() 也接收将在其上运行的数组的名称,因此我们可以简单地将 $(v) 参数传递给它。
    • 在 CFEngine 中,您可以在变量名称前后使用大括号(braces)或括号(parenthesis)。它们是等效的,因此 ${v} 的含义相同。
  • (3)接下来,我们生成规范参数名称(canonified parameter names)的数组。

    • 在 CFEngine 中,规范化字符串是可以随时用作类名(class name)的字符串。
    • 由于某些字符在 CFEngine 类名中无效,因此 CFEngine 函数 canonify() 允许我们采用任意字符串并从中删除无效字符。
    • 我们将这些规范化值存储在名为 cindex 的数组中,该数组由真实参数名称索引,因此我们可以将它们与它们的规范化版本相关联。
    • 我们使用 CFEngine 的隐式循环来填充整个数组。

CFEngine 中的隐式循环(Implicit Looping in CFEngine)

尽管我们已经在第3章的 “在 CFEngine 中进行循环(Looping in CFEngine” 中介绍了隐式循环。

让我们详细查看此变量赋值中发生的事情,以刷新您的记忆(to refresh your memory)。
如果将列表变量引用为标量(使用 $ 前缀而不是 @),则 CFEngine 自动循环遍历列表中的所有值,依次替换每个元素。
因此,通过以标量(用 $(index) 替代 @(index))访问索引数组,我们告诉 CFEngine 对数组的每个元素执行一次相应的语句。

实际上,下面的行:

"cindex[$(index)]" string => canonify("$(index)");

将对 @(index) 的每个元素重复执行,其中 $(index) 依次获取每个值。
这样就可以创建一个逐个元素的数组,该数组由参数名称索引,并包含每个名称的规范化版本作为值。


  • (4)在文件上执行编辑的下一步是更新文件中已经存在的参数的值。

    • 为此(for this),我们使用 field_edits: 部分,该部分还使用隐式循环(implicit looping)为每个参数应用编辑承诺。
  • (5)field_edits: promise 从一个正则表达式开始,该正则表达式选择文件中需要应用编辑的行。

    • 在这种情况下,我们要编辑以当前参数名称($(index))开头,由可选空格(\s*),后跟等号(=)和任意字符串( 我们不在乎现有的值,因为我们将用新的价值代替它)。

    重点注意:field_edits: promise 中,CFEngine 自动将给定的正则表达式锚定到行的开头和结尾,因此我们提供的正则表达式需要匹配整行

    再次注意(notice again),由于 CFEngine 的隐式循环,将针对存储在 configfiles.sysctl 数组中的每个参数执行一次整个承诺。
    在我们的示例中,数组包含 5 个元素,因此 field_edits: promise 将被评估 5 次,其中 $(index) 遍历以下值:

  • net.ipv4.tcp_syncookies

  • net.ipv4.conf.all.accept_source_route

  • net.ipv4.conf.all.accept_redirects

  • net.ipv4.conf.all.rp_filter

  • net.ipv4.conf.all.log_martians

    有了一个承诺,并且没有任何明确的流程控制指令,CFEngine 允许我们将所有编辑应用于整个文件。
    在此示例中,我们在 field_edits: 部分中只有一个 promise,但是如果我们想对文件应用不同类型的基于字段的编辑,则可以有多个 promise

  • (6)如果文件中的任何行与正则表达式匹配(即包含给定参数的定义),我们将对它们应用 promiseedit_field 属性定义的更改。

    • 为此,我们使用标准库中的另一个定义,即 col(),该定义允许基于字段的通用文件编辑。
    • 在这种情况下,col() 的参数告诉它使用 = 作为字段分隔符,并将行的第二个字段设置为表达式 "$($(v)[$(index)])" 所给的值。 ”。

    这里有一些可变插值魔术(variable interpolation magic)。字符串中的变量值由 CFEngine 从内向外(from the inside out)扩展。

    首先,扩展 $(v) 的值,因此在我们的示例中,字符串现在将理解(read)为 $(configfiles.sysctl[$(index)])
    接下来,将在每个参数值上自动迭代 $(index) 的值。例如,对于 net.ipv4.tcp_syncookies 参数,它将扩展为 $(configfiles.sysctl[net.ipv4.tcp_syncookies])
    现在,它看起来像 CFEngine 中的常规变量引用,它将为我们提供要为给定参数设置的值,在这种情况下为字符串 "1"

字符串中的变量值由 CFEngine 从内向外(from the inside out)扩展。

  • (7)如果 promise 是可以的(promise is ok)(在 CFEngine 中,这意味着承诺已经满足(satisfied),或者不满足但已修复(repaired)),则 classes 属性将设置 "$(cindex[$(index)])_in_file" 类。

    例如,如果文件中已经存在参数 net.ipv4.tcp_syncookies,则将设置 net_ipv4_tcp_syncookies_in_file 类。这是参数名称的规范化版本,与字符串 _in_file 串联。
    请记住,在 类和决策(Classes and Decision Making) 中,CFEngine 中的类是已设置或未设置(set or unset)的标识符,它们使我们能够执行布尔决策(perform Boolean decisions)。
    在这种情况下,我们设置的类将包含文件中已经存在的所有参数的名称,无论它们的值是否正确。这些类的存在表明,对于那些特定的参数,没有更多的工作要做。

  • (8)如果在文件中找不到参数,则需要添加该参数,此任务由捆绑包的 insert_lines: 部分执行。

    • 在这种情况下,承诺者是我们要插入的行,形式为 parameter = value,仅当 ifvarclass 属性给出的类表达式为 true 时,才承诺将其插入文件中。
    • 在这种情况下,ifvarclass 的的值是文件中已经存在参数时 field_edits: promise 定义的类的取反(!)。
    • 如果未定义该类(这意味着在文件中找不到该参数),则 ifvarclass 表达式的计算结果为 true,并将插入缺少的行。

    例如,假设文件中不存在 net.ipv4.conf.all.log_martians 参数。
    然后,field_edits: promise 将失败(因为没有匹配正则表达式的行会搜索以参数名开头的行),因此不会设 net_ipv4_conf_all_log_martians_in_file 类。
    当执行 insert_lines: promise 时,类表达式 !$(cindex[$(index)])_in_file 的值(扩展为字符串 !net_ipv4_conf_all_log_martians_in_file)为 true,表示需要插入该行。

    您将在 CFEngine 策略中经常注意到这种行为模式:进行一些检查和修复,根据结果设置某些类,然后根据这些类的存在触发其他操作。
    乍一看似乎很复杂(It seems convoluted at first),但它具有很大的灵活性,特别是允许策略趋同(convergent,即收敛配置),除非必要,否则不进行更改。

    我必须注意,CFEngine 中的 insert_lines: promise 非常聪明。
    特别是,他们将不会插入文件中已经存在的行(这是承诺理论基础的结果,如果该行已在文件中,则承诺已经收敛到所需状态,因此无需再次插入),因此原则上我们不需要设置类,然后在其上限制行的插入。
    在这种特殊情况下,使用该类可以使我们解决诸如间隔差异(例如等号周围的空格)之类的问题,而无条件的 insert_lines: promise 则不会考虑这些差异。

为了完成这一讨论,我们将在实现链中再下一层(go down one more level),讨论三个低层次主体(low-level body)部分 if_repaired()if_ok()col()
这些都不是原生(native)CFEngine 函数,而是在标准库中定义如下:

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

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

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";
}

【提示】
通常,您不必担心标准库中捆绑软件(bundles in the standard library)的实现细节,就像您不必担心 C 标准库或 Perl CPAN 模块中的实现细节一样。
我们在这里研究细节,是您有机会学习更多关于 CFEngine 策略语言(policy language)以及它运行的所有不同层次(different levels at which it operates)的机会。


if_repairedif_ok() 都是类的主体(classes body)部分,这意味着它们可用作 classes 属性的值。几乎所有 CFEngine promise 都允许使用此属性,并根据 promise 的结果定义要设置的类。

这里显示的两个示例应该是不言自明的(self-explanatory)。

  • if_repaired() 中,我们指定仅当 promise 修复后(repaired,即,为了使 promise 达到期望值而必须进行一些更改时),才定义名称为参数 $(x) 的类。
  • if_ok() 中,我们指定将在修复或保留(either repaired or keptpromise 时定义该类(这已经是正确的)。

在这种情况下,我们将指定字段分隔符,选择用于编辑的字段,其值以及要执行的操作。
至此,我们的解释就完成了。

我想提醒您,即使在这个简单的示例中,也存在不同的抽象级别:

  • (1)在最高级别(At the highest level)——configfiles() bundle,该策略只说 “配置 /etc/sysctl.conf 文件(configure the /etc/sysctl.conf file)”。
  • (2)下一级别(The next level)——edit_sysctl bundle 说:“在 /etc/sysctl.conf 文件中设置这些值。”
  • (3)下一级别(The next level)——set_variable_values() bundle:说明了文件的结构以及应如何设置参数。
  • (4)最低级别(The lowest level)——col()if_ok()if_repaired():说明了如何在文件中执行字段编辑,应如何处理类以及其他实现细节。

CFEngine 的优点在于,您仅需要在当前(at the moment)所需的抽象级别上进行工作。实际上,不同级别的人员可以在每个级别进行操作。
政策制定者(A policy maker)可以将需求设置为最高级别(实际上甚至高于此处显示的级别),并且系统管理员(system administrators)和 CFEngine 管理员都可以根据需要在较低级别上进行操作。

编辑 /etc/sshd_config(Editing /etc/sshd_config)

初始安装系统时的另一项常见任务是配置某些服务,SSH(Secure Shell)是特别有用的服务,而 OpenSSH 是最受欢迎(the most popular)的 SSH 实现之一。
默认情况下,OpenSSH 守护程序附带了相当可用的配置,但是您可能仍希望对其进行更改以使其更加安全或遵守本地策略(adhere to local policies)。

在上一节中了解了如何编辑 /etc/sysctl.conf 之后,您应该已经开始了解如何执行此配置。
就我们的示例而言,假设我们要从 OpenSSH 安装中的默认配置中修改 /etc/ssh/sshd_config 中的以下参数:

#Protocol 1,2
#X11Forwarding no
#UseDNS yes

在 OpenSSH 中,默认情况下,大多数配置参数都会显示为注释掉,并显示其默认值。
我们想将这些参数修改为以下内容:

Protocol 2
X11Forwarding yes
UseDNS no

也就是说,我们要取消注释相应的行,并将其值修改为所需的值。
如果所需参数的行尚不存在,则要将其添加到配置文件中。

考虑到这一点,我们可以将之前的顶级(top-levelconfigfiles() bundle 重写为以下内容:

bundle agent configfiles
{
  vars:
    # Files to edit
    "files[sysctl]" string => "/etc/sysctl.conf";
    "files[sshd]" string => "/etc/ssh/sshd_config";

    # Sysctl variables to set
    "sysctl[net.ipv4.tcp_syncookies]" string => "1";
    "sysctl[net.ipv4.conf.all.accept_source_route]" string => "0";
    "sysctl[net.ipv4.conf.all.accept_redirects]" string => "0";
    "sysctl[net.ipv4.conf.all.rp_filter]" string => "1";
    "sysctl[net.ipv4.conf.all.log_martians]" string => "1";

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

  methods:
    "sysctl" usebundle => edit_sysctl;
    "sshd" usebundle => edit_sshd;
}

您可以看到我们在文件数组 files 中添加了第二个元素:files[sshd],其中包含 /etc/ssh/sshd_config 文件的路径。
我们还添加了一个名为 sshd 的新数组,其中包含我们要在配置文件中设置的参数。
最后,在 method: 部分中,我们添加了对 edit_sshd() bundle 的调用,该包执行必要的编辑。

再次注意,CFEngine 在指定要做的事情(what to do,我们要设置的参数值)和如何做的事情(how to do itmethods: 调用及其各自的实现)之间非常清楚地分开了。

这是新的 edit_sshd() bundle

bundle agent edit_sshd
{
  files:
    "$(configfiles.files[sshd])"
      handle => "edit_sshd",
      comment => "Set desired sshd_config parameters",
      edit_line => set_config_values("configfiles.sshd"),
      classes => if_repaired("restart_sshd");

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

edit_sshd() 捆绑包与 edit_sysctl() 非常相似。
不同之处在于,我们不使用 set_variable_values() 捆绑包来编辑文件(用于设置 variable=value 形式的行),而是使用 set_config_values() 捆绑包,它用于设置 variable value 形式的行,其附加功能是如果注释行中已经存在的行自动取消注释。

edit_sshd() 捆绑包还具有一个 command: 部分,如果更改了配置文件,则该部分用于重新启动 sshd 守护程序。
和以前一样,如果修复了文件编辑承诺(file-editing promise was repaired)(即,如果对文件进行了任何更改),那么我们将设置 restart_sshd 类,并根据该类发出必要的命令。

现在让我们看一下 set_config_values() bundle,它也在标准库中定义。

bundle edit_line set_config_values(v)
{
  vars:
    "index" slist => getindices("$(v)"); # (1)
    "cindex[$(index)]" string => canonify("$(index)");

  replace_patterns: # (2)
    "^\s*($(index)\s+(?!$($(v)[$(index)])).*|# ?$(index)\s+.*)$"
      replace_with => value("$(index) $($(v)[$(index)])"), # (3)
      classes => always("replace_attempted_$(cindex[$(index)])"); # (4)

  insert_lines:
    "$(index) $($(v)[$(index)])" # (5)
      ifvarclass => "replace_attempted_$(cindex[$(index)])";
}

该捆绑软件使用与 set_variable_values() 完全不同的逻辑,即使它执行类似的功能。这使我向您介绍(introduce)一些新概念和技巧(a couple of new concepts and tricks)。

  • (1)捆绑软件的第一部分已经很熟悉:它从传递给捆绑软件的数组中获取索引列表,将其存储在 index 变量中,并使用它的参数名称的规范化版本(canonified versions)填充 cindex,稍后在类名称中使用。
  • (2)现在,实际的行编辑是通过 replace_patterns: 部分而不是 field_edits: 来完成的,这样可以进行更灵活的转换。这种类型的承诺使我们可以搜索和替换文件中的正则表达式。
    replace_patterns: promise 中的承诺者是我们要匹配的正则表达式。
括号太多,通过颜色区分

在这种情况下,我们要求它查找两种类型的行,它们与用竖线字符(a pipe character|)分隔的两个正则表达式相对应:

    1. 以可选空格(\s*)开头(^)的行,后跟(followed by)当前参数名称($(index)),后跟(followed by)空格(\s+)和不是当前参数正确值的任何字符串( (?!$($(v)[$(index)])).*)。这表示已经设置了我们要寻找的参数但值不正确的行。
    1. 行(^)以可选的空格(\s*)开头,后跟注释字符和可选的空格(# ?),后跟当前参数名称($(index)),后跟空格(\s+)以及任何任意字符串(any arbitrary string)。这表示包含参数但已注释掉的行。

同样,我们使用隐式循环来迭代要设置的所有参数,方法是在 promise 中使用 $(index) 而不是 @(index)

第一个正则表达式的最后一部分很复杂,因为我们需要查找尚未包含正确值的行,并替换它们。为此,我们使用负向前查找表达式(?!...)a negative-lookahead expression),表示空格后的文本必须与所需的值((?!$($(v)[$(index)])))不匹配。
最后一部分(.*)必须匹配空格后的实际字符,因为整个负超前表达式的长度为零(whole negative-lookahead expression is zero-length),并且在正则表达式求值过程中不会 “消费(consume)” 任何字符。

replace_with 属性告诉我们使用什么作为替换(replacement)。在这种情况下,替换为当前参数及其所需的值,并用空格分隔:

replace_with => value("$(index) $($(v)[$(index)])"),

value() 是另一个复合主体(compound body),用于指定替换文本的值和特征(characteristics)。
它在标准库中定义:

body replace_with value(x)
{
  replace_value => "$(x)";
  occurrences => "all";
}
  • (4)由于我稍后会解释的原因(For reasons I will explain in a moment),我们要记住 replace_patterns: promise 无论(whether or not)它是否实际找到其模式,它都已经运行。

因此,通过使用带有 always() body 部分的 classes 属性来设置 replace_attempted_parameter 类来结束。
cfengine_stdlib.cf 中也可以找到 always() body 部分的定义:

body classes always(x)
{
  promise_repaired => { "$(x)" };
  promise_kept => { "$(x)" };
  repair_failed => { "$(x)" };
  repair_denied => { "$(x)" };
  repair_timeout => { "$(x)" };
}

使用 always() 的作用是针对其中列出的任何条件(promise_repairedpromise_keptrepair_failedrepair_deniedrepair_timeout)都设置了作为参数给出的类。这些是 CFEngine 中的 promise 的所有可能结果,因此最终结果是不管发生什么都设置类。

  • (5)到目前为止(up to this point),我们已经处理了行中已经存在的参数(可能已被注释掉),但是我们还需要插入尚未出现在文件中的参数。

    • 如何做到这一点(How to do this)有些棘手(a little tricky)和违反直觉(counter-intuitive),但它使我们有机会了解更多有关 CFEngine 如何工作的信息。

    正如我们在的 “常规排序” 中所看到的,CFEngine 策略中的 promise 部分以称为常规排序的硬编码序列(a hard-coded sequence known as normal ordering)执行。

    根据常规排序,insert_lines: 部分在 replace_patterns: 部分之前执行。
    这在我们当前的示例中造成了一个问题,因为我们想在添加任何新行之前尝试修复已经存在的参数(可能已注释掉或具有错误的值)。
    如果我们先执行 insert_lines: promise,那么最终可能会在配置文件中得到重复的参数定义。


edit_line bundle 中的常规排序(Normal Ordering in edit_line Bundles)

edit_line 包中,这些部分(sections)按以下顺序最多执行 3 次(up to three times):

vars
classes
delete_lines
field_edits
insert_lines
replace_patterns
reports

为了更改执行顺序,我们以 replace_patterns: promise 时定义的 replace_attempted_parameter 类的存在为条件来限制的执行对 insert_lines: promise 进行评估。

由于 CFEngine 最多可以完成三遍承诺(up to three passes over the promises),这使得 insert_lines: promise 只在第二遍(second pass)执行,replace_patterns: 部分有机会在第一遍(first pass)取消注释并更正任何现有行。如果此时仍然不存在具有正确值的行,则插入它是正确的行为。

我知道这可能会造成混淆,因此这里有一个示例来说明这一点。假设我们的 /etc/ssh/sshd_config 文件包含以下行:

#Protocol 1,2

set_config_values() 捆绑软件的行为如下(假设 $(index) 当前具有值 "Protocol"):

  • (1)First pass——因为未定义 replace_attempted_protocol 类,所以未执行 insert_lines:"Protocol 2" 的承诺。请注意,类名称包含参数名称的规范版本(the canonified version),包括将其全部变为小写形式(lowercase)。
  • (2)First pass——replace_patterns: promise 用其未注释(uncommented)的正确值替换原始行,并定义了 replace_attempted_protocol 类:
Protocol 2
  • (3)Second pass——insert_lines: promise 现在执行(now execute),但是由于文件中已经存在正确的行,因此不会再次插入。

现在考虑文件中根本没有注释掉(commented-out)的 “协议” 行的情况。然后,流程如下(Then the flow would be the following):

  • (1)First pass——因为未定义 replace_attempted_protocol 类,所以未执行 "Protocol 2"insert_lines: promise
  • (2)First pass——replace_patterns: promise 已执行但由于该行不存在而未能成功。由于使用 always() body,无论如何(anyway)总是定义了 replace_attempted_protocol 类。
  • (3)Second pass——现在执行 insert_lines: promise,并且由于文件中不存在 "Protocol 2" 行,因此将其插入。

在这两种情况下,最终结果都是相同的:将 Protocol 参数设置为其正确的值。
重要的是要注意,我们先前检查过(previously-examined)的 set_variable_values() bundle可以使用 set_config_values() bundle所使用的相同技术来简单地重写,这将增加 允许它正确处理注释掉的行 的功能。


【警告】
请注意,我们在本节中看到的示例假定(assumes)每个参数在文件中只能出现一次(assumes each parameter can only appear once in the file)。

如果文件包含 "Match" 块(blocks)(允许指定条件配置值),则此假设(assumption)不成立。

为了清楚起见,我仅考虑了本书中最简单的示例。

有关全部功能,请参见 CFEngine 设计中心中的 networking/ssh 草图


编辑 /etc/inittab(Editing /etc/inittab)

设置(setting up) Unix 或 Linux 系统时,另一个常见的初始任务(Another common initial task)是自定义(customize/etc/inittab

对于我们的示例(For our example),我们将执行以下任务(we will do the following tasks):

  • (1)将默认运行级别(default runlevel)从 5 修改为 3,在默认情况下(by default)禁用图形登录。
    • 这通常是在 Linux 服务器上完成的,以防止(prevent)在未使用的图形控制台(an unused graphical console)上浪费资源(wasting resources)。
  • (2)禁用 Ctrl-Alt-Del 处理,以防止该组合键(key combination)重启系统(rebooting the system)。

为了完成第一个任务,我们需要在以下行中修改第二个字段:

id:5:initdefault:

现在您已经了解了我们之前完成的编辑任务,这是一个相当简单的任务。
这是实现它的承诺:

files: # (1)
  "/etc/inittab"
    handle => "inittab_set_initdefault",
    comment => "Ensure graphical mode is disabled (default runmode=3)",
    create => "false",
    edit_defaults => backup_timestamp, # (2)
    edit_line => set_colon_field("id","2","3"); # (3)
  • (1)这是一个 files: promise,表示要编辑的文件。并指出该文件如果尚未存在就不能创建(create => "false"),因为 /etc/inittab 应该始终存在于 Unix 系统中。

  • (2)edit_defaults 属性指定文件编辑操作的行为。在标准库中可以找到 backup_timestamp的定义:

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

说明:

  • empty_file_before_editing => "false":编辑之前不应该清空文件(当 promise 会完整地重新创建文件时,可以将其设置为 true),
  • edit_backup => "timestamp":应保留旧版本的副本,并在末尾标记时间戳。
    • 这使您可以保留文件的历史记录,尤其适合于关键系统文件,因此,如果出现问题,您可以快速还原所有更改。
  • max_file_size => "300000":文件的大小不得超过 300,000 字节。
    • 这只是一项健全性检查,以确保文件不会超出正常范围

您会注意到,我们在先前的文件编辑承诺(file-editing promises)中已省略了 edit_defaults 属性。 这是有效的,并提供合理的默认行为。
我们现在特别使用 edit_defaults,因为最好保留 /etc/inittab 文件的备份副本,以防万一出问题。

  • (3)/etc/inittab 的实际编辑是通过标准库 set_colon_field() 捆绑包完成的,该包允许我们编辑以冒号分隔的文件中的字段。
    这是它的定义:
bundle edit_line set_colon_field(key,field,val)
{
  field_edits:
    "$(key):.*"
      comment => "Edit a colon-separated file, using the first field as a key",
      edit_field => col(":","$(field)","$(val)","set");
}

该捆绑软件使用的是我们在 “编辑 /etc/sysctl.conf” 中使用的相同的较低层 col() body,只是这次使用冒号作为分隔符,将适当的字段设置为我们提供的值。
正如我们的承诺中所使用的,col() 导致第一个字段(first field)为 "id" 的行的第二个字段(the second field)将被设置为 "3"

为了完成第二项任务,我们需要注释掉以下行:

ca::ctrlaltdel:/sbin/shutdown -r -t 4 now

我们可以使用以下承诺来实现(achieve)这一目标:

files:
  "/etc/inittab"
    handle => "inittab_disable_ctrlaltdel",
    comment => "Ensure handling of ctrl-alt-del is disabled",
    create => "false",
    edit_defaults => backup_timestamp,
    edit_line => comment_lines_matching("ca::ctrlaltdel:.*", "#");

同样,此承诺中的实际工作是由 edit_line 属性执行的,在本例中,该属性调用(callscomment_lines_matching() 捆绑包。
此标准库包用于在与第一个参数匹配的任何行的开头插入注释字符(在此为第二个参数,在本例中为 "#")。
这里是它的定义:

bundle edit_line comment_lines_matching(regex,comment)
{
  replace_patterns:
    "^($(regex))$"
      replace_with => comment("$(comment)"),
      comment => "Search and replace string";
}

如您所料,它由一个简单的 replace_patterns: promise 组成。

替换字符串(replacement string)由 comment compound body 定义,该定义也在标准库中:

body replace_with comment(c)
{
  replace_value => "$(c) $(match.1)";
  occurrences => "all";
}

replace_value 属性中,$(c) 是我们作为参数传递的注释字符串,而 $(match.1) 则是指用于选择该行的正则表达式中第一组括号(first set of parenthesis)的内容。
如果您回顾 comment_lines_matching() bundle,您会发现正则表达式为 "^($(regex))$",带有分组括号(grouping parenthesis)的分组可以捕获整个匹配的行。这导致匹配的行被注释字符替换,然后是空格,然后是该行的先前内容。

将它们放在一起(Putting it all together),并扩展我们先前的 configfiles() 捆绑包以处理 /etc/inittab 文件的编辑,我们得到以下信息:

bundle agent configfiles
{
  vars:  
      # Files to edit
      "files[sysctlconf]" string => "/etc/sysctl.conf";
      "files[sshdconfig]" string => "/etc/ssh/sshd_config";
      "files[inittab]"    string => "/etc/inittab";

      # Sysctl variables to set
      "sysctl[net.ipv4.tcp_syncookies]"               string => "1";
      "sysctl[net.ipv4.conf.all.accept_source_route]" string => "0 ";
      "sysctl[net.ipv4.conf.all.accept_redirects]"    string => "0";
      "sysctl[net.ipv4.conf.all.rp_filter]"           string => "1";
      "sysctl[net.ipv4.conf.all.log_martians]"        string => "1";

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

  methods:
      "sysctl"  usebundle => edit_sysctl;
      "sshd"    usebundle => edit_sshd;
      "inittab" usebundle => edit_inittab;
}

bundle agent edit_inittab
{
  files:
      "$(configfiles.files[inittab])"
        handle => "inittab_set_initdefault",
        comment => "Ensure graphical mode is disabled (default runmode=3)",
        create => "false",
        edit_defaults => backup_timestamp,
        edit_line => set_colon_field("id","2","3");

      "$(configfiles.files[inittab])"
        handle => "inittab_disable_ctrlaltdel",
        comment => "Ensure handling of ctrl-alt-del is disabled",
        create => "false",
        edit_defaults => backup_timestamp,
        edit_line => comment_lines_matching("ca::ctrlaltdel:.*", "#");
}

在这里,我们只是将文件名移到了我们一直在使用的 files 数组中,并将对 edit_inittab() 的调用添加到 method: 部分。

内容可变的配置文件(Configuration Files with Variable Content)

到目前为止,我们已经对配置文件进行了固定的更改(making fixed changes),这很有用,但是 CFEngine 能够处理更复杂的情况。

在实际的网络中(In a real network),并非所有系统都是相同的,并且操作系统,发行版和参数的混合(mixture of)使用会影响每台计算机的配置方式(affect how each machine should be configured)。

手工处理(by hand)这些几乎相同但略有不同的配置(almost-the-same-but-slightly-different configurations)是灾难的必由之路(a certain recipe for disaster)——最终,有人会:

  • 忘记了必须进行的更改(lose track of the changes that have to be made,`),
  • 忘记进行某些更改(forget to make certain changes),
  • 或者做出错误的更改(make the wrong set of changes),
  • 系统将停止工作(a system will stop working)。

使用 CFEngine(With CFEngine),可以一致地(consistently)进行这些配置,而不会出错(without errors)。

基于类的配置(Class-based configuration)

CFEngine 自动发现(automatically discovers)有关系统及其当前状态的大量信息,并根据(based on)这些信息设置类。
这些在 CFEngine 术语(terminology)中称为硬类(hard classes),因为它们是由 CFEngine 根据系统特征(system characteristics)设置的。它们与软类(soft classes)不同,软类由策略在其执行期间设置。

使用硬类(Using hard classes),我们可以指示 CFEngine 根据每个系统的特征或执行 CFEngine 的时刻采取不同的行动。
要知道 CFEngine 发现了哪些类,我们可以使用 cf-promises 命令,如下所示:

$ cf-promises -V
CFEngine Core 3.12.1

$  cf-promises --show-classes
Class name                                                   Meta tags
10_1_16_23                                                   inventory,attribute_name=none,source=agent,hardclass
127_0_0_1                                                    inventory,attribute_name=none,source=agent,hardclass
172_17_0_1                                                   inventory,attribute_name=none,source=agent,hardclass
172_18_0_1                                                   inventory,attribute_name=none,source=agent,hardclass
172_19_0_1                                                   inventory,attribute_name=none,source=agent,hardclass
...

命令参考地址:https://docs.cfengine.com/docs/3.15/reference-language-concepts-classes.html

让我们看一下其中的一些类,以及这些名称告诉我们的内容。

  • 时间信息由以下类提供(Time information is given by classes such as):

    • Day19:每月的 19 号(19th of the month
    • Friday
    • Hr3:上午 3 时
    • Min05_10:在 3:05 和 3:10 之间(it’s between 3:05 and 3:10
    • Hr03_Q1
    • Q1:当前的四分之一小时(the current quarter-hour
    • Night:它是在晚上(it’s a t night
    • November
    • Yr2010
    • Lcycle_0:这是一个“生命周期指数(lifecycle index)”,定义为年份模(modulo)3,可用于长期计划(can be used for long-term scheduling
    • 所有时间均以当地时区(local timezone)表示。
  • 网络信息由以下类提供(Network information is given by classes such as):

    • 10_123_6_61:主机的IP地址(the host’s IP address
    • ipv4_10
    • ipv4_10_123
    • ipv4_10_123_6
    • ipv4_10_123_6_61:IP 地址的不同部分(the different portions of the IP address
    • net_iface_eth0
    • net_iface_eth1:系统中定义的网络接口(the network interfaces defined in the system
  • 系统信息由以下类提供(System information is given by classes such as):

    • cfhost1:主机名(the host name
    • cfhost1_ec2_internalFQDN,用下划线代替点(its FQDN, with the dots replaced by underscores)。
      • FQDN:全限定域名(Fully Qualified Domain Name),同时带有主机名和域名的名称。(通过符号 "."
    • linux
    • SuSE
    • SLES11:操作系统类型,在这种情况下为 Linux 发行信息(operating system type and, in this case, Linux distribution information
    • i686:系统架构(system architecture
    • linux_2_6_32_19_0_3_ec2:Linux内核版本和内部版本信息(Linux kernel version and build information
    • xen这是 Xen 虚拟机it’s a Xen virtual machine
  • CFEngine 信息由以下类提供(CFEngine information is given by classes such as):

    • cfengine_3
    • cfengine_3_1
    • cfengine_3_1_0:版本号((version number
    • nova_edition:CFEngine 版本(CFEngine edition
    • PK_SHA_1d71...:主机 cfengine 生成的公共密钥的加密签名(cryptographic signature),可用于唯一标识系统(can be used to uniquely identify the system
    • verbose_mode:它告诉我们 CFEngine 是使用 -v 选项运行的,因此您可以将自己的详细输出绑定到该选项的使用。

硬类通过提供(by offering)可以挂起更改(hang changes)的非常详细的因素(very detailed factors),在编写配置时提供了很大的灵活性(flexibility)。
您很少需要定义新类来区分用于特殊处理的机器。

例如,您可以使用系统类型(system type)来决定(to decide)要用于特定任务(a certain task)的命令:

bundle agent reboot
{
  commands:
    linux::
      "/sbin/shutdown -r now";
    windows::
      "c:/Windows/system32/shutdown.exe /r /t 01";
}

请记住,在 CFEngine 中,以双冒号(double colon)结尾的行被解释(interpreted as)为类表达式,表示仅当表达式的计算结果为 true 时,才应评估其后的行。

在这种情况下,选择(selection)非常简单:我们使用一个硬类 linuxwindows 作为类表达式,使用一个命令来重启 Linux 系统(rebooting Linux systems,),使用另一个命令来重启 Windows 系统(a different one for Windows machines,)。

我们还可以将类组合成更复杂的表达式。
扩展前面的示例,我们可以使用 and(. or &)运算符,在 reboot_needed 类和相应的操作系统类都存在的条件下来重新启动。

此外,如果计算机 not(!)Linux 和(.not(!)Windows(我们可以使用括号对表达式的各个部分进行分组),我们可能会产生错误:

bundle agent reboot
{
  commands:
    reboot_needed.linux::
      "/sbin/shutdown -r now";
    reboot_needed.windows::
      "c:/Windows/system32/shutdown.exe /r /t 01";
      
  reports:
    reboot_needed.!(linux|windows)::
      "I know how to reboot only Linux and Windows machines.";
}

基于时间的类(Time-based classes)可用于使用 CFEngine 模仿(emulate类 cron(cron-like的行为。
例如:

bundle agent cron_tasks
{
  commands:
    Min00_05::   # Commands to run hourly
      "/usr/sbin/updatedb";
    Hr00::       # Commands to run daily at different times
      "/usr/local/sbin/logrotate";
      "/usr/sbin/tmpclean";
    Hr03::    
      "/usr/local/sbin/run_backups";
    Monday::     # Commands to run weekly
      "/usr/sbin/usercheck";
    Lcycle_0::   # Commands to run every four years
      "/usr/sbin/random_catastrophic_failure";
}

在这样的捆绑软件中,您可以定义任意数量的要执行的任务。

最好的部分(best part)是,由于 CFEngine 的锁定机制(locking mechanisms),您不必担心多次执行命令——如果已经根据当前条件执行了该命令,则不会再次执行该命令(除非(unless)您使用 -K 选项运行 cf-agent,指示 CFEngine 忽略所有内部锁)。

另一个大的优点是,使用 CFEngine 替代(replacementcron 可以使您不仅安排(schedule)命令和 shell 脚本,而且还可以安排任意 CFEngine promise,与单独使用 cron 相比,您可以使用它们执行更复杂的任务。

系统信息类(System-information classes)使您可以根据系统状态或配置执行不同的任务。
例如,您可以使用 CFEngine 轻松创建不同的网络配置文件:

bundle agent network_profiles
{
  commands:
    # At home, 192.168.23.0/24, start my backup
    ipv4_192_168_23::
      "/usr/local/sbin/open_services.sh";
      "/usr/local/sbin/run_backup.sh";
      "/usr/local/sbin/configure_home_printer.sh";
    # At work, 9.4.0.0/16, configure the appropriate printers
    ipv4_9_4::
      "/usr/local/sbin/open_services.sh";
      "/usr/local/sbin/configure_work_printers.sh";
    # Anywhere else, close some services for additional protection
    !(ipv4_192_168_23|ipv4_9_4)::
      "/usr/local/sbin/close_services.sh";
}

在这种情况下,我们将根据当前配置系统的 IP 地址范围来修改系统设置。可能性是无止境(The possibilities are endless)。

基于系统状态的配置(System-state-based configuration)

配置系统的另一种甚至更灵活(even more flexible)的方法涉及使用其当前状态来确定所需的最终状态,从而使策略完全动态,具体取决于每个特定的系统。

在我的一个项目中,我们有大量具有两个网络接口的 Linux 机器,其中:

  • 一个连接到生产网络(production network),我们称为 “绿色” 网络(“green” network
  • 另一个连接到管理网络(management network),我们称为 “黑色” 网络(“black” network

由于网络基础架构的特性,我们必须在绿色网络(green network)上的接口上禁用 TSO flagTCP Segmentation Offload)。
在自动执行此操作的第一次尝试中,我观察到绿色接口(green interface)始终为 eth0(这些都是 Linux 系统),并对 CFEngine 配置进行了硬编码(hard-coded),以将以下行添加到 /etc/inittab 中:

tso:3:once:/usr/sbin/ethtool -K eth0 tso off

这导致 ethtool 命令在系统引导时运行以禁用此标志。

实现此目标的策略与我们之前所看到的非常相似,因此我将不展示其确切的实现方式。

这很好用……直到出现异常:绿色接口(green interface)不一定是 eth0 的系统。然后必须修改规则,而使用 CFEngine 则很容易完成。

在这种特殊情况下,可以通过其 IP 地址范围轻松识别这两个网络。

  • 绿色网络(green network)的范围为 192.168.0.0/16
  • 黑色网络(black network)的范围为 10.10.0.0/16

有了这些信息,我就能够修改策略,以便在 ethtool 命令中使用正确的接口。
这是完整的捆绑包:

bundle agent disable_tso_flag
{
  vars:
      "ipregex" string => "192\.168\..*";  # (1)
      "nics"    slist  => getindices("sys.ipv4");

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

  files:  # (3)
      "$(configfiles.files[inittab])"
        handle => "inittab_add_ethtool",
        comment => "Ensure ethtool is run on startup to disable the TSO flag",
        create => "false",
        edit_defaults => edit_backup,
        edit_line => replace_or_add("tso:3:.*",  # (4)
                                    "tso:3:once:/usr/sbin/ethtool -K $(nics) tso off"),
        ifvarclass => "isgreen_$(nics)";
}

由于该捆绑包引用了 configfiles() bundle,因此打算把它纳入本章中我们一直在开发的主要策略中。
让我们更详细地研究一下。

  • (1)首先,我们为 $(ipregex) 变量分配正则表达式以选择所需的接口(在本例中为绿色接口)。

    • 接下来,我们在 @(nics) 列表中存储特殊 CFEngine 数组 sys.ipv4 的索引。
    • 这是 CFEngine 创建的特殊变量,包含系统中配置的所有 IP 地址,并按接口名称索引。
    • 因此,getindices("sys.ipv4") 为我们提供了系统上所有网络接口的列表。
  • (2)获得此列表后,我们再次使用 CFEngine 的隐式循环来分配多个名为 isgreen_ifname 的类,其中 ifname 表示系统上的每个网络接口(network interfaces)。

    • 如果通过值 "$(sys.ipv4[$(nics)])" 指定的所述接口的 IP 地址与 $(ipregex) 相匹配,则每个类都是 true(请记住,$(nics) 依次设置为每个接口名称)。

    因此,如果系统具有以下网络接口:

  • eth09.4.21.16

  • eth1189.177.231.225

  • eth2192.168.13.56

  • eth310.10.54.25

    那么对这些类的评估将如下:

  • isgreen_eth0:未设置(unset

  • isgreen_eth1:未设置

  • isgreen_eth2:设置(set

  • isgreen_eth3:未设置

这准确地告诉我们哪个接口是我们需要在 ethtool 命令中使用的接口。

  • (3)有了这些知识(Armed with this knowledge),我们就可以进入 files: promise,它将执行 ethtool 命令的行添加到 /etc/inittab 中。

    • 仅在设置了相应的 isgreen_ifname 类的情况下,此命令包含 $(nics) 变量(隐式循环再次起作用)给出的接口名称,如 promise 中的 ifvarclass => "isgreen_$(nics)" 子句(clause)所示(indicated)。
  • (4)为了实际添加该行,我们使用标准库中的另一个捆绑软件 replace_or_add,该捆绑软件执行以下操作:

    • 如果一行与第一个参数给出的正则表达式匹配,则将其全部替换为第二个参数。
    • 如果找不到匹配项,则将第二个参数中给出的行添加到文件中。

replace_or_add 捆绑包非常简单。它使用与我们之前讨论过的 set_config_values 捆绑包相同的技巧(uses the same trick)(在执行 replace_patterns: promise 时无条件(unconditionally)设置类)来实现所需的操作:

bundle edit_line replace_or_add(pattern,line)
{
  vars:
    "cline" string => canonify("$(line)");

  replace_patterns:
    "^(?!$(line))$(pattern)$"
      replace_with => value("$(line)"),
      classes => always("replace_done_$(cline)");

  insert_lines:
    "$(line)"
      ifvarclass => "replace_done_$(cline)";
}

了解 CFEngine 中的内置类(built-in classes),变量和函数很有用(It pays to know),因为它们有助于完成大多数必要的处理和数据提取任务。

我强烈建议您通读参考手册中的相应部分,以至少在一般意义上熟悉可用的功能。
我们已经多次描述了 CFEngine 隐式循环的用法。在大多数编程语言中都找不到这种概念(concept),因此一开始(at the beginning)可能很难将在大脑中生根(wrap your head around it)。

一旦掌握了这些技巧(Once you get the hang of it),您就会意识到它可以节省许多其他语言所需的流控制代码行,而 CFEngine 中缺少这些行可以让您集中精力编写策略。
实际上,CFEngine 使用隐式循环和常规排序等概念来确定事物的执行方式,从而避免了您担心策略执行流程的麻烦。

在开始时要与这种自动化水平抗衡(to fight this level of automation)是一种自然的趋势(natural tendency),但是 CFEngine 真正的优势(mastery)之处在于(lies in)放开了将一切控制到最后细节的冲动,并以应有的方式使用 CFEngine。

告诉 CFEngine 您想要什么(what you want)以及如何做(how to do it),让 CFEngine 担心诸如执行操作的顺序之类的细节。

用户管理(User Management)

任何系统管理员的基本任务(basic tasks)之一就是控制用户帐户(to control user accounts)。

无论是本地帐户还是使用 LDAP 等网络范围机制的集中式帐户,CFEngine 都能为您提供所需的精确控制。
从高级别的角度来看(From a high-level perspective),用户帐户的定义可以表示为:

bundle agent manage_users
{
  vars:
      # Users to create
      "users[root][fullname]"  string => "System administrator";
      "users[root][uid]"       string => "0";
      "users[root][gid]"       string => "0";
      "users[root][home]"      string => "/root";
      "users[root][shell]"     string => "/bin/bash";
      "users[root][flags]"     string => "-o -m";
      "users[root][password]"  string => "FkDMzhB1WnOp2";

      "users[zamboni][fullname]"  string => "Diego Zamboni";
      "users[zamboni][uid]"       string => "501";
      "users[zamboni][gid]"       string => "users";
      "users[zamboni][home]"      string => "/home/zamboni";
      "users[zamboni][shell]"     string => "/bin/bash";
      "users[zamboni][flags]"     string => "-m";
      "users[zamboni][password]"  string => "dk52ia209rfuh";

  methods:
      "users"   usebundle => create_users("manage_users.users");
}

本示例将用户特征(user characteristics)存储在二维数组中,该二维数组由用户名和每个用户记录的不同字段索引。

从策略的 method: 部分调用 create_users() 捆绑包,并将配置数组作为参数传递。
这是 create_users() 捆绑包:

bundle agent create_users(info)
{
  vars:
      "user"        slist => getindices("$(info)");  #(1)

  classes:
      "add_$(user)" not => userexists("$(user)");  #(2)

  commands: #(3)
    linux::
      "/usr/sbin/useradd $($(info)[$(user)][flags]) -u $($(info)[$(user)][uid]) 
       -g $($(info)[$(user)][gid]) -d $($(info)[$(user)][home]) 
       -s $($(info)[$(user)][shell]) -c '$($(info)[$(user)][fullname])' $(user)"
        ifvarclass => "add_$(user)";
    windows::
      "c:/Windows/system32/net user $(user) $($(info)[$(user)][password]) /add 
       \"/fullname:$($(info)[$(user)][fullname])\" \"/homedir:$($(info)[$(user)][home])\""
        ifvarclass => "add_$(user)";
      # On Windows we use a command to set the password
      # unconditionally in case it has changed.
      "c:/Windows/system32/net user $(user) $($(info)[$(user)][password])";  #(4)

  files:
    linux::
      # This is not conditioned to the add_* classes
      # to always check and reset the passwords if needed.
      "/etc/shadow"  #(5)
        edit_line => set_user_field("$(user)", 2, "$($(info)[$(user)][password])");

  reports: #(6)
    !linux.!windows::
      "I only know how to create users under Linux and Windows.";
    verbose_mode::
      "Created user $(user)"
        ifvarclass => "add_$(user)";
}

在 Linux 和 Windows 上,create_users() 的此特定实现仅处理本地用户帐户。

  • (1)在 vars: 部分,我们使用 getindices() 函数在 @(user) 中存储要检查的用户帐户列表(配置数组的顶级(top-level)索引)。

    • 通过 CFEngine 的隐式循环使用此列表,将捆绑包的其余部分应用于这些帐户中的每个帐户。
  • (2)classes: 部分通过使用内置(built-in)的 userexists() 函数依次(in turn)检查每个用户的存在,为每个不存在的用户帐户定义一个名为 add_username 的类。


【警告】
使用 CFEngine 社区版时,userexists() 函数在 Windows 上不会返回有效结果,但是如果您使用的是商业版之一,则它可以正常工作。


正确的(Proper)Windows 支持是 CFEngine 商业版的优势之一。

请注意,我们再次使用了隐式循环,但是这一次在两个地方使用了变量 $(user):作为类名的一部分,以及作为 userexists() 函数的参数。

  • (3)使用 CFEngine 提供的预定义 OS 类型硬类(predefined OS-type hard classes),将 commands: 部分按操作系统划分。
    • 在此,我们发出必要的命令行指令(issue the necessary command-line instructions)来创建用户,但前提是该用户尚不存在。
    • 这由添加到每个 commands: promiseifvarclass 属性控制,这使语句仅在给定的类表达式为 true 时执行。

【提示】
请注意,在此版本的捆绑软件中,未验证其他帐户属性(除密码外,请参见下文)的准确性。
如果该帐户已经存在,则该承诺被视为已兑现(satisfied)。


  • (4)由于我们还想为每个帐户强制使用密码,因此,我们必须确保检查密码,并在需要时每次都进行更改。

    • 对于 Windows,每次执行该策略时,我们都会发出命令将密码重置为所需的值。这是通过 commands: 部分完成的。(在这种情况下,必须在用户数组中以明文形式提供密码)
  • (5)对于 Linux,我们通过直接编辑 /etc/shadow 文件将 password 字段设置为用户矩阵中给定的值,从而在 files: 部分中重置用户密码(此值必须是所需的密码——已经使用适用于操作系统的 crypt() 函数进行了编码)。

    • 可以在标准库中找到 set_user_field() 包,它与我之前介绍的
      set_colon_field() 函数非常相似。
  • (6)最后,如果启用了详细模式(当将 -v 命令行选项(command-line option)提供给 cf-agent 时,verbose_mode 类会自动设置),则 reports: 部分会为创建的每个用户生成一个报告。并且如果我们在不受支持的系统上运行,还会生成一个错误。

通过向 methods: 部分添加另一行,可以轻松地将对 manage_users() 的调用集成到我们正在构建的 configfiles() bundle 中:

"users" usebundle => manage_users;

为了使事情更易于管理,我们还可以完全摆脱 manage_users(),将用户帐户的定义从 manage_users() 捆绑包移动到 configfiles() 捆绑包,在其中设置所有其他用户可配置变量, 并直接调用 create_users()

bundle agent configfiles
{
  vars:  
      ...
      # Users to create
      "users[root][gecos]"     string => "System administrator";
      "users[root][uid]"       string => "0";
      "users[root][gid]"       string => "0";
      "users[root][home]"      string => "/root";
      "users[root][shell]"     string => "/bin/bash";
      "users[root][flags]"     string => "-o -m";
      "users[root][password]"  string => "FkDMzhB1WnOp2";

      "users[zamboni][gecos]"     string => "Diego Zamboni";
      "users[zamboni][uid]"       string => "501";
      "users[zamboni][gid]"       string => "users";
      "users[zamboni][home]"      string => "/home/zamboni";
      "users[zamboni][shell]"     string => "/bin/bash";
      "users[zamboni][flags]"     string => "-m";
      "users[zamboni][password]"  string => "dk52ia209rfuh";

  methods:
      ...
      "users"   usebundle => create_users("configfiles.users");
}

这是一个非常简单的示例,它仅管理本地帐户。但这很有用,例如,可以在通用本地帐户(例如 root)上设置已知属性。

但是,CFEngine 可以管理更复杂的场景(much more complex scenarios),包括集中的用户目录(centralized user directories)。

CFEngine 的商业版本中正确支持 LDAP 集成(包括 Active Directory)。

软件安装(Software Installation)

系统维护(system maintenance)的主要任务之一是软件(software)的安装(installation),配置(configuration),升级(upgrading)和删除(removal)。

在过去(In old times),系统上的大多数软件是

  • (a)操作系统的一部分(part of the operating system),每当安装或升级整个系统时都进行安装或升级,
  • (b)具有自己的安装机制(installation mechanisms)的商业软件(commercial software
  • (c)必须(had to be)手动编译和安装(compiled and installed manually)的开源软件(open-source software)。

随着时间的流逝(Over time),大多数操作系统都开发了软件包管理机制(package-management mechanisms),从而使安装和管理任何类型的软件变得更加容易。

不幸的是(Unfortunately),程序包管理机制(package-management mechanisms)在功能(capabilities)和用户界面方面千差万别(vary wildly),这使得编写其中任何一个接口的软件都是一项艰巨的任务(a daunting task)。

此外(Furthermore),仍然需要(尽管只是偶尔)手动编译和安装软件。

CFEngine 提供(provides)了强大且通用的机制(powerful and generic mechanisms)来处理此任务(for dealing with this task),从而使其能够(make it possible)适应(adapt)每个特定系统的需求(the needs of every particular system)。

基于软件包的软件管理(Package-Based Software Management)

CFEngine 将包管理理解(understands)为一个通用概念(as a generic concept)。

每个软件包(Each package)都由三个属性表示(represented):

  • 名称(its name
  • 版本(its version
  • 体系结构(its architecture

CFEngine 允许您执行诸如(such as):添加(add),删除(delete)和更新(update)之类的操作(operations)。

如何与程序包管理系统进行交互的细节被抽象到策略的离散组件(discrete components)中,并且可以进行自定义以与任何命令行驱动的程序包管理器(any command-line-driven package manager)进行交互。

CFEngine 中的所有软件包管理承诺(package-management promises)都出现(occur)在 agent bundlepackages: 部分中。
CFEngine 允许我们对系统中程序包的状态作出承诺(make promises),而将实际修改程序包的工作留给了基础的软件包系统(underlying packaging system)。

请记住,鉴于程序包管理系统(package management systems)的功能千差万别(widely varying capabilities),在编写程序包管理 promises 时(writing package-management promises),必须考虑其功能(we must take their capabilities into consideration)。——例如,如果系统使用 rpm,则应考虑到它不会自动获取(automatically fetch)并安装要安装的软件包的依赖项。

让我们看一个非常简单的例子:

bundle agent software
{
  vars:
      "pkgs" slist => {
                        "subversion",
                        "tcpdump"
                      };
  packages:
      "$(pkgs)" 
        package_policy => "addupdate",
        package_method => apt;   # For Debian and Ubuntu
}

我们定义一个列表变量,其中包含要安装或更新的软件包(Subversiontcpdump),并将其用于指定 addupdate 软件包政策(package policy)的 promise 中,这意味着 “如果已安装,请更新该软件包,否则请进行安装。(update the package if it’s installed, install it if not)”

我们还将 apt 指定为软件包方法(package method),这是基于 DebianLinux 发行版中的软件包管理系统。
在标准库中定义了一些标准的 package_method body,包括 apt

让我们看一下它的定义:

body package_method apt
{
package_changes => "bulk";
package_list_command => "/usr/bin/dpkg -l"; # (1)
package_list_name_regex    => "ii\s+([^\s]+).*"; # (2)
package_list_version_regex => "ii\s+[^\s]+\s+([^\s]+).*";
package_installed_regex => ".*"; # all reported are installed
package_name_convention => "$(name)"; # (3)

# set it to "0" to avoid caching of list during upgrade
package_list_update_ifelapsed => "240";

have_aptitude:: # (4)
   package_add_command => "/usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C /usr/bin/aptitude -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef --assume-yes install";
   package_list_update_command => "/usr/bin/aptitude update";
   package_delete_command => "/usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C /usr/bin/aptitude -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef --assume-yes -q remove";
   package_update_command =>  "/usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C /usr/bin/aptitude -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef --assume-yes install";
   package_verify_command =>  "/usr/bin/aptitude show";
   package_noverify_regex => "(State: not installed|E: Unable to locate package .*)";

!have_aptitude::
   package_add_command => "/usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C /usr/bin/apt-get -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef --yes install";
   package_list_update_command => "/usr/bin/apt-get update";
   package_delete_command => "/usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C /usr/bin/apt-get -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef --yes -q remove";
   package_update_command =>  "/usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C /usr/bin/apt-get -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef --yes install";
   package_verify_command => "/usr/bin/dpkg -s";
   package_noverify_returncode => "1";
}

package_method 主体告诉 CFEngine 如何执行实际执行操作的命令,以及如何处理其输出以获得必要的信息:

  • (1)package_list_command 属性指定要运行以在系统上生成软件包列表的命令。
  • (2)与此结合(Coupled with this),package_list_name_regexpackage_list_version_regex 属性告诉 CFEngine 正则表达式,该正则表达式应用于 package-listing 命令的输出,以确定每个软件包的名称和版本。
    • 另外,package_installed_regex 用于确定清单中的哪些软件包已实际安装(在这种情况下,由于使用了这个命令,因此将输出所有已经安装软件包,但是在其他软件包管理系统中可能不是这种情况) 。
  • (3)package_name_convention 属性告诉 CFEngine 在执行任何命令时如何指定软件包(how to specify a package)。某些程序包管理系统可能需要名称和版本才能运行。 apt 只需要名称,因此是这样指定的。
  • (4)have_aptitude 类是一个硬类,安装了 aptitude 软件包管理程序后,CFEngine 会在类似 Debian 的系统上自动定义该类,因为它提供了一些其他功能。根据此类,主体(the body)会设置用于添加,删除或更新程序包的特定命令。

标准库包括用于几个常用软件包管理器的预定义 package_method 主体,包括 zypperaptrpmyumWindows MSI 安装程序(Installers)Solaris 软件包管理器(package manager)FreeBSD 软件包管理器
还有一种通用的打包方法,可以结合以上所有内容,并根据适当的操作系统硬类提供正确的值。

需要重点注意的是,package_method 定义确切指定了如何与包管理器进行交互,因此允许您通过编写适当的 package_method 与所需的任何软件包机制进行交互(packaging mechanism)。

有用的示例是用于流行语言特定或工具特定的包管理器(例如 PearRuby Gems)的 package_method 定义。

package promises 可能要复杂得多。名称,版本和体系结构属性(The name, version, and architecture attributes)可以在 package promises 中使用,以定义所需的结果(to define the desired result)。

我们还可以使用版本比较运算符(version-comparison operators)来进一步完善操作。

bundle agent software
{
  vars:
      "version[openssl]"  string => "0.9.8k-7ubuntu8";
      "version[ssl-cert]" string => "1.0.23ubuntu2";

      "architectures" slist => { "x86_64" };
      "allpkgs"       slist => getindices("version");

  packages:
      "$(allpkgs)" 
        package_policy => "add",
        package_select => "==",
        package_version => "$(version[$(allpkgs)])",
        package_architectures => @(architectures),
        package_method => apt;   # For Debian and Ubuntu
}

在这种情况下,我们使用数组来存储所需的版本,并按包名称索引。然后,我们使用数组中的索引列表为每个软件包安装所需的特定版本,同时指定所需的体系结构。

我们再次使用数组和隐式循环为每个软件包请求所需的版本。

值为 "=="package_select 属性告诉 CFEngine 我们正好需要指定版本的软件包(默认情况下,其值为 ">=",它为我们提供了比指定版本大的最新可用版本)。

package_policy 属性的值是 verify(这是它的默认值)时,CFEngine 所做的只是检查是否正确安装了所需的软件包。这可用于简单地报告系统的正确性,而无需尝试修复任何问题。

tips:verify 的概念取决于程序包管理器,某些 package_method 主体不支持它。

例如:

bundle agent verify_packages
{
  vars:
      "allpkgoutput" string => execresult("/usr/bin/rpm -qa --queryformat \"%{name}\n\"");
      "allpkgs"      slist => splitstring("$(allpkgoutput)", "\s+", 999999);

  packages:
      "$(allpkgs)" 
        package_policy => "verify",
        package_method => rpm,
        classes => if_notkept("incorrect_$(allpkgs)");

  reports:
      "Problem: package $(allpkgs) is not installed correctly."
        ifvarclass => "incorrect_$(allpkgs)";
}

该捆绑包首先(start)通过使用 execresult() 函数运行外部命令(an external command)来获取所有软件包的列表,然后将其存储在 $(allpkgoutput) 字符串中,然后将其通过 splitstring() 函数拆分为 @(allpkgs) 列表。 。

然后,我们遍历此列表,依次验证每个软件包。 如果未兑现承诺(not kept)(即,如果未正确验证软件包),则 packages bundle 定义了一个 incorrect_packagename 类。

reports: 部分中,我们再次遍历 @(allpkgs),为定义了incorrect_packagename` 类的软件包输出一条消息。

我们可以将其用作系统的常规 “健全性检查(sanity check)”。例如,如果我们拥有要管理的新系统(that comes under our management),则可以生成其当前状态的报告,或触发(trigger)自动纠正措施(automatic corrective actions)。

手动软件管理(Manual Software Management

尽管(Although)软件包管理软件(package management software)是在系统上安装和卸载软件(to install and uninstall software on a system)的理想方法(the ideal way),但是在某些情况下,您可能想要或需要(want or need)手动管理软件(to manage software manually)。

一种情况(One such case)是,当您需要安装的软件在操作系统的软件库(operating system’s software repository)中不可用,或者您需要以自定义方式进行编译或安装,或者您需要的版本比在存储库中存在的太旧或太新(too old or too new to be in the repository)。

在本节中,我们将开发 CFEngine 策略以手动安装应用程序(to manually install an application)。

这需要更多的手动工作,并且每个策略对于正在安装的应用程序都是唯一的,因此您可能希望最大程度地减少使用此方法安装的应用程序的数量。

但是,了解当前如何执行此任务很有用
在需要的时候。

对于我们的示例,我们将安装WordPress博客和CMS应用程序。

从WordPress文档中,我们可以看到它具有相当简单的安装过程:

1.安装系统要求:Apache,PHP和MySQL;
2.下载并解压缩软件包;
3.创建一个可与WordPress一起使用的MySQL数据库和用户;
4.使用wp-configsample用必要的数据库参数设置wp-config.php。

php作为起点。

这些步骤为我们提供了一个相当不错的使用CFEngine进行安装的指导。

我们将创建一个wp_install捆绑包,但首先要考虑如何调用它:

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容