更佳编程之路: 第 10 章. 使用 cfperl 进行用户管理_VMware, Unix及操作系统讨论区_Weblogic技术|Tuxedo技术|中间件技术|Oracle论坛|JAVA论坛|Linux/Unix技术|hadoop论坛_联动北方技术论坛  
网站首页 | 关于我们 | 服务中心 | 经验交流 | 公司荣誉 | 成功案例 | 合作伙伴 | 联系我们 |
联动北方-国内领先的云技术服务提供商
»  游客             当前位置:  论坛首页 »  自由讨论区 »  VMware, Unix及操作系统讨论区 »
总帖数
1
每页帖数
101/1页1
返回列表
0
发起投票  发起投票 发新帖子
查看: 3798 | 回复: 0   主题: 更佳编程之路: 第 10 章. 使用 cfperl 进行用户管理        下一篇 
谁是天蝎
注册用户
等级:大元帅
经验:90210
发帖:106
精华:0
注册:2011-7-21
状态:离线
发送短消息息给谁是天蝎 加好友    发送短消息息给谁是天蝎 发消息
发表于: IP:您无权察看 2011-8-25 16:38:17 | [全部帖] [楼主帖] 楼主

如同文章的标题所示,本文是连载中的系列文章的一部分。我们建议您先阅读 前面几章以了解 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 节解析器调用。




赞(0)    操作        顶端 
总帖数
1
每页帖数
101/1页1
返回列表
发新帖子
请输入验证码: 点击刷新验证码
您需要登录后才可以回帖 登录 | 注册
技术讨论