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