1package DBD::MySQL::Cluster; 2 3use strict; 4use Time::HiRes; 5use IO::File; 6use Data::Dumper; 7 8my %ERRORS=( OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3 ); 9my %ERRORCODES=( 0 => 'OK', 1 => 'WARNING', 2 => 'CRITICAL', 3 => 'UNKNOWN' ); 10 11{ 12 our $verbose = 0; 13 our $scream = 0; # scream if something is not implemented 14 our $access = "dbi"; # how do we access the database. 15 our $my_modules_dyn_dir = ""; # where we look for self-written extensions 16 17 my @clusters = (); 18 my $initerrors = undef; 19 20 sub add_cluster { 21 push(@clusters, shift); 22 } 23 24 sub return_clusters { 25 return @clusters; 26 } 27 28 sub return_first_cluster() { 29 return $clusters[0]; 30 } 31 32} 33 34sub new { 35 my $class = shift; 36 my %params = @_; 37 my $self = { 38 hostname => $params{hostname}, 39 port => $params{port}, 40 username => $params{username}, 41 password => $params{password}, 42 timeout => $params{timeout}, 43 warningrange => $params{warningrange}, 44 criticalrange => $params{criticalrange}, 45 version => 'unknown', 46 nodes => [], 47 ndbd_nodes => 0, 48 ndb_mgmd_nodes => 0, 49 mysqld_nodes => 0, 50 }; 51 bless $self, $class; 52 $self->init_nagios(); 53 if ($self->connect(%params)) { 54 DBD::MySQL::Cluster::add_cluster($self); 55 $self->init(%params); 56 } 57 return $self; 58} 59 60sub init { 61 my $self = shift; 62 my %params = @_; 63 if ($self->{show}) { 64 my $type = undef; 65 foreach (split /\n/, $self->{show}) { 66 if (/\[(\w+)\((\w+)\)\]\s+(\d+) node/) { 67 $type = uc $2; 68 } elsif (/id=(\d+)(.*)/) { 69 push(@{$self->{nodes}}, DBD::MySQL::Cluster::Node->new( 70 type => $type, 71 id => $1, 72 status => $2, 73 )); 74 } 75 } 76 } else { 77 } 78 if ($params{mode} =~ /^cluster::ndbdrunning/) { 79 foreach my $node (@{$self->{nodes}}) { 80 $node->{type} eq "NDB" && $node->{status} eq "running" && $self->{ndbd_nodes}++; 81 $node->{type} eq "MGM" && $node->{status} eq "running" && $self->{ndb_mgmd_nodes}++; 82 $node->{type} eq "API" && $node->{status} eq "running" && $self->{mysqld_nodes}++; 83 } 84 } else { 85 printf "broken mode %s\n", $params{mode}; 86 } 87} 88 89sub dump { 90 my $self = shift; 91 my $message = shift || ""; 92 printf "%s %s\n", $message, Data::Dumper::Dumper($self); 93} 94 95sub nagios { 96 my $self = shift; 97 my %params = @_; 98 my $dead_ndb = 0; 99 my $dead_api = 0; 100 if (! $self->{nagios_level}) { 101 if ($params{mode} =~ /^cluster::ndbdrunning/) { 102 foreach my $node (grep { $_->{type} eq "NDB"} @{$self->{nodes}}) { 103 next if $params{selectname} && $params{selectname} ne $_->{id}; 104 if (! $node->{connected}) { 105 $self->add_nagios_critical( 106 sprintf "ndb node %d is not connected", $node->{id}); 107 $dead_ndb++; 108 } 109 } 110 foreach my $node (grep { $_->{type} eq "API"} @{$self->{nodes}}) { 111 next if $params{selectname} && $params{selectname} ne $_->{id}; 112 if (! $node->{connected}) { 113 $self->add_nagios_critical( 114 sprintf "api node %d is not connected", $node->{id}); 115 $dead_api++; 116 } 117 } 118 if (! $dead_ndb) { 119 $self->add_nagios_ok("all ndb nodes are connected"); 120 } 121 if (! $dead_api) { 122 $self->add_nagios_ok("all api nodes are connected"); 123 } 124 } 125 } 126 $self->add_perfdata(sprintf "ndbd_nodes=%d ndb_mgmd_nodes=%d mysqld_nodes=%d", 127 $self->{ndbd_nodes}, $self->{ndb_mgmd_nodes}, $self->{mysqld_nodes}); 128} 129 130 131sub init_nagios { 132 my $self = shift; 133 no strict 'refs'; 134 if (! ref($self)) { 135 my $nagiosvar = $self."::nagios"; 136 my $nagioslevelvar = $self."::nagios_level"; 137 $$nagiosvar = { 138 messages => { 139 0 => [], 140 1 => [], 141 2 => [], 142 3 => [], 143 }, 144 perfdata => [], 145 }; 146 $$nagioslevelvar = $ERRORS{OK}, 147 } else { 148 $self->{nagios} = { 149 messages => { 150 0 => [], 151 1 => [], 152 2 => [], 153 3 => [], 154 }, 155 perfdata => [], 156 }; 157 $self->{nagios_level} = $ERRORS{OK}, 158 } 159} 160 161sub check_thresholds { 162 my $self = shift; 163 my $value = shift; 164 my $defaultwarningrange = shift; 165 my $defaultcriticalrange = shift; 166 my $level = $ERRORS{OK}; 167 $self->{warningrange} = $self->{warningrange} ? 168 $self->{warningrange} : $defaultwarningrange; 169 $self->{criticalrange} = $self->{criticalrange} ? 170 $self->{criticalrange} : $defaultcriticalrange; 171 if ($self->{warningrange} !~ /:/ && $self->{criticalrange} !~ /:/) { 172 # warning = 10, critical = 20, warn if > 10, crit if > 20 173 $level = $ERRORS{WARNING} if $value > $self->{warningrange}; 174 $level = $ERRORS{CRITICAL} if $value > $self->{criticalrange}; 175 } elsif ($self->{warningrange} =~ /([\d\.]+):/ && 176 $self->{criticalrange} =~ /([\d\.]+):/) { 177 # warning = 98:, critical = 95:, warn if < 98, crit if < 95 178 $self->{warningrange} =~ /([\d\.]+):/; 179 $level = $ERRORS{WARNING} if $value < $1; 180 $self->{criticalrange} =~ /([\d\.]+):/; 181 $level = $ERRORS{CRITICAL} if $value < $1; 182 } 183 return $level; 184 # 185 # syntax error must be reported with returncode -1 186 # 187} 188 189sub add_nagios { 190 my $self = shift; 191 my $level = shift; 192 my $message = shift; 193 push(@{$self->{nagios}->{messages}->{$level}}, $message); 194 # recalc current level 195 foreach my $llevel (qw(CRITICAL WARNING UNKNOWN OK)) { 196 if (scalar(@{$self->{nagios}->{messages}->{$ERRORS{$llevel}}})) { 197 $self->{nagios_level} = $ERRORS{$llevel}; 198 } 199 } 200} 201 202sub add_nagios_ok { 203 my $self = shift; 204 my $message = shift; 205 $self->add_nagios($ERRORS{OK}, $message); 206} 207 208sub add_nagios_warning { 209 my $self = shift; 210 my $message = shift; 211 $self->add_nagios($ERRORS{WARNING}, $message); 212} 213 214sub add_nagios_critical { 215 my $self = shift; 216 my $message = shift; 217 $self->add_nagios($ERRORS{CRITICAL}, $message); 218} 219 220sub add_nagios_unknown { 221 my $self = shift; 222 my $message = shift; 223 $self->add_nagios($ERRORS{UNKNOWN}, $message); 224} 225 226sub add_perfdata { 227 my $self = shift; 228 my $data = shift; 229 push(@{$self->{nagios}->{perfdata}}, $data); 230} 231 232sub merge_nagios { 233 my $self = shift; 234 my $child = shift; 235 foreach my $level (0..3) { 236 foreach (@{$child->{nagios}->{messages}->{$level}}) { 237 $self->add_nagios($level, $_); 238 } 239 #push(@{$self->{nagios}->{messages}->{$level}}, 240 # @{$child->{nagios}->{messages}->{$level}}); 241 } 242 push(@{$self->{nagios}->{perfdata}}, @{$child->{nagios}->{perfdata}}); 243} 244 245 246sub calculate_result { 247 my $self = shift; 248 if ($ENV{NRPE_MULTILINESUPPORT} && 249 length join(" ", @{$self->{nagios}->{perfdata}}) > 200) { 250 foreach my $level ("CRITICAL", "WARNING", "UNKNOWN", "OK") { 251 # first the bad news 252 if (scalar(@{$self->{nagios}->{messages}->{$ERRORS{$level}}})) { 253 $self->{nagios_message} .= 254 "\n".join("\n", @{$self->{nagios}->{messages}->{$ERRORS{$level}}}); 255 } 256 } 257 $self->{nagios_message} =~ s/^\n//g; 258 $self->{perfdata} = join("\n", @{$self->{nagios}->{perfdata}}); 259 } else { 260 foreach my $level ("CRITICAL", "WARNING", "UNKNOWN", "OK") { 261 # first the bad news 262 if (scalar(@{$self->{nagios}->{messages}->{$ERRORS{$level}}})) { 263 $self->{nagios_message} .= 264 join(", ", @{$self->{nagios}->{messages}->{$ERRORS{$level}}}).", "; 265 } 266 } 267 $self->{nagios_message} =~ s/, $//g; 268 $self->{perfdata} = join(" ", @{$self->{nagios}->{perfdata}}); 269 } 270 foreach my $level ("OK", "UNKNOWN", "WARNING", "CRITICAL") { 271 if (scalar(@{$self->{nagios}->{messages}->{$ERRORS{$level}}})) { 272 $self->{nagios_level} = $ERRORS{$level}; 273 } 274 } 275} 276 277sub debug { 278 my $self = shift; 279 my $msg = shift; 280 if ($DBD::MySQL::Cluster::verbose) { 281 printf "%s %s\n", $msg, ref($self); 282 } 283} 284 285sub connect { 286 my $self = shift; 287 my %params = @_; 288 my $retval = undef; 289 $self->{tic} = Time::HiRes::time(); 290 eval { 291 use POSIX ':signal_h'; 292 local $SIG{'ALRM'} = sub { 293 die "alarm\n"; 294 }; 295 my $mask = POSIX::SigSet->new( SIGALRM ); 296 my $action = POSIX::SigAction->new( 297 sub { die "connection timeout\n" ; }, $mask); 298 my $oldaction = POSIX::SigAction->new(); 299 sigaction(SIGALRM ,$action ,$oldaction ); 300 alarm($self->{timeout} - 1); # 1 second before the global unknown timeout 301 my $ndb_mgm = "ndb_mgm"; 302 $params{hostname} = "127.0.0.1" if ! $params{hostname}; 303 $ndb_mgm .= sprintf " --ndb-connectstring=%s", $params{hostname} 304 if $params{hostname}; 305 $ndb_mgm .= sprintf ":%d", $params{port} 306 if $params{port}; 307 $self->{show} = `$ndb_mgm -e show 2>&1`; 308 if ($? == -1) { 309 $self->add_nagios_critical("ndb_mgm failed to execute $!"); 310 } elsif ($? & 127) { 311 $self->add_nagios_critical("ndb_mgm failed to execute $!"); 312 } elsif ($? >> 8 != 0) { 313 $self->add_nagios_critical("ndb_mgm unable to connect"); 314 } else { 315 if ($self->{show} !~ /Cluster Configuration/) { 316 $self->add_nagios_critical("got no cluster configuration"); 317 } else { 318 $retval = 1; 319 } 320 } 321 }; 322 if ($@) { 323 $self->{errstr} = $@; 324 $self->{errstr} =~ s/at $0 .*//g; 325 chomp $self->{errstr}; 326 $self->add_nagios_critical($self->{errstr}); 327 $retval = undef; 328 } 329 $self->{tac} = Time::HiRes::time(); 330 return $retval; 331} 332 333sub trace { 334 my $self = shift; 335 my $format = shift; 336 $self->{trace} = -f "/tmp/check_mysql_health.trace" ? 1 : 0; 337 if ($self->{verbose}) { 338 printf("%s: ", scalar localtime); 339 printf($format, @_); 340 } 341 if ($self->{trace}) { 342 my $logfh = new IO::File; 343 $logfh->autoflush(1); 344 if ($logfh->open("/tmp/check_mysql_health.trace", "a")) { 345 $logfh->printf("%s: ", scalar localtime); 346 $logfh->printf($format, @_); 347 $logfh->printf("\n"); 348 $logfh->close(); 349 } 350 } 351} 352 353sub DESTROY { 354 my $self = shift; 355 my $handle1 = "null"; 356 my $handle2 = "null"; 357 if (defined $self->{handle}) { 358 $handle1 = ref($self->{handle}); 359 if (defined $self->{handle}->{handle}) { 360 $handle2 = ref($self->{handle}->{handle}); 361 } 362 } 363 $self->trace(sprintf "DESTROY %s with handle %s %s", ref($self), $handle1, $handle2); 364 if (ref($self) eq "DBD::MySQL::Cluster") { 365 } 366 $self->trace(sprintf "DESTROY %s exit with handle %s %s", ref($self), $handle1, $handle2); 367 if (ref($self) eq "DBD::MySQL::Cluster") { 368 #printf "humpftata\n"; 369 } 370} 371 372sub save_state { 373 my $self = shift; 374 my %params = @_; 375 my $extension = ""; 376 mkdir $params{statefilesdir} unless -d $params{statefilesdir}; 377 my $statefile = sprintf "%s/%s_%s", 378 $params{statefilesdir}, $params{hostname}, $params{mode}; 379 $extension .= $params{differenciator} ? "_".$params{differenciator} : ""; 380 $extension .= $params{socket} ? "_".$params{socket} : ""; 381 $extension .= $params{port} ? "_".$params{port} : ""; 382 $extension .= $params{database} ? "_".$params{database} : ""; 383 $extension .= $params{tablespace} ? "_".$params{tablespace} : ""; 384 $extension .= $params{datafile} ? "_".$params{datafile} : ""; 385 $extension .= $params{name} ? "_".$params{name} : ""; 386 $extension =~ s/\//_/g; 387 $extension =~ s/\(/_/g; 388 $extension =~ s/\)/_/g; 389 $extension =~ s/\*/_/g; 390 $extension =~ s/\s/_/g; 391 $statefile .= $extension; 392 $statefile = lc $statefile; 393 open(STATE, ">$statefile"); 394 if ((ref($params{save}) eq "HASH") && exists $params{save}->{timestamp}) { 395 $params{save}->{localtime} = scalar localtime $params{save}->{timestamp}; 396 } 397 printf STATE Data::Dumper::Dumper($params{save}); 398 close STATE; 399 $self->debug(sprintf "saved %s to %s", 400 Data::Dumper::Dumper($params{save}), $statefile); 401} 402 403sub load_state { 404 my $self = shift; 405 my %params = @_; 406 my $extension = ""; 407 my $statefile = sprintf "%s/%s_%s", 408 $params{statefilesdir}, $params{hostname}, $params{mode}; 409 $extension .= $params{differenciator} ? "_".$params{differenciator} : ""; 410 $extension .= $params{socket} ? "_".$params{socket} : ""; 411 $extension .= $params{port} ? "_".$params{port} : ""; 412 $extension .= $params{database} ? "_".$params{database} : ""; 413 $extension .= $params{tablespace} ? "_".$params{tablespace} : ""; 414 $extension .= $params{datafile} ? "_".$params{datafile} : ""; 415 $extension .= $params{name} ? "_".$params{name} : ""; 416 $extension =~ s/\//_/g; 417 $extension =~ s/\(/_/g; 418 $extension =~ s/\)/_/g; 419 $extension =~ s/\*/_/g; 420 $extension =~ s/\s/_/g; 421 $statefile .= $extension; 422 $statefile = lc $statefile; 423 if ( -f $statefile) { 424 our $VAR1; 425 eval { 426 require $statefile; 427 }; 428 if($@) { 429printf "rumms\n"; 430 } 431 $self->debug(sprintf "load %s", Data::Dumper::Dumper($VAR1)); 432 return $VAR1; 433 } else { 434 return undef; 435 } 436} 437 438sub valdiff { 439 my $self = shift; 440 my $pparams = shift; 441 my %params = %{$pparams}; 442 my @keys = @_; 443 my $last_values = $self->load_state(%params) || eval { 444 my $empty_events = {}; 445 foreach (@keys) { 446 $empty_events->{$_} = 0; 447 } 448 $empty_events->{timestamp} = 0; 449 $empty_events; 450 }; 451 foreach (@keys) { 452 $self->{'delta_'.$_} = $self->{$_} - $last_values->{$_}; 453 $self->debug(sprintf "delta_%s %f", $_, $self->{'delta_'.$_}); 454 } 455 $self->{'delta_timestamp'} = time - $last_values->{timestamp}; 456 $params{save} = eval { 457 my $empty_events = {}; 458 foreach (@keys) { 459 $empty_events->{$_} = $self->{$_}; 460 } 461 $empty_events->{timestamp} = time; 462 $empty_events; 463 }; 464 $self->save_state(%params); 465} 466 467sub requires_version { 468 my $self = shift; 469 my $version = shift; 470 my @instances = DBD::MySQL::Cluster::return_clusters(); 471 my $instversion = $instances[0]->{version}; 472 if (! $self->version_is_minimum($version)) { 473 $self->add_nagios($ERRORS{UNKNOWN}, 474 sprintf "not implemented/possible for MySQL release %s", $instversion); 475 } 476} 477 478sub version_is_minimum { 479 # the current version is newer or equal 480 my $self = shift; 481 my $version = shift; 482 my $newer = 1; 483 my @instances = DBD::MySQL::Cluster::return_clusters(); 484 my @v1 = map { $_ eq "x" ? 0 : $_ } split(/\./, $version); 485 my @v2 = split(/\./, $instances[0]->{version}); 486 if (scalar(@v1) > scalar(@v2)) { 487 push(@v2, (0) x (scalar(@v1) - scalar(@v2))); 488 } elsif (scalar(@v2) > scalar(@v1)) { 489 push(@v1, (0) x (scalar(@v2) - scalar(@v1))); 490 } 491 foreach my $pos (0..$#v1) { 492 if ($v2[$pos] > $v1[$pos]) { 493 $newer = 1; 494 last; 495 } elsif ($v2[$pos] < $v1[$pos]) { 496 $newer = 0; 497 last; 498 } 499 } 500 #printf STDERR "check if %s os minimum %s\n", join(".", @v2), join(".", @v1); 501 return $newer; 502} 503 504sub instance_rac { 505 my $self = shift; 506 my @instances = DBD::MySQL::Cluster::return_clusters(); 507 return (lc $instances[0]->{parallel} eq "yes") ? 1 : 0; 508} 509 510sub instance_thread { 511 my $self = shift; 512 my @instances = DBD::MySQL::Cluster::return_clusters(); 513 return $instances[0]->{thread}; 514} 515 516sub windows_cluster { 517 my $self = shift; 518 my @instances = DBD::MySQL::Cluster::return_clusters(); 519 if ($instances[0]->{os} =~ /Win/i) { 520 return 1; 521 } else { 522 return 0; 523 } 524} 525 526sub system_vartmpdir { 527 my $self = shift; 528 if ($^O =~ /MSWin/) { 529 return $self->system_tmpdir(); 530 } else { 531 return "/var/tmp/check_mysql_health"; 532 } 533} 534 535sub system_oldvartmpdir { 536 my $self = shift; 537 return "/tmp"; 538} 539 540sub system_tmpdir { 541 my $self = shift; 542 if ($^O =~ /MSWin/) { 543 return $ENV{TEMP} if defined $ENV{TEMP}; 544 return $ENV{TMP} if defined $ENV{TMP}; 545 return File::Spec->catfile($ENV{windir}, 'Temp') 546 if defined $ENV{windir}; 547 return 'C:\Temp'; 548 } else { 549 return "/tmp"; 550 } 551} 552 553 554package DBD::MySQL::Cluster::Node; 555 556use strict; 557 558our @ISA = qw(DBD::MySQL::Cluster); 559 560my %ERRORS=( OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3 ); 561my %ERRORCODES=( 0 => 'OK', 1 => 'WARNING', 2 => 'CRITICAL', 3 => 'UNKNOWN' ); 562 563sub new { 564 my $class = shift; 565 my %params = @_; 566 my $self = { 567 mode => $params{mode}, 568 timeout => $params{timeout}, 569 type => $params{type}, 570 id => $params{id}, 571 status => $params{status}, 572 }; 573 bless $self, $class; 574 $self->init(%params); 575 if ($params{type} eq "NDB") { 576 bless $self, "DBD::MySQL::Cluster::Node::NDB"; 577 $self->init(%params); 578 } 579 return $self; 580} 581 582sub init { 583 my $self = shift; 584 my %params = @_; 585 if ($self->{status} =~ /@(\d+\.\d+\.\d+\.\d+)\s/) { 586 $self->{addr} = $1; 587 $self->{connected} = 1; 588 } elsif ($self->{status} =~ /accepting connect from (\d+\.\d+\.\d+\.\d+)/) { 589 $self->{addr} = $1; 590 $self->{connected} = 0; 591 } 592 if ($self->{status} =~ /starting,/) { 593 $self->{status} = "starting"; 594 } elsif ($self->{status} =~ /shutting,/) { 595 $self->{status} = "shutting"; 596 } else { 597 $self->{status} = $self->{connected} ? "running" : "dead"; 598 } 599} 600 601 602package DBD::MySQL::Cluster::Node::NDB; 603 604use strict; 605 606our @ISA = qw(DBD::MySQL::Cluster::Node); 607 608my %ERRORS=( OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3 ); 609my %ERRORCODES=( 0 => 'OK', 1 => 'WARNING', 2 => 'CRITICAL', 3 => 'UNKNOWN' ); 610 611sub init { 612 my $self = shift; 613 my %params = @_; 614 if ($self->{status} =~ /Nodegroup:\s*(\d+)/) { 615 $self->{nodegroup} = $1; 616 } 617 $self->{master} = ($self->{status} =~ /Master\)/) ? 1 : 0; 618} 619 620 621