内容:
集中化的配置文件策略
Teodor Zlatanov (tzz@iglou.com)
程序员,Northern Light
UNIX 系统管理总是一个棘手的问题,运用正确的工具会使这个问题变得容易。在这一部分中,Teodor 提出了关于使用 Perl 来简化和牢固系统管理的想法。在这种环境中,系统配置引擎 cfengine 是一个极其重要的工具。
要完成本文中的练习,系统中必须安装了 Perl 5.6.0。操作系统最好是主流 UNIX 安装(Linux、Solaris、BSD)的最近版本(2000 或更新)。在较早版本的 Perl 和 UNIX 以及其它操作系统上也可以使用本文中的示例,但应当将可能的功能故障作为练习来解决。
UNIX 管理具有挑战性的一大原因是每个 UNIX 供应商认为标准是针对低能傻瓜。所以,即使是同一供应商的操作系统(SunOS 4.x 和 Solaris 5.x)也可以是根本不同。在某些情况下,甚至根本没有供应商。例如,Linux 没有单独的供应商(虽然 Red Hat 目前是最大的 Linux 分发版),每一个版本的 Linux 都有其独到之处。如果 POSIX 标准化做得正确,那么它是解决这一问题的正确方向上的一个步骤。遗憾的是,它只能保证系统管理所需功能的一个小的子集。
正如我经常所说:了解您的工具。如果试图仅用一种工具、语言、或方法做每件事情,可能是一场噩梦。要具有灵活性。
如果存在一个系统管理公理,那就是:两次过后,没有系统管理任务是有趣的。如果您发现正在重复做单调而枯燥的事,那么自动化它。当然,有时很难自动化,但应该至少考虑这个问题,并且权衡其优势及自动化所花费的时间。
cfengine 工具
如果您对自动化系统管理是认真的,那么应该了解 cfengine 工具。仅当您宁愿把时间都花在 vi 编辑器时,可以不去了解 cfengine。
cfengine 是一种系统配置引擎。它获取配置脚本作为输入,然后根据这些脚本来行动。目前版本是 1.6.3(非常稳定的发行版),而且版本 2.0 也呼之欲出。有关 cfengine 开发的更多信息,请访问 cfengine 网站(请参阅本文后面的参考资料)。
不一定要用 cfengine 提供您的所有东西,而且您不可能立刻需要所有东西。一开始时,您的 cfengine 配置文件应该很简单,并且随着发现更多东西希望自动化而增长。
来自 cfengine 命令参考大全,这里有其最值得注意的特性:
可以监控和修改文件许可权和 ACL。例如,/etc/shadow 可以与 0400/root/sys 许可权保持一致,而且如果那些许可权发生变化,可以警告系统管理员或即刻纠正它们。
根据相应 fstab 变化,可自动安装和卸载 NFS 文件。
可以通过单一文件来管理子网掩码、DNS 配置、缺省路由和主网络接口;
文件和目录可以递归复制至另一位置,要么本地复制,要么从远程服务器复制。
可以编辑(这是一个非常强大的特性,提供了正则表达式和全局查找/替换)、轮转(譬如,日志文件)或删除文件。
可以链接文件(单一的和/或目录下的所有文件或与正则表达式匹配的文件)和整个目录。
可以根据进程表中正则表达式的匹配来启动、杀死、重启进程或发送任意信号。
可以运行任意命令。
上述所有这些根据操作系统类型和修订版本、一天中的时间、任意用户定义的类、文件中文件、目录或数据的有无等等可以是有条件的。
即使用 Perl 可以做 cfengine 所做的所有事情,为什么要从头开始呢?例如,如果想用另一个词替换某个词,编辑文件可以是简单的一行程序。当开始允许系统的子类型、逻辑系统部分以及所有其它杂项因素时,这一行程序会变成 300 行。为什么不在 cfengine 中做呢?它产生 100 行可读的配置代码。
根据我自己的经验,因为可以从最小配置文件开始,然后随着时间流逝逐步地向 cfengine 添加一些东西,所以将 cfengine 介绍给站点是很容易的。没有人喜欢突然的变化,所有系统管理员更是如此(因为如果任何事出错,他们理所当然地会受到责难)。
配置文件管理
管理配置文件是艰苦的。可以通过考虑 cfengine 是否胜任该任务开始。遗憾的是,cfengine 的编辑是面向行的,所以它可能不太适合复杂的配置文件。但对于如 TCP 包装器配置文件 /etc/hosts.allow 那样的简单文件 cfengine 是最适合的。
通常,希望保留配置文件的多个版本。譬如,可能需要在 /etc/resolv.conf 中有两组 DNS 配置设置,一组是用于外部机器,另一组是用于内部机器。很自然,外部 DNS resolv.conf 可以进入称为 "external" 的目录,而内部 resolv.conf 可以进入相应的 "internal" 目录。让我们假定这两个目录都在一个全局 "spec" 目录下,该目录是配置文件的一种根目录。
下列代码会遍历 spec 目录,搜索适合于给定机器的文件名。它将从 /usr/local/spec 开始,然后往下,寻找与请求相匹配的文件。而且,它将检查每个目录的名称是否与属于某些机器的类相同。因此,如果我们请求 locate_global('resolv.conf', 'wonka'),该函数将在 /usr/local/spec 目录下查找 resolv.conf 文件,该文件要么在根目录下,要么在该根目录的子目录下,它的名称应与 "wonka" 机器所属的类相匹配。所以,如果 "wonka" 属于 "chocolate" 类,并且如果有 /usr/local/spec/chocolate/resolv.conf 文件,那么 locate_global() 将返回 "/usr/local/spec/chocolate/resolv.conf"。
如果 locate_global() 找到与文件相匹配的多个版本(譬如,/usr/local/spec/chocolate/resolv.conf 和 /usr/local/spec/resolv.conf),则它会放弃。这里假设没有配置比有两个错误之一要好。还有,请注意,机器可以属于不止一个类。
可以构建这样的结构。譬如,
/usr/local/spec/external/chocolate/resolv.conf
/usr/local/spec/internal/chocolate/resolv.conf
/usr/local/spec/external/sugar/resolv.conf
/usr/local/spec/internal/sugar
将包含外部和内部 "chocolate" 以及 "sugar" 机器的文件。只需要正确地设置 your machine_belongs_to_class() 函数。
一旦 locate_global() 返回一个文件名,将它用 scp 或 rsync 复制至远程系统是相当简单的。请记住,总是要保持该文件的许可权和属性。scp 需要 "-p" 标志,rsync 需要 "-a" 标志。查阅想要使用的文件复制命令的文档。这样就有了一个统一的配置文件树。
清单 1:Spec 目录遍历
# {{{ locate_global: use spec directory to find a file matching the current class
sub locate_global($$)
{
# this code uses File::Find
my $spec_dir = '/usr/local/spec';
my $file = shift || return undef; # file name sought
my $machine = shift || return undef; # machine name
my @matches;
my $find_sub =
sub
{
print "found file $_\n";
push @matches, $File::Find::name if ($_ eq $file);
# the machine_belongs_to_class sub returns true if a machine
# belongs to a class; we stop traversing down otherwise
$File::Find::prune = 1 unless
machine_belongs_to_class($machine, $_) || $_ eq '.';
};
find($find_sub, $spec_dir);
if (scalar @matches > 1)
{
print "More than one match for file $file,",
"machine $machine found: @matches\n" ;
return undef;
}
elsif (scalar @matches == 1)
{
return $matches[0]; # this is the right match
}
else {
return undef; # no files found
}
}
# }}}
一旦建立了这种 /usr/local/spec 结构的一个问题是:我们怎么知道 resolv.conf 应当进入 /etc?要么没有如这里所示的漂亮层次结构,改写它(譬如,用 "+" 替代 "/" - 一种危险的和有点丑陋的方法),要么在链接名与真实名之间保持单独的映射。譬如,"root-profile" 可以是 "~root/.profile" 的链接名。最后一种方法,也是我喜欢的方法,由于它平铺文件名并且消除了有隐藏文件名的问题。在一个目录结构下,每一样都是可见的和整洁的。当然,每次在将文件添加到列表时,需要多做一些工作。程序必须知道 "resolv.conf" 应该复制到远程系统的 "/etc/resolv.conf",并且 "dfstab" 应该进入 "/etc/dfs/dfstab"(共享 NFS 文件系统的 Solaris 文件)。
一旦设置完 spec 目录层次结构,现在让我们讨论可以做什么。如果想做,可以查找所有名为 Joe 的用户:
清单 2:查找所有 password 文件并用 grep 找出 Joe
grep Joe `find /usr/local/spec -name passwd`
或者可以使用工具,如 rep.pl(链接到 rep.pl),由 David Pitts 编写,来用另一个词替换每一个词:
清单 3:查找所有 host 文件并将 "wonka" 改成 "willy"
find /usr/local/spec -name hosts -exec rep.pl wonka willy {} \;
现在,如果愿意,可以用 Perl 编写清单 2 和 3;find2perl 就是为此编写的实用程序。虽然它非常简单,从开始只使用 find。它真的是极好的实用程序,每个系统管理员都应该使用。更重要的是,编写这两个清单只花了我 5 分钟。了解如何使用 find2perl,将它生成的代码存储在文件中,然后运行该文件,要花多长时间呢?自己试试看!
任务自动化
任务自动化是一个很泛的主题。我将本节仅限于非交互式 UNIX 命令的简单自动化。对于交互式命令的自动化,Expect 是当前可用的最好工具。应该要么了解它的语法,要么用 Perl Expect.pm 模块。可以从 CPAN 获取 Expect.pm;请参阅参考资料以了解更多详细信息。
利用 cfengine,可以根据任意标准自动化几乎任何任务。但是,它的功能非常象 Makefile 功能,对变量的复杂操作是很难处理的。当发现需要运行这样的命令,该命令的参数来自于散列或通过单独的函数时,通常最好切换到 shell 脚本或 Perl。由于 Perl 的功能,其可能是较好的选择。虽然,不应该将 shell 脚本弃为替代来使用。有时,Perl 是不必要的,您只需要运行一些简单的命令。
自动添加用户是一个常见问题。可以编写自己的 adduser.pl 脚本,或者用大多数现代 UNIX 系统提供的 adduser 程序。请确保使用的所有 UNIX 系统间语法是一致的,但不要尝试编写一个通用的 adduser 程序接口。它太难了,在您认为涵盖了所有 UNIX 变体后,迟早会有人要求 Win32 或 MacOS 版本。这不是仅仅用 Perl 就能解决的问题之一,除非您是非常有野心的。这里只是让脚本询问用户名、密码、主目录等等,并以 system() 调用来调用 adduser。
清单 4:用简单的脚本调用 adduser
#!/usr/bin/perl -w
use strict;
my %values; # will hold the values to fill in
# these are the known adduser switches
my %switches = ( home_dir => '-d', comment => '-c', group => '-G',
password => '-p', shell => '-s', uid => '-u');
# this location may vary on your system
my $command = '/usr/sbin/adduser ';
# for every switch, ask the user for a value
foreach my $setting (sort keys %switches, 'username')
{
print "Enter the $setting or press Enter to skip: ";
$values{$setting} = ;
chomp $values{$setting};
# if the user did not enter data, kill this setting
delete $values{$setting} unless length $values{$setting};
}
die "Username must be provided" unless exists $values{username};
# for every filled-in value, add it with the right switch to the command
foreach my $setting (sort keys %switches)
{
next unless exists $values{$setting};
$command .= "$switches{$setting} $values{$setting} ";
}
# append the username itself
$command .= $values{username};
# important - let the user know what's going to happen
print "About to execute [$command]\n";
# return the exit status of the command
exit system($command);
用 Perl 来处理的另一个常见任务是监控和重新启动进程。通常,这是用 Proc::ProcessTable CPAN 模块进行的,它浏览整个进程表,并返回给用户带许多重要属性的进程列表。然而,在这里,我必须推荐 cfengine。与快速的 Perl 工具相比,它提供了更好的进程监控和重新启动进程的选项。如果您想编写这样的工具,那么这只是在做别人做过的事情(而且 cfengine 已经偷了您的轮毂盖)。如果由于个人原因,不想用 cfengine,考虑一下大多数现代 UNIX 系统中附带的 pgrep 和 pkill 实用程序。pkill -HUP inetd 将用一条简洁的命令可以做四行或更多行 Perl 脚本所做的事情。这就是说,如果正在做的进程监控是很复杂或对时间敏感,那么应该明确用 Perl。
为了完整性缘故,这里是一个演示了如何使用 kill() Perl 函数的 Proc::ProcessTable 示例。"9" 作为参数,是最强的 kill() 参数,大体意味着“不管三七二十一,杀死进程再说”。不要以 root 运行这条命令,除非真想杀死 inetd 进程。
清单 5:遍历进程,然后杀死所有 inetd
use Proc::ProcessTable;
$t = new Proc::ProcessTable;
foreach $p (@{$t->table})
{
# note that we will also kill "xinetd" and all processes
# whose command line contains "inetd"
kill 9, $p->pid if $p->cmndline =~ 'inetd';
}
结束语
UNIX 系统管理最让人失望的部分是 UNIX 供应商逃避标准而找到的各种方式。由于这种原因,当 Perl 单独应付 UNIX 系统中所有问题时,它是无能为力的。如果没有象 cfengine 这样的工具,象密码文件语法、共享文件系统以及跟踪日志等问题很快就变得无法管理。然而,还是存在一些希望;毕竟,我们只是查看了 Perl 可简化系统管理的一些方法。
Perl 与 cfengine 结合得很好。可以用 Perl 生成定制的 cfengine 配置,或者可以从 cfengine 运行 Perl 脚本。我用过这两者,发现集成不难。然而,cfengine 受过分简单的配置语言和缺乏数据结构影响。我将在有关 cfengine 的未来文章中展开这一问题。
如果选择实现的话,本文中介绍的集中化配置文件策略应当是非常实用的。在我的站点上现在已经使用了六个月,而且获得了巨大成功。如果将完整的层次结构检入一个如 CVS 那样的版本控制系统,您还将享受到版本化系统文件的好处,即可以回复到已检入版本控制系统的任一状态。
参考资料
请访问 CPAN 以查找您所感兴趣的所有 Perl 模块。
Perl.com 有关于 Perl 信息和相关资源。
请获取 cfengine 模块,这是多合一的 UNIX 管理工具。
UNIX System Administration Handbook, 3rd Edition,由 Evi Nemeth、Garth Snyder、Scott Seebass 和 Trent R. Hein 著 (Prentice Hall 2000),描述了系统管理许多不同方面,从基本问题到 UNIX 秘籍,并且明确地包含了四种流行的系统:Solaris 2.7、Red Hat Linux 6.2、HP-UX 11.00 和 FreeBSD 3.4。
Programming Perl Third Edition,由 Larry Wall、Tom Christiansen 和 Jon Orwant 著 (O'Reilly & Associates 2000), 这本书是现今最好的、针对 Perl 5.005 和 5.6.0 的指南。
Perl for System Administration: Ma