File Coverage

blib/lib/FusionInventory/Agent.pm
Criterion Covered Total %
statement 96 259 37.0
branch 12 92 13.0
condition 1 33 3.0
subroutine 23 37 62.1
pod 7 7 100.0
total 139 428 32.4


line stmt bran cond sub pod time code
1             package FusionInventory::Agent;
2              
3 20     20   87518 use strict;
  20         47  
  20         795  
4 20     20   99 use warnings;
  20         32  
  20         667  
5              
6 20     20   87 use Cwd;
  20         66  
  20         1689  
7 20     20   619 use English qw(-no_match_vars);
  20         1567  
  20         168  
8 20     20   9377 use UNIVERSAL::require;
  20         2413  
  20         136  
9 20     20   480 use File::Glob;
  20         26  
  20         1494  
10 20     20   11482 use IO::Handle;
  20         111748  
  20         1046  
11 20     20   11776 use POSIX ":sys_wait_h"; # WNOHANG
  20         115948  
  20         134  
12              
13 20     20   31580 use FusionInventory::Agent::Config;
  20         58  
  20         269  
14 20     20   9837 use FusionInventory::Agent::HTTP::Client::OCS;
  20         61  
  20         295  
15 20     20   2947 use FusionInventory::Agent::Logger;
  20         39  
  20         1459  
16 20     20   8796 use FusionInventory::Agent::Storage;
  20         73  
  20         210  
17 20     20   7156 use FusionInventory::Agent::Task;
  20         47  
  20         292  
18 20     20   8414 use FusionInventory::Agent::Target::Local;
  20         39  
  20         273  
19 20     20   8620 use FusionInventory::Agent::Target::Server;
  20         38  
  20         186  
20 20     20   508 use FusionInventory::Agent::Tools;
  20         29  
  20         2682  
21 20     20   8512 use FusionInventory::Agent::Tools::Hostname;
  20         35  
  20         1051  
22 20     20   8473 use FusionInventory::Agent::XML::Query::Prolog;
  20         44  
  20         193  
