#!/usr/bin/perl ################################################################################ # cacti2zabbix converter ############################################# vim: set tabstop=4 filetype=perl # use strict; use warnings; my $KEY_CF_AVERAGE = 'AVERAGE'; my $FILENAME_PROHIBITED_CHARS = '\s\'"#|;:@`\[\]\$\%\&\~\?\{\}\+\*\/\.\\\\'; ################################################################################ # Prototypes ################################################################################ sub walk_rrd($); sub walk_ds($); sub walk_rra($); sub walk_database($); sub prompt($); sub trim($); # Print information message. sub info($); # Print warning message. sub warning($); # Print error message. sub error($); # Print Usage. sub usage($); ################################################################################ # Main ################################################################################ ## # Check args. # if ($#ARGV != 0) { usage('Invalid number of arguments.'); exit 1; } my $infile = shift(@ARGV); if (!(-f $infile)) { error('"${infile}"??'); exit 2; } ## # Load rrd-xml (rrdtool dump xxx.rrd > xxx.xml) # my $fp_in; if (!open($fp_in, '<', $infile)) { error("Cannot open file: '${infile}'."); exit 3; } info("Start load '${infile}'."); my $rrd; while (my $line = <$fp_in>) { if ($line =~ m|^\s*|o) { $rrd = walk_rrd($fp_in); } } close($fp_in); if (! $rrd) { error('Conversion failed.'); exit 4; } ## # Generate file for zabbix-sender. # my $hostname; until ($hostname = prompt('hostname ?')) { warning('Hostname is mandatory parameter.'); } my $idx = 0; foreach my $ds (@{$rrd->{'dss'}}) { my $itemkey = prompt("itemkey of ds:${ds} ?"); if (! $itemkey) { $idx++; next; } $itemkey =~ s/"/\\"/g; my $data_format = prompt( "data format string for 'sprintf' (eg : %s, %d, %f ...) ?"); # numeric(16,4) on postgres. $data_format = '%.4f' if ($data_format eq '%f'); eval { my $test = sprintf($data_format, 1); }; if ($@) { error($@); next; } my $outfile = "${hostname}_${itemkey}"; $outfile =~ s/[${FILENAME_PROHIBITED_CHARS}]/_/og; $outfile =~ s/_{2,}/_/g; $outfile .= '.zbx'; info("Set file name to '${outfile}'."); info("Start process to collect data."); my %datas = (); my $min_pdp_per_row = 999999; foreach my $rra (@{$rrd->{'rras'}}) { next if ($rra->{'cf'} ne $KEY_CF_AVERAGE); my $pdp_per_row = $rra->{'pdp_per_row'}; my $overwrite = 0; if ($pdp_per_row < $min_pdp_per_row) { $min_pdp_per_row = $pdp_per_row; $overwrite = 1; } foreach my $data (@{$rra->{'database'}}) { next if ( exists($datas{$data->{'unix_ts'}}) && !($overwrite) ); $datas{$data->{'unix_ts'}} = ${$data->{'values'}}[$idx]; } } info("Start sort and output zabbix-data to '${outfile}'."); my $fp_out; if (!open($fp_out, '>', $outfile)) { error("Cannot open '${outfile}' for write."); next; } foreach my $unix_ts (sort keys %datas) { print $fp_out "\"${hostname}\" ", "\"${itemkey}\" ", "${unix_ts} ", sprintf($data_format, $datas{$unix_ts}), "\n"; } info("Finished to convert '${ds}' to '${itemkey}'."); } exit 0; ################################################################################ # Sub routines ################################################################################ sub walk_rrd($) { my $fp = shift(@_); my @dss; my @rras; my $step = 300; while (my $line = <$fp>) { if ($line =~ m|^\s*|o) { return { 'dss' => \@dss, 'rras' => \@rras, }; } if ($line =~ m|^\s*(\d+)|) { $step = int($1); } if ($line =~ m|^\s*|o) { my $child = walk_ds($fp); if (! $child) { error('Syntax error: not closed.'); return undef; } if ($child->{'name'}) { push(@dss, trim($child->{'name'})); info("Found named '${dss[$#dss]}'."); } else { error(' not contains .'); return undef; } } if ($line =~ m|^\s*|) { my $rra = walk_rra($fp); if (! $rra) { error(' not closed.'); return undef; } my $cf = $rra->{'cf'}; my $sec_per_row = int($rra->{'pdp_per_row'}) * $step; push(@rras, $rra); info("Found [cf:${cf}, sec_per_row:${sec_per_row}]."); } } error(' not closed.'); return undef; } sub walk_ds($) { my $fp = shift(@_); my %elements = (); while (my $line = <$fp>) { return \%elements if ($line =~ m|^\s*|o); if ($line =~ m|^\s*<([a-z_]+)>(.+)|o) { my $element = $1; my $value = $2; chomp($value); $elements{$element} = $value; } } error(' not closed.'); return undef; } sub walk_rra($) { my $fp = shift(@_); my $cf = undef; my $pdp_per_row = undef; my @database; while (my $line = <$fp>) { if ($line =~ m|^\s*|o) { error(' not found.') unless(defined($cf)); error(' not found.') unless(defined($pdp_per_row)); error(' not found.') unless(@database); if (defined($cf) && defined($pdp_per_row) && @database) { return { 'cf' => $cf, 'pdp_per_row' => $pdp_per_row, 'database' => \@database, }; } else { return undef; } } if ($line =~ m|^\s*(.+)|o) { $cf = trim($1); } if ($line =~ m|^\s*(\d+)|o) { $pdp_per_row = trim($1); } if ($line =~ m|^\s*|o) { @database = walk_database($fp); } } error(' not closed'); return undef; } sub walk_database($) { my $fp = shift(@_); my @database; while (my $line = <$fp>) { return @database if ($line =~ m|^\s*|); my ($ts, $unix_ts) = $line =~ m|^\s*|o; ## # hmm... # my @values = # $line =~ m|\s*(?:([0-9eE\+\.\-]+))+\s*\s*$|go; # my ($values_part) = $line =~ m|((?:[0-9eE\+\.\-]+)+)|o; next unless ($values_part); my @values = $values_part =~ m|([0-9eE\+\.\-]+)|og; next unless (@values); push ( @database, { 'unix_ts' => $unix_ts, 'values' => \@values, } ); } error(' not closed.'); return undef; } sub prompt($) { print STDOUT "\n", $_[0], ' '; my $in = ; print STDOUT "\n"; return trim($in); } sub trim($) { my $trimed = $_[0]; $trimed =~ s/^\s+//o; $trimed =~ s/\s+$//o; return $trimed; } sub info($) { _printMessage('II', $_[0]); } sub warning($) { _printMessage('WW', $_[0]); } sub error($) { _printMessage('EE', $_[0]); } sub _printMessage($$) { my ($type, $message) = @_; print STDERR localtime(time()), " [${type}] ${message}\n"; } sub usage($) { print STDERR "\n", $_[0], "\n\n" if ($_[0]); print STDERR < EOF }