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   76356 use strict;
  2         10  
  2         60  
4 2     2   18 use warnings;
  2         4  
  2         57  
5 2     2   514 use boolean qw(true false);
  2         3527  
  2         9  
6              
7 2     2   157 use Carp qw(croak);
  2         4  
  2         143  
8 2     2   13 use File::Basename qw(basename);
  2         5  
  2         199  
9 2     2   1158 use File::HomeDir ();
  2         11678  
  2         46  
10 2     2   13 use File::Spec ();
  2         6  
  2         61  
11 2     2   1212 use Mail::Sendmail qw(sendmail);
  2         34391  
  2         136  
12 2     2   1335 use Storable qw(store retrieve);
  2         6841  
  2         147  
13 2     2   19 use Sys::Hostname qw(hostname);
  2         5  
  2         824  
14              
15             our $VERSION = '0.05';
16              
17             sub new
18             {
19 1     1 1 700 my $class = shift;
20 1         8 my %args = @_;
21              
22 1 100   3   11 my $defined_or = sub { defined $_[0] ? $_[0] : $_[1] };
  3         36  
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     36 _tests => $defined_or->($args{_tests}, false),
      33        
      50        
      50        
      50        
      50        
      33        
39             }
40             }, ref($class) || $class;
41              
42 1         8 $self->_init;
43              
44 1         8 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   2 my $self = shift;
62              
63 1 50       2 eval { store({}, $self->{Config}->{data_file}) }
  1         11  
64             or croak "Cannot store to $self->{Config}->{data_file}: $!";
65              
66 1         291 my $pkg_tmpl = join '::', (__PACKAGE__, '_template');
67 2     2   16 no strict 'refs';
  2         9  
  2         4274  
68              
69 1 50       3 if (defined ${$pkg_tmpl}) {
  1         9  
70 0         0 $self->{template} = ${$pkg_tmpl};
  0         0  
71             }
72             else {
73 1         3 ${$pkg_tmpl} = $self->{template} = do {
  1         5  
74 1         5 local $/ = '__END__';
75 1         8 local $_ = ;
76 1         4 chomp;
77 1         7 s/^\s+//;
78 1         14 s/\s+\z//;
79 1         6 $_
80             };
81             }
82              
83             $self->{excluded} = {
84             map {
85 7         26 my $field = $_;
86 7 100       9 (scalar grep $_ eq $field, @{$self->{Config}->{_exclude_fields}})
  7         28  
87             ? ($field => true)
88             : ($field => false)
89 1         2 } @{$self->{Config}->{_field_names}}
  1         4  
90             };
91              
92 1         10 my $i;
93 1         4 $self->{index} = { map { $_ => $i++ } @{$self->{Config}->{_field_names}} };
  7         16  
  1         4  
