用Perl管理Linux配置文件

来源:岁月联盟 编辑:zhu 时间:2009-03-02

  一般的开发者用于定位、学习和调试配置文件的所花费的时间,比您预期的要更长。不过,使用一个您可能每天都在用的工具:CVS 树,您就可以节约那些时间——并减轻精力和挫折的负担。使用这些技巧来备份、发布最令您头痛的 Linux ™(和 UNIX ®)配置文件,并使其变得可移植。

  使用 Linux 和计算机时,配置文件的用法通常是令人迷惑的。尽管已经提出了一些,但现在还不存在标准。例如,Samba 和 rsync 使用 INI 风格的配置;passwd 用的是几十年前的用冒号隔开的格式,不允许冒号出现在任何域中;sudo 附带了一些 visudo 程序,让人们不会在 sudoers 文件中输入错误的信息;Emacs 使用 Lisp 作为配置文件。还有...

  现在,我不再抱怨配置文件的多样性。我理解了这个配置通天塔(Configuration Tower of Babel)的历史原因和实践原因。例如,如果改变了 Samba 配置的格式,就会使上千的管理员面临麻烦。另一个例子,Emacs 的内部语言是 Lisp,这是一门强大的高层次语言,所以,使用任何其他东西作为 Eamcs 的配置文件都是荒谬的。

  不,我要指出的是这一多样性对 Linux 用户造成的影响:Linux 用户的计算机时间有一大部分用在学习、编写和调试配置文件。这样,有必要拥有一个系统,在这个系统中这些配置文件(1)是自动备份的,(2)是自动发布的,(3)可以用于多种风格的 UNIX 和多种 Linux 的发行版本。本文阐明了如何达成前两个目标,并引导您走上达成第三个目标的途径。

  计划

  我们将使用 CVS 来控制配置文件。可以随意使用任何其他版本系统。Subversion 正在迅速流行。FSF 有 GNU tla( GNU arch),是另一个优秀的版本系统。所有那些以及很多其他系统,包括并不免费的 Rational ®ClearCase ® 等,都会提供您所需要的功能。

  在我的配置模式中,每个配置文件在一个单独的目录或者其子目录中。配置文件被唯一命名,目录表示的是 机器或者平台,而不是 位置。这样,文件名唯一地映射到文件系统中的一个位置。例如, passwd 将总是用于 /etc/passwd,而 cshrc 将由用户 tzz用于 /home/tzz/.cshrc。

  对于我日常使用的一些程序,我将展示如何在我的配置系统的帮助下来管理多个平台,使它们自己修改配置文件。

  我展示的所有例子都使用 C shell 来设置环境变量。修改它们以使用 GNU bash 或者其它 shell 应该不是特别困难。

  安装设置 CVS

  您可能已经在您的机器上安装了 CVS。如果没有,那么获取(查看 参考资料 部分)并安装它。如果您正在使用另一个版本系统,那么尝试设置类似我下面展示的一些内容。

  首先,您需要创建一个 CVS 仓库。我将假定您可以通过 OpenSSh 或 Pserver CVS access(Pserver 是 CVS 所使用的一个通信协议;查看 参考资料 以获得更多资料)访问一台可以用作 CVS 服务器的机器。然后,您需要创建一个名为 config 的模块,我将用它来管理示例配置文件。最后,您需要安排一个远程非交互地使用您的 CVS 仓库的途径,可以通过 OpenSSH、Pserver 或者任何可行途径。最后一点极度依赖于您具体的系统管理技巧、偏执程度以及环境,所以我只是在 参考资料 中为您指出了一些资料。在本文的其余部分,我将假定您已经配置了通过 OpenSSH 进行的非交互(ssh-agent)登录。

  清单 1. 在一台机器上建立 CVS 仓库

# assume that /cvsroot is your repository's home
> setenv CVSROOT /cvsroot
# this will use $CVSROOT if no -d option is specified
> cvs init
# check that it worked
> ls /cvsroot
# you should see one directory called CVSROOT
CVSROOT

  既然仓库已经建立起来,您接下来就可以远程使用它(您也可以在 CVS 服务器上执行下面的步骤——只是让 CVSROOT 仍是如清单 1 中所示)。

  清单 2. 远程地向 CVS 添加 config 模块

