groups 节是 cfengine 和 cfperl 不可或缺的部分。通过定义包含几组机器的类,可以向用户提供功能强大而又灵活的类机制。
作为全捕捉(catch-all)处理程序,缺省解析器是必要的。当 cfperl 没有专用于节的处理程序时,则将节传递给缺省解析器。
一切都是从全局解析器开始
全局解析器(我在先前的章节中讨论过它)创建 cfrun 原子。 cfrun 原子是单一的配置行(例如,“add this user”),它带有附加节和类条件执行(class-conditional execution)。例如,“add user”可能在“users”节中,并有可能仅用于“linux”类。
cfrun() 函数执行这些 cfrun 原子。但是,在执行功能原子之前, cfrun() 必需处理 import 和 group 语句。在 cfperl 的未来版本中,导入将和 cfengine 中一样可以根据条件完成。在这个版本中,导入是无条件的。
清单 1. 样本 import 和 group 语句
import:
# import the file 'cf.extra'
cf.extra
groups:
# declare the classes 'internal' and 'external' with
# corresponding member machines
internal = ( server1 server2 )
external = ( server3 server4 )
# declare the class 'primary' with member 'server5'
primary = ( server5 )
# this line will be processed only when 'primary' is defined
primary::
# define the 'secondary' class if we're running on a Sunday
secondary = ( Sunday )
清单 2. 在 cfrun() 中处理 import 和 control/group 语句
foreach my $cfrun_atom ( grep { $_->{section} eq IMPORT_SECTION } @cfrun_queue)
{
dispatch(\%parsers, $cfrun_atom )
if allowed_cfrun_atom({ section => $cfrun_atom->{section},
classes => undef, actual => $cfrun_atom->{section} }, $cfrun_atom);
}
foreach my $cfrun_atom ( grep { $_->{section} eq CONTROL_SECTION ||
$_->{section} eq GROUPS_SECTION } @cfrun_queue)
{
dispatch(\%parsers, $cfrun_atom )
if allowed_cfrun_atom({ section => $cfrun_atom->{section},
classes => undef, actual => $cfrun_atom->{section} }, $cfrun_atom);
}
请参阅 cfperl CVS 资源库中的样本配置 cftest.conf 或用于说明导入如何在用户端工作的示例的 cfengine 样本配置(两者都在 参考资料一节中)。在 cfperl 中,由顶级解析器使用 load_file() 函数来处理导入。要简化程序流,必须首先执行导入。
在导入之后但在用户端功能原子之前处理 control 和 groups 节。例如, control: cfengine 节定义了 actionsequence 和用户定义的类。 actionsequence 声明 cfengine/cfperl 节将拥有何种顺序。cfperl 用 cfqueue 原子实现了 actionsequence ,该原子被插入称为 @cfrun_order 的数组中。为了声明 cfengine/cfperl 稍后将为条件执行解释的类, groups 节是必要的。
dispatch() 函数接受 cfrun 原子,并将它分派给适当的二级解析器。在本章稍后的部分中,我将解释 group 节二级解析器和空的缺省二级解析器。
缺省解析器
任何传递到 dispatch() 函数的 cfrun 原子都具有 section 属性。 dispatch() 函数将根据节为每个原子寻找适当的解析器。例如,专用于 users: 节的解析器能理解象 add_user 这样的命令。
在没有显式地定义处理 cfrun 原子的解析器的情况下,则调用缺省解析器。缺省解析器根本不进行任何处理 — 它只是个占位符。cfperl 是显式设计的,这样它可以补充 cfengine,所以根本不应该由 cfperl 处理诸如 editfiles: 和 files: 之类的 cfengine 节。因此,缺省解析器是由 dispatch() 调用的:
清单 3. 缺省解析器,有点象禅语,但更为简洁
$parsers{DEFAULT_SECTION()} = new Parse::RecDescent(q{ input: });
清单 4. dispatch() 调用适当的解析器,否则就调用缺省解析器
if (exists $parsers->{$section})
{
out (3, "dispatch: Invoking specific parser for section $section");
$retval = $parsers->{$section}->input($line);
}
else
{
die "No default parser found, quitting" unless exists $parsers->{DEFAULT_SECTION()};
out(3, "dispatch: Given section $section does not have a parser,
using default for command [$line]");
$retval = $parsers->{DEFAULT_SECTION()}->input($line);
}
作为有趣的副注:必须将 DEFAULT_SECTION 常数作为函数调用,否则 Perl 会认为您所指的是 字符串“DEFAULT_SECTION”而不是名为 DEFAULT_SECTION 的常数。
group 节解析器
groups: 节的解析器会根据其它类的存在来定义类。例如,仅当已经定义“b”的情况下,“a = ( b )”才会定义“a”。cfengine/cfperl 的基于组的类定义能力具有十分强大的功能。
清单 5. group 节解析器
$parsers{GROUPS_SECTION()} = new Parse::RecDescent (q{
input: define_group
define_group: class '=' '(' class_definition ')'
{ ::define_group($item{class}, $item{class_definition}); 1; }
class_definition: class(s)
class: word
word: /\w+/
});
与所有其它 cfperl 解析器一样,这个解析器也有 input() 方法。这里, input() 只能做一件事情:定义组。请记住,使用 Parse::RecDescent 模块时,所有规则同时也是方法,因此我们将 input() 看作方法。
define_group 规则是基于类和 class_definition 规则的。类只能是一个字,这个字是由 Perl 认为是字( \w )的字符组成的,但如有必要,我们可以更改该定义。例如,如果希望连字符也成为有效的类字符,则可以将“word: /[\w-]+/”作为规则。
class_definition 规则至少由一个类名组成。这样,在 cfperl 的 groups: 节中,“groupname = ()”将不是有效的行。如果希望允许上述情况,则在规则中使用“class_definition: class(s?)”。但是我们没有这样做,因为这样没有意义。
define_group 规则使用 class 和 class_definition 来构建组定义操作。这比 cfengine 的组定义规则宽松得多,后者要求在圆括号前后留空格,因此“groupname = (linux)”是无效的。而 cfperl 将顺利地解析该行。如果希望强制 cfengine 的行为,可以使用下列代码:
清单 6. 更严格的 define_group 规则
define_group: class '=' '(' /\s+/ class_definition /\s+/ ')'
{ ::define_group($item{class}, $item{class_definition}); 1; }
但是,在我看来,不需要那种更严格的行为。
define_group 规则使用 Parse::RecDescent 数据结构来构建外部 define_group() 函数的两个参数。注:正如其名称前面的两个冒号所示,外部 define_group() 函数是全局的。
define_group() 函数
全局(因此在 groups 节解析器外部) define_group() 函数接受两个变量。第一个是可能被定义的新类的名称,第二个是对现有类名的数组引用。
Parse::RecDescent 可以自动完成类名验证,但在我看来,外部函数更容易编码和理解。
清单 7. 全局 define_group() 函数
# {{{ define_group: insert a new group into the list of defined groups,
# if one of its conditionals is defined
sub define_group($$)
{
my $group_name = shift @_ || return undef;
my $conditional_names = shift @_ || return undef;
foreach my $conditional (@$conditional_names)
{
next unless exists $classes{$conditional};
$classes{$group_name} = 1;
return $group_name;
}
return undef;
}
# }}}
注:“next unless”控制流以及我们调用 return() 的方式,我们对 @$conditional_names 数组进行最快的可能的遍历,然后对成功的匹配调用 return() 。
结束语
本章总结了对 cfperl 内幕的讨论。在接下来的章节中,我将讨论 cfperl 的功能,从 cfperl 的添加、删除以及更改用户和组的工具入手。