上篇说到怎样使用ENC脚本控制puppet的客户端配置,这篇说如何监控和展示客户端运行状态报告。
目前还是使用puppet默认的store方式,也就是报告都存在/var/lib/puppet/reports/$host/$dates.yaml里。所以分析只要针对这个目录下的文件即可,主要使用File::Stat和File::Find两个模块搞定。注意这两个模块在Perl5.16里是默认内核模块了~~
#!/usr/bin/perl
use strict;
use warnings;
use autodie;
use File::Stat qw/:stat/;
use YAML::Syck;
use AnyEvent::Filesys::Notify;
use EV;
my $interval = "60";
my $watch_dir = "/var/lib/puppet/reports";
my $periodic = EV::periodic 0, $interval, 0, sub {
my $dirs = watchdir($interval);
process( $_, 'No reports return.' ) for @{$dirs};
};
my $notifier = AnyEvent::Filesys::Notify->new(
dirs => [ $watch_dir, ],
interval => 0.5,
filter => sub { shift =~ /\.yaml$/ },
cb => sub {
my @events = @_;
for ( @events ) {
if ( $_->type =~ m/^(created|modified)$/ ) {
my $file = $_->path;
my $logs = LoadFile($file)->{'logs'};
for ( @{$logs} ) {
if ( $_->{'level'} eq 'err' ) {
process( $file, $_->{'message'} );
};
};
};
};
},
);
EV::loop();
sub process {
my ( $path, $message ) = @_;
if ( $path =~ m/\/([^\/]+\.opi\.com)/ ) {
print $1," has err: ",$message,"\n";
};
};
sub watchdir {
my $interval = shift;
my $dirs = grep { time - stat($_)->[9] > $interval } glob("${watch_dir}/*");
return $dirs;
};
上面这个脚本,通过libev的timer和io分别完成对diretory的mtime的遍历和对file的inotify的监听。process作为公共处理函数,可以随意改造成sms/email/msn等等方式。
定时器没有用AnyEvent的封装,因为没看到AE有periodic,只有timer。而在io运行的时候,timer是中断的。如果不停有文件inotify发生,timer就没法进行了……periodic的方式与timer不同,是绝对定时而不是相对定时——虽然我个人的浅薄理解觉得应该也被io阻塞,但试验结果是OK的。
#!/usr/bin/perl
use strict;
use warnings;
use autodie;
use Dancer;
use Template;
use YAML::Syck;
use File::Find;
use File::Stat qw/:stat/;
use POSIX qw/strftime/;
use Data::Section::Simple qw/get_data_section/;
set port => "8080";
set daemon => 1;
set logger => 'console';
my $tt = Template->new(
# DEBUG => 'all',
);
my $ds_check = get_data_section('check.tt');
get '/' => sub {
my $html = '<form action="/ppcheck">';
$html .= 'Write an interval minutes for reports timestamp check: ';
$html .= '<input type="text" name="interval"/> <input type="submit" value="submit" />';
$html .= '</form>';
return $html;
};
get '/ppcheck' => sub {
my $interval = params->{'interval'} * 60 || 300;
my $watch_dir = "/var/lib/puppet/reports";
return "Too large number" if $interval > 60 * 60 * 24; # one day
my $context = {};
$context->{'dirs'} = watch_timeout( $interval, $watch_dir );
$context->{'logs'} = watch_errlogs( $interval, $watch_dir );
my $output;
$tt->process(\$ds_check, $context, \$output);
# $tt->process(\*DATA, $context, \$output);
# seek *DATA, 1234, 0;
return $output;
};
sub watch_timeout {
my ( $interval, $watch_dir ) = @_;
my @dirs = grep { time - stat($_)->[9] > $interval } glob("${watch_dir}/*");
my @ret;
for ( @dirs ) {
my $dirtime = strftime("%F %T", localtime(stat($_)->[9]));
my $dirname = $1 if $_ =~ s#([^/]+\.opi\.com)#$1#;
push @ret, {name => $dirname, time => $dirtime, };
};
return \@ret;
};
sub watch_errlogs {
my( $interval, $watch_dir ) = @_;
my( $wanted, $list_reporter ) = find_file_by_mtime($interval);
File::Find::find( $wanted, $watch_dir );
my @ret = $list_reporter->();
return \@ret;
};
sub find_file_by_mtime {
my $interval = shift;
my @found = ();
my $finder = sub {
if ( -f $File::Find::name && time - stat($File::Find::name)->[9] < $interval ) {
my $yaml = YAML::Syck::LoadFile($File::Find::name);
my @logs = grep { $_->{'level'} eq 'err' } @{$yaml->{'logs'}};
for ( @logs ) {
push @found, { host => $yaml->{'host'}, message => $_->{'message'}, };
};
};
};
my $reporter = sub { @found };
return( $finder, $reporter );
};
dance;
__DATA__
@@ check.tt
<div id="timeoutdirs">
List of nodes whose report is timeout: <br />
<ul>
[% FOREACH dir IN dirs %]
<li style="width:200px;float:left">[% dir.name %]</li><li style="width:200px;margin:0;float:left">[% dir.time %]</li>
[% END %]
</ul>
</div>
<br /><hr /><br />
<div id="runerrlogs">
Error messages of running nodes: <br />
<ul>
[% FOREACH log IN logs %]
<li style="width:200px;float:left">[% log.host %]</li>
<li style="width:400px;margin:0;float:left">[% log.message %]</li>
[% END %]
</ul>
</div>
这个脚本实现的功能其实和上面那个类似。不过报警改成web页面,event触发改成web请求触发。
这里两个新难点:
其一是没有inotify后如何根据web请求参数查找范围内新建的报表。File::Find模块只有一个函数find(\&wanted,@dirs)。其中&wanted是不能传参进去的。
在CPAN上看到一个叫做File::Find::Closures的模块,提供了一系列可以给File::Find使用的&wanted函数,包括一个示例。于是稍微改造一下,就写成了find_file_by_mtime()函数。
其二是因为偷懒没有用dancer建立完整项目,使用了perl virtual file来提供template。所以不能直接使用Dancer的template ‘ttname’, {var=>$var};定义了。
Template::Toolkit提供的process()可以操作的template来源很多,可以是字符串/文件/句柄。所以process(*DATA)也是生效的。但是问题出现了:这样启动后,第一次访问正常;第二次访问返回空!
打开Template的DEBUG看到,第二次访问的时候,从*DATA里读不到数据了。也就是说,必须重新seek回去——而且试验证明seek的起始点是shebang行。。。。
所以只能先从__DATA__里读出数据,然后以字符串形式传递给process()了。
这里用到了Data::Section模块。从CloudForecast项目里学来的。CloudForecast中的web页面,使用的Text::Xslate模板技术读取__DATA__。其中包括有index/server/servers/service等页面。也就是说,在一个__DATA__里实现了多个virtual file。用的就是Data::Section::Simple模块。以@@为标签分割即可。