# user tzz, machine home.com, directory /cvsroot is the CVSROOT
> setenv CVSROOT tzz@home.com:/cvsroot
# use SSH as the transport
> setenv CVS_RSH ssh
# use a temporary directory for the module creation
> cd /tmp
> mkdir config
> cd config
# tzz is the "vendor name" and initial is the "release tag", they can
# be anything; the -m flag tells CVS not to ask us for a message
# if this fails due to SSH problems, see the Resources
> cvs import -m '' config tzz initial
No conflicts created by this import
# now let's do a test checkout
> cd ~
> rm -rf /tmp/config
> cvs co config
cvs checkout: Updating config
# check everything is correct
> ls config
CVS

 

  现在您已经在主目录下查验了 config CVS 模块的一个拷贝;我们将以此为出发点。本文中我将使用我的用户名 tzz 以及主目录 /home/tzz,不过,当然,您应该恰当地使用您自己的用户名和目录。

  让我们来创建一个单独的文件。CVS 选项文件 cvsrc 看起来比较合适,因为我将会更多地用到 CVS。

  清单 3. 创建并添加 cvsrc 文件

> cd ~/config
> echo "cvs -z3" > cvsrc
> echo "update -P -d" >> cvsrc
> cvs add cvsrc
# you really don't need log messages here
> cvs commit -m ''
> ln -s ~/config/cvsrc ~/.cvsrc

  从此以后,您的所有的 CVS 选项都将位于 ~/config/cvsrc 中,您将更新那个文件而不是 ~/.cvsrc。您所添加的特定选项告诉 CVS 当目录不存在时重新找回目录,以及删除空目录。这通常是用户所期望的。对于其他您希望这样设置的机器来说,您需要再次查验 config 模块,并重新做链接。

  清单 4. 查验 config 模块并构造 cvsrc 链接

> cd ~
# set the following two for remote access
> setenv CVSROOT ...
> setenv CVS_RSH ...
# now check out "config" -- this will get all the files
> cvs checkout config
> cd ~/config
> ln -s ~/config/cvsrc ~/.cvsrc

 

  除了刚才您创建的符号链接以外,您可能知道 Linux 也支持硬链接。出于硬链接的局限性,它们不适用于这一模式。例如,假设您创建了一个 ~/.cvsrc 到 ~/config/cvsrc 的硬链接,而后来您又移动了 ~/config/cvsrc (很多条件下会发生这种情况)。~/.cvsrc 文件将仍然持有 ~/config/cvsrc 的原有的旧内容。现在,您再次查验 ~/config/cvsrc。不过,~/.cvsrc 文件将不会被更新。这就是为什么符号链接在这种情形下更好的原因。

  让我们假定您修改 cvsrc 以添加更多选项:

  清单 5. 修改并提交 cvsrc

> cd ~/config
> echo "checkout -P" > cvsrc
> cvs commit -m ''

 

  现在,为了更新您所使用的每一台机器上的 ~/.cvsrc,只需要做下面的工作:

  清单 6: 修改并提交 cvsrc

> cd ~/config
> cvs update

 

  这很简单。更令人满意的是,上面所展示的 CVS 更新将更新 ~/config 中的 每一个 文件,所以,使用一个命令您就可以立即使得在这种 CVS 模式下保持的文件成为最新的。这是这里所展示的配置模式的本质;其他的只是起辅助作用。

  注意,一旦您查验了一个模块,在其中就会有一个名为“CVS”的目录。CVS 目录中有关于 CVS 的足够的信息,不需要指定 CVSROOT 变量您就可以做更新、提交以及其他 CVS 操作。

  自动更新和提交

  为了自动更新和提交,我已经编写了一个特别简单的 Perl 程序,maintain.pl。程序中最长的部分是帮助文本,所以您可以想像到它不全是复杂的代码。不管怎样,我将详细描述它,不过不要忘记,如果需要,shell 脚本可以完成同样的任务。

  maintain.pl 唯一不做的事情是构造符号链接。由于符号链接只能构造一次,而且在一些系统上您 不 希望大规模构造链接,所以任务的复杂相对于手工完成此任务的简单就一目了然了。我之所以知道是因为我曾经编写了符号链接代码,后来又删除了。

  我不得不编写并维持另一个映射到很多操作系统的配置文件。会有很多异常;例如,我使用的 Linux 和 Solaris 系统有着本质上不同的设置。有太多的事情需要考虑,而我发现手工安装链接更为简单。当然,您的体验可能是不同的——我鼓励您去尝试找出最适合您自己的环境的方法。

  maintain.pl 脚本的开头是按惯例的定义,包括配置选项、命令行参数的加载以及帮助文本。

  清单 7. maintain.pl 脚本的预备工作

