如同文章的标题所示,本文是连载中的系列文章的一部分。我们建议您先阅读 前面几章以了解 cfperl 的背景知识、基本原理和结构。
用户和组管理是一个困难的问题。遗憾的是,通用的系统管理工具(如 cfengine)通常并不具备用户和组管理工具,或者即使具有用户和组管理工具,为了完成最简单的用户和组管理任务也要进行繁琐的配置。因此,许多 UNIX 管理员都自己编写脚本或手工过程用来添加或除去用户和组。
当我着手向 cfperl 添加用户和组管理能力时,我首先列出了我的目标。具体来说,我要确保生成的代码能够:
- 仍然与 cfengine 语法兼容
- 通过灵活的配置格式允许众多的 UNIX 平台
- 允许多种用户和组特性,而不是只允许标准用户和组特性
- 允许通过 NIS、本地帐户或者外部(通过脚本进行验证)方法进行各种形式的用户和组帐户验证
- 如果用户或组还不存在,添加它们
- 如果用户或组已经存在,将它们更改成新状态
- 如果用户或组已经存在,删除它们
用 cfperl 灵活的语法来实现这一切是相当简单的,但是在使用过程中还是要做一些有趣的调整。
深入 cfperl 语法
使用 cfperl 的语法来处理用于管理用户和组的新节(我把这一节称为 users
)并不困难。我在全局 %parsers 散列中添加了一个新的常量 USERS_SECTION 及相应的解析器:
清单 1. 添加一个新节
use constant USERS_SECTION => 'users';
$parsers{USERS_SECTION()} = new Parse::RecDescent(q{
# ...grammar omitted...
});
这就行了。不,真的!从这里开始,所有的工作都在该语法内部进行。cfperl 将自动识别称为 users
的节,然后将对它的解析工作交给 Parse::RecDescent 解析器对象,该对象在 $parser{'users'} 中引用。
要使用这一新功能,用户所必须完成的全部工作就是根据通常的 cfengine 规则编写一个 users 节。不出所料,基于类的条件执行也受到支持;新的解析器与类条件语句的解析方式无关,因为这些解析在新的解析器获取需要执行的语句之前都已经完成。
用户要做的最后一件事情是在 cfperl actionsequence 变量中添加 users 节。这是标准的 cfengine/cfperl 实践,确保不会执行不想要执行的节。
清单 2. 新的 actionsequence
control:
actionsequence = ( users )
特定于平台的用户和组管理配置
这里所说的平台就是运行 cfperl 的操作系统。截止撰写本章时为止,cfperl 的用户和组管理功能可以用于 Linux 和 Solaris 平台,但是用于添加其它平台的格式也很简单。
所有特定于平台的用户和组管理功能都在 %system_matrix 散列中。稍后也会将该散列用于用户和组管理以外的其它功能,这样就使数据格式尽可能变得灵活。
清单 3. 系统矩阵
use constant USER_OP => 'user';
use constant ADD_USER => 'useradd';
use constant DELETE_USER => 'userdel';
use constant CHANGE_USER => 'usermod';
use constant GROUP_OP => 'group';
use constant ADD_GROUP => 'groupadd';
use constant DELETE_GROUP => 'groupdel';
use constant CHANGE_GROUP => 'groupmod';
# the groupadd/groupmod/groupdel/useradd/usermod/userdel
# matrix for GNU/Linux
my %system_matrix = (
linux => {
CRON_OP() => {
name => '/usr/bin/crontab',
username => '-u %s'
},
ADD_USER() => {
name => ADD_USER,
uid => '-u %s',
gid => '-g %s',
secondary_gid => '-G %s',
homedir => '-d %s',
shell => '-s %s',
gecos => '-c %s',
},
CHANGE_USER() => {
name => CHANGE_USER,
uid => '-u %s',
gid => '-g %s',
secondary_gid => '-G %s',
homedir => '-d %s',
shell => '-s %s',
gecos => '-c %s',
username => '-l %s',
},
DELETE_USER() => {
name => DELETE_USER,
full => '-r',
},
ADD_GROUP() => {
name => ADD_GROUP,
gid => '-g %s',
},
CHANGE_GROUP() => {
name => CHANGE_GROUP,
gid => '-g %s',
},
DELETE_GROUP() => {
name => DELETE_GROUP,
},
},
);
%system_matrix 中的每一项都以操作系统名称作为键。例如,Linux 项的键是 linux 而 Solaris 项的键是 solaris 。为什么要使用小写版本呢?因为您应该仅使用 Perl $OSNAME 变量返回的名称。既然 Perl 会帮您确定 OS 名称,为什么还要另外创造一组 OS 名称呢?
%system_matrix 中的每一项都是一个散列引用,正如前面提到的那样,它们都以 $OSNAME 作为键。在特定于系统的散列中,键是操作的名称,在 cfperl 自身已将它们指定为常量。圆括号调用函数;从程序员的观点说,Perl 常量实际上是一个函数,编写该函数时不带圆括号只是为了方便。在这种情形下,省略圆括号会自动对散列键加引号,因此,举例来说, ADD_GROUP 应该是键 ADD_GROUP ,而不是所期望的键 groupadd ,后者是 ADD_GROUP 常量的值。
操作由常量引用,但其实际值容易引起一点混淆。例如,为什么使用 groupadd 而不使用 add group 呢?因为该值是用于实现该操作的 程序
的最常见名称的速记。这样,Linux 和 Solaris 中的 useradd 命令将添加一个用户。为了方便起见,严格地执行了这一点。
特定于系统的 $system_matrix{$OSNAME} 散列中的值告诉 cfperl 如何运行用于用户和组管理的 OS 级程序。例如, ADD_USER 的项:
清单 4. ADD_USER 说明
ADD_USER() => {
name => ADD_USER,
uid => '-u %s',
gid => '-g %s',
secondary_gid => '-G %s',
homedir => '-d %s',
shell => '-s %s',
gecos => '-c %s',
},
该项告诉 cfperl, ADD_USER 操作由一个称为 useradd 的程序来执行(请参阅上面对为什么将 ADD_USER 改为 useradd 的解释)。 name 参数是唯一一个未用作用户端参数的参数。
uid 、 gid 、 secondary_gid 、 homedir 、 shell 和 gecos 字段告诉 cfperl:在 users 节中有相应的参数。这样,稍后我们将看到用户可以如何要求将用户的 shell 设置成(例如)‘/bin/tcsh’,以及 shell 参数如何直接链接到 $system_matrix{$OSNAME} 散列。
特定于后端的用户和组管理配置
cfperl 使用的后端有:
清单 5. 可用的用户和组检查后端
use constant USERS_CHECK_NIS => 'nis';
use constant USERS_CHECK_EXTERNAL => 'external';
use constant USERS_CHECK_LOCAL => 'local';
用户在 control 节中使用“users check engines”项指定应该检查哪些后端,以确定用户和组是否存在。
清单 6. “users check engines”功能的语法定义
users_check: /users/i /check/i /engines/i /=/ engine(s)
{ $::users_check_backends = $item{engine}; 1; }
engine: 'nis' | 'external' | 'local'
这样,用户可以设定 users check engines = nis local 或 users check engines = external 。生成的数组(即使只有 0 或 1 个元素)将存储在全局 $users_check_backends 变量中。
此外,如果用户觉得本地和 NIS 检查还不够,那么可以指定一个外部检查程序来验证用户或组是否存在(例如,检查 LDAP 后端)。在 control 节中使用 external check 变量来指定程序。该程序由 cfperl 作为 program MODE USER_OR_GROUP_NAME 调用。MODE 是 user 或 group (在 cfperl 中为常量 USER_OP 和 GROUP_OP )。 USER_OR_GROUP_NAME 参数是自解释的。
一个完整的示例
好,在讨论实际运行所有命令的 users_op() 函数之前,现在让我们先来看看有关该功能的一个完整示例。注:cfperl 手册中提供了该示例及对该示例较详细的说明,而 cfperl 手册则可以在 cfperl 主页上找到(请参阅 参考资料以获取链接)。
清单 7. 使用用户和组管理功能
control:
any::
actionsequence = ( users )
users check engines = local
users:
any::
# the user will be created if they don't exist, otherwise the settings
# will only be adjusted
user cftest uid=1500 gid = 500 secondary_gid= 7 gecos="The 'test' Mongoose"
user cftest uid=1501
user cftest delete full
# the group will be created if they don't exist, otherwise the
# settings will only be adjusted
group cftest gid =1500
group cftest gid=1501
group cftest delete
请注意语句是如何告诉 cfperl 事情 应该
是怎样的,而不是明确地告诉它做什么。cfperl 将决定:如果用户 cftest 不存在,那么就应该添加该用户。这允许系统管理员简单地列出所有应该在机器上的用户,然后 cfperl 将自动添加任何已添加到列表的用户,以实现使用户需求列表与现有用户状态一致所必须做出的任何更改。
总是
会执行用户修改命令,即使该用户已经存在,并具有那些参数。这是无害的,并且在以消耗系统 CPU 和内存资源为代价的情况下极大地简化了 cfperl 代码。我们最终可以对它进行优化,但现在没有必要那么做。例如,如果您指定 user joe uid=1500 ,然后运行 cfperl,那么它将总是运行 usermod -u 1500 joe ,即便 joe 的 uid 已经是 1500。
users 节解析器
至于 users 部分解析器,没有什么很特别的事情。它只是对诸如 user joe uid=1500 之类的语句进行解释,并相应地调用 users_op 函数。唯一使我们感兴趣的部分是 simple_property 规则,该规则将为不获取参数的特性在特性值中放入一个 undef 值。如果您在理解该语法方面有困难,那么请参考在下面的 参考资料一节中的 Parse::RecDescent 手册参考资料。
parameterized_property 规则是对所提供的参数进行解释的关键。注:我们在这里不会拒绝任何无效的参数;参数验证在 users_op 函数中完成。
清单 8. users 节解析器
$parsers{USERS_SECTION()} = new Parse::RecDescent(q{
input: user_delete | user | group_delete | group | <error>
user: /user/i username userprop(s?)
{ ::users_op($item{username}, $item{userprop}, ::USER_OP); 1; }
user_delete: /user/i username /delete/i userprop(s?)
{ ::users_op($item{username}, $item{userprop}, ::USER_OP, ::DELETE_USER); 1; }
group: /group/i groupname groupprop(s?)
{ ::users_op($item{groupname}, $item{groupprop}, ::GROUP_OP); 1; }
group_delete: /group/i groupname /delete/i groupprop(s?)
{ ::users_op($item{groupname}, $item{groupprop}, ::GROUP_OP, ::DELETE_GROUP); 1; }
username: word
groupname: word
userprop: parameterized_property | simple_property
groupprop: parameterized_property | simple_property
# note that simple properties get a value of 'undef'
simple_property: property { $return = { $item{property} => undef }; 1 }
parameterized_property: property /=/ propvalue
{ $return = { $item{property} => $item{propvalue} }; 1 }
property: /\w+/
# we handle property values by first checking for quoted strings,
# then anything without spaces
propvalue: /".*?"/ | /\S+/
word: /\w+/
});
users_op() 函数
users_op() 函数运行全部用户和组操作功能。它由 users 节解析器使用三个参数进行调用:用户或组名称、参数散列以及模式常量( USER_OP 或 GROUP_OP )。
当使用三个参数执行 users_op() 时,该函数知道还需要第四个参数。第四个参数指定要执行的低级操作(添加用户、删除用户和更改用户;添加组、删除组和更改组)。users 节解析器并不知道正确的低级操作,它也不需要知道该操作。用户和组的删除操作是例外, DELETE_USER 和 DELETE_GROUP 操作由解析器指定,因为这些操作总是有效的。在 users_op() 中,对后端进行了检查以查看用户或组可用性:
清单 9. 获取低级用户操作
# Is the operation specified? If not, figure out the operation and
# re-run users_op. The operation will be specified from the start
# only for the DELETE_USER and DELETE_GROUP operations
unless (defined $op)
{
my $detected_backend;
out (1, "users_op: The users_check_engines are " . Dumper($users_check_backends));
# does the user/group name exist?
foreach my $check (@$users_check_backends)
{
out (1, sprintf("users_op: Checking the %s backend for %s %s",
$check,
$mode,
$name));
if ($check eq USERS_CHECK_EXTERNAL)
{
if (defined $external_user_check)
{
if (0 ==system("$external_user_check $mode $name"))
{
$detected_backend = $check;
}
}
else
{
out (0, "users_op: You requested an external check for users_check_engines,
but the external_user_check control variable is not defined");
}
}
elsif ($check eq USERS_CHECK_NIS)
{
eval
{
require Net::NIS;
my %names;
my $map = "${mode}s.byname"; # the usual name of the map is "users.byname"
# or "groups.byname"
tie %names, 'Net::NIS', $map;
if (defined $names{$name})
{
$detected_backend = $check;
}
};
if ($@)
{
out (0, "users_op: An error happened while checking the $check backend: $@");
}
}
elsif ($check eq USERS_CHECK_LOCAL)
{
if ($mode eq USER_OP && getpwnam($name) ||
$mode eq GROUP_OP && getgrnam($name))
{
$detected_backend = $check;
}
}
else
{
out (0, "users_op: The $check backend is not supported");
}
last if $detected_backend; # break out of the loop if we found the user
}
if ($detected_backend)
{
out (0, "users_op: $mode $name was detected in the $detected_backend
users_check_engine, modifying");
return users_op($name, $parameters, $mode,
($mode eq USER_OP) ? CHANGE_USER : CHANGE_GROUP);
}
else
{
out (0, "users_op: $mode $name was not detected in the
users_check_engine backends, adding");
return users_op($name, $parameters, $mode,
($mode eq USER_OP) ? ADD_USER : ADD_GROUP);
}
die "users_op reached an invalid execution point";
}
在上面的代码中,您可以看到:根据用户是否已经存在, users_op() 将使用一个新参数 重新调用它自己。它对一些相应的后端进行检查,以得出用户是否存在的结论。
同样,当且仅当请求 NIS 后端检查时,才会动态装入 CPAN Net::NIS 模块。
一旦 users_op() 知道了要执行的正确操作,它就可以简单地执行该操作:
清单 10. 构建用户/组命令
my $param_string = Dumper($parameters);
out (2, "users_op: invoked with name $name, parameters $param_string,
mode $mode, operation $op\n");
# build command string
unless (exists $system_matrix{$OSNAME})
{
out (0, "users_op: no system command matrix available for OS '$OSNAME'.
Skipping [$op $name] command");
return;
}
my $matrix = $system_matrix{$OSNAME}->{$op};
my $options = '';
foreach my $option_hash (@$parameters)
{
# note that the option hash can only have one key/value pair from
# the parser
my ($key, $value) = each %$option_hash;
if (exists $matrix->{$key})
{
if (defined $value)
{
$options .= sprintf($matrix->{$key}, $value) . ' ';
}
else # undefined values indicate a non-parameterized option
{
$options .= $matrix->{$key} . ' ';
}
}
else
{
out(0, "users_op: Unknown option $key specified, skipping");
}
}
my $command = sprintf ("%s %s %s", $matrix->{name}, $options, $name);
out (0, "users_op: Running '$command'");
my $return = system $command;
if ($return) # unsuccessful execution
{
out (0, "users_op: Command '$command' did not complete successfully");
}
else
{
out (1, "users_op: Command '$command' ran successfully");
}
}
请注意 undef 的参数值是如何告诉 cfperl:选项只有一个并且没有参数。例如, DELETE_USER 命令的 full 选项只有一个,就由 -r 指定。
结束语
使用 cfperl 灵活的解析器体系结构来添加复杂的用户和组管理功能十分方便。 users_op() 函数是用户和组管理的关键所在,它由 users 节解析器调用。