1 package FS::part_export::cacti;
11 Cacti integration for Freeside
15 This module in particular handles FS::part_export object creation for Cacti integration;
16 consult any existing L<FS::part_export> documentation for details on how that works.
22 use base qw( FS::part_export );
23 use FS::Record qw( qsearchs qsearch );
24 use FS::UID qw( dbh );
28 use File::Slurp qw( slurp );
30 use MIME::Base64 qw( encode_base64 );
36 tie my %options, 'Tie::IxHash',
37 'user' => { label => 'User Name',
38 default => 'freeside' },
39 'script_path' => { label => 'Script Path',
40 default => '/usr/share/cacti/cli/' },
41 'template_id' => { label => 'Host Template ID',
43 'tree_id' => { label => 'Graph Tree ID (optional)',
45 'description' => { label => 'Description (can use tokens $contact, $ip_addr and $description)',
46 default => 'Freeside $contact $description $ip_addr' },
47 'graphs_path' => { label => 'Graph Export Directory (user@host:/path/to/graphs/)',
49 'import_freq' => { label => 'Minimum minutes between graph imports',
51 'max_graph_size' => { label => 'Maximum size per graph (MB)',
53 # 'delete_graphs' => { label => 'Delete associated graphs and data sources when unprovisioning',
59 'svc' => 'svc_broadband',
60 'desc' => 'Export service to cacti server, for svc_broadband services',
61 'options' => \%options,
63 Add service to cacti upon provisioning, for broadband services.<BR>
64 See <A HREF="http://www.freeside.biz/mediawiki/index.php/Freeside:4:Documentation:Cacti#Connecting_Cacti_To_Freeside">documentation</A> for details.
68 # standard hooks for provisioning/unprovisioning service
71 my ($self, $svc_broadband) = @_;
72 my ($q,$error) = _insert_queue($self, $svc_broadband);
77 my ($self, $svc_broadband) = @_;
78 my ($q,$error) = _delete_queue($self, $svc_broadband);
83 my($self, $new, $old) = @_;
84 return '' if $new->ip_addr eq $old->ip_addr; #important part didn't change
85 #delete old then insert new, with second job dependant on the first
86 my $oldAutoCommit = $FS::UID::AutoCommit;
87 local $FS::UID::AutoCommit = 0;
89 my ($dq, $iq, $error);
90 ($dq,$error) = _delete_queue($self,$old);
92 $dbh->rollback if $oldAutoCommit;
95 ($iq,$error) = _insert_queue($self,$new);
97 $dbh->rollback if $oldAutoCommit;
100 $error = $iq->depend_insert($dq->jobnum);
102 $dbh->rollback if $oldAutoCommit;
105 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
109 sub _export_suspend {
113 sub _export_unsuspend {
120 my ($self, $svc_broadband) = @_;
121 my $queue = new FS::queue {
122 'svcnum' => $svc_broadband->svcnum,
123 'job' => "FS::part_export::cacti::ssh_insert",
125 my $error = $queue->insert(
126 'host' => $self->machine,
127 'user' => $self->option('user'),
128 'hostname' => $svc_broadband->ip_addr,
129 'script_path' => $self->option('script_path'),
130 'template_id' => $self->option('template_id'),
131 'tree_id' => $self->option('tree_id'),
132 'description' => $self->option('description'),
133 'svc_desc' => $svc_broadband->description,
134 'contact' => $svc_broadband->cust_main->contact,
135 'svcnum' => $svc_broadband->svcnum,
137 return ($queue,$error);
141 my ($self, $svc_broadband) = @_;
142 my $queue = new FS::queue {
143 'svcnum' => $svc_broadband->svcnum,
144 'job' => "FS::part_export::cacti::ssh_delete",
146 my $error = $queue->insert(
147 'host' => $self->machine,
148 'user' => $self->option('user'),
149 'hostname' => $svc_broadband->ip_addr,
150 'script_path' => $self->option('script_path'),
151 # 'delete_graphs' => $self->option('delete_graphs'),
153 return ($queue,$error);
156 # routines run by queued jobs
162 die "Non-numerical Host Template ID, check export configuration\n"
163 unless $opt{'template_id'} =~ /^\d+$/;
164 die "Non-numerical Graph Tree ID, check export configuration\n"
165 unless $opt{'tree_id'} =~ /^\d*$/;
168 my $desc = $opt{'description'};
169 $desc =~ s/\$ip_addr/$opt{'hostname'}/g;
170 $desc =~ s/\$description/$opt{'svc_desc'}/g;
171 $desc =~ s/\$contact/$opt{'contact'}/g;
172 $desc =~ s/'/'\\''/g;
174 . $opt{'script_path'}
175 . q(add_device.php --description=')
180 . $opt{'template_id'};
181 my $response = ssh_cmd(%opt, 'command' => $cmd);
182 unless ( $response =~ /Success - new device-id: \((\d+)\)/ ) {
183 die "Error adding device: $response";
188 if ($opt{'tree_id'}) {
190 . $opt{'script_path'}
191 . q(add_tree.php --type=node --node-type=host --tree-id=)
195 $response = ssh_cmd(%opt, 'command' => $cmd);
196 unless ( $response =~ /Added Node node-id: \((\d+)\)/ ) {
197 die "Error adding host to tree: $response";
201 # # Get list of graph templates for new id
203 # . $opt{'script_path'}
204 # . q(freeside_cacti.php --get-graph-templates --host-template=)
205 # . $opt{'template_id'};
206 # my @gtids = split(/\n/,ssh_cmd(%opt, 'command' => $cmd));
207 # die "No graphs configured for host template"
211 # foreach my $gtid (@gtids) {
213 # # sanity checks, should never happen
215 # die "Bad graph template: $gtid"
216 # unless $gtid =~ /^\d+$/;
220 # . $opt{'script_path'}
221 # . q(add_graphs.php --graph-type=cg --graph-template-id=)
225 # $response = ssh_cmd(%opt, 'command' => $cmd);
226 # die "Error creating graph $gtid: $response"
227 # unless $response =~ /Graph Added - graph-id: \((\d+)\)/;
230 # # add the graph to the tree
232 # . $opt{'script_path'}
233 # . q(add_tree.php --type=node --node-type=graph --tree-id=)
237 # $response = ssh_cmd(%opt, 'command' => $cmd);
238 # die "Error adding graph $gid to tree: $response"
239 # unless $response =~ /Added Node/;
249 . $opt{'script_path'}
250 . q(freeside_cacti.php --drop-device --ip=')
253 # $cmd .= q( --delete-graphs)
254 # if $opt{'delete_graphs'};
255 my $response = ssh_cmd(%opt, 'command' => $cmd);
256 die "Error removing from cacti: " . $response
265 =item process_graphs JOB PARAM
267 Intended to be run as an FS::queue job.
269 Copies graphs for a single service from Cacti export directory to FS cache,
270 generates basic html pages for this service with base64-encoded graphs embedded,
271 and stores the generated pages in the database.
278 my ($job,$param) = @_;
280 $job->update_statustext(10);
281 my $cachedir = $FS::UID::cache_dir . '/cacti-graphs/';
284 my $svcnum = $param->{'svcnum'} || die "No svcnum specified";
286 'table' => 'svc_broadband',
287 'hashref' => { 'svcnum' => $svcnum },
288 }) || die "Could not load svcnum $svcnum";
290 # load relevant FS::part_export::cacti object
291 my ($self) = $svc->cust_svc->part_svc->part_export('cacti');
293 $job->update_statustext(20);
295 my $oldAutoCommit = $FS::UID::AutoCommit;
296 local $FS::UID::AutoCommit = 0;
299 # check for existing pages
301 my @oldpages = qsearch({
302 'table' => 'cacti_page',
303 'hashref' => { 'svcnum' => $svcnum, 'exportnum' => $self->exportnum },
304 'select' => 'cacti_pagenum, exportnum, svcnum, graphnum, imported', #no need to load old content
305 'order_by' => 'ORDER BY graphnum',
308 #if pages are recent enough, do nothing and return
309 if ($oldpages[0]->imported > $self->exptime($now)) {
310 $job->update_statustext(100);
314 foreach my $oldpage (@oldpages) {
315 my $error = $oldpage->delete;
317 $dbh->rollback if $oldAutoCommit;
323 $job->update_statustext(30);
325 # get list of graphs for this svc from cacti server
327 . $self->option('script_path')
328 . q(freeside_cacti.php --get-graphs --ip=')
331 my @graphs = map { [ split(/\t/,$_) ] }
333 'host' => $self->machine,
334 'user' => $self->option('user'),
338 $job->update_statustext(40);
340 # copy graphs from cacti server to cache
341 # requires version 2.6.4 of rsync, released March 2005
342 my $rsync = File::Rsync->new({
346 'source' => $self->option('graphs_path'),
349 (map { q('**graph_).${$_}[0].q(*.png') } @graphs),
350 (map { q('**thumb_).${$_}[0].q(.png') } @graphs),
355 #don't know why a regular $rsync->exec isn't doing includes right, but this does
356 my $error = system(join(' ',@{$rsync->getcmd()}));
357 die "rsync failed with exit status $error" if $error;
359 $job->update_statustext(50);
361 # create html file contents
362 my $svchead = q(<!-- UPDATED ) . $now . qq( -->)
363 . '<H2 STYLE="margin-top: 0;">Service #' . $svcnum . '</H2>'
364 . q(<P>Last updated ) . scalar(localtime($now)) . q(</P>);
365 my $svchtml = $svchead;
366 my $maxgraph = 1024 * 1024 * ($self->options('max_graph_size') || 5);
368 for (my $i = 0; $i <= $#graphs; $i++) {
369 my $graph = $graphs[$i];
370 my $thumbfile = $cachedir . 'graphs/thumb_' . $$graph[0] . '.png';
373 ( stat($thumbfile)->size() < $maxgraph )
376 # add graph to main file
377 my $graphhead = q(<H3>) . $$graph[1] . q(</H3>);
378 $svchtml .= $graphhead;
379 $svchtml .= anchor_tag( $svcnum, $$graph[0], img_tag($thumbfile) );
380 # create graph details file
381 my $graphhtml = $svchead . $graphhead;
384 while (-e (my $graphfile = $cachedir.'graphs/graph_'.$$graph[0].'_'.$j.'.png')) {
385 if ( stat($graphfile)->size() < $maxgraph ) {
387 $graphhtml .= img_tag($graphfile);
391 $graphhtml .= '<P>No detail graphs to display for this graph</P>'
393 my $newobj = new FS::cacti_page {
394 'exportnum' => $self->exportnum,
396 'graphnum' => $$graph[0],
398 'content' => $graphhtml,
400 $error = $newobj->insert;
402 $dbh->rollback if $oldAutoCommit;
406 $job->update_statustext(49 + int($i / $#graphs) * 50);
408 $svchtml .= '<P>No graphs to display for this service</P>'
410 my $newobj = new FS::cacti_page {
411 'exportnum' => $self->exportnum,
415 'content' => $svchtml,
417 $error = $newobj->insert;
419 $dbh->rollback if $oldAutoCommit;
423 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
425 $job->update_statustext(100);
430 my $somefile = shift;
431 return q(<IMG SRC="data:image/png;base64,)
432 . encode_base64(slurp($somefile,binmode=>':raw'),'')
433 . qq(" STYLE="margin-bottom: 1em;"><BR>);
437 my ($svcnum, $graphnum, $contents) = @_;
438 return q(<A HREF="?svcnum=)
447 #this gets used by everything else
448 #fake false laziness, other ssh_cmds handle error/output differently
452 my $ssh = Net::OpenSSH->new($opt->{'user'}.'@'.$opt->{'host'});
453 die "Couldn't establish SSH connection: ". $ssh->error if $ssh->error;
454 my ($output, $errput) = $ssh->capture2($opt->{'command'});
455 die "Error running SSH command: ". $ssh->error if $ssh->error;
456 die $errput if $errput;
466 Removes all expired graphs for this export from the database.
472 my $oldAutoCommit = $FS::UID::AutoCommit;
473 local $FS::UID::AutoCommit = 0;
475 my $sth = $dbh->prepare('DELETE FROM cacti_page WHERE exportnum = ? and imported <= ?')
477 $dbh->rollback if $oldAutoCommit;
480 $sth->execute($self->exportnum,$self->exptime)
482 $dbh->rollback if $oldAutoCommit;
485 $dbh->commit or return $dbh->errstr if $oldAutoCommit;
489 =item exptime [ TIME ]
491 Accepts optional current time, defaults to actual current time.
493 Returns timestamp for the oldest possible non-expired graph import,
494 based on the import_freq option.
500 my $now = shift || time;
501 return $now - 60 * ($self->option('import_freq') || 5);
509 jonathan@freeside.biz
511 =head1 LICENSE AND COPYRIGHT
513 Copyright 2015 Freeside Internet Services
515 This program is free software; you can redistribute it and/or
516 modify it under the terms of the GNU General Public License
517 as published by the Free Software Foundation.