#!/usr/local/bin/perl -w
# {{{ modules and constants
use strict;
use AppConfig qw/:expand :argcount/;
# }}}
$| = 1;        # autoflush the output
my $config = AppConfig->new();
$config->define(
'HELP'   =>
{ ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'H'},
# update level, higher checks out more
'LEVEL'  =>
{ ARGCOUNT => ARGCOUNT_ONE, DEFAULT => 5 },
'CONFFILE' =>
{ ARGCOUNT => ARGCOUNT_ONE, ALIAS => 'F',
  DEFAULT => glob("~/config/maintain.conf") },
'CVS'   =>
{ ARGCOUNT => ARGCOUNT_ONE, DEFAULT => 'cvs' },
'CVS_RSH' =>
{ ARGCOUNT => ARGCOUNT_ONE, DEFAULT => 'ssh' },
'UPDATE'  =>
{ ARGCOUNT => ARGCOUNT_HASH },
'DRYRUN'  =>
{ ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'N' },
'COMMIT'  =>
{ ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'C' },
);
$config->args();
if (-r $config->CONFFILE() && -f $config->CONFFILE())
{
$config->file($config->CONFFILE());
}
else
{
print "The file " . $config->CONFFILE() .
    " was not readable, skipping
";
}
if ($config->HELP())
{
print <<EOHIPPUS;
$0
Run $0 without any arguments to load
@{[$config->CONFFILE()]}
and update everything in it at level
@{[$config->LEVEL()]} or less.
Switches:
-level (default @{[$config->LEVEL()]}) :
  check out everything at this level or less
-help (-h) : print this help
-conffile (-f, default @{[$config->CONFFILE()]}) :
  load this configuration
-cvs (default @{[$config->CVS()]}) :
  where to find the cvs program
-cvs_rsh (default @{[$config->CVS_RSH()]}) :
  sets the CVS_RSH environment variable
-update : populate the UPDATE hash in the configuration
      file or like this:
      -update /home/tzz/      see below for explanation
-commit (-c) : don't just update, also do a commit of
        anything changed
-dryrun (-n) : don't run anything, just test directories
        and levels
Configuration file:
Very simple AppConfig format; everything in the switches can be
specified in the configuration file as well, e.g.
COMMIT = 1
UPDATE /home/tzz/config = 0
The example above says that /home/tzz/config will be updated at level
0 or higher, and that you always want to commit when you run this
program.
EOHIPPUS
exit 0;
}
$ENV{CVS_RSH} = $config->CVS_RSH();

  如果您不熟悉 AppConfig 模块,那么您应该查看 参考资料 部分,以获得关于管理配置的实用资料。

  我编写了一个 glob() 调用来测定默认的 CONFFILE,因为用户的主目录可能在任意位置。如果 CONFFILE 中包含非法数据, AppConfig 就会自动地停止整个程序(这可以修改为只提出一个警告)。脚本甚至不需要配置文件就可以运行。

  打印了帮助文本后,我将 CVS_RSH 设置为适当的值(默认是 ssh )。这是为了让用户不必再以其他方式设置环境变量,这对那些将 maintain.pl 放入 crontab 中的用户来说尤其方便。

  完成预备工作后,让我们来看脚本的核心部分:

  清单 8: maintain.pl 的主循环

foreach my $spot (keys %{$config->UPDATE()})
{
my $level = 0 $config->UPDATE()->{$spot};
next if $level > $config->LEVEL();
print "Spot $spot, Level $level
";
chdir $spot;
if ($config->DRYRUN())
{
 print "Not updating due to DRYRUN
";
}
else
{
 system($config->CVS() . " -q update");
}
if ($config->COMMIT())
{
 if ($config->DRYRUN())
 {
  print "Not committing due to DRYRUN
";
 }
 else
 {
  system($config->CVS() . " commit -m ''")
 }
}
}

 

  这是一个简单的循环。我遍历每个 spot (这些实际上是目录),如果 spot 的级别低于或等于 LEVEL 配置变量(默认为 5),则执行 cvs update 。另外,如果设置了 COMMIT 标记,我会执行 cvs commit -m '' ,这将提交所有的修改,并给出一个空日志消息。实际上,如果没有 DRYRUN 标识,这个循环将只有几行长。

  我以字符串格式而不是多参数格式来使用 system() 。您可以用第二种方式来使用——查看 perldoc -f system 以获得关于这个函数调用用法的细节。

  此外,我没有检查 system() 调用 的结果,因为没有必要。在发生 CVS 更新或提交问题的时,maintain.pl 不能(或者不应该)任何事情,因为这些是重要的配置文件,我们不希望盲目地更新。

  配置文件本身是简单的:

  清单 9. maintain.conf

