1package Munin::Master::UpdateWorker;
2use base qw(Munin::Master::Worker);
3
4use warnings;
5use strict;
6
7use Carp;
8use English qw(-no_match_vars);
9use Log::Log4perl qw( :easy );
10
11use File::Basename;
12use File::Path;
13use File::Spec;
14use Munin::Master::Config;
15use Munin::Master::Node;
16use Munin::Master::Utils;
17use RRDs;
18use Time::HiRes;
19use Data::Dumper;
20use Scalar::Util qw(weaken);
21
22use List::Util qw(max);
23
24my $config = Munin::Master::Config->instance()->{config};
25
26# Flags that have RRD autotuning enabled.
27my $rrd_tune_flags = {
28	type => '--data-source-type',
29	max => '--maximum',
30	min => '--minimum',
31};
32
33sub new {
34    my ($class, $host, $worker) = @_;
35
36    my $self = $class->SUPER::new($host->get_full_path);
37    $self->{host} = $host;
38    $self->{node} = Munin::Master::Node->new($host->{address},
39                                             $host->{port},
40                                             $host->{host_name},
41					     $host);
42    $self->{state} = {};
43    $self->{worker} = $worker;
44    weaken($self->{worker});
45
46    return $self;
47}
48
49
50sub do_work {
51    my ($self) = @_;
52
53    my $update_time = Time::HiRes::time;
54
55    my $host = $self->{host}{host_name};
56    my $path = $self->{host}->get_full_path;
57    $path =~ s{[:;]}{-}g;
58
59    # Parameters are space-separated from the main address
60    my ($url, $params) = split(/ +/, $self->{host}{address}, 2);
61    my $uri = new URI($url);
62
63    # If the scheme is not defined, it's a plain host.
64    # Prefix it with munin:// to be able to parse it like others
65    $uri = new URI("munin://" . $url) unless $uri->scheme;
66
67    my $nodedesignation;
68    if ($uri->scheme eq "ssh" || $uri->scheme eq "cmd") {
69      $nodedesignation = $host." (".$self->{host}{address}.")";
70    }else{
71      $nodedesignation = $host." (".$self->{host}{address}.":".$self->{host}{port}.")";
72    }
73
74    my $lock_file = sprintf ('%s/munin-%s.lock',
75			     $config->{rundir},
76			     $path);
77
78    if (!munin_getlock($lock_file)) {
79	WARN "Could not get lock $lock_file for $nodedesignation. Skipping node.";
80        die "Could not get lock $lock_file for $nodedesignation. Skipping node.\n";
81    }
82
83    # Reading the state file, no need to lock it, since it's per node and we
84    # already have a lock on this.
85    my $state_file = sprintf ('%s/state-%s.storable', $config->{dbdir}, $path);
86    DEBUG "[DEBUG] Reading state for $path in $state_file";
87    $self->{state} = munin_read_storable($state_file) || {};
88
89    my %all_service_configs = (
90	data_source => {},
91	global => {},
92	);
93
94    INFO "[INFO] starting work in $$ for $nodedesignation.\n";
95    my $done = $self->{node}->do_in_session(sub {
96
97	eval {
98	    # A I/O timeout results in a violent exit.  Catch and handle.
99
100	    my @node_capabilities = $self->{node}->negotiate_capabilities();
101
102            # Handle spoolfetch, one call to retrieve everything
103	    my %whole_config;
104	    my @plugins;
105	    if (grep /^spool$/, @node_capabilities) {
106		    my $spoolfetch_last_timestamp = $self->get_spoolfetch_timestamp();
107		    local $0 = "$0 -- spoolfetch($spoolfetch_last_timestamp)";
108		    %whole_config = $self->uw_spoolfetch($spoolfetch_last_timestamp);
109
110		    # XXX - Commented out, should be protect by a "if logger.isDebugEnabled()"
111		    #       since it is quite expensive
112		    #DEBUG "[DEBUG] whole_config:" . Dumper(\%whole_config);
113
114		    # spoolfetching reported no data, skipping it.
115		    if (! $whole_config{global}{multigraph}[1]) {
116			    INFO "[INFO] $nodedesignation didn't send any data for spoolfetch. Ignoring it.";
117			    # adding ourself to failed_workers, so we use
118			    push @{ $self->{worker}->{failed_workers} },  $self->{ID};
119			   die "NO_SPOOLFETCH_DATA";
120		    }
121
122		    # Gets the plugins from spoolfetch
123		    # Only keep the first one, the others will be multigraph-fetched
124		    @plugins = ( $whole_config{global}{multigraph}[0] ) ;
125	    }
126
127	    # Note: A multigraph plugin can present multiple services.
128	    @plugins = $self->{node}->list_plugins() unless @plugins;
129
130	    for my $plugin (@plugins) {
131		if (%{$config->{limit_services}}) {
132		    next unless $config->{limit_services}{$plugin};
133		}
134
135		DEBUG "[DEBUG] for my $plugin (@plugins)";
136
137		# Ask for config only if spoolfetch didn't already send it
138		my %service_config = %whole_config;
139	        unless (%service_config) {
140		       local $0 = "$0 -- config($plugin)";
141		       %service_config = $self->uw_fetch_service_config($plugin);
142		}
143
144		unless (%service_config) {
145		    WARN "[WARNING] Service $plugin on $nodedesignation ".
146			"returned no config";
147		    next;
148		}
149
150		# Check if this plugin has already sent its data via a dirtyconfig
151		# Note that spoolfetch also uses dirtyconfig
152		my %service_data = $self->handle_dirty_config(\%service_config);
153
154		# default is 0 sec : always update when asked
155		my $update_rate = get_global_service_value(\%service_config, $plugin, "update_rate", 0);
156		my ($update_rate_in_seconds, $is_update_aligned) = parse_update_rate($update_rate);
157		DEBUG "[DEBUG] update_rate $update_rate_in_seconds for $plugin on $nodedesignation";
158
159		if (! %service_data) {
160			# Check if this plugin has to be updated
161			if ($update_rate_in_seconds
162				&& $self->is_fresh_enough($nodedesignation, $plugin, $update_rate_in_seconds)) {
163			    # It's fresh enough, skip this $service
164			    DEBUG "[DEBUG] $plugin is fresh enough, not updating it";
165			    next;
166			}
167
168			# __root__ is only a placeholder plugin for
169			# an empty spoolfetch so we should ignore it
170			# if asked to fetch it.
171			# But we should still do everything after than.
172			if ($plugin ne "__root__") {
173				DEBUG "[DEBUG] No service data for $plugin, fetching it";
174				local $0 = "$0 -- fetch($plugin)";
175				%service_data = $self->{node}->fetch_service_data($plugin);
176			}
177		}
178
179		# If update_rate is aligned, round the "when" for alignement
180		if ($is_update_aligned) {
181			foreach my $service (keys %service_data) {
182				my $current_service_data = $service_data{$service};
183				foreach my $field (keys %$current_service_data) {
184					my $whens = $current_service_data->{$field}->{when};
185					for (my $i = 0; $i < scalar @$whens; $i ++) {
186						my $when = $whens->[$i];
187						my $rounded_when = round_to_granularity($when, $update_rate_in_seconds);
188						$whens->[$i] = $rounded_when;
189					}
190				}
191			}
192		}
193
194
195		# Since different plugins can populate multiple
196		# positions in the service namespace we'll check for
197		# collisions and warn of them.
198
199		for my $service (keys %{$service_config{data_source}}) {
200		    if (defined($all_service_configs{data_source}{$service})) {
201			WARN "[WARNING] Service collision: plugin $plugin on "
202			    ."$nodedesignation reports $service which already "
203			    ."exists on that host.  Deleting new data.";
204			delete($service_config{data_source}{$service});
205		    delete($service_data{$service})
206			if defined $service_data{$service};
207		    }
208		}
209
210		# .extinfo fields come from "fetch" but must be saved
211		# like "config".
212
213		for my $service (keys %service_data) {
214		    for my $ds (keys %{$service_data{$service}}) {
215			my $extinfo = $service_data{$service}{$ds}{extinfo};
216			if (defined $extinfo) {
217			    $service_config{data_source}{$service}{$ds}{extinfo} =
218				$extinfo;
219			    DEBUG "[DEBUG] Copied extinfo $extinfo into "
220				."service_config for $service / $ds on "
221				.$nodedesignation;
222			}
223		    }
224		}
225
226		$self->_compare_and_act_on_config_changes(\%service_config);
227
228		%{$all_service_configs{data_source}} = (
229		    %{$all_service_configs{data_source}},
230		    %{$service_config{data_source}});
231
232		%{$all_service_configs{global}} = (
233		    %{$all_service_configs{global}},
234		    %{$service_config{global}});
235
236		my $last_updated_timestamp = $self->_update_rrd_files(\%service_config, \%service_data);
237		if ($last_updated_timestamp) {
238		    $self->set_spoolfetch_timestamp($last_updated_timestamp);
239		}
240	    } # for @plugins
241
242	    # Send "quit" to node
243	    $self->{node}->quit();
244
245	}; # eval
246
247	# kill the remaining process if needed
248	if ($self->{node}->{pid} && kill(0, $self->{node}->{pid})) {
249		INFO "[INFO] Killing subprocess $self->{node}->{pid}";
250		kill 'TERM', $self->{node}->{pid};
251	}
252
253	if ($EVAL_ERROR =~ m/^NO_SPOOLFETCH_DATA /) {
254	    INFO "[INFO] No spoofetch data for $nodedesignation";
255	    return;
256	} elsif ($EVAL_ERROR) {
257	    ERROR "[ERROR] Error in node communication with $nodedesignation: "
258		.$EVAL_ERROR;
259	    return;
260	}
261
262	# Everything went smoothly.
263	DEBUG "[DEBUG] Everything went smoothly.";
264	return 1;
265
266    }); # do_in_session
267
268    munin_removelock($lock_file);
269
270    # Update the state file
271    DEBUG "[DEBUG] Writing state for $path in $state_file";
272    munin_write_storable($state_file, $self->{state});
273
274    # This handles failure in do_in_session,
275    return undef if ! $done || ! $done->{exit_value};
276
277    return {
278        time_used => Time::HiRes::time - $update_time,
279        service_configs => \%all_service_configs,
280    }
281}
282
283sub get_global_service_value {
284	my ($service_config, $service, $conf_field_name, $default) = @_;
285	foreach my $array (@{$service_config->{global}{$service}}) {
286		my ($field_name, $field_value) = @$array;
287		if ($field_name eq $conf_field_name) {
288			return $field_value;
289		}
290	}
291
292	return $default;
293}
294
295sub is_fresh_enough {
296	my ($self, $nodedesignation, $service, $update_rate_in_seconds) = @_;
297
298	DEBUG "is_fresh_enough asked for $service with a rate of $update_rate_in_seconds";
299
300	my $last_updated = $self->{state}{last_updated}{$service} || "0 0";
301	DEBUG "last_updated{$service}: " . $last_updated;
302	my @last = split(/ /, $last_updated);
303
304	use Time::HiRes qw(gettimeofday tv_interval);
305	my $now = [ gettimeofday ];
306
307	my $age = tv_interval(\@last, $now);
308	DEBUG "last: [" . join(",", @last) . "], now: [" . join(", ", @$now) . "], age: $age";
309	my $is_fresh_enough = ($age < $update_rate_in_seconds) ? 1 : 0;
310	DEBUG "is_fresh_enough  $is_fresh_enough";
311
312	if (! $is_fresh_enough) {
313		DEBUG "new value: " . join(" ", @$now);
314		$self->{state}{last_updated}{$service} = join(" ", @$now);
315	}
316
317	return $is_fresh_enough;
318}
319
320sub get_spoolfetch_timestamp {
321	my ($self) = @_;
322
323	my $last_updated_value = $self->{state}{spoolfetch} || "0";
324	return $last_updated_value;
325}
326
327sub set_spoolfetch_timestamp {
328	my ($self, $timestamp) = @_;
329	DEBUG "[DEBUG] set_spoolfetch_timestamp($timestamp)";
330
331	# Using the last timestamp sended by the server :
332	# -> It can be be different than "now" to be able to process the backlock slowly
333	$self->{state}{spoolfetch} = $timestamp;
334}
335
336sub parse_update_rate {
337	my ($update_rate_config) = @_;
338
339	my ($is_update_aligned, $update_rate_in_sec);
340	if ($update_rate_config =~ m/(\d+[a-z]?)( aligned)?/) {
341		$update_rate_in_sec = to_sec($1);
342		$is_update_aligned = $2;
343	} else {
344		return (0, 0);
345	}
346
347	return ($update_rate_in_sec, $is_update_aligned);
348}
349
350sub round_to_granularity {
351	my ($when, $granularity_in_sec) = @_;
352	$when = time if ($when eq "N"); # N means "now"
353
354	my $rounded_when = $when - ($when % $granularity_in_sec);
355	return $rounded_when;
356}
357
358sub handle_dirty_config {
359	my ($self, $service_config) = @_;
360
361	my %service_data;
362
363	my $services = $service_config->{global}{multigraph};
364	foreach my $service (@$services) {
365		my $service_data_source = $service_config->{data_source}->{$service};
366		foreach my $field (keys %$service_data_source) {
367			my $field_value = $service_data_source->{$field}->{value};
368			my $field_when = $service_data_source->{$field}->{when};
369
370			# If value not present, this field is not dirty fetched
371			next if (! defined $field_value);
372
373			DEBUG "[DEBUG] handle_dirty_config:$service, $field, @$field_when";
374			# Moves the "value" to the service_data
375			$service_data{$service}->{$field} ||= { when => [], value => [], };
376	                push @{$service_data{$service}{$field}{value}}, @$field_value;
377			push @{$service_data{$service}{$field}{when}}, @$field_when;
378
379			delete($service_data_source->{$field}{value});
380			delete($service_data_source->{$field}{when});
381		}
382	}
383
384	return %service_data;
385}
386
387
388sub uw_spoolfetch {
389    my ($self, $timestamp) = @_;
390
391    my %whole_config = $self->{node}->spoolfetch($timestamp);
392
393    # munin.conf might override stuff
394    foreach my $plugin (keys %whole_config) {
395	    my $merged_config = $self->uw_override_with_conf($plugin, $whole_config{$plugin});
396	    $whole_config{$plugin} = $merged_config;
397    }
398
399    return %whole_config;
400}
401
402sub uw_fetch_service_config {
403    my ($self, $plugin) = @_;
404
405    # Note, this can die for several reasons.  Caller must eval us.
406    my %service_config = $self->{node}->fetch_service_config($plugin);
407    my $merged_config = $self->uw_override_with_conf($plugin, \%service_config);
408
409    return %$merged_config;
410}
411
412sub uw_override_with_conf {
413    my ($self, $plugin, $service_config) = @_;
414
415    if ($self->{host}{service_config} &&
416	$self->{host}{service_config}{$plugin}) {
417
418        my %merged_config = (%$service_config, %{$self->{host}{service_config}{$plugin}});
419	$service_config = \%merged_config;
420    }
421
422    return $service_config;
423}
424
425
426sub _compare_and_act_on_config_changes {
427    my ($self, $nested_service_config) = @_;
428
429    # Kjellm: Why do we need to tune RRD files after upgrade?
430    # Shouldn't we create a upgrade script or something instead?
431    #
432    # janl: Upgrade script sucks.  This way it's inline in munin and
433    #  no need to remember anything or anything.
434
435    my $just_upgraded = 0;
436
437    my $old_config = Munin::Master::Config->instance()->{oldconfig};
438
439    if (not defined $old_config->{version}
440        or ($old_config->{version}
441            ne $Munin::Common::Defaults::MUNIN_VERSION)) {
442        $just_upgraded = 1;
443    }
444
445    for my $service (keys %{$nested_service_config->{data_source}}) {
446
447        my $service_config = $nested_service_config->{data_source}{$service};
448
449	for my $data_source (keys %{$service_config}) {
450	    my $old_data_source = $data_source;
451	    my $ds_config = $service_config->{$data_source};
452
453	    my $group = $self->{host}{group}{group_name};
454	    my $host = $self->{host}{host_name};
455
456	    my $old_host_config = $old_config->{groups}{$group}{hosts}{$host};
457	    my $old_ds_config = undef;
458
459	    if ($old_host_config) {
460		$old_ds_config =
461		    $old_host_config->get_canned_ds_config($service,
462							   $data_source);
463	    }
464
465	    if (defined($old_ds_config)
466		and %$old_ds_config
467		and defined($ds_config->{oldname})
468		and $ds_config->{oldname}) {
469
470		$old_data_source = $ds_config->{oldname};
471		$old_ds_config =
472		    $old_host_config->get_canned_ds_config($service,
473							   $old_data_source);
474	    }
475
476	    if (defined($old_ds_config)
477		and %$old_ds_config
478		and not $self->_ds_config_eq($old_ds_config, $ds_config)) {
479		$self->_ensure_filename($service,
480					$old_data_source, $data_source,
481					$old_ds_config, $ds_config)
482		    and $self->_ensure_tuning($service, $data_source,
483					      $ds_config);
484		# _ensure_filename prints helpfull warnings in the log
485	    } elsif ($just_upgraded) {
486		$self->_ensure_tuning($service, $data_source,
487				      $ds_config);
488	    }
489	}
490    }
491}
492
493
494sub _ds_config_eq {
495    my ($self, $old_ds_config, $ds_config) = @_;
496
497    $ds_config = $self->_get_rrd_data_source_with_defaults($ds_config);
498    $old_ds_config = $self->_get_rrd_data_source_with_defaults($old_ds_config);
499
500    # We only compare keys that are autotuned to avoid needless RRD tuning,
501    # since RRD tuning is bad for perf (flush rrdcached)
502    for my $key (keys %$rrd_tune_flags) {
503	my $old_value = $old_ds_config->{$key};
504	my $value = $ds_config->{$key};
505
506        # if both keys undefined, look no further
507        next unless (defined($old_value) || defined($value));
508
509	# so, at least one of the 2 is defined
510
511	# False if the $old_value is not defined
512	return 0 unless (defined($old_value));
513
514	# if something isn't the same, return false
515        return 0 if (! defined $value || $old_value ne $value);
516    }
517
518    # Nothing different found, it has to be equal.
519    return 1;
520}
521
522
523sub _ensure_filename {
524    my ($self, $service, $old_data_source, $data_source,
525        $old_ds_config, $ds_config) = @_;
526
527    my $rrd_file = $self->_get_rrd_file_name($service, $data_source,
528                                             $ds_config);
529    my $old_rrd_file = $self->_get_rrd_file_name($service, $old_data_source,
530                                                 $old_ds_config);
531
532    my $hostspec = $self->{node}{host}.'/'.$self->{node}{address}.':'.
533	$self->{node}{port};
534
535    if ($rrd_file ne $old_rrd_file) {
536        if (-f $old_rrd_file and -f $rrd_file) {
537            my $host = $self->{host}{host_name};
538            WARN "[WARNING]: $hostspec $service $data_source config change "
539		. "suggests moving '$old_rrd_file' to '$rrd_file' "
540		. "but both exist; manually merge the data "
541                . "or remove whichever file you care less about.\n";
542	    return '';
543        } elsif (-f $old_rrd_file) {
544            INFO "[INFO]: Config update, changing name of '$old_rrd_file'"
545                   . " to '$rrd_file' on $hostspec ";
546            unless (rename ($old_rrd_file, $rrd_file)) {
547                ERROR "[ERROR]: Could not rename '$old_rrd_file' to"
548		    . " '$rrd_file' for $hostspec: $!\n";
549                return '';
550            }
551        }
552    }
553
554    return 1;
555}
556
557
558sub _ensure_tuning {
559    my ($self, $service, $data_source, $ds_config) = @_;
560    my $success = 1;
561
562    my $rrd_file =
563        $self->_get_rrd_file_name($service, $data_source,
564                                  $ds_config);
565
566    $ds_config = $self->_get_rrd_data_source_with_defaults($ds_config);
567    for my $rrd_prop (keys %$rrd_tune_flags) {
568        INFO "[INFO]: Config update, ensuring $rrd_prop of"
569	    . " '$rrd_file' is '$ds_config->{$rrd_prop}'.\n";
570        RRDs::tune($rrd_file, $rrd_tune_flags->{$rrd_prop},
571                   "42:$ds_config->{$rrd_prop}");
572        if (my $tune_error = RRDs::error()) {
573            ERROR "[ERROR] Tuning $rrd_prop of '$rrd_file' to"
574		. " '$ds_config->{$rrd_prop}' failed.\n";
575            $success = '';
576        }
577    }
578
579    return $success;
580}
581
582
583sub _update_rrd_files {
584    my ($self, $nested_service_config, $nested_service_data) = @_;
585
586    my $nodedesignation = $self->{host}{host_name}."/".
587	$self->{host}{address}.":".$self->{host}{port};
588
589    my $last_timestamp =
590    	max(0,
591    	    map {
592    		my $svc = $_;
593    		map {
594    		    my $ds = $_;
595    		    @{$nested_service_data->{$svc}->{$ds}->{when} || []};
596    		} keys %{$nested_service_config->{data_source}{$svc}};
597    	    } keys %{$nested_service_config->{data_source}}
598    	);
599    my $last_timestamp_or_now = ($last_timestamp > 0) ? $last_timestamp : time;
600
601    for my $service (keys %{$nested_service_config->{data_source}}) {
602
603	my $service_config = $nested_service_config->{data_source}{$service};
604	my $service_data   = $nested_service_data->{$service};
605
606	for my $ds_name (keys %{$service_config}) {
607	    my $ds_config = $service_config->{$ds_name};
608
609	    unless (defined($ds_config->{label})) {
610		ERROR "[ERROR] Unable to update $service on $nodedesignation -> $ds_name: Missing data source configuration attribute: label";
611		next;
612	    }
613
614	    # Sets the DS resolution, searching in that order :
615	    # - per field
616	    # - per plugin
617	    # - globally
618            my $configref = $self->{node}{configref};
619	    $ds_config->{graph_data_size} ||= get_config_for_service($nested_service_config->{global}{$service}, "graph_data_size");
620	    $ds_config->{graph_data_size} ||= $config->{graph_data_size};
621
622	    $ds_config->{update_rate} ||= get_config_for_service($nested_service_config->{global}{$service}, "update_rate");
623	    $ds_config->{update_rate} ||= $config->{update_rate};
624	    $ds_config->{update_rate} ||= 300; # default is 5 min
625
626	    DEBUG "[DEBUG] asking for a rrd of size : " . $ds_config->{graph_data_size};
627
628	    # Avoid autovivification (for multigraphs)
629	    my $first_epoch = (defined($service_data) and defined($service_data->{$ds_name})) ? ($service_data->{$ds_name}->{when}->[0]) : 0;
630	    my $rrd_file = $self->_create_rrd_file_if_needed($service, $ds_name, $ds_config, $first_epoch);
631
632	    if (defined($service_data) and defined($service_data->{$ds_name})) {
633		$self->_update_rrd_file($rrd_file, $ds_name, $service_data->{$ds_name}, $last_timestamp_or_now);
634	    }
635	    else {
636		WARN "[WARNING] Service $service on $nodedesignation returned no data for label $ds_name";
637	    }
638	}
639    }
640
641    return $last_timestamp;
642}
643
644sub get_config_for_service {
645	my ($array, $key) = @_;
646
647	for my $elem (@$array) {
648		next unless $elem->[0] && $elem->[0] eq $key;
649		return $elem->[1];
650	}
651
652	# Not found
653	return undef;
654}
655
656
657sub _get_rrd_data_source_with_defaults {
658    my ($self, $data_source) = @_;
659
660    # Copy it into a new hash, we don't want to alter the $data_source
661    # and anything already defined should not be overridden by defaults
662    my $ds_with_defaults = {
663	    type => 'GAUGE',
664	    min => 'U',
665	    max => 'U',
666
667	    update_rate => 300,
668	    graph_data_size => 'normal',
669    };
670    for my $key (keys %$data_source) {
671	    $ds_with_defaults->{$key} = $data_source->{$key};
672    }
673
674    return $ds_with_defaults;
675}
676
677
678sub _create_rrd_file_if_needed {
679    my ($self, $service, $ds_name, $ds_config, $first_epoch) = @_;
680
681    my $rrd_file = $self->_get_rrd_file_name($service, $ds_name, $ds_config);
682    unless (-f $rrd_file) {
683        $self->_create_rrd_file($rrd_file, $service, $ds_name, $ds_config, $first_epoch);
684    }
685
686    return $rrd_file;
687}
688
689
690sub _get_rrd_file_name {
691    my ($self, $service, $ds_name, $ds_config) = @_;
692
693    $ds_config = $self->_get_rrd_data_source_with_defaults($ds_config);
694    my $type_id = lc(substr(($ds_config->{type}), 0, 1));
695
696    my $path = $self->{host}->get_full_path;
697    $path =~ s{[;:]}{/}g;
698
699    # Multigraph/nested services will have . in the service name in this function.
700    $service =~ s{\.}{-}g;
701
702    # The following is rigged to match the corresponding function in
703    # munin-graph/munin-html where it's less clear what are groups and
704    # what are hosts and what are services, and they simply pop
705    # elements off the end and so on.
706
707    my $file = sprintf("%s-%s-%s-%s.rrd",
708                       $path,
709                       $service,
710                       $ds_name,
711                       $type_id);
712
713    $file = File::Spec->catfile($config->{dbdir},
714				$file);
715
716    DEBUG "[DEBUG] rrd filename: $file\n";
717
718    return $file;
719}
720
721
722sub _create_rrd_file {
723    my ($self, $rrd_file, $service, $ds_name, $ds_config, $first_epoch) = @_;
724
725    INFO "[INFO] creating rrd-file for $service->$ds_name: '$rrd_file'";
726
727    munin_mkdir_p(dirname($rrd_file), oct(777));
728
729    my @args;
730
731    $ds_config = $self->_get_rrd_data_source_with_defaults($ds_config);
732    my $resolution = $ds_config->{graph_data_size};
733    my $update_rate = $ds_config->{update_rate};
734    if ($resolution eq 'normal') {
735	$update_rate = 300; # 'normal' means hard coded RRD $update_rate
736        push (@args,
737              "RRA:AVERAGE:0.5:1:576",   # resolution 5 minutes
738              "RRA:MIN:0.5:1:576",
739              "RRA:MAX:0.5:1:576",
740              "RRA:AVERAGE:0.5:6:432",   # 9 days, resolution 30 minutes
741              "RRA:MIN:0.5:6:432",
742              "RRA:MAX:0.5:6:432",
743              "RRA:AVERAGE:0.5:24:540",  # 45 days, resolution 2 hours
744              "RRA:MIN:0.5:24:540",
745              "RRA:MAX:0.5:24:540",
746              "RRA:AVERAGE:0.5:288:450", # 450 days, resolution 1 day
747              "RRA:MIN:0.5:288:450",
748              "RRA:MAX:0.5:288:450");
749    }
750    elsif ($resolution eq 'huge') {
751	$update_rate = 300; # 'huge' means hard coded RRD $update_rate
752        push (@args,
753              "RRA:AVERAGE:0.5:1:115200",  # resolution 5 minutes, for 400 days
754              "RRA:MIN:0.5:1:115200",
755              "RRA:MAX:0.5:1:115200");
756    } elsif ($resolution =~ /^custom (.+)/) {
757        # Parsing resolution to achieve computer format as defined on the RFC :
758        # FULL_NB, MULTIPLIER_1 MULTIPLIER_1_NB, ... MULTIPLIER_NMULTIPLIER_N_NB
759        my @resolutions_computer = parse_custom_resolution($1, $update_rate);
760        foreach my $resolution_computer(@resolutions_computer) {
761            my ($multiplier, $multiplier_nb) = @{$resolution_computer};
762	    # Always add 10% to the RRA size, as specified in
763	    # http://munin-monitoring.org/wiki/format-graph_data_size
764	    $multiplier_nb += int ($multiplier_nb / 10) || 1;
765            push (@args,
766                "RRA:AVERAGE:0.5:$multiplier:$multiplier_nb",
767                "RRA:MIN:0.5:$multiplier:$multiplier_nb",
768                "RRA:MAX:0.5:$multiplier:$multiplier_nb"
769            );
770        }
771    }
772
773    # Add the RRD::create prefix (filename & RRD params)
774    my $heartbeat = $update_rate * 2;
775    unshift (@args,
776        $rrd_file,
777        "--start", ($first_epoch - $update_rate),
778	"-s", $update_rate,
779        sprintf('DS:42:%s:%s:%s:%s',
780                $ds_config->{type}, $heartbeat, $ds_config->{min}, $ds_config->{max}),
781    );
782
783    DEBUG "[DEBUG] RRDs::create @args";
784    RRDs::create @args;
785    if (my $ERROR = RRDs::error) {
786        ERROR "[ERROR] Unable to create '$rrd_file': $ERROR";
787    }
788}
789
790sub parse_custom_resolution {
791	my @elems = split(',\s*', shift);
792	my $update_rate = shift;
793
794	DEBUG "[DEBUG] update_rate: $update_rate";
795
796        my @computer_format;
797
798	# First element is always the full resolution
799	my $full_res = shift @elems;
800	if ($full_res =~ m/^\d+$/) {
801		# Only numeric, computer format
802		unshift @elems, "1 $full_res";
803	} else {
804		# Human readable. Adding $update_rate in front of
805		unshift @elems, "$update_rate for $full_res";
806	}
807
808        foreach my $elem (@elems) {
809                if ($elem =~ m/(\d+) (\d+)/) {
810                        # nothing to do, already in computer format
811                        push @computer_format, [$1, $2];
812                } elsif ($elem =~ m/(\w+) for (\w+)/) {
813                        my $nb_sec = to_sec($1);
814                        my $for_sec = to_sec($2);
815
816			my $multiplier = int ($nb_sec / $update_rate);
817                        my $multiplier_nb = int ($for_sec / $nb_sec);
818
819			DEBUG "[DEBUG] $elem"
820				. " -> nb_sec:$nb_sec, for_sec:$for_sec"
821				. " -> multiplier:$multiplier, multiplier_nb:$multiplier_nb"
822			;
823                        push @computer_format, [$multiplier, $multiplier_nb];
824                }
825	}
826
827        return @computer_format;
828}
829
830# return the number of seconds
831# for the human readable format
832# s : second,  m : minute, h : hour
833# d : day, w : week, t : month, y : year
834sub to_sec {
835	my $secs_table = {
836		"s" => 1,
837		"m" => 60,
838		"h" => 60 * 60,
839		"d" => 60 * 60 * 24,
840		"w" => 60 * 60 * 24 * 7,
841		"t" => 60 * 60 * 24 * 31, # a month always has 31 days
842		"y" => 60 * 60 * 24 * 365, # a year always has 365 days
843	};
844
845	my ($target) = @_;
846	if ($target =~ m/(\d+)([smhdwty])/i) {
847		return $1 * $secs_table->{$2};
848	} else {
849		# no recognised unit, return the int value as seconds
850		return int $target;
851	}
852}
853
854sub to_mul {
855	my ($base, $target) = @_;
856	my $target_sec = to_sec($target);
857	if ($target %% $base != 0) {
858		return 0;
859	}
860
861	return round($target / $base);
862}
863
864sub to_mul_nb {
865	my ($base, $target) = @_;
866	my $target_sec = to_sec($target);
867	if ($target %% $base != 0) {
868		return 0;
869	}
870}
871
872sub _update_rrd_file {
873    my ($self, $rrd_file, $ds_name, $ds_values, $max_timestamp) = @_;
874
875    my $values = $ds_values->{value};
876
877    # Some kind of mismatch between fetch and config can cause this.
878    return if !defined($values);
879
880    my ($previous_updated_timestamp, $previous_updated_value) = @{ $self->{state}{value}{"$rrd_file:42"}{current} || [ ] };
881    my @update_rrd_data;
882	if ($config->{"rrdcached_socket"}) {
883		if (! -e $config->{"rrdcached_socket"} || ! -w $config->{"rrdcached_socket"}) {
884			WARN "[WARN] RRDCached feature ignored: rrdcached socket not writable";
885		} elsif($RRDs::VERSION < 1.3){
886			WARN "[WARN] RRDCached feature ignored: perl RRDs lib version must be at least 1.3. Version found: " . $RRDs::VERSION;
887		} else {
888			# Using the RRDCACHED_ADDRESS environnement variable, as
889			# it is way less intrusive than the command line args.
890			$ENV{RRDCACHED_ADDRESS} = $config->{"rrdcached_socket"};
891		}
892	}
893
894    my ($current_updated_timestamp, $current_updated_value) = ($previous_updated_timestamp, $previous_updated_value);
895    for (my $i = 0; $i < scalar @$values; $i++) {
896        my $value = $values->[$i];
897        my $when = $ds_values->{when}[$i];
898
899	if ($when == $self->{node}->NO_TIMESTAMP) {
900	    $when = $max_timestamp;
901	}
902
903	# Ignore values that is not in monotonic increasing timestamp for the RRD.
904	# Otherwise it will reject the whole update
905	next if ($current_updated_timestamp && $when <= $current_updated_timestamp);
906
907        if ($value =~ /\d[Ee]([+-]?\d+)$/) {
908            # Looks like scientific format.  RRDtool does not
909            # like it so we convert it.
910            my $magnitude = $1;
911            if ($magnitude < 0) {
912                # Preserve at least 4 significant digits
913                $magnitude = abs($magnitude) + 4;
914                $value = sprintf("%.*f", $magnitude, $value);
915            } else {
916                $value = sprintf("%.4f", $value);
917            }
918        }
919
920        # Schedule for addition
921        push @update_rrd_data, "$when:$value";
922
923	$current_updated_timestamp = $when;
924	$current_updated_value = $value;
925    }
926
927    DEBUG "[DEBUG] Updating $rrd_file with @update_rrd_data";
928    if ($ENV{RRDCACHED_ADDRESS} && (scalar @update_rrd_data > 32) ) {
929        # RRDCACHED only takes about 4K worth of commands. If the commands is
930        # too large, we have to break it in smaller calls.
931        #
932        # Note that 32 is just an arbitrary choosed number. It might be tweaked.
933        #
934        # For simplicity we only call it with 1 update each time, as RRDCACHED
935        # will buffer for us as suggested on the rrd mailing-list.
936        # https://lists.oetiker.ch/pipermail/rrd-users/2011-October/018196.html
937        for my $update_rrd_data (@update_rrd_data) {
938            RRDs::update($rrd_file, $update_rrd_data);
939            # Break on error.
940            last if RRDs::error;
941        }
942    } else {
943        RRDs::update($rrd_file, @update_rrd_data);
944    }
945
946    if (my $ERROR = RRDs::error) {
947        #confess Dumper @_;
948        ERROR "[ERROR] In RRD: Error updating $rrd_file: $ERROR";
949    }
950
951    # Stores the previous and the current value in the state db to avoid having to do an RRD lookup if needed
952    $self->{state}{value}{"$rrd_file:42"}{current} = [ $current_updated_timestamp, $current_updated_value ];
953    $self->{state}{value}{"$rrd_file:42"}{previous} = [ $previous_updated_timestamp, $previous_updated_value ];
954
955    return scalar @update_rrd_data;
956}
957
958sub dump_to_file
959{
960	my ($filename, $obj) = @_;
961	open(DUMPFILE, ">> $filename");
962
963	use Data::Dumper;
964	print DUMPFILE Dumper($obj);
965
966	close(DUMPFILE);
967}
968
9691;
970
971
972__END__
973
974=head1 NAME
975
976Munin::Master::UpdateWorker - FIX
977
978=head1 SYNOPSIS
979
980FIX
981
982=head1 METHODS
983
984=over
985
986=item B<new>
987
988FIX
989
990=item B<do_work>
991
992FIX
993
994=back
995
996=head1 COPYING
997
998Copyright (C) 2002-2009  Jimmy Olsen, et al.
999
1000  This program is free software; you can redistribute it and/or modify
1001  it under the terms of the GNU General Public License as published by
1002  the Free Software Foundation; version 2 dated June, 1991.
1003
1004  This program is distributed in the hope that it will be useful,
1005  but WITHOUT ANY WARRANTY; without even the implied warranty of
1006  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1007  GNU General Public License for more details.
1008
1009  You should have received a copy of the GNU General Public License along
1010  with this program; if not, write to the Free Software Foundation, Inc.,
1011  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
1012
1013
1014