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