+=head1 SUBROUTINES
+
+=over 4
+
+=item process_graphs JOB PARAM
+
+Intended to be run as an FS::queue job.
+
+Copies graphs for a single service from Cacti export directory to FS cache,
+generates basic html pages for this service with base64-encoded graphs embedded,
+and stores the generated pages in the database.
+
+=back
+
+=cut
+
+sub process_graphs {
+ my ($job,$param) = @_;
+
+ $job->update_statustext(10);
+ my $cachedir = trailslash($FS::UID::cache_dir,'cache.'.$FS::UID::datasrc,'cacti-graphs');
+
+ # load the service
+ my $svcnum = $param->{'svcnum'} || die "No svcnum specified";
+ my $svc = qsearchs({
+ 'table' => 'svc_broadband',
+ 'hashref' => { 'svcnum' => $svcnum },
+ }) || die "Could not load svcnum $svcnum";
+
+ # load relevant FS::part_export::cacti object
+ my ($self) = $svc->cust_svc->part_svc->part_export('cacti');
+
+ $job->update_statustext(20);
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ # check for existing pages
+ my $now = time;
+ my %oldpages = map { ($_->graphnum || 'MAIN') => $_ } qsearch({
+ 'table' => 'cacti_page',
+ 'hashref' => { 'svcnum' => $svcnum, 'exportnum' => $self->exportnum },
+ 'select' => 'cacti_pagenum, exportnum, svcnum, graphnum, imported, thumbnail', #no need to load old content
+ 'order_by' => 'ORDER BY graphnum',
+ });
+
+ # if all existing pages are recent enough, do nothing and return
+ # (won't detect newly introduced graphs, but they can wait for next run)
+ my $uptodate = 0;
+ if (keys %oldpages) {
+ $uptodate = 1;
+ foreach my $oldpage (keys %oldpages) {
+ if ($oldpages{$oldpage}->imported <= $self->exptime($now)) {
+ $uptodate = 0;
+ last;
+ }
+ }
+ }
+ if ($uptodate) {
+ $job->update_statustext(100);
+ return '';
+ }
+
+ $job->update_statustext(30);
+
+ # get list of graphs for this svc from cacti server
+ my $cmd = $php
+ . trailslash($self->option('script_path'))
+ . q(freeside_cacti.php --get-graphs --ip=')
+ . $svc->ip_addr
+ . q(');
+ $cmd .= q( --include-path=') . $self->option('include_path') . q(')
+ if $self->option('include_path');
+ my @graphs = map { [ split(/\t/,$_) ] }
+ split(/\n/, ssh_cmd(
+ 'host' => $self->machine,
+ 'user' => $self->option('user'),
+ 'command' => $cmd
+ ));
+
+ $job->update_statustext(40);
+
+ # copy graphs from cacti server to cache
+ # requires version 2.6.4 of rsync, released March 2005
+ my $rsync = File::Rsync->new({
+ 'rsh' => 'ssh',
+ 'verbose' => 1,
+ 'recursive' => 1,
+ 'quote-src' => 1,
+ 'quote-dst' => 1,
+ 'source' => trailslash($self->option('graphs_path')),
+ 'dest' => $cachedir,
+ 'include' => [
+ (map { q('**graph_).${$_}[0].q(*.png') } @graphs),
+ (map { q('**thumb_).${$_}[0].q(.png') } @graphs),
+ q('*/'),
+ q('- *'),
+ ],
+ });
+ #don't know why a regular $rsync->exec isn't doing includes right, but this does
+ my $rscmd = join(' ',@{$rsync->getcmd()});
+ my $error = system($rscmd);
+ die "rsync ($rscmd) failed with exit status $error" if $error;
+
+ $job->update_statustext(50);
+
+ # create html file contents
+ my $svchead = q(<!-- UPDATED ) . $now . qq( -->)
+ . '<H2 STYLE="margin-top: 0;">Service #' . $svcnum . '</H2>'
+ . q(<P>Last updated ) . scalar(localtime($now)) . q(</P>);
+ my $svchtml = $svchead;
+ my $maxgraph = 1024 * 1024 * ($self->options('max_graph_size') || 5);
+ my $nographs = 1;
+ for (my $i = 0; $i <= $#graphs; $i++) {
+ my $graph = $graphs[$i];
+ my $thumbfile = $cachedir . 'graphs/thumb_' . $$graph[0] . '.png';
+ if (-e $thumbfile) {
+ if ( stat($thumbfile)->size() < $maxgraph ) {
+ $nographs = 0;
+ my $thumbnail = img_tag($thumbfile);
+ # add graph to main file
+ my $graphhead = q(<H3>) . $$graph[1] . q(</H3>);
+ $svchtml .= $graphhead;
+ $svchtml .= anchor_tag( $svcnum, $$graph[0], $thumbnail );
+ # create graph details file
+ my $graphhtml = $svchead . $graphhead;
+ my $nodetail = 1;
+ my $j = 1;
+ # no easy way to tell what detail graphs should exist,
+ # and don't want detail graphs that are out of sync with thumbnail,
+ # so just use what we can find
+ while (-e (my $graphfile = $cachedir.'graphs/graph_'.$$graph[0].'_'.$j.'.png')) {
+ if ( stat($graphfile)->size() < $maxgraph ) {
+ $nodetail = 0;
+ $graphhtml .= img_tag($graphfile);
+ }
+ unlink($graphfile);
+ $j++;
+ }
+ $graphhtml .= '<P>No detail graphs to display for this graph</P>'
+ if $nodetail;
+ #delete old detail page
+ if ($oldpages{$$graph[0]}) {
+ $error = $oldpages{$$graph[0]}->delete;
+ if ($error) {
+ $dbh->rollback if $oldAutoCommit;
+ die $error;
+ }
+ }
+ #insert new detail page
+ my $newobj = new FS::cacti_page {
+ 'exportnum' => $self->exportnum,
+ 'svcnum' => $svcnum,
+ 'graphnum' => $$graph[0],
+ 'imported' => $now,
+ 'content' => $graphhtml,
+ 'thumbnail' => $thumbnail,
+ };
+ $error = $newobj->insert;
+ if ($error) {
+ $dbh->rollback if $oldAutoCommit;
+ die $error;
+ }
+ } else {
+ $svchtml .= qq(<P STYLE="color: #FF0000">File $thumbfile is too large, skipping</P>);
+ }
+ unlink($thumbfile);
+ } else {
+ # try to use old page for this graph
+ if ($oldpages{$$graph[0]} && $oldpages{$$graph[0]}->thumbnail) {
+ $nographs = 0;
+ # add old graph to main file
+ my $graphhead = q(<H3>) . $$graph[1] . q(</H3>);
+ $svchtml .= $graphhead;
+ $svchtml .= qq(<P STYLE="color: #FF0000">Current graphs unavailable; using previously imported data.</P>);
+ $svchtml .= anchor_tag( $svcnum, $$graph[0], $oldpages{$$graph[0]}->thumbnail );
+ } else {
+ $svchtml .= qq(<P STYLE="color: #FF0000">Error loading graph: $$graph[0]</P>);
+ }
+ }
+ # remove old page from hash even if it is being reused,
+ # remaining entries in hash will be deleted from database below
+ delete $oldpages{$$graph[0]} if $oldpages{$$graph[0]};
+ $job->update_statustext(49 + int($i / @graphs) * 50);
+ }
+ $svchtml .= '<P>No graphs to display for this service</P>'
+ if $nographs;
+ # delete remaining old pages, including svc index
+ foreach my $oldpage (keys %oldpages) {
+ $error = $oldpages{$oldpage}->delete;
+ if ($error) {
+ $dbh->rollback if $oldAutoCommit;
+ die $error;
+ }
+ }
+ # insert new index page for svc
+ my $newobj = new FS::cacti_page {
+ 'exportnum' => $self->exportnum,
+ 'svcnum' => $svcnum,
+ 'graphnum' => '',
+ 'imported' => $now,
+ 'content' => $svchtml,
+ 'thumbnail' => '',
+ };
+ $error = $newobj->insert;
+ if ($error) {
+ $dbh->rollback if $oldAutoCommit;
+ die $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ $job->update_statustext(100);
+ return '';
+}
+
+sub img_tag {
+ my $somefile = shift;
+ return q(<IMG SRC="data:image/png;base64,)
+ . encode_base64(slurp($somefile,binmode=>':raw'),'')
+ . qq(" STYLE="margin-bottom: 1em;"><BR>);
+}
+
+sub anchor_tag {
+ my ($svcnum, $graphnum, $contents) = @_;
+ return q(<A HREF="?svcnum=)
+ . $svcnum
+ . q(&graphnum=)
+ . $graphnum
+ . q(">)
+ . $contents
+ . q(</A>);
+}
+
+#this gets used by everything else