File Coverage

blib/lib/App/OpenVZ/BCWatch.pm
Criterion Covered Total %
statement 161 181 88.9
branch 27 40 67.5
condition 7 16 43.7
subroutine 21 23 91.3
pod 2 2 100.0
total 218 262 83.2


line stmt bran cond sub pod time code
1             package App::OpenVZ::BCWatch;
2              
3 2     2   64481 use strict;
  2         8  
  2         50  
4 2     2   8 use warnings;
  2         3  
  2         50  
5 2     2   405 use boolean qw(true false);
  2         3053  
  2         8  
6              
7 2     2   132 use Carp qw(croak);
  2         4  
  2         104  
8 2     2   9 use File::Basename qw(basename);
  2         4  
  2         143  
9 2     2   970 use File::HomeDir ();
  2         8972  
  2         38  
10 2     2   11 use File::Spec ();
  2         4  
  2         158  
11 2     2   927 use Mail::Sendmail qw(sendmail);
  2         26574  
  2         114  
12 2     2   1296 use Storable qw(store retrieve);
  2         5163  
  2         121  
13 2     2   17 use Sys::Hostname qw(hostname);
  2         4  
  2         626  
14              
15             our $VERSION = '0.07';
16              
17             sub new
18             {
19 1     1 1 566 my $class = shift;
20 1         7 my %args = @_;
21              
22 1 100   3   9 my $defined_or = sub { defined $_[0] ? $_[0] : $_[1] };
  3         24  
23              
24             my $self = bless {
25             Config => {
26             input_file => $args{input_file} || '/proc/user_beancounters',
27             data_file => $args{data_file} || File::Spec->catfile(File::HomeDir->my_home, 'vzwatchd.dat'),
28             _field_names => [ qw(uid resource held maxheld barrier limit failcnt) ],
29             _exclude_fields => [ qw(uid resource) ],
30             monitor_fields => $args{monitor_fields} || [ qw(failcnt) ],
31             mail => {
32             from => $args{mail}->{from} || 'root@localhost',
33             to => $args{mail}->{to} || 'root@localhost',
34             subject => $args{mail}->{subject} || 'vzwatchd: NOTICE',
35             },
36             sleep => $defined_or->($args{sleep}, 60),
37             verbose => $defined_or->($args{verbose}, false),
38 1   50     25 _tests => $defined_or->($args{_tests}, false),
      33        
      50        
      50        
      50        
      50        
      33        
39             }
40             }, ref($class) || $class;
41              
42 1         5 $self->_init;
43              
44 1         5 return $self;
45             }
46              
47             sub process
48             {
49 0     0 1 0 my $self = shift;
50              
51 0         0 delete @$self{qw(data stored)};
52              
53 0         0 $self->_get_data_running;
54 0         0 $self->_get_data_file;
55 0         0 $self->_compare_data;
56 0         0 $self->_put_data_file;
57             }
58              
59             sub _init
60             {
61 1     1   1 my $self = shift;
62              
63 1 50       2 eval { store({}, $self->{Config}->{data_file}) }
  1         8  
64             or croak "Cannot store to $self->{Config}->{data_file}: $!";
65              
66 1         232 my $pkg_tmpl = join '::', (__PACKAGE__, '_template');
67 2     2   13 no strict 'refs';
  2         4  
  2         3086  
68              
69 1 50       2 if (defined ${$pkg_tmpl}) {
  1         7  
70 0         0 $self->{template} = ${$pkg_tmpl};
  0         0  
71             }
72             else {
73 1         2 ${$pkg_tmpl} = $self->{template} = do {
  1         4  
74 1         4 local $/ = '__END__';
75 1         6 local $_ = ;
76 1         3 chomp;
77 1         5 s/^\s+//;
78 1         10 s/\s+\z//;
79 1         4 $_
80             };
81             }
82              
83             $self->{excluded} = {
84             map {
85 7         20 my $field = $_;
86 7 100       8 (scalar grep $_ eq $field, @{$self->{Config}->{_exclude_fields}})
  7         19  
87             ? ($field => true)
88             : ($field => false)
89 1         2 } @{$self->{Config}->{_field_names}}
  1         3  
90             };
91              
92 1         8 my $i;
93 1         2 $self->{index} = { map { $_ => $i++ } @{$self->{Config}->{_field_names}} };
  7         13  
  1         2  
94             }
95              
96             sub _get_data_running
97             {
98 1     1   5 my $self = shift;
99              
100             open(my $fh, '<', $self->{Config}->{input_file})
101 1 50       35 or croak "Cannot read $self->{Config}->{input_file}: $!";
102 1         2 my $output = do { local $/; <$fh> };
  1         3  
  1         30  
103 1         10 close($fh);
104              
105 1         2 my $valid_format = join '\s+', @{$self->{Config}->{_field_names}};
  1         5  
106              
107 1 50       20 unless ($output =~ /$valid_format/) {
108 0         0 croak "Format of $self->{Config}->{input_file} not recognized";
109             }
110              
111 1         6 my $re = qr{
112             \s*?
113             (?:\d+?\:)? \s+?
114             (?:\w+?) \s+?
115             (?:(?:\d+?) \s*?){5}
116             }x;
117              
118 1         2 my $uid;
119              
120             my @names = grep {
121 7 100       93 !$self->{excluded}->{$_}
122             ? $_ : ()
123 1         1 } @{$self->{Config}->{_field_names}};
  1         4  
124              
125 1         10 local $1;
126 1         458 while ($output =~ /^($re)$/gm) {
127 144         296 my $line = $1;
128 144 100       255 if ($line =~ /^ \s+? (\d+?)\:/gx) {
129 6         11 $uid = $1;
130             }
131 144         147 my $res;
132 144 50       415 if ($line =~ /\G \s+? (\w+)/gx) {
133 144         224 $res = $1;
134             }
135 144         148 my @fields;
136 144 50       329 if ($line =~ /\G \s+ (.*) $/x) {
137 144         482 @fields = split /\s+/, $1;
138             }
139 144         371 push @{$self->{data}{$uid}{$res}},
140 144         158 { map { $names[$_] => $fields[$_] } (0 .. $#fields) };
  720         12918  
141             }
142             }
143              
144             sub _get_data_file
145             {
146 1     1   732 my $self = shift;
147              
148 1 50       3 eval { $self->{stored} = retrieve($self->{Config}->{data_file}) }
  1         4  
149             or croak "Cannot retrieve from $self->{Config}->{data_file}: $!";
150             }
151              
152             sub _compare_data
153             {
154 1     1   493 my $self = shift;
155              
156             my $has_changed = sub
157             {
158 144     144   198 my ($uid, $res, $i) = @_;
159              
160             my ($data, $stored) = map {
161 144 100       165 my $type = $_; sub { $self->{$type}{$uid}{$res}->[$i]->{$_[0]} || 0 }
  288         322  
  288         1085  
162 288         615 } qw(data stored);
163              
164 144         174 return scalar grep { $data->($_) > $stored->($_) } @{$self->{Config}->{monitor_fields}};
  144         184  
  144         235  
165 1         7 };
166              
167 1         3 foreach my $uid (sort {$a <=> $b} keys %{$self->{stored}}) {
  11         17  
  1         7  
168 6         8 foreach my $res (sort {$a cmp $b} keys %{$self->{stored}{$uid}}) {
  419         428  
  6         34  
169 126         157 foreach my $index (0 .. $#{$self->{stored}{$uid}{$res}}) {
  126         220  
170 144 100       180 if ($has_changed->($uid, $res, $index)) {
171 2         7 $self->_create_report($uid, $res, $index);
172             }
173             }
174             }
175             }
176             }
177              
178             sub _create_report
179             {
180 2     2   4 my $self = shift;
181 2         3 my ($uid, $res, $index) = @_;
182              
183 2 50       12 if ($self->{Config}->{_tests}) {
184 2         16 push @{$self->{tests}->{report}}, $self->_prepare_report($uid, $res, $index);
  2         10  
185             }
186             else {
187 0         0 my $report = $self->_prepare_report($uid, $res, $index);
188 0         0 $self->_send_mail($report);
189              
190 0 0       0 if ($self->{Config}->{verbose}) {
191 0         0 print "Report for \"$uid: $res\" sent to '$self->{Config}->{mail}->{to}'\n";
192             }
193             }
194             }
195              
196             sub _put_data_file
197             {
198 1     1   10 my $self = shift;
199              
200 1 50       3 eval { store($self->{data}, $self->{Config}->{data_file}) }
  1         6  
201             or croak "Cannot store to $self->{Config}->{data_file}: $!";
202             }
203              
204             sub _prepare_report
205             {
206 2     2   3 my $self = shift;
207 2         4 my ($uid, $res, $index) = @_;
208              
209 2         7 my @fixed_fields = ($uid, $res) x 2;
210             my @mapping = (
211             [
212             { map {
213 4         11 $_ => shift @fixed_fields,
214 2         5 } @{$self->{Config}->{_exclude_fields}} },
215             { map {
216 10         83 $_ => $self->{stored}{$uid}{$res}->[$index]->{$_},
217 2         9 } grep !$self->{excluded}->{$_}, @{$self->{Config}->{_field_names}} }
218             ],
219             [
220             { map {
221 4         9 $_ => shift @fixed_fields,
222 2         5 } @{$self->{Config}->{_exclude_fields}} },
223             { map {
224 10         86 $_ => $self->{data}{$uid}{$res}->[$index]->{$_},
225 2         3 } grep !$self->{excluded}->{$_}, @{$self->{Config}->{_field_names}} }
  2         5  
226             ],
227             );
228              
229 2         7 my @values;
230 2         3 foreach my $map (@mapping) {
231 4         5 my @v;
232 4         6 foreach my $entry (@$map) {
233             push @v, map $entry->{$_}, sort {
234 8         19 $self->{index}->{$a} <=> $self->{index}->{$b}
  29         59  
235             } keys %$entry;
236             }
237 4         12 push @values, [ @v ];
238             }
239              
240 2         3 my %changed;
241 2         8 foreach my $field (keys %{$mapping[0]->[1]}) {
  2         7  
242 10 100       30 if ($mapping[0]->[1]->{$field} != $mapping[1]->[1]->{$field}) {
243 2         6 $changed{$self->{index}->{$field}} = true;
244             }
245             }
246              
247 2         4 my $tmpl = \@values;
248 2         4 my $report = $self->{template};
249              
250 2         5 local $1;
251              
252 2         14 while (my ($var) = $report =~ /(\$\S+)/) {
253 28 100       274 unless ($report =~ /\Q$var\E$/m) {
254 14         20 my $len = length($var) - length do { eval $var };
  14         528  
255 14         293 $report =~ s/(?<=\Q$var\E)/' ' x $len/e;
  14         48  
256             }
257 28         238 $report =~ s/(\Q$var\E)/$1/ee;
  28         1079  
258             }
259              
260 2         10 while (my ($pos) = $report =~ /\((\d+)\)/) {
261 14 100       33 my $marked = $changed{$pos} ? '*' : ' ';
262 14         144 $report =~ s/\($pos\)/ $marked/;
263             }
264              
265 2         19 return $report;
266             }
267              
268             sub _send_mail
269             {
270 0     0     my $self = shift;
271 0           my ($report) = @_;
272              
273             my %mail = (
274             From => $self->{Config}->{mail}->{from},
275             To => $self->{Config}->{mail}->{to},
276             Subject => $self->{Config}->{mail}->{subject},
277 0           Message => <<"EOT",
278 0           ${\hostname}
279              
280             $report
281              
282             --
283 0           ${\basename($0)} v$VERSION - ${\scalar localtime}
  0            
284             EOT
285             );
286 0 0         sendmail(%mail)
287             or croak "Cannot send mail: $Mail::Sendmail::error";
288             }
289              
290             1;
291             __DATA__