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( decode_base64 encode_base64 );
31 use Storable qw(thaw);
38 tie my %options, 'Tie::IxHash',
39 'user' => { label => 'User Name',
40 default => 'freeside' },
41 'script_path' => { label => 'Script Path',
42 default => '/usr/share/cacti/cli/' },
43 'template_id' => { label => 'Host Template ID',
45 'tree_id' => { label => 'Graph Tree ID (optional)',
47 'description' => { label => 'Description (can use tokens $contact, $ip_addr and $description)',
48 default => 'Freeside $contact $description $ip_addr' },
49 'graphs_path' => { label => 'Graph Export Directory (user@host:/path/to/graphs/)',
51 'import_freq' => { label => 'Minimum minutes between graph imports',
53 'max_graph_size' => { label => 'Maximum size per graph (MB)',
55 # 'delete_graphs' => { label => 'Delete associated graphs and data sources when unprovisioning',
61 'svc' => 'svc_broadband',
62 'desc' => 'Export service to cacti server, for svc_broadband services',
63 'options' => \%options,
65 Add service to cacti upon provisioning, for broadband services.<BR>
66 See <A HREF="http://www.freeside.biz/mediawiki/index.php/Freeside:4:Documentation:Cacti#Connecting_Cacti_To_Freeside">documentation</A> for details.
70 # standard hooks for provisioning/unprovisioning service
73 my ($self, $svc_broadband) = @_;
74 my ($q,$error) = _insert_queue($self, $svc_broadband);
79 my ($self, $svc_broadband) = @_;
80 my ($q,$error) = _delete_queue($self, $svc_broadband);
85 my($self, $new, $old) = @_;
86 return '' if $new->ip_addr eq $old->ip_addr; #important part didn't change
87 #delete old then insert new, with second job dependant on the first
88 my $oldAutoCommit = $FS::UID::AutoCommit;
89 local $FS::UID::AutoCommit = 0;
91 my ($dq, $iq, $error);
92 ($dq,$error) = _delete_queue($self,$old);
94 $dbh->rollback if $oldAutoCommit;
97 ($iq,$error) = _insert_queue($self,$new);
99 $dbh->rollback if $oldAutoCommit;
102 $error = $iq->depend_insert($dq->jobnum);
104 $dbh->rollback if $oldAutoCommit;
107 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
111 sub _export_suspend {
115 sub _export_unsuspend {
122 my ($self, $svc_broadband) = @_;
123 my $queue = new FS::queue {
124 'svcnum' => $svc_broadband->svcnum,
125 'job' => "FS::part_export::cacti::ssh_insert",
127 my $error = $queue->insert(
128 'host' => $self->machine,
129 'user' => $self->option('user'),
130 'hostname' => $svc_broadband->ip_addr,
131 'script_path' => $self->option('script_path'),
132 'template_id' => $self->option('template_id'),
133 'tree_id' => $self->option('tree_id'),
134 'description' => $self->option('description'),
135 'svc_desc' => $svc_broadband->description,
136 'contact' => $svc_broadband->cust_main->contact,
137 'svcnum' => $svc_broadband->svcnum,
139 return ($queue,$error);
143 my ($self, $svc_broadband) = @_;
144 my $queue = new FS::queue {
145 'svcnum' => $svc_broadband->svcnum,
146 'job' => "FS::part_export::cacti::ssh_delete",
148 my $error = $queue->insert(
149 'host' => $self->machine,
150 'user' => $self->option('user'),
151 'hostname' => $svc_broadband->ip_addr,
152 'script_path' => $self->option('script_path'),
153 # 'delete_graphs' => $self->option('delete_graphs'),
155 return ($queue,$error);
158 # routines run by queued jobs
164 die "Non-numerical Host Template ID, check export configuration\n"
165 unless $opt{'template_id'} =~ /^\d+$/;
166 die "Non-numerical Graph Tree ID, check export configuration\n"
167 unless $opt{'tree_id'} =~ /^\d*$/;
170 my $desc = $opt{'description'};
171 $desc =~ s/\$ip_addr/$opt{'hostname'}/g;
172 $desc =~ s/\$description/$opt{'svc_desc'}/g;
173 $desc =~ s/\$contact/$opt{'contact'}/g;
174 $desc =~ s/'/'\\''/g;
176 . $opt{'script_path'}
177 . q(add_device.php --description=')
182 . $opt{'template_id'};
183 my $response = ssh_cmd(%opt, 'command' => $cmd);
184 unless ( $response =~ /Success - new device-id: \((\d+)\)/ ) {
185 die "Error adding device: $response";
190 if ($opt{'tree_id'}) {
192 . $opt{'script_path'}
193 . q(add_tree.php --type=node --node-type=host --tree-id=)
197 $response = ssh_cmd(%opt, 'command' => $cmd);
198 unless ( $response =~ /Added Node node-id: \((\d+)\)/ ) {
199 die "Error adding host to tree: $response";
203 # # Get list of graph templates for new id
205 # . $opt{'script_path'}
206 # . q(freeside_cacti.php --get-graph-templates --host-template=)
207 # . $opt{'template_id'};
208 # my @gtids = split(/\n/,ssh_cmd(%opt, 'command' => $cmd));
209 # die "No graphs configured for host template"
213 # foreach my $gtid (@gtids) {
215 # # sanity checks, should never happen
217 # die "Bad graph template: $gtid"
218 # unless $gtid =~ /^\d+$/;
222 # . $opt{'script_path'}
223 # . q(add_graphs.php --graph-type=cg --graph-template-id=)
227 # $response = ssh_cmd(%opt, 'command' => $cmd);
228 # die "Error creating graph $gtid: $response"
229 # unless $response =~ /Graph Added - graph-id: \((\d+)\)/;
232 # # add the graph to the tree
234 # . $opt{'script_path'}
235 # . q(add_tree.php --type=node --node-type=graph --tree-id=)
239 # $response = ssh_cmd(%opt, 'command' => $cmd);
240 # die "Error adding graph $gid to tree: $response"
241 # unless $response =~ /Added Node/;
251 . $opt{'script_path'}
252 . q(freeside_cacti.php --drop-device --ip=')
255 # $cmd .= q( --delete-graphs)
256 # if $opt{'delete_graphs'};
257 my $response = ssh_cmd(%opt, 'command' => $cmd);
258 die "Error removing from cacti: " . $response
267 =item process_graphs JOB PARAM
269 Intended to be run as an FS::queue job.
271 Copies graphs for a single service from Cacti export directory to FS cache,
272 generates basic html pages for this service with base64-encoded graphs embedded,
273 and stores the generated pages in the database.
281 my $param = thaw(decode_base64(shift));
283 $job->update_statustext(10);
284 my $cachedir = $FS::UID::cache_dir . '/cacti-graphs/';
287 my $svcnum = $param->{'svcnum'} || die "No svcnum specified";
289 'table' => 'svc_broadband',
290 'hashref' => { 'svcnum' => $svcnum },
291 }) || die "Could not load svcnum $svcnum";
293 # load relevant FS::part_export::cacti object
294 my ($self) = $svc->cust_svc->part_svc->part_export('cacti');
296 $job->update_statustext(20);
298 my $oldAutoCommit = $FS::UID::AutoCommit;
299 local $FS::UID::AutoCommit = 0;
302 # check for existing pages
304 my @oldpages = qsearch({
305 'table' => 'cacti_page',
306 'hashref' => { 'svcnum' => $svcnum, 'exportnum' => $self->exportnum },
307 'select' => 'cacti_pagenum, exportnum, svcnum, graphnum, imported', #no need to load old content
308 'order_by' => 'ORDER BY graphnum',
311 #if pages are recent enough, do nothing and return
312 if ($oldpages[0]->imported > $self->exptime($now)) {
313 $job->update_statustext(100);
317 foreach my $oldpage (@oldpages) {
318 my $error = $oldpage->delete;
320 $dbh->rollback if $oldAutoCommit;
326 $job->update_statustext(30);
328 # get list of graphs for this svc from cacti server
330 . $self->option('script_path')
331 . q(freeside_cacti.php --get-graphs --ip=')
334 my @graphs = map { [ split(/\t/,$_) ] }
336 'host' => $self->machine,
337 'user' => $self->option('user'),
341 $job->update_statustext(40);
343 # copy graphs from cacti server to cache
344 # requires version 2.6.4 of rsync, released March 2005
345 my $rsync = File::Rsync->new({
349 'source' => $self->option('graphs_path'),
352 (map { q('**graph_).${$_}[0].q(*.png') } @graphs),
353 (map { q('**thumb_).${$_}[0].q(.png') } @graphs),
358 #don't know why a regular $rsync->exec isn't doing includes right, but this does
359 my $error = system(join(' ',@{$rsync->getcmd()}));
360 die "rsync failed with exit status $error" if $error;
362 $job->update_statustext(50);
364 # create html file contents
365 my $svchead = q(<!-- UPDATED ) . $now . qq( -->)
366 . '<H2 STYLE="margin-top: 0;">Service #' . $svcnum . '</H2>'
367 . q(<P>Last updated ) . scalar(localtime($now)) . q(</P>);
368 my $svchtml = $svchead;
369 my $maxgraph = 1024 * 1024 * ($self->options('max_graph_size') || 5);
371 for (my $i = 0; $i <= $#graphs; $i++) {
372 my $graph = $graphs[$i];
373 my $thumbfile = $cachedir . 'graphs/thumb_' . $$graph[0] . '.png';
376 ( stat($thumbfile)->size() < $maxgraph )
379 # add graph to main file
380 my $graphhead = q(<H3>) . $$graph[1] . q(</H3>);
381 $svchtml .= $graphhead;
382 $svchtml .= anchor_tag( $svcnum, $$graph[0], img_tag($thumbfile) );
383 # create graph details file
384 my $graphhtml = $svchead . $graphhead;
387 while (-e (my $graphfile = $cachedir.'graphs/graph_'.$$graph[0].'_'.$j.'.png')) {
388 if ( stat($graphfile)->size() < $maxgraph ) {
390 $graphhtml .= img_tag($graphfile);
394 $graphhtml .= '<P>No detail graphs to display for this graph</P>'
396 my $newobj = new FS::cacti_page {
397 'exportnum' => $self->exportnum,
399 'graphnum' => $$graph[0],
401 'content' => $graphhtml,
403 $error = $newobj->insert;
405 $dbh->rollback if $oldAutoCommit;
409 $job->update_statustext(49 + int($i / $#graphs) * 50);
411 $svchtml .= '<P>No graphs to display for this service</P>'
413 my $newobj = new FS::cacti_page {
414 'exportnum' => $self->exportnum,
418 'content' => $svchtml,
420 $error = $newobj->insert;
422 $dbh->rollback if $oldAutoCommit;
426 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
428 $job->update_statustext(100);
433 my $somefile = shift;
434 return q(<IMG SRC="data:image/png;base64,)
435 . encode_base64(slurp($somefile,binmode=>':raw'),'')
436 . qq(" STYLE="margin-bottom: 1em;"><BR>);
440 my ($svcnum, $graphnum, $contents) = @_;
441 return q(<A HREF="?svcnum=)
450 #this gets used by everything else
451 #fake false laziness, other ssh_cmds handle error/output differently
455 my $ssh = Net::OpenSSH->new($opt->{'user'}.'@'.$opt->{'host'});
456 die "Couldn't establish SSH connection: ". $ssh->error if $ssh->error;
457 my ($output, $errput) = $ssh->capture2($opt->{'command'});
458 die "Error running SSH command: ". $ssh->error if $ssh->error;
459 die $errput if $errput;
469 Removes all expired graphs for this export from the database.
475 my $oldAutoCommit = $FS::UID::AutoCommit;
476 local $FS::UID::AutoCommit = 0;
478 my $sth = $dbh->prepare('DELETE FROM cacti_page WHERE exportnum = ? and imported <= ?')
480 $dbh->rollback if $oldAutoCommit;
483 $sth->execute($self->exportnum,$self->exptime)
485 $dbh->rollback if $oldAutoCommit;
488 $dbh->commit or return $dbh->errstr if $oldAutoCommit;
492 =item exptime [ TIME ]
494 Accepts optional current time, defaults to actual current time.
496 Returns timestamp for the oldest possible non-expired graph import,
497 based on the import_freq option.
503 my $now = shift || time;
504 return $now - 60 * ($self->option('import_freq') || 5);
512 jonathan@freeside.biz
514 =head1 LICENSE AND COPYRIGHT
516 Copyright 2015 Freeside Internet Services
518 This program is free software; you can redistribute it and/or
519 modify it under the terms of the GNU General Public License
520 as published by the Free Software Foundation.