1 package FS::part_export::cacti;
5 use base qw( FS::part_export );
6 use FS::Record qw( qsearchs );
10 use File::Slurp qw( append_file slurp write_file );
12 use MIME::Base64 qw( encode_base64 );
18 tie my %options, 'Tie::IxHash',
19 'user' => { label => 'User Name',
20 default => 'freeside' },
21 'script_path' => { label => 'Script Path',
22 default => '/usr/share/cacti/cli/' },
23 'template_id' => { label => 'Host Template ID',
25 'tree_id' => { label => 'Graph Tree ID (optional)',
27 'description' => { label => 'Description (can use $ip_addr and $description tokens)',
28 default => 'Freeside $description $ip_addr' },
29 'graphs_path' => { label => 'Graph Export Directory (user@host:/path/to/graphs/)',
31 'import_freq' => { label => 'Minimum minutes between graph imports',
33 'max_graph_size' => { label => 'Maximum size per graph (MB)',
35 # 'delete_graphs' => { label => 'Delete associated graphs and data sources when unprovisioning',
41 'svc' => 'svc_broadband',
42 'desc' => 'Export service to cacti server, for svc_broadband services',
43 'options' => \%options,
45 Add service to cacti upon provisioning, for broadband services.<BR>
46 See FS::part_export::cacti documentation for details.
50 # standard hooks for provisioning/unprovisioning service
53 my ($self, $svc_broadband) = @_;
54 my ($q,$error) = _insert_queue($self, $svc_broadband);
59 my ($self, $svc_broadband) = @_;
60 my ($q,$error) = _delete_queue($self, $svc_broadband);
65 my($self, $new, $old) = @_;
66 return '' if $new->ip_addr eq $old->ip_addr; #important part didn't change
67 #delete old then insert new, with second job dependant on the first
68 my $oldAutoCommit = $FS::UID::AutoCommit;
69 local $FS::UID::AutoCommit = 0;
71 my ($dq, $iq, $error);
72 ($dq,$error) = _delete_queue($self,$old);
74 $dbh->rollback if $oldAutoCommit;
77 ($iq,$error) = _insert_queue($self,$new);
79 $dbh->rollback if $oldAutoCommit;
82 $error = $iq->depend_insert($dq->jobnum);
84 $dbh->rollback if $oldAutoCommit;
87 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
95 sub _export_unsuspend {
102 my ($self, $svc_broadband) = @_;
103 my $queue = new FS::queue {
104 'svcnum' => $svc_broadband->svcnum,
105 'job' => "FS::part_export::cacti::ssh_insert",
107 my $error = $queue->insert(
108 'host' => $self->machine,
109 'user' => $self->option('user'),
110 'hostname' => $svc_broadband->ip_addr,
111 'script_path' => $self->option('script_path'),
112 'template_id' => $self->option('template_id'),
113 'tree_id' => $self->option('tree_id'),
114 'description' => $self->option('description'),
115 'svc_desc' => $svc_broadband->description,
116 'svcnum' => $svc_broadband->svcnum,
118 return ($queue,$error);
122 my ($self, $svc_broadband) = @_;
123 my $queue = new FS::queue {
124 'svcnum' => $svc_broadband->svcnum,
125 'job' => "FS::part_export::cacti::ssh_delete",
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 # 'delete_graphs' => $self->option('delete_graphs'),
134 return ($queue,$error);
137 # routines run by queued jobs
143 die "Non-numerical Host Template ID, check export configuration\n"
144 unless $opt{'template_id'} =~ /^\d+$/;
145 die "Non-numerical Graph Tree ID, check export configuration\n"
146 unless $opt{'tree_id'} =~ /^\d+$/;
149 my $desc = $opt{'description'};
150 $desc =~ s/\$ip_addr/$opt{'hostname'}/g;
151 $desc =~ s/\$description/$opt{'svc_desc'}/g;
152 $desc =~ s/'/'\\''/g;
154 . $opt{'script_path'}
155 . q(add_device.php --description=')
160 . $opt{'template_id'};
161 my $response = ssh_cmd(%opt, 'command' => $cmd);
162 unless ( $response =~ /Success - new device-id: \((\d+)\)/ ) {
163 die "Error adding device: $response";
168 if ($opt{'tree_id'}) {
170 . $opt{'script_path'}
171 . q(add_tree.php --type=node --node-type=host --tree-id=)
175 $response = ssh_cmd(%opt, 'command' => $cmd);
176 unless ( $response =~ /Added Node node-id: \((\d+)\)/ ) {
177 die "Error adding host to tree: $response";
181 # # Get list of graph templates for new id
183 # . $opt{'script_path'}
184 # . q(freeside_cacti.php --get-graph-templates --host-template=)
185 # . $opt{'template_id'};
186 # my @gtids = split(/\n/,ssh_cmd(%opt, 'command' => $cmd));
187 # die "No graphs configured for host template"
191 # foreach my $gtid (@gtids) {
193 # # sanity checks, should never happen
195 # die "Bad graph template: $gtid"
196 # unless $gtid =~ /^\d+$/;
200 # . $opt{'script_path'}
201 # . q(add_graphs.php --graph-type=cg --graph-template-id=)
205 # $response = ssh_cmd(%opt, 'command' => $cmd);
206 # die "Error creating graph $gtid: $response"
207 # unless $response =~ /Graph Added - graph-id: \((\d+)\)/;
210 # # add the graph to the tree
212 # . $opt{'script_path'}
213 # . q(add_tree.php --type=node --node-type=graph --tree-id=)
217 # $response = ssh_cmd(%opt, 'command' => $cmd);
218 # die "Error adding graph $gid to tree: $response"
219 # unless $response =~ /Added Node/;
229 . $opt{'script_path'}
230 . q(freeside_cacti.php --drop-device --ip=')
233 # $cmd .= q( --delete-graphs)
234 # if $opt{'delete_graphs'};
235 my $response = ssh_cmd(%opt, 'command' => $cmd);
236 die "Error removing from cacti: " . $response
241 # NOT A METHOD, run as an FS::queue job
242 # copies graphs for a single service from Cacti export directory to FS cache
243 # generates basic html pages for this service's graphs, and stores them in FS cache
245 my ($job,$param) = @_; #
247 $job->update_statustext(10);
248 my $cachedir = $FS::UID::cache_dir . '/cacti-graphs/';
251 my $svcnum = $param->{'svcnum'} || die "No svcnum specified";
253 'table' => 'svc_broadband',
254 'hashref' => { 'svcnum' => $svcnum },
255 }) || die "Could not load svcnum $svcnum";
257 # load relevant FS::part_export::cacti object
258 my ($self) = $svc->cust_svc->part_svc->part_export('cacti');
260 $job->update_statustext(20);
262 # check for recent uploads, avoid doing this too often
263 my $svchtml = $cachedir.'svc_'.$svcnum.'.html';
265 open(my $fh, "<$svchtml");
266 my $firstline = <$fh>;
268 if ($firstline =~ /UPDATED (\d+)/) {
269 if ($1 > time - 60 * ($self->option('import_freq') || 5)) {
270 $job->update_statustext(100);
276 $job->update_statustext(30);
278 # get list of graphs for this svc
280 . $self->option('script_path')
281 . q(freeside_cacti.php --get-graphs --ip=')
284 my @graphs = map { [ split(/\t/,$_) ] }
286 'host' => $self->machine,
287 'user' => $self->option('user'),
291 $job->update_statustext(40);
293 # copy graphs to cache
294 # requires version 2.6.4 of rsync, released March 2005
295 my $rsync = File::Rsync->new({
299 'source' => $self->option('graphs_path'),
302 (map { q('**graph_).${$_}[0].q(*.png') } @graphs),
303 (map { q('**thumb_).${$_}[0].q(.png') } @graphs),
308 #don't know why a regular $rsync->exec isn't doing includes right, but this does
309 my $error = system(join(' ',@{$rsync->getcmd()}));
310 die "rsync failed with exit status $error" if $error;
312 $job->update_statustext(50);
314 # create html files in cache
316 my $svchead = q(<!-- UPDATED ) . $now . qq( -->\n)
317 . '<H2 STYLE="margin-top: 0;">Service #' . $svcnum . '</H2>' . "\n"
318 . q(<P>Last updated ) . scalar(localtime($now)) . q(</P>) . "\n";
319 write_file($svchtml,$svchead);
320 my $maxgraph = 1024 * 1024 * ($self->options('max_graph_size') || 5);
322 for (my $i = 0; $i <= $#graphs; $i++) {
323 my $graph = $graphs[$i];
324 my $thumbfile = $cachedir . 'graphs/thumb_' . $$graph[0] . '.png';
327 ( stat($thumbfile)->size() < $maxgraph )
330 # add graph to main file
331 my $graphhead = q(<H3>) . $$graph[1] . q(</H3>) . "\n";
332 append_file( $svchtml, $graphhead,
334 $svcnum, $$graph[0], img_tag($thumbfile)
337 # create graph details file
338 my $graphhtml = $cachedir . 'svc_' . $svcnum . '_graph_' . $$graph[0] . '.html';
339 write_file($graphhtml,$svchead,$graphhead);
342 while (-e (my $graphfile = $cachedir.'graphs/graph_'.$$graph[0].'_'.$j.'.png')) {
343 if ( stat($graphfile)->size() < $maxgraph ) {
345 append_file( $graphhtml, img_tag($graphfile) );
349 append_file($graphhtml, '<P>No detail graphs to display for this graph</P>')
352 $job->update_statustext(50 + ($i / $#graphs) * 50);
354 append_file($svchtml,'<P>No graphs to display for this service</P>')
357 $job->update_statustext(100);
362 my $somefile = shift;
363 return q(<IMG SRC="data:image/png;base64,)
364 . encode_base64(slurp($somefile,binmode=>':raw'))
365 . qq(" STYLE="margin-bottom: 1em;"><BR>\n);
369 my ($svcnum, $graphnum, $contents) = @_;
370 return q(<A HREF="?svcnum=)
379 #this gets used by everything else
380 #fake false laziness, other ssh_cmds handle error/output differently
384 my $ssh = Net::OpenSSH->new($opt->{'user'}.'@'.$opt->{'host'});
385 die "Couldn't establish SSH connection: ". $ssh->error if $ssh->error;
386 my ($output, $errput) = $ssh->capture2($opt->{'command'});
387 die "Error running SSH command: ". $ssh->error if $ssh->error;
388 die $errput if $errput;
396 FS::part_export::cacti
400 Cacti integration for Freeside
404 This module in particular handles FS::part_export object creation for Cacti integration;
405 consult any existing L<FS::part_export> documentation for details on how that works.
406 What follows is more general instructions for connecting your Cacti installation
407 to your Freeside installation.
409 =head2 Connecting Cacti To Freeside
411 Copy the freeside_cacti.php script from the bin directory of your Freeside
412 installation to the cli directory of your Cacti installation. Give this file
413 the same permissions as the other files in that directory, and create
414 (or choose an existing) user with sufficient permission to read these scripts.
416 In the regular Cacti interface, create a Host Template to be used by
417 devices exported by Freeside, and note the template's id number. Optionally,
418 create a Graph Tree for these devices to be automatically added to, and note
419 the tree's id number. Configure a Graph Export (under Settings) and note
420 the Export Directory.
422 In Freeside, go to Configuration->Services->Provisioning exports to
423 add a new export. From the Add Export page, select cacti for Export then enter...
425 * the Hostname or IP address of your Cacti server
427 * the User Name with permission to run scripts in the cli directory
429 * the full Script Path to that directory (eg /usr/share/cacti/cli/)
431 * the Host Template ID for adding new devices
433 * the Graph Tree ID for adding new devices (optional)
435 * the Description for new devices; you can use the tokens
436 $ip_addr and $description to include the equivalent fields
437 from the broadband service definition
439 * the Graph Export Directory, including connection information
440 if necessary (user@host:/path/to/graphs/)
442 * the minimum minutes between graph imports to Freeside (graphs will
443 otherwise be imported into Freeside as needed.) This should be at least
444 as long as the minumum time between graph exports configured in Cacti.
445 Defaults to 5 if unspecified.
447 * the maximum size per graph, in MB; individual graphs that exceed this size
448 will be quietly ignored by Freeside. Defaults to 5 if unspecified.
450 After adding the export, go to Configuration->Services->Service definitions.
451 The export you just created will be available for selection when adding or
452 editing broadband service definitions; check the box to activate it for
453 a given service. Note that you should only have one cacti export per
454 broadband service definition.
456 When properly configured broadband services are provisioned, they will now
457 be added to Cacti using the Host Template you specified. If you also specified
458 a Graph Tree, the created device will also be added to that.
460 Once added, a link to the graphs for this host will be available when viewing
461 the details of the provisioned service in Freeside.
463 Devices will be deleted from Cacti when the service is unprovisioned in Freeside,
464 and they will be deleted and re-added if the ip address changes.
466 Currently, graphs themselves must still be added in Cacti by hand or some
467 other form of automation tailored to your specific graph inputs and data sources.
472 jonathan@freeside.biz
474 =head1 LICENSE AND COPYRIGHT
476 Copyright 2015 Freeside Internet Services
478 This program is free software; you can redistribute it and/or
479 modify it under the terms of the GNU General Public License
480 as published by the Free Software Foundation.