23              
24             our $VERSION = '2.3.16';
25             our $VERSION_STRING = _versionString($VERSION);
26             our $AGENT_STRING = "FusionInventory-Agent_v$VERSION";
27              
28             sub _versionString {
29 20     20   44 my ($VERSION) = @_;
30              
31 20         58 my $string = "FusionInventory Agent ($VERSION)";
32 20 50       103 if ($VERSION =~ /^\d\.\d\.99(\d\d)/) {
33 0         0 $string .= " **THIS IS A DEVELOPMENT RELEASE **";
34             }
35              
36 20         49 return $string;
37             }
38              
39             sub new {
40 1     1 1 830 my ($class, %params) = @_;
41              
42 1         8 my $self = {
43             status => 'unknown',
44             confdir => $params{confdir},
45             datadir => $params{datadir},
46             libdir => $params{libdir},
47             vardir => $params{vardir},
48             };
49 1         2 bless $self, $class;
50              
51 1         3 return $self;
52             }
53              
54             sub init {
55 0     0 1 0 my ($self, %params) = @_;
56              
57 0         0 my $config = FusionInventory::Agent::Config->new(
58             confdir => $self->{confdir},
59             options => $params{options},
60             );
61 0         0 $self->{config} = $config;
62              
63 0 0 0     0 my $verbosity = $config->{debug} && $config->{debug} == 1 ? LOG_DEBUG :
    0 0        
64             $config->{debug} && $config->{debug} == 2 ? LOG_DEBUG2 :
65             LOG_INFO ;
66              
67 0         0 my $logger = FusionInventory::Agent::Logger->new(
68             config => $config,
69             backends => $config->{logger},
70             verbosity => $verbosity
71             );
72 0         0 $self->{logger} = $logger;
73              
74 0         0 $logger->debug("Configuration directory: $self->{confdir}");
75 0         0 $logger->debug("Data directory: $self->{datadir}");
76 0         0 $logger->debug("Storage directory: $self->{vardir}");
77 0         0 $logger->debug("Lib directory: $self->{libdir}");
78              
79 0         0 $self->{storage} = FusionInventory::Agent::Storage->new(
80             logger => $logger,
81             directory => $self->{vardir}
82             );
83              
84             # handle persistent state
85 0         0 $self->_loadState();
86              
87 0 0       0 $self->{deviceid} = _computeDeviceId() if !$self->{deviceid};
88              
89 0         0 $self->_saveState();
90              
91             # create target list
92 0 0       0 if ($config->{local}) {
93 0         0 foreach my $path (@{$config->{local}}) {
  0         0  
94 0         0 push @{$self->{targets}},
  0         0  
95             FusionInventory::Agent::Target::Local->new(
96             logger => $logger,
97             deviceid => $self->{deviceid},
98             delaytime => $config->{delaytime},
99             basevardir => $self->{vardir},
100             path => $path,
101             html => $config->{html},
102             );
103             }
104             }
105              
106 0 0       0 if ($config->{server}) {
107 0         0 foreach my $url (@{$config->{server}}) {
  0         0  
108 0         0 push @{$self->{targets}},
  0         0  
109             FusionInventory::Agent::Target::Server->new(
110             logger => $logger,
111             deviceid => $self->{deviceid},
112             delaytime => $config->{delaytime},
113             basevardir => $self->{vardir},
114             url => $url,
115             tag => $config->{tag},
116             );
117             }
118             }
119              
120 0 0       0 if (!$self->{targets}) {
121 0         0 $logger->error("No target defined, aborting");
122 0         0 exit 1;
123             }
124              
125             # compute list of allowed tasks
126 0         0 my %available = $self->getAvailableTasks(disabledTasks => $config->{'no-task'});
127 0         0 my @tasks = keys %available;
128              
129 0 0       0 if (!@tasks) {
130 0         0 $logger->error("No tasks available, aborting");
131 0         0 exit 1;
132             }
133              
134 0         0 $logger->debug("Available tasks:");
135 0         0 foreach my $task (keys %available) {
136 0         0 $logger->debug("- $task: $available{$task}");
137             }
138              
139 0         0 $self->{tasks} = \@tasks;
140              
141 0 0       0 if ($config->{daemon}) {
142 0   0     0 my $pidfile = $config->{pidfile} ||
143             $self->{vardir} . '/fusioninventory.pid';
144              
145 0 0       0 if ($self->_isAlreadyRunning($pidfile)) {
146 0         0 $logger->error("An agent is already running, exiting...");
147 0         0 exit 1;
148             }
149 0 0       0 if (!$config->{'no-fork'}) {
150              
151 0         0 Proc::Daemon->require();
152 0 0       0 if ($EVAL_ERROR) {
153 0         0 $logger->error("Failed to load Proc::Daemon: $EVAL_ERROR");
154 0         0 exit 1;
155             }
156              
157             # If we use relative path, we must stay in the current directory
158 0 0       0 my $workdir = substr($self->{libdir}, 0, 1) eq '/' ? '/' : getcwd();
159              
160 0         0 Proc::Daemon::Init({
161             work_dir => $workdir,
162             pid_file => $pidfile
163             });
164              
165 0         0 $self->{logger}->debug("Agent daemonized");
166             }
167             }
168              
169             # create HTTP interface
170 0 0 0     0 if (($config->{daemon} || $config->{service}) && !$config->{'no-httpd'}) {
      0        
171 0         0 FusionInventory::Agent::HTTP::Server->require();
172 0 0       0 if ($EVAL_ERROR) {
173 0         0 $logger->error("Failed to load HTTP server: $EVAL_ERROR");
174             } else {
175 0         0 $self->{server} = FusionInventory::Agent::HTTP::Server->new(
176             logger => $logger,
177             agent => $self,
178             htmldir => $self->{datadir} . '/html',
179             ip => $config->{'httpd-ip'},
180             port => $config->{'httpd-port'},
181             trust => $config->{'httpd-trust'}
182             );
183 0         0 $self->{server}->init();
184             }
185             }
186              
187             # install signal handler to handle graceful exit
188 0     0   0 $SIG{INT} = sub { $self->terminate(); exit 0; };
  0         0  
  0         0  
189 0     0   0 $SIG{TERM} = sub { $self->terminate(); exit 0; };
  0         0  
  0         0  
190              
191 0 0 0     0 $self->{logger}->info("FusionInventory Agent starting")
192             if $self->{config}->{daemon} || $self->{config}->{service};
193             }
194              
195             sub run {
196 0     0 1 0 my ($self) = @_;
197              
198 0         0 $self->{status} = 'waiting';
199              
200 0 0 0     0 if ($self->{config}->{daemon} || $self->{config}->{service}) {
201              
202             # background mode:
203 0         0 while (1) {
204 0         0 my $time = time();
205 0         0 foreach my $target (@{$self->{targets}}) {
  0         0  
206 0 0       0 next if $time < $target->getNextRunDate();
207              
208 0         0 eval {
209 0         0 $self->_runTarget($target);
210             };
211 0 0       0 $self->{logger}->error($EVAL_ERROR) if $EVAL_ERROR;
212 0         0 $target->resetNextRunDate();
213             }
214              
215             # check for http interface messages
216 0 0       0 $self->{server}->handleRequests() if $self->{server};
217 0         0 delay(1);
218             }
219             } else {
220             # foreground mode: check each targets once
221 0         0 my $time = time();
222 0         0 foreach my $target (@{$self->{targets}}) {
  0         0  
223 0 0 0     0 if ($self->{config}->{lazy} && $time < $target->getNextRunDate()) {
224 0         0 $self->{logger}->info(
225             "$target->{id} is not ready yet, next server contact " .
226             "planned for " . localtime($target->getNextRunDate())
227             );
228 0         0 next;
229             }
230              
231 0         0 eval {
232 0         0 $self->_runTarget($target);
233             };
234 0 0       0 $self->{logger}->error($EVAL_ERROR) if $EVAL_ERROR;
235             }
236             }
237             }
238              
239             sub terminate {
240 0     0 1 0 my ($self) = @_;
241              
242 0 0 0     0 $self->{logger}->info("FusionInventory Agent exiting")
243             if $self->{config}->{daemon} || $self->{config}->{service};
244 0 0       0 $self->{current_task}->abort() if $self->{current_task};
245             }
246              
247             sub _runTarget {
248 0     0   0 my ($self, $target) = @_;
249              
250             # the prolog dialog must be done once for all tasks,
251             # but only for server targets
252 0         0 my $response;
253 0 0       0 if ($target->isa('FusionInventory::Agent::Target::Server')) {
254 0         0 my $client = FusionInventory::Agent::HTTP::Client::OCS->new(
255             logger => $self->{logger},
256             timeout => $self->{timeout},
257             user => $self->{config}->{user},
258             password => $self->{config}->{password},
259             proxy => $self->{config}->{proxy},
260             ca_cert_file => $self->{config}->{'ca-cert-file'},
261             ca_cert_dir => $self->{config}->{'ca-cert-dir'},
262             no_ssl_check => $self->{config}->{'no-ssl-check'},
263             );
264              
265 0         0 my $prolog = FusionInventory::Agent::XML::Query::Prolog->new(
266             deviceid => $self->{deviceid},
267             );
268              
269 0         0 $self->{logger}->info("sending prolog request to server $target->{id}");
270 0         0 $response = $client->send(
271             url => $target->getUrl(),
272             message => $prolog
273             );
274 0 0       0 die "No answer from the server" unless $response;
275              
276             # update target
277 0         0 my $content = $response->getContent();
278 0 0       0 if (defined($content->{PROLOG_FREQ})) {
279 0         0 $target->setMaxDelay($content->{PROLOG_FREQ} * 3600);
280             }
281             }
282              
283 0         0 foreach my $name (@{$self->{tasks}}) {
  0         0  
284 0         0 eval {
285 0         0 $self->_runTask($target, $name, $response);
286             };
287 0 0       0 $self->{logger}->error($EVAL_ERROR) if $EVAL_ERROR;
288 0         0 $self->{status} = 'waiting';
289             }
290             }
291              
292             sub _runTask {
293 0     0   0 my ($self, $target, $name, $response) = @_;
294              
295 0         0 $self->{status} = "running task $name";
296              
297 0 0 0     0 if ($self->{config}->{daemon} || $self->{config}->{service}) {
298             # server mode: run each task in a child process
299 0 0       0 if (my $pid = fork()) {
300             # parent
301 0         0 while (waitpid($pid, WNOHANG) == 0) {
302 0 0       0 $self->{server}->handleRequests() if $self->{server};
303 0         0 delay(1);
304             }
305             } else {
306             # child
307 0 0       0 die "fork failed: $ERRNO" unless defined $pid;
308              
309 0         0 $self->{logger}->debug("forking process $PID to handle task $name");
310 0         0 $self->_runTaskReal($target, $name, $response);
311 0         0 exit(0);
312             }
313             } else {
314             # standalone mode: run each task directly
315 0         0 $self->_runTaskReal($target, $name, $response);
316             }
317             }
318              
319             sub _runTaskReal {
320 0     0   0 my ($self, $target, $name, $response) = @_;
321              
322 0         0 my $class = "FusionInventory::Agent::Task::$name";
323              
324 0         0 $class->require();
325              
326 0         0 my $task = $class->new(
327             config => $self->{config},
328             confdir => $self->{confdir},
329             datadir => $self->{datadir},
330             logger => $self->{logger},
331             target => $target,
332             deviceid => $self->{deviceid},
333             );
334              
335 0 0       0 return if !$task->isEnabled($response);
336              
337 0         0 $self->{logger}->info("running task $name");
338 0         0 $self->{current_task} = $task;
339              
340 0         0 $task->run(
341             user => $self->{config}->{user},
342             password => $self->{config}->{password},
343             proxy => $self->{config}->{proxy},
344             ca_cert_file => $self->{config}->{'ca-cert-file'},
345             ca_cert_dir => $self->{config}->{'ca-cert-dir'},
346             no_ssl_check => $self->{config}->{'no-ssl-check'},
347             );
348 0         0 delete $self->{current_task};
349             }
350              
351             sub getStatus {
352 0     0 1 0 my ($self) = @_;
353 0         0 return $self->{status};
354             }
355              
356             sub getTargets {
357 0     0 1 0 my ($self) = @_;
358              
359 0         0 return @{$self->{targets}};
  0         0  
360             }
361              
362             sub getAvailableTasks {
363 4     4 1 16676 my ($self, %params) = @_;
364              
365 4         8 my %tasks;
366 4         7 my %disabled = map { lc($_) => 1 } @{$params{disabledTasks}};
  0         0  
  4         16  
367              
368             # tasks may be located only in agent libdir
369 4         12 my $directory = $self->{libdir};
370 4         10 $directory =~ s,\\,/,g;
371 4         8 my $subdirectory = "FusionInventory/Agent/Task";
372             # look for all perl modules here
373 4         393 foreach my $file (File::Glob::glob("$directory/$subdirectory/*.pm")) {
374 10 50       109 next unless $file =~ m{($subdirectory/(\S+)\.pm)$};
375 10         39 my $module = file2module($1);
376 10         25 my $name = file2module($2);
377              
378 10 50       32 next if $disabled{lc($name)};
379              
380 10         8 my $version;
381 10 50 33     54 if ($self->{config}->{daemon} || $self->{config}->{service}) {
382             # server mode: check each task version in a child process
383 0         0 my ($reader, $writer);
384 0         0 pipe($reader, $writer);
385 0         0 $writer->autoflush(1);
386              
387 0 0       0 if (my $pid = fork()) {
388             # parent
389 0         0 close $writer;
390 0         0 $version = <$reader>;
391 0         0 close $reader;
392 0         0 waitpid($pid, 0);
393             } else {
394             # child
395 0 0       0 die "fork failed: $ERRNO" unless defined $pid;
396              
397 0         0 close $reader;
398 0         0 $version = $self->_getTaskVersion($module);
399 0 0       0 print $writer $version if $version;
400 0         0 close $writer;
401 0         0 exit(0);
402             }
403             } else {
404             # standalone mode: check each task version directly
405 10         24 $version = $self->_getTaskVersion($module);
406             }
407              
408             # no version means non-functionning task
409 10 100       29 next unless $version;
410              
411 7         22 $tasks{$name} = $version;
412             }
413              
414 4         28 return %tasks;
415             }
416              
417             sub _getTaskVersion {
418 10     10   16 my ($self, $module) = @_;
419              
420 10         15 my $logger = $self->{logger};
421              
422 10 100       81 if (!$module->require()) {
423 2 50       27 $logger->debug2("module $module does not compile: $@") if $logger;
424 2         4 return;
425             }
426              
427 8 100       310 if (!$module->isa('FusionInventory::Agent::Task')) {
428 1 50       3 $logger->debug2("module $module is not a task") if $logger;
429 1         3 return;
430             }
431              
432 7         11 my $version;
433             {
434 20     20   44205 no strict 'refs'; ## no critic
  20         46  
  20         5961  
  7         7  
435 7         8 $version = ${$module . '::VERSION'};
  7         24  
436             }
437              
438 7         13 return $version;
439             }
440              
441             sub _isAlreadyRunning {
442 0     0     my ($self, $pidfile) = @_;
443              
444 0           Proc::PID::File->require();
445 0 0         if ($EVAL_ERROR) {
446 0           $self->{logger}->debug(
447             'Proc::PID::File unavailable, unable to check for running agent'
448             );
449 0           return 0;
450             }
451              
452 0           my $pid = Proc::PID::File->new();
453 0           $pid->{path} = $pidfile;
454 0           return $pid->alive();
455             }
456              
457             sub _loadState {
458 0     0     my ($self) = @_;
459              
460 0           my $data = $self->{storage}->restore(name => 'FusionInventory-Agent');
461              
462 0 0         $self->{deviceid} = $data->{deviceid} if $data->{deviceid};
463             }
464              
465             sub _saveState {
466 0     0     my ($self) = @_;
467              
468 0           $self->{storage}->save(
469             name => 'FusionInventory-Agent',
470             data => {
471             deviceid => $self->{deviceid},
472             }
473             );
474             }
475              
476             # compute an unique agent identifier, based on host name and current time
477             sub _computeDeviceId {
478 0     0     my $hostname = getHostname();
479              
480 0           my ($year, $month , $day, $hour, $min, $sec) =
481             (localtime (time))[5, 4, 3, 2, 1, 0];
482              
483 0           return sprintf "%s-%02d-%02d-%02d-%02d-%02d-%02d",
484             $hostname, $year + 1900, $month + 1, $day, $hour, $min, $sec;
485             }
486              
487             1;
488             __END__