File Coverage

blib/lib/MMM.pm
Criterion Covered Total %
statement 50 180 27.7
branch 6 82 7.3
condition 3 15 20.0
subroutine 15 31 48.3
pod 14 14 100.0
total 88 322 27.3


line stmt bran cond sub pod time code
1             package MMM;
2              
3 3     3   862 use strict;
  3         6  
  3         217  
4 3     3   19 use warnings;
  3         7  
  3         98  
5 3     3   1161 use MMM::Host;
  3         11  
  3         305  
6 3     3   2700 use MMM::MirrorTask;
  3         12  
  3         218  
7 3     3   36 use MMM::Config;
  3         8  
  3         415  
8 3     3   7019 use Config::IniFiles;
  3         124826  
  3         221  
9 3     3   41 use POSIX qw(:sys_wait_h);
  3         7  
  3         36  
10 3     3   743 use IO::Select;
  3         7  
  3         192  
11 3     3   3961 use Sys::Hostname ();
  3         5372  
  3         71  
12 3     3   5081 use Sys::Syslog ();
  3         39087  
  3         10385  
13              
14             our $VERSION = '0.43';
15              
16             =head1 NAME
17              
18             MMM - MMM Mirror Manager
19              
20             =head1 METHODS
21              
22             =head2 new( %options )
23              
24             Create a new MMM object. %options can provide:
25              
26             =over 4
27              
28             =item configfile
29              
30             The configuration file to use (default is 'mmm.cfg')
31              
32             =item mirror_dir
33              
34             The location of locales lists (default is '.')
35              
36             =item loghandle
37              
38             A ref to an handle where to write message
39              
40             =item verbosity
41              
42             The verbosity level (default is 3)
43              
44             =back
45              
46             =cut
47              
48             sub new {
49 2     2 1 1149 my ( $class, %options ) = @_;
50 2   33     23 my $mmm = {
      33        
51             configfile => $options{configfile} || CONFIGFILE,
52             mirrordir => $options{mirrordir} || MIRRORDIR,
53             logcallback => $options{logcallback},
54             nofork => $options{nofork},
55             dryrun => $options{dryrun},
56             verbosity => VERBOSITY, # default is enough at this step
57             runtime_verbosity => $options{verbosity},
58             };
59 2 50       20 $mmm->{config} = Config::IniFiles->new(
60             -file => $mmm->{configfile},
61             -default => 'default',
62             ) or return;
63 2         7672 bless( $mmm, $class );
64              
65 2 50       7 $mmm->_parse_config() or return;
66 2         7 my ($res, $message) = MMM::Utils::setid(
67             $mmm->{config}->val('default', 'user'),
68             $mmm->{config}->val('default', 'group')
69             );
70 2 50       5 if (!$res) {
71 0         0 $mmm->log('ERROR', $message);
72             }
73              
74             $mmm
75 2         8 }
76              
77             sub _parse_config {
78 3     3   7 my ($self) = @_;
79            
80 3         16 $self->{statedir} = $self->{config}->val( 'default', 'statedir', STATEDIR );
81              
82 3   33     66 $self->{hostinfo} = MMM::Host->new(
83             hostname => $self->{config}->val( 'default', 'hostname' )
84             || Sys::Hostname::hostname(),
85             latitude => $self->{config}->val( 'default', 'latitude' ),
86             longitude => $self->{config}->val( 'default', 'longitude' ),
87             );
88              
89 3 50       21 $self->set_verbosity(
90             defined( $self->{runtime_verbosity} )
91             ? $self->{runtime_verbosity}
92             : $self->{config}->val('default', 'verbosity', VERBOSITY)
93             );
94              
95 3         8 1;
96             }
97              
98             =head2 statedir
99              
100             Return the state directory
101              
102             =cut
103              
104             sub statedir {
105 0     0 1 0 my ($self) = @_;
106 0         0 return $self->{config}->val( 'default', 'statedir', $self->{statedir} );
107             }
108              
109             =head2 set_log_callback( $callback )
110              
111             Set the callback where message are written
112              
113             =cut
114              
115             sub set_log_callback {
116 0     0 1 0 my ( $self, $callback ) = @_;
117 0         0 $self->{logcallback} = $callback;
118             }
119              
120             our $loglevel = {
121             'PIPE' => [ -1, '', '' ], # internal use only
122             'FATAL' => [ 0, 'Fatal: ', 'crit' ],
123             'ERROR' => [ 1, 'Error: ', 'err' ],
124             'WARNING' => [ 2, 'Warning: ', 'warning' ],
125             'NOTICE' => [ 3, '', 'notice' ],
126             'INFO' => [ 4, '', 'info' ],
127             'DEBUG' => [ 5, 'Debug: ', 'debug' ],
128             };
129              
130             =head2 log( $level, $message, @args )
131              
132             Log a message. $level is one of
133              
134             LEVEL
135             FATAL
136             ERROR
137             WARNING
138             NOTICE
139             INFO
140             DEBUG
141              
142             =cut
143              
144             sub log {
145 0     0 1 0 my ( $self, $level, $message, @args ) = @_;
146 0 0       0 $message or return;
147 0         0 my ($keylogl) =
148             $level =~ /^\d$/
149 0 0       0 ? ( grep { $loglevel->{$_}[0] == $level } keys %{$loglevel} )
  0         0  
150             : ( uc($level) );
151 0 0       0 $loglevel->{$keylogl} or $keylogl = 'NOTICE';
152 0 0       0 if ($loglevel->{$keylogl}[0] == -1) { # PIPE
153 0         0 my ($levelb, $messageb) = $message =~ /^(\w+) (.*)/;
154 0         0 return $self->log($levelb, $messageb, @args);
155             }
156 0 0       0 $loglevel->{$keylogl}[0] > $self->{verbosity} and return;
157 0 0 0     0 if ( $self->{logcallback} && ref $self->{logcallback} eq 'CODE' ) {
158 0         0 $self->{logcallback}
159             ->( $level, $message, @args );
160             }
161             else {
162 0 0       0 my $h = $loglevel->{$keylogl}[0] > 2 ? \*STDOUT : \*STDERR;
163 0 0       0 if ($self->{use_syslog}) {
164 0         0 Sys::Syslog::syslog($loglevel->{$keylogl}[2], $message, @args);
165             } else {
166 0         0 printf $h $self->fmt_log($level, $message, @args) . "\n";
167             }
168             }
169             }
170              
171             =head2 fmt_log($level, $message, @args)
172              
173             Format a log message and return it
174              
175             =cut
176              
177             sub fmt_log {
178 0     0 1 0 my ( $self, $level, $message, @args ) = @_;
179 0 0       0 $message or return;
180 0         0 my ($keylogl) =
181             $level =~ /^\d$/
182 0 0       0 ? ( grep { $loglevel->{$_}[0] == $level } keys %{$loglevel} )
  0         0  
183             : ( uc($level) );
184 0 0       0 $loglevel->{$keylogl} or $keylogl = 'NOTICE';
185 0 0       0 $loglevel->{$keylogl}[0] > $self->{verbosity} and return;
186 0         0 sprintf( "$loglevel->{$keylogl}[1]$message", @args );
187             }
188              
189             =head2 set_verbosity($verbosity)
190              
191             Set the verbosity
192              
193             =cut
194              
195             sub set_verbosity {
196 3     3 1 59 my ($self, $verbosity) = @_;
197 3 50       19 if (exists($loglevel->{uc($verbosity)})) {
    50          
198 0         0 $self->{verbosity} = $loglevel->{uc($verbosity)}[0];
199             } elsif ($verbosity =~ /^\d+$/) {
200 3         9 $self->{verbosity} = $verbosity;
201             } else {
202 0         0 $self->log('ERROR', 'Invalid verbosity level %s', $verbosity);
203             }
204             }
205              
206             =head2 hostname
207              
208             Return the hostname setup in the configuration
209              
210             =cut
211              
212             sub hostname {
213 0     0 1 0 my ($self) = @_;
214 0         0 $self->{hostinfo}->hostname;
215             }
216              
217             =head2 hostinfo
218              
219             Return the MMM::Host which identify the host where the process is running
220              
221             =cut
222              
223             sub hostinfo {
224 1     1 1 390 $_[0]->{hostinfo};
225             }
226              
227             =head2 configval($section, $var, $default)
228              
229             Return a value from configuation
230              
231             =cut
232              
233             sub configval {
234 0     0 1 0 my ( $self, $section, $var, $default ) = @_;
235 0         0 $self->{config}->val( $section, $var, $default );
236             }
237              
238             =head2 list_tasks
239              
240             Return the list of setup task
241              
242             =cut
243              
244             sub list_tasks {
245 1     1 1 326 my ($self) = @_;
246 1         6 return grep { $_ ne 'default' } $self->{config}->Sections;
  5         24  
247             }
248              
249             =head2 get_tasks_by_name(@tasks_name)
250              
251             Return a MMM::MirrorTask object for the each @tasks_name
252              
253             =cut
254              
255             sub get_tasks_by_name {
256 0     0 1   my ($self, @jobs_name) = @_;
257 0 0         $self->{config} or return;
258 0           my @res = ();
259 0           foreach my $job ( @jobs_name ) {
260 0 0         $job eq 'default' and next;
261 0 0         if (!$self->{config}->SectionExists($job)) {
262 0           $self->log('WARNING', 'Job `%s\' don\'t exists, Ignoring...', $job);
263 0           next;
264             }
265 0           push(@res,
266             MMM::MirrorTask->new(
267             $self,
268             $job,
269             dryrun => $self->{dryrun},
270             )
271             );
272             }
273 0           grep { $_ } @res;
  0            
274             }
275              
276             sub _get_geo {
277 0     0     my ($self, $ml) = @_;
278 0           $self->log('DEBUG', 'Fetching geo loc data From %s %s %s', caller);
279 0           my %src;
280 0           foreach ($self->get_tasks_by_name($self->list_tasks)) {
281 0 0         $_->is_disable and next;
282 0 0         if ($_->val('url')) {
283 0 0         if (my $mi = MMM::Mirror->new(url => $_->val('url'))) {
284 0           $mi->get_geo();
285 0           $ml->add_mirror($mi);
286             }
287             } else {
288 0 0         $_->source or next;
289 0           $src{$_->source} = 1;
290             }
291             }
292 0           $ml->get_geo([ keys %src ]);
293 0 0         if (open(my $h, '>', $self->statedir . '/hosts.xml')) {
294 0           print $h $ml->xml_hosts;
295 0           close($h);
296             }
297             }
298              
299             =head2 run
300              
301             Process all load rsync job
302              
303             =cut
304              
305             sub run {
306 0     0 1   my ( $self, @job_names ) = @_;
307              
308 0           foreach my $q (grep { (!$_->is_disable) }
  0            
309             $self->get_tasks_by_name(@job_names)
310             ) {
311 0           $self->_run_fork($q);
312             }
313              
314 0           $self->_reap_message();
315 0           $self->_reap_child();
316              
317             }
318              
319             =head2 post_process
320              
321             function called at the end of process
322              
323             =cut
324              
325             sub post_process {
326 0     0 1   my ($self, $job) = @_;
327 0           $self->log('DEBUG', 'Post processing %s', $job->name);
328             }
329              
330             sub _reap_child {
331 0     0     my ($self) = @_;
332 0           my $kid = 0;
333 0           do {
334 0           $kid = waitpid( -1, &WNOHANG );
335 0 0         if ($kid > 0) {
336 0           $self->log('DEBUG', 'Reaping pid %d', $kid);
337 0 0         if ($self->{process}{$kid}) {
338 0 0         $self->log('DEBUG', 'Successive failure %s was: %d is: %d changed: %d',
339             $self->{process}{$kid}->name,
340 0           map { defined($_) ? $_ : '-1' } $self->{process}{$kid}->failure_count()
341             );
342 0           $self->post_process($self->{process}{$kid});
343             } else {
344 0           $self->log('WARNING',
345             "I have no trace of subprocess pid %d, please report",
346             $kid
347             );
348             }
349             }
350 0           delete($self->{process}{$kid});
351             } until $kid <= 0;
352             }
353              
354             sub _reap_message {
355 0     0     my ($self) = @_;
356 0 0         $self->{ios} or return;
357 0           while (my @hs = $self->{ios}->can_read() ) {
358 0           foreach my $h (@hs) {
359 0           my $l = <$h>;
360 0 0         if ( !defined($l) ) {
361 0           $self->{ios}->remove($h);
362 0           next;
363             }
364 0           chomp($l);
365 0           $self->log( 'PIPE', $l );
366             }
367             }
368             }
369              
370             sub _task_is_registred {
371 0     0     my ($self, $taskname) = @_;
372 0 0         if (my ($pid) = grep { $self->{process}{$_}->name eq $taskname }
  0 0          
  0            
373             (keys %{ $self->{process} || {} })) {
374 0           return $pid;
375             } else {
376 0           return;
377             }
378             }
379              
380             sub _run_fork {
381 0     0     my ($self, $process, $notraplog) = @_;
382 0 0         if (my $pid = $self->_task_is_registred($process->name)) {
383 0           $self->log('DEBUG', '%s is already running ! (pid %d)', $process->name, $pid);
384 0           return;
385             }
386 0           $self->log('DEBUG', 'Forking to run %s', $process->name);
387 0           my ($reader, $writer );
388 0 0         if (!$notraplog) {
389 0           pipe($reader, $writer );
390             }
391 0           my $oldInt = $SIG{'INT'};
392 0           $SIG{'INT'} = 'IGNORE';
393 0           my $pid = fork();
394 0 0         defined($pid) or die("Can't fork");
395 0 0         if ($pid) {
396 0           $self->log('DEBUG', 'PID %d for %s started', $pid, $process->name);
397 0           $self->{process}{$pid} = $process;
398 0 0         if (!$notraplog) {
399 0   0       $self->{ios} ||= IO::Select->new();
400 0           $self->{ios}->add($reader);
401             # keep a trac of this process
402             }
403 0 0         $SIG{'INT'} = $oldInt if (defined($oldInt));
404             } else {
405 0           $SIG{'CHLD'} = 'DEFAULT';
406 0           $self->set_verbosity(
407             $self->configval($process->name, 'verbosity', $self->{verbosity})
408             );
409 0           foreach (qw(ALRM TERM INT HUP CHLD)) {
410 0           $SIG{$_} = 'DEFAULT';
411             }
412             $self->set_log_callback(
413             sub {
414 0     0     my ($level, $message, @args ) = @_;
415 0           print $writer "$level " . sprintf($message, @args) . "\n";
416             }
417 0 0         ) unless($notraplog);
418 0           exit(!$process->sync());
419             }
420             }
421              
422             =head2 check_config
423              
424             Check the config
425              
426             =cut
427              
428             sub check_config {
429 0     0 1   my ($self) = @_;
430 0           my $res = 1;
431              
432 0 0         if (! -d $self->statedir) {
433 0           $self->log('FATAL', qq{Statedir `%s' don't exists}, $self->statedir);
434 0           $res = 0;
435             }
436              
437             $res
438 0           }
439              
440             1;
441              
442             =head1 AUTHOR
443              
444             Olivier Thauvin
445              
446             =head1 COPYRIGHT AND LICENSE
447              
448             Copyright (C) 2006 Olivier Thauvin
449              
450             This program is free software; you can redistribute it and/or
451             modify it under the terms of the GNU General Public License
452             as published by the Free Software Foundation; either version 2
453             of the License, or (at your option) any later version.
454              
455             This program is distributed in the hope that it will be useful,
456             but WITHOUT ANY WARRANTY; without even the implied warranty of
457             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
458             GNU General Public License for more details.
459              
460             You should have received a copy of the GNU General Public License
461             along with this program; if not, write to the Free Software
462             Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
463              
464             =cut
465