Prerequisites:
* Perl Modules: DBI, DBD::Mysql, Net::SNMP, Sys::Syslog, RRDs (rrdtool perl module)
* Make a backup of your RRA files first!! or test it on a clean cacti installation.
* (Recommended) Try to optimize Mysql tables with indexes... (view a previous post)
Installation:
* Copy & Paste the code on a file... then chmod 0755 <filename>
* Put it anywhere on the file system, and edit the configuration parameters at the top of the file.... Sorry... no code comments yet!
* Test it manually... it will write a log in /tmp/spine.log
* Put it to run on the crontab every 5 minutes as you do with cmd.php.
Limitations:
* a lot!
* it can handle only those data sources that use SNMP Network Data or SNMP Data.... any DS that needs an external program to gather data will be ignored.
* Be aware that I based the code on the version 0.6.7 of Cacti... I don't know if it will break anything on other versions.
Results so far:
* on my production CACTI Box with 520 data sources (mostly SNMP) it takes less than a minute to complete with no noticeable load on the machine.
What it does:
* Almost the same as cmd.php, except that doesn't touch all Data Sources that fork an external program... only those that use INTERNAL SNMP functions. Also it cannot handle Multi output DS yet... (I don't get them yet...)
* It uses the Net::SNMP capability to do non-blocking SNMP requests... so they execute "simultaneously" (sort of)
Please test it out... and if anyone thinks its worth it to complete/extend, send me an email. as always bug reports and comments are welcome.
The code follows:
Code: Select all
#!/usr/bin/perl -w
# Program: spine.pl
# Description: Subtituto del cmd.php incluido en el CACTI
# quien es el que se encarga de colectar los
# datos via SNMP para actualizar las graficas RRD.
use DBI;
use Net::SNMP;
use RRDs;
use Sys::Syslog;
$0 = 'spine';
$path_cacti = '/var/www/html/cacti';
%config = ( 'debug' => 1,
'MYSQL_HOST' => 'localhost',
'MYSQL_DB' => 'cacti',
'MYSQL_USER' => 'cacti',
'MYSQL_PASS' => 'secret',
'snmp_timeout' => 3,
'path_cacti' => "$path_cacti",
'path_images' => "$path_cacti/graphs",
'path_rra' => "$path_cacti/rra-spine",
'path_log' => "$path_cacti/log/rrd.log",
'path_rrdtool' => "/usr/local/rrdtool/bin/rrdtool",
'syslog_facility'=> "local4"
);
%oidOctets = ( 'in' => ".1.3.6.1.2.1.2.2.1.10",
'out' => ".1.3.6.1.2.1.2.2.1.16",
'hcin' => ".1.3.6.1.2.1.2.2.1.10",
'hcout' => ".1.3.6.1.2.1.2.2.1.16" );
printlog('debug', "INICIO spine.pl " . localtime());
$dbh = DBI->connect( 'dbi:mysql:host=' . $config{'MYSQL_HOST'} . ';database=' . $config{'MYSQL_DB'},
$config{'MYSQL_USER'},
$config{'MYSQL_PASS'} ) or do {
printlog('err', "ERROR: No se pudo conectar a Servidor MySQL! (" . $DBI::errstr . ")");
exit 1;
};
$sql = "select d.id, d.name, d.srcid, s.formatstrin, s.formatstrout, s.id as sid, s.type
from rrd_ds d left join src s on d.srcid=s.id
where
d.active=\"on\" and
d.subdsid=0";
$sth1 = $dbh->prepare($sql);
$sth1->execute() or do {
printlog('err', "ERROR: No se pudo ejecutar Query MySQL! Error: " . $dbh->errstr);
exit 1;
};
while ($sql_id = $sth1->fetchrow_hashref()) {
printlog('debug', "Datasource: " . $sql_id->{name});
if ($sql_id->{formatstrout} =~ /<(\S+)>/i) {
$out_field = $1;
} else {
printlog('err', "ERROR: formatstrout = " . $sql_id->{formatstrout} . " is not valid!");
next;
}
$sql = "select d.fieldid, d.dsid, d.value, f.srcid, f.dataname
from src_data d left join src_fields f on d.fieldid=f.id
where
d.dsid=" . $sql_id->{id} . " and
f.srcid=" . $sql_id->{srcid};
$sth2 = $dbh->prepare($sql);
$sth2->execute() or do {
printlog('err', "ERROR: No se pudo ejecutar Query MySQL! Error: " . $dbh->errstr);
exit 1;
};
while ($sql_id_field = $sth2->fetchrow_hashref()) {
printlog('debug', "\tField: " . $sql_id_field->{dataname} . " Value: " . $sql_id_field->{value});
$sql_id_field_value{$sql_id_field->{dataname}} = $sql_id_field->{value};
}
if ($sql_id->{type} eq "") {
# We have to fork a external program...
# Not implemented Yet...
next;
} else {
# It's an internal SNMP function...
if ($sql_id->{type} eq "snmp_net") {
my ($snmp, $error) = Net::SNMP->session(
-hostname => $sql_id_field_value{"ip"},
-community => $sql_id_field_value{"community"},
-port => 161,
-timeout => $config{"snmp_timeout"},
-nonblocking => 0x1 );
if (!defined($snmp)) {
printlog('err', "ERROR: SNMP connect error: $error");
next;
}
my $oid = $oidOctets{$sql_id_field_value{"inout"}} . "." . $sql_id_field_value{"ifnum"};
printlog('debug', "\tOID to get = $oid");
my $result = $snmp->get_request(
-varbindlist => [$oid],
-callback => [ \&get_snmp_net, $oid, $sql_id, $out_field ] );
} elsif ($sql_id->{type} eq "snmp") {
my ($snmp, $error) = Net::SNMP->session(
-hostname => $sql_id_field_value{"ip"},
-community => $sql_id_field_value{"community"},
-port => 161,
-timeout => $config{"snmp_timeout"},
-nonblocking => 0x1 );
if (!defined($snmp)) {
printlog('err', "ERROR: SNMP connect error: $error");
next;
}
my $oid = $sql_id_field_value{"oid"};
printlog('debug', "\tOID to get = $oid");
my $result = $snmp->get_request(
-varbindlist => [$oid],
-callback => [ \&get_snmp_data, $oid, $sql_id, $out_field ] );
} else {
# Bad Type...!
}
}
}
printlog('debug', "SENDING ALL SNMP requests...");
Net::SNMP->snmp_dispatcher();
printlog('debug', "FIN spine.pl " . localtime());
exit 0;
#
# FUNCTIONS....
#
sub get_snmp_net {
my ($session, $oid, $sql_id, $out_field) = @_;
my $host = $session->hostname;
my $out_data = '';
if (!defined($session->var_bind_list)) {
my $error = $session->error;
printlog('err', "ERROR: get_snmp_net($host) Error: $error");
} else {
my $result = $session->var_bind_list->{$oid};
printlog('debug', "get_snmp_net($host) $oid = $result");
$out_data = "$result";
}
update_src_data($sql_id, $out_field, $out_data);
rrdtool_function_create($sql_id->{id}, 0);
if ($sql_id->{srcid} != 0) {
rrdtool_function_update($sql_id->{id}, 0, 0);
}
return;
}
sub get_snmp_data {
my ($session, $oid, $sql_id, $out_field) = @_;
my $host = $session->hostname;
my $out_data = "";
if (!defined($session->var_bind_list)) {
my $error = $session->error;
printlog('err', "ERROR: get_snmp_data($host) Error: $error");
} else {
my $result = $session->var_bind_list->{$oid};
printlog('debug', "get_snmp_data($host) $oid = $result");
$out_data = "$result";
}
update_src_data($sql_id, $out_field, $out_data);
rrdtool_function_create($sql_id->{id}, 0);
if ($sql_id->{srcid} != 0) {
rrdtool_function_update($sql_id->{id}, 0, 0);
}
return;
}
sub update_src_data {
my ($sql_id, $out_field, $out_data) = @_;
if ($sql_id->{srcid} == 0) {
printlog('err', "ERROR: Data Source: " . $sql_id->{name} . " does not have a data input source assigned to it. No data will be gathered; if cacti does not gather data for this data source please deactivate it.");
return;
}
$sql = "SELECT id " .
"FROM src_fields " .
"WHERE dataname = '$out_field' " .
" and srcid = " . $sql_id->{srcid} .
" and inputoutput='out'";
my $sth1 = $dbh->prepare($sql);
$sth1->execute() or do {
printlog('err', "ERROR: No se pudo ejecutar Query MySQL! Error: " . $dbh->errstr );
exit 1;
};
my $sql_id_fields = $sth1->fetchrow_hashref();
my $current_data_source_id = $sql_id->{id};
$sql = "SELECT id " .
"FROM src_data " .
"WHERE fieldid = " . $sql_id_fields->{id} .
" and dsid = $current_data_source_id";
my $sth2 = $dbh->prepare($sql);
$sth2->execute() or do {
printlog('err', "ERROR: No se pudo ejecutar Query MySQL! Error: " . $dbh->errstr);
exit 1;
};
my $new_data_id = 0;
if ($sth2->rows > 0) {
my $sql_id_data = $sth2->fetchrow_hashref();
$new_data_id = $sql_id_data->{id};
}
$sql = "REPLACE INTO src_data " .
"(id, fieldid, dsid, value) " .
"values (" .
"$new_data_id, " .
$sql_id_fields->{id} . ", " .
"$current_data_source_id, " .
"'$out_data')";
printlog('debug', "update_src_data(" . $sql_id->{name} . ") Replace SQL = $sql");
my $affected_rows = 0;
$affected_rows = $dbh->do($sql);
if (! $affected_rows ) {
printlog('err', "WARN: El REPLACE no modifico ninguna fila...");
}
return;
}
#
# Cacti's support functions...
#
sub CheckDataSourceName {
my $data_source_name = shift;
$data_source_name =~ s/\s/_/g;
$data_source_name =~ s/[\/\*\\\&\%\"\',\.]+//g;
$data_source_name = substr($data_source_name,0,19);
return lc($data_source_name);
}
sub GetDataSourceName {
my $dsid = shift;
if ($dsid == 0) { return ""; }
$sql = "select name, dsname from rrd_ds where id=$dsid";
my $sth1 = $dbh->prepare($sql);
$sth1->execute() or do {
printlog('err', "ERROR: No se pudo ejecutar Query MySQL! Error: " . $dbh->errstr);
exit 1;
};
if ($sth1->rows <= 0) {
printlog('err', "ERROR: DataSource Name NOT found!");
return "";
}
my $sql_id = $sth1->fetchrow_hashref();
if ($sql_id->{dsname} eq "") {
return CheckDataSourceName($sql_id->{name});
} else {
return $sql_id->{dsname};
}
}
sub GetDataSourcePath {
my ($data_source_id, $expand_paths) = @_;
my $data_source_path = '';
if ($data_source_id == 0) { return ""; }
$sql = "select name, dspath from rrd_ds where id = $data_source_id";
my $sth1 = $dbh->prepare($sql);
$sth1->execute() or do {
printlog('err', "ERROR: No se pudo ejecutar Query MySQL! Error: " . $dbh->errstr );
exit 1;
};
if ($sth1->rows > 0) {
my $sql_id = $sth1->fetchrow_hashref();
if ($sql_id->{dspath} eq "") {
$data_source_path = "<path_rra>/" . CheckDataSourceName($sql_id->{name}) . ".rrd";
} else {
if ($sql_id->{dspath} !~ /\//) {
$data_source_path = "<path_rra>/" . $sql_id->{dspath};
} else {
$data_source_path = $sql_id->{dspath};
}
}
if ($expand_paths) {
$data_source_path =~ s/<path_rra>/$config{"path_rra"}/g;
}
return $data_source_path;
}
}
sub rrdtool_function_create {
my ($dsid, $show_source) = @_;
my $data_source_path = GetDataSourcePath($dsid, 1);
my $create_ds = "";
my $create_rra = "";
my @args = ();
if (!$show_source) {
if ( -f $data_source_path ) {
printlog('err', "WARN: rrd file $data_source_path already exists!");
return -1;
}
}
$sql = "select
d.step,
r.xfilesfactor, r.steps, r.rows,
c.name as cname,
(r.rows*r.steps) as rs
from rrd_ds d left join lnk_ds_rra l on l.dsid=d.id
left join rrd_rra r on l.rraid=r.id
left join lnk_rra_cf rc on rc.rraid=r.id
left join def_cf c on rc.consolidationfunctionid=c.id
where d.id=$dsid
order by rc.consolidationfunctionid, rs";
my $sth1 = $dbh->prepare($sql);
$sth1->execute() or do {
printlog('err', "ERROR: No se pudo ejecutar Query MySQL! Error: " . $dbh->errstr);
exit 1;
};
if ($sth1->rows <= 0) {
printlog('err', "ERROR: There are no RRA's assigned to DSID $dsid!");
return -1;
}
my $sql_id = $sth1->fetchrow_hashref();
$create_ds .= " \\\n--step " . $sql_id->{step} . " \\\n";
push @args, "--step";
push @args, $sql_id->{step};
$sql = "select
d.id, d.heartbeat, d.minvalue, d.maxvalue,d.subdsid,
t.name
from rrd_ds d left join def_ds t on d.datasourcetypeid=t.id
where d.id=$dsid
or d.subdsid=$dsid
order by d.id";
my $sth2 = $dbh->prepare($sql);
$sth2->execute() or do {
printlog('err', "ERROR: No se pudo ejecutar Query MySQL! Error: " . $dbh->errstr);
exit 1;
};
my $sql_id_ds;
my $rows = $sth2->rows;
while ($sql_id_ds = $sth2->fetchrow_hashref()) {
if ( ($rows > 1 and $sql_id_ds->{subdsid} != 0) or ($rows == 1)) {
my $data_source_name = GetDataSourceName($sql_id_ds->{id});
$create_ds .= "DS:$data_source_name:" .
$sql_id_ds->{name} . ":" .
$sql_id_ds->{heartbeat} . ":" .
$sql_id_ds->{minvalue} . ":" .
$sql_id_ds->{maxvalue} . " \\\n";
push @args, "DS:$data_source_name:" .
$sql_id_ds->{name} . ":" .
$sql_id_ds->{heartbeat} . ":" .
$sql_id_ds->{minvalue} . ":" .
$sql_id_ds->{maxvalue};
}
}
do {
$create_rra .= "RRA:" . $sql_id->{cname} . ":" .
$sql_id->{xfilesfactor} . ":" .
$sql_id->{steps} . ":" .
$sql_id->{rows} . " \\\n";
push @args, "RRA:" . $sql_id->{cname} . ":" .
$sql_id->{xfilesfactor} . ":" .
$sql_id->{steps} . ":" .
$sql_id->{rows};
} while ($sql_id = $sth1->fetchrow_hashref());
printlog('debug', $config{"path_rrdtool"} . " create \\\n$data_source_path$create_ds$create_rra");
if (!$show_source) {
RRDs::create($data_source_path, @args);
if ($RRDs::error) {
printlog('err', "ERROR: rrdtool create $data_source_path failed! (" . $RRDs::error . ")");
} else {
printlog('debug', "RRA file $data_source_path Created!");
}
}
return;
}
sub rrdtool_function_update {
my ($dsid, $multi_data_source, $show_source) = @_;
my $data_source_path= GetDataSourcePath($dsid, 1);
my $update_string = "N";
my $template_string = "";
if ($multi_data_source eq "") {
$sql = "select id from rrd_ds where subdsid=$dsid";
my $sth1 = $dbh->prepare($sql);
$sth1->execute() or do {
printlog('err', "ERROR: No se pudo ejecutar Query MySQL! Error: " . $dbh->errstr);
exit 1;
};
if ($sth1->rows == 0) {
$multi_data_source = 0;
} else {
$multi_data_source = 1;
}
}
if ($multi_data_source == 1) {
$sql = "select
d.dsname,
a.value
from rrd_ds d left join src_fields f on d.subfieldid=f.id
left join src_data a on d.id=a.dsid
where d.subdsid=$dsid
and f.inputoutput=\"out\"
and f.updaterra=\"on\"
order by d.id";
} else {
$sql = "select
d.dsname,
a.value
from rrd_ds d left join src_data a on d.id=a.dsid
left join src_fields f on a.fieldid=f.id
where d.id=$dsid
and f.inputoutput=\"out\"
and f.updaterra=\"on\"
order by d.id";
}
my $sth2 = $dbh->prepare($sql);
$sth2->execute() or do {
printlog('err', "ERROR: No se pudo ejecutar Query MySQL! Error: " . $dbh->errstr);
exit 1;
};
my $rows = $sth2->rows();
my $data_value = "U";
my $i = 0;
while (my $sql_id = $sth2->fetchrow_hashref()) {
if ($sql_id->{value} =~ /[0-9\.]+/i) {
$data_value = $sql_id->{value};
}
$update_string .= ":$data_value";
$template_string .= $sql_id->{dsname};
$i++;
if ($i < $rows) { $template_string .= ":"; }
}
my $command_line = "update $data_source_path --template $template_string $update_string";
printlog('debug', $config{"path_rrdtool"} . " $command_line");
if (!$show_source) {
RRDs::update($data_source_path, "--template", $template_string, $update_string);
if ($RRDs::error) {
printlog('err', "ERROR: rrdtool update $data_source_path failed! (" . $RRDs::error . ")");
} else {
printlog('debug', "RRA file $data_source_path Updated!");
}
}
return;
}
sub printlog {
my($level, $msg, $facility);
$level = shift @_;
$msg = shift @_;
$facility = ( ( !defined($config{'syslog_facility'}) ) ? 'local5' : $config{'syslog_facility'} );
if ( $level =~ /debug/i && !$config{'debug'} ) {
return;
}
if ( ! openlog($0, 'cons,pid', $facility) ) {
print STDERR "No se pudo abrir SYSLOG: $0\t$facility\.$level\t$msg\n";
return;
}
syslog($level, '%s', $msg);
closelog();
if ($config{'debug'}) {
open LOG, ">>/tmp/$0.log" or return;
print LOG localtime() . " [$0 -> $facility\.$level] $msg\n";
close(LOG);
}
return;
}
Roberto Carlos Navas
Internet de Telemovil
rcnavas@telemovil.com