# the number is the update level
UPDATE /home/tzz/emacs = 0
UPDATE /home/tzz/config = 0
UPDATE /home/tzz/articles = 1
UPDATE /home/tzz/gnus/gnus = 1

 

  不要忘记,在这里您可以设置任何 AppConfig 变量 ,所以,举例来说,您可以覆盖默认的 LEVEL 或 CVS_RSH 。我通过 maintain.pl 更新我的 Emacs、config、articles 和 gnus 目录,不过它们的更新级别不同,以反映我更新的频率(我每天更新级别为 0 的条目两次,级别为 1 的一次)。

  组织您的新配置

  本节的内容将涵盖我个人关于迄今为止您建立的配置系统的经验。请随意利用这些思想,不过不要忘记我的个人设置并不是对每个人都适用。

  我基于机器和操作系统以及它们所需要的具体程度来保存目录。例如,我在“linux”下保存 Linux 独有的配置,由于我的家用机器“heechee”有一个特殊的键盘,因此我有一个用于 heechee 独有配置的 heechee 目录。

  不过,覆盖规则应该是,如果您可以在一个文件中而不是在用于多个平台的多个版本中描述一个配置,那么就那样去做。否则,您的大部分时间将消耗于维护同一个文件的两个或更多版本,那可不有趣。

  让我们以我的 cshrc 文件中的一个例子开始,这个文件的一个版本可以用于所有机器。我利用了 C shell 语言内置的判断逻辑来做出抉择:

  清单 10. 为不同平台定义 precmd

switch ($OSTYPE)
case "solaris":
case "SunOS":
 alias precmd '/bin/echo "33]0;${HOST}:$cwd07c"'
breaksw
case "linux":
 alias precmd 'echo -n "33]0;${HOST}:$cwd07"'
breaksw
endsw

 

  上面的命令指明了同一内容的不同版本。Linux echo 需要一个 -n 开关来避免打印到一个新行,而 Solaris 版本需要在字符串末尾有一个 c 。这样做的效果是,每当打印提示时,让 xterm 窗口的标题设置为 HOST:/DIRECTORY 。

  显然,只要您可以在配置文件本身中做出决定,就不需要在不同的目录中生成同一个文件的多个版本。例如,用于我经常使用的所有大约六种机器的 Emacs 配置只有一个版本——其中一些运行的是 Emacs 20,那可是很多年前的老古董!

  有时您不得不做一些分解。例如,xmodmaprc 文件设置的是键盘编码(keycodes)和键名之间的映射(还可以做很其他事情)。我在 ~/config/heechee/xmodmaprc 中保存了一个版本用于我的家用机器,在 ~/config/sun/xmodmaprc 中保存了另一个版本用于所有我使用的 Sun 机器。xmodmaprc 中没有逻辑,所以分解是唯一的解决方法。不过,我确实为所有的 Sun 机器只创建了一个 xmodmaprc 文件,因为他们的键盘类型相同。

  crontab 文件(我保存在 ~/.crontab 中,定时地重装装载到 crontab)是需要为每台机器指定的配置文件的极端的例子。我的家用机器的 crontab 将不会适用于任何其他机器,而且在标准的 crontab 格式中没有逻辑可以用来在 cron 任务中基于任何除了时间以外的条件进行选择。

 

  关键是您应该确定一个配置文件是否需要有多个版本,然后决定组织那些多个版本的最佳方式。您的目标应该是拥有一个稳定的环境,不需要花费很多时间用于编写和维护配置文件。我希望可以证实本文中阐述的技术对您探求配置的理想境界有所帮助。

  结束语

  我希望您觉得本文有趣而且实用。尽您所能去利用它——我已经用了多年的时间来完善我的设置,它应该会为您带来好处。

  多次分步骤转换到这个模式,不要被吓倒。您可以轻松地用几天时间来重新编写您的配置——所以逐步来完成,您将从这个过程中得到乐趣。

  您将看到的最大的好处是自动更新功能。在您的任何机器上,您可以提交一个文件,然后下次 maintain.pl 运行时它就会出现于所有其他机器上。即使您不赞同那个目录结构,也应该考虑自动更新的能力以及它们会如何给您带来帮助。

  您获得的第二个好处是配置存档。您的配置的每个版本都将在版本控制系统中!如果您犯了错误,可以恢复到一个较早的版本。如果您失去了整个机器,比如,出现了硬盘错误——您可以在几分钟之内恢复花了很多时间为其编写的所有配置文件。

  不要尝试将所有内容转换到这一模式。只转换您希望保存或重用的内容。二进制文件不适于使用 CVS——至少,不具备 CVS 为文本文件所提供的 diff 能力。此外,重命名目录时 CVS 会有问题,尽管当然有可能您也重命名了仓库中的目录。

  最后,为您的 CVSROOT 仓库做好备份,不管是在哪里。希望您永远不要用到它们。