94             }
95              
96             sub _get_data_running
97             {
98 1     1   9 my $self = shift;
99              
100             open(my $fh, '<', $self->{Config}->{input_file})
101 1 50       45 or croak "Cannot read $self->{Config}->{input_file}: $!";
102 1         4 my $output = do { local $/; <$fh> };
  1         5  
  1         44  
103 1         12 close($fh);
104              
105 1         4 my $valid_format = join '\s+', @{$self->{Config}->{_field_names}};
  1         6  
106              
107 1 50       28 unless ($output =~ /$valid_format/) {
108 0         0 croak "Format of $self->{Config}->{input_file} not recognized";
109             }
110              
111 1         7 my $re = qr{
112             \s*?
113             (?:\d+?\:)? \s+?
114             (?:\w+?) \s+?
115             (?:(?:\d+?) \s*?){5}
116             }x;
117              
118 1         3 my $uid;
119              
120             my @names = grep {
121 7 100       184 !$self->{excluded}->{$_}
122             ? $_ : ()
123 1         4 } @{$self->{Config}->{_field_names}};
  1         4  
124              
125 1         30 local $1;
126 1         566 while ($output =~ /^($re)$/gm) {
127 144         372 my $line = $1;
128 144 100       318 if ($line =~ /^ \s+? (\d+?)\:/gx) {
129 6         13 $uid = $1;
130             }
131 144         187 my $res;
132 144 50       496 if ($line =~ /\G \s+? (\w+)/gx) {
133 144         288 $res = $1;
134             }
135 144         192 my @fields;
136 144 50       426 if ($line =~ /\G \s+ (.*) $/x) {
137 144         644 @fields = split /\s+/, $1;
138             }
139 144         459 push @{$self->{data}{$uid}{$res}},
140 144         195 { map { $names[$_] => $fields[$_] } (0 .. $#fields) };
  720         16853  
141             }
142             }
143              
144             sub _get_data_file
145             {
146 1     1   812 my $self = shift;
147              
148 1 50       2 eval { $self->{stored} = retrieve($self->{Config}->{data_file}) }
  1         6  
149             or croak "Cannot retrieve from $self->{Config}->{data_file}: $!";
150             }
151              
152             sub _compare_data
153             {
154 1     1   621 my $self = shift;
155              
156             my $has_changed = sub
157             {
158 144     144   232 my ($uid, $res, $i) = @_;
159              
160             my ($data, $stored) = map {
161 144 100       201 my $type = $_; sub { $self->{$type}{$uid}{$res}->[$i]->{$_[0]} || 0 }
  288         426  
  288         1343  
162 288         846 } qw(data stored);
163              
164 144         211 return scalar grep { $data->($_) > $stored->($_) } @{$self->{Config}->{monitor_fields}};
  144         221  
  144         251  
165 1         8 };
166              
167 1         3 foreach my $uid (sort {$a <=> $b} keys %{$self->{stored}}) {
  9         18  
  1         9  
168 6         11 foreach my $res (sort {$a cmp $b} keys %{$self->{stored}{$uid}}) {
  421         561  
  6         36  
169 126         187 foreach my $index (0 .. $#{$self->{stored}{$uid}{$res}}) {
  126         273  
170 144 100       233 if ($has_changed->($uid, $res, $index)) {
171 2         13 $self->_create_report($uid, $res, $index);
172             }
173             }
174             }
175             }
176             }
177              
178             sub _create_report
179             {
180 2     2   5 my $self = shift;
181 2         5 my ($uid, $res, $index) = @_;
182              
183 2 50       9 if ($self->{Config}->{_tests}) {
184 2         20 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   13 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   4 my $self = shift;
207 2         5 my ($uid, $res, $index) = @_;
208              
209 2         12 my @fixed_fields = ($uid, $res) x 2;
210             my @mapping = (
211             [
212             { map {
213 4         15 $_ => shift @fixed_fields,
214 2         6 } @{$self->{Config}->{_exclude_fields}} },
215             { map {
216 10         100 $_ => $self->{stored}{$uid}{$res}->[$index]->{$_},
217 2         9 } grep !$self->{excluded}->{$_}, @{$self->{Config}->{_field_names}} }
218             ],
219             [
220             { map {
221 4         10 $_ => shift @fixed_fields,
222 2         5 } @{$self->{Config}->{_exclude_fields}} },
223             { map {
224 10         126 $_ => $self->{data}{$uid}{$res}->[$index]->{$_},
225 2         4 } grep !$self->{excluded}->{$_}, @{$self->{Config}->{_field_names}} }
  2         7  
226             ],
227             );
228              
229 2         7 my @values;
230 2         6 foreach my $map (@mapping) {
231 4         5 my @v;
232 4         9 foreach my $entry (@$map) {
233             push @v, map $entry->{$_}, sort {
234 8         37 $self->{index}->{$a} <=> $self->{index}->{$b}
  33         82  
235             } keys %$entry;
236             }
237 4         17 push @values, [ @v ];
238             }
239              
240 2         6 my %changed;
241 2         5 foreach my $field (keys %{$mapping[0]->[1]}) {
  2         7  
242 10 100       36 if ($mapping[0]->[1]->{$field} != $mapping[1]->[1]->{$field}) {
243 2         8 $changed{$self->{index}->{$field}} = true;
244             }
245             }
246              
247 2         6 my $tmpl = \@values;
248 2         5 my $report = $self->{template};
249              
250 2         7 local $1;
251              
252 2         15 while (my ($var) = $report =~ /(\$\S+)/) {
253 28 100       339 unless ($report =~ /\Q$var\E$/m) {
254 14         26 my $len = length($var) - length do { eval $var };
  14         702  
255 14         376 $report =~ s/(?<=\Q$var\E)/' ' x $len/e;
  14         59  
256             }
257 28         296 $report =~ s/(\Q$var\E)/$1/ee;
  28         1719  
258             }
259              
260 2         13 while (my ($pos) = $report =~ /\((\d+)\)/) {
261 14 100       49 my $marked = $changed{$pos} ? '*' : ' ';
262 14         179 $report =~ s/\($pos\)/ $marked/;
263             }
264              
265 2         29 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__