RT#18834 Cacti integration [phase one, simple but stable]
authorJonathan Prykop <jonathan@freeside.biz>
Fri, 20 Mar 2015 20:25:12 +0000 (15:25 -0500)
committerJonathan Prykop <jonathan@freeside.biz>
Mon, 13 Apr 2015 23:10:53 +0000 (18:10 -0500)
FS/FS/Schema.pm
FS/FS/part_export/cacti.pm [new file with mode: 0644]
bin/freeside_cacti.php [new file with mode: 0755]
httemplate/view/svc_broadband.cgi

index cd74a38..3c90892 100644 (file)
@@ -3248,6 +3248,7 @@ sub tables_hashref {
         'suid',                    'int', 'NULL',        '', '', '',
         'shared_svcnum',           'int', 'NULL',        '', '', '',
         'serviceid',           'varchar', 'NULL',        64, '', '',#srvexport/reportfields
+        'cacti_leaf_id',           'int', 'NULL',        '', '', '',
       ],
       'primary_key' => 'svcnum',
       'unique'      => [ [ 'ip_addr' ], [ 'mac_addr' ] ],
diff --git a/FS/FS/part_export/cacti.pm b/FS/FS/part_export/cacti.pm
new file mode 100644 (file)
index 0000000..6877c8f
--- /dev/null
@@ -0,0 +1,331 @@
+package FS::part_export::cacti;
+
+use strict;
+use base qw( FS::part_export );
+use FS::Record qw( qsearchs );
+use FS::UID qw( dbh );
+
+use vars qw( %info );
+
+my $php = 'php -q ';
+
+tie my %options, 'Tie::IxHash',
+  'user'              => { label   => 'User Name',
+                           default => 'freeside' },
+  'script_path'       => { label   => 'Script Path',
+                           default => '/usr/share/cacti/cli/' },
+  'base_url'          => { label   => 'Base Cacti URL',
+                           default => '' },
+  'template_id'       => { label   => 'Host Template ID',
+                           default => '' },
+  'tree_id'           => { label   => 'Graph Tree ID',
+                           default => '' },
+  'description'       => { label   => 'Description (can use $ip_addr and $description tokens)',
+                           default => 'Freeside $description $ip_addr' },
+#  'delete_graphs'     => { label   => 'Delete associated graphs and data sources when unprovisioning', 
+#                           type    => 'checkbox',
+#                         },
+;
+
+%info = (
+  'svc'             => 'svc_broadband',
+  'desc'            => 'Export service to cacti server, for svc_broadband services',
+  'options'         => \%options,
+  'notes'           => <<'END',
+Add service to cacti upon provisioning, for broadband services.<BR>
+See FS::part_export::cacti documentation for details.
+END
+);
+
+# standard hooks for provisioning/unprovisioning service
+
+sub _export_insert {
+  my ($self, $svc_broadband) = @_;
+  my ($q,$error) = _insert_queue($self, $svc_broadband);
+  return $error;
+}
+
+sub _export_delete {
+  my ($self, $svc_broadband) = @_;
+  my ($q,$error) = _delete_queue($self, $svc_broadband);
+  return $error;
+}
+
+sub _export_replace {
+  my($self, $new, $old) = @_;
+  return '' if $new->ip_addr eq $old->ip_addr; #important part didn't change
+  #delete old then insert new, with second job dependant on the first
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+  my ($dq, $iq, $error);
+  ($dq,$error) = _delete_queue($self,$old);
+  if ($error) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+  ($iq,$error) = _insert_queue($self,$new);
+  if ($error) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+  $error = $iq->depend_insert($dq->jobnum);
+  if ($error) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  return '';
+}
+
+sub _export_suspend {
+  return '';
+}
+
+sub _export_unsuspend {
+  return '';
+}
+
+# create queued jobs
+
+sub _insert_queue {
+  my ($self, $svc_broadband) = @_;
+  my $queue = new FS::queue {
+    'svcnum' => $svc_broadband->svcnum,
+    'job'    => "FS::part_export::cacti::ssh_insert",
+  };
+  my $error = $queue->insert(
+    'host'        => $self->machine,
+    'user'        => $self->option('user'),
+    'hostname'    => $svc_broadband->ip_addr,
+    'script_path' => $self->option('script_path'),
+    'template_id' => $self->option('template_id'),
+    'tree_id'     => $self->option('tree_id'),
+    'description' => $self->option('description'),
+       'svc_desc'    => $svc_broadband->description,
+    'svcnum'      => $svc_broadband->svcnum,
+  );
+  return ($queue,$error);
+}
+
+sub _delete_queue {
+  my ($self, $svc_broadband) = @_;
+  my $queue = new FS::queue {
+    'svcnum' => $svc_broadband->svcnum,
+    'job'    => "FS::part_export::cacti::ssh_delete",
+  };
+  my $error = $queue->insert(
+    'host'          => $self->machine,
+    'user'          => $self->option('user'),
+    'hostname'      => $svc_broadband->ip_addr,
+    'script_path'   => $self->option('script_path'),
+#    'delete_graphs' => $self->option('delete_graphs'),
+  );
+  return ($queue,$error);
+}
+
+# routines run by queued jobs
+
+sub ssh_insert {
+  my %opt = @_;
+
+  # Option validation
+  die "Non-numerical Host Template ID, check export configuration\n"
+    unless $opt{'template_id'} =~ /^\d+$/;
+  die "Non-numerical Graph Tree ID, check export configuration\n"
+    unless $opt{'tree_id'} =~ /^\d+$/;
+
+  # Add host to cacti
+  my $desc = $opt{'description'};
+  $desc =~ s/\$ip_addr/$opt{'hostname'}/g;
+  $desc =~ s/\$description/$opt{'svc_desc'}/g;
+  $desc =~ s/'/'\\''/g;
+  my $cmd = $php
+          . $opt{'script_path'} 
+          . q(add_device.php --description=')
+          . $desc
+          . q(' --ip=')
+          . $opt{'hostname'}
+          . q(' --template=)
+          . $opt{'template_id'};
+  my $response = ssh_cmd(%opt, 'command' => $cmd);
+  unless ( $response =~ /Success - new device-id: \((\d+)\)/ ) {
+    die "Error adding device: $response";
+  }
+  my $id = $1;
+
+  # Add host to tree
+  $cmd = $php
+       . $opt{'script_path'}
+       . q(add_tree.php --type=node --node-type=host --tree-id=)
+       . $opt{'tree_id'}
+       . q( --host-id=)
+       . $id;
+  $response = ssh_cmd(%opt, 'command' => $cmd);
+  unless ( $response =~ /Added Node node-id: \((\d+)\)/ ) {
+      die "Error adding host to tree: $response";
+  }
+  my $leaf_id = $1;
+
+  # Store id for generating graph urls
+  my $svc_broadband = qsearchs({
+    'table'   => 'svc_broadband',
+    'hashref' => { 'svcnum' => $opt{'svcnum'} },
+  });
+  die "Could not reload broadband service" unless $svc_broadband;
+  $svc_broadband->set('cacti_leaf_id',$leaf_id);
+  my $error = $svc_broadband->replace;
+  return $error if $error;
+
+#  # Get list of graph templates for new id
+#  $cmd = $php
+#       . $opt{'script_path'} 
+#       . q(freeside_cacti.php --get-graph-templates --host-template=)
+#       . $opt{'template_id'};
+#  my @gtids = split(/\n/,ssh_cmd(%opt, 'command' => $cmd));
+#  die "No graphs configured for host template"
+#    unless @gtids;
+#
+#  # Create graphs
+#  foreach my $gtid (@gtids) {
+#
+#    # sanity checks, should never happen
+#    next unless $gtid;
+#    die "Bad graph template: $gtid"
+#      unless $gtid =~ /^\d+$/;
+#
+#    # create the graph
+#    $cmd = $php
+#         . $opt{'script_path'}
+#         . q(add_graphs.php --graph-type=cg --graph-template-id=)
+#         . $gtid
+#         . q( --host-id=)
+#         . $id;
+#    $response = ssh_cmd(%opt, 'command' => $cmd);
+#    die "Error creating graph $gtid: $response"
+#      unless $response =~ /Graph Added - graph-id: \((\d+)\)/;
+#    my $gid = $1;
+#
+#    # add the graph to the tree
+#    $cmd = $php
+#         . $opt{'script_path'}
+#         . q(add_tree.php --type=node --node-type=graph --tree-id=)
+#         . $opt{'tree_id'}
+#         . q( --graph-id=)
+#         . $gid;
+#    $response = ssh_cmd(%opt, 'command' => $cmd);
+#    die "Error adding graph $gid to tree: $response"
+#      unless $response =~ /Added Node/;
+#
+#  } #foreach $gtid
+
+  return '';
+}
+
+sub ssh_delete {
+  my %opt = @_;
+  my $cmd = $php
+          . $opt{'script_path'} 
+          . q(freeside_cacti.php --drop-device --ip=')
+          . $opt{'hostname'}
+          . q(');
+#  $cmd .= q( --delete-graphs)
+#    if $opt{'delete_graphs'};
+  my $response = ssh_cmd(%opt, 'command' => $cmd);
+  die "Error removing from cacti: " . $response
+    if $response;
+  return '';
+}
+
+#fake false laziness, other ssh_cmds handle error/output differently
+sub ssh_cmd {
+  use Net::OpenSSH;
+  my $opt = { @_ };
+  my $ssh = Net::OpenSSH->new($opt->{'user'}.'@'.$opt->{'host'});
+  die "Couldn't establish SSH connection: ". $ssh->error if $ssh->error;
+  my ($output, $errput) = $ssh->capture2($opt->{'command'});
+  die "Error running SSH command: ". $ssh->error if $ssh->error;
+  die $errput if $errput;
+  return $output;
+}
+
+=pod
+
+=head1 NAME
+
+FS::part_export::cacti
+
+=head1 SYNOPSIS
+
+Cacti integration for Freeside
+
+=head1 DESCRIPTION
+
+This module in particular handles FS::part_export object creation for Cacti integration;
+consult any existing L<FS::part_export> documentation for details on how that works.
+What follows is more general instructions for connecting your Cacti installation
+to your Freeside installation.
+
+=head2 Connecting Cacti To Freeside
+
+Copy the freeside_cacti.php script from the bin directory of your Freeside
+installation to the cli directory of your Cacti installation.  Give this file 
+the same permissions as the other files in that directory, and create 
+(or choose an existing) user with sufficient permission to read these scripts.
+
+In the regular Cacti interface, create a Host Template to be used by 
+devices exported by Freeside, and note the template's id number.
+
+In Freeside, go to Configuration->Services->Provisioning exports to
+add a new export.  From the Add Export page, select cacti for Export then enter...
+
+* the User Name with permission to run scripts in the cli directory
+
+* enter the full Script Path to that directory (eg /usr/share/cacti/cli/)
+
+* enter the Base Cacti URL for your cacti server (eg https://example.com/cacti/)
+
+* the Host Template ID for adding new devices
+
+* the Graph Tree ID for adding new devices
+
+* the Description for new devices;  you can use the tokens
+  $ip_addr and $description to include the equivalent fields
+  from the broadband service definition
+
+After adding the export, go to Configuration->Services->Service definitions.
+The export you just created will be available for selection when adding or
+editing broadband service definitions.
+
+When properly configured broadband services are provisioned, they should now
+be added to Cacti using the Host Template you specified, and the created device
+will also be added to the specified Graph Tree.
+
+Once added, a link to the graphs for this host will be available when viewing 
+the details of the provisioned service in Freeside (you will need to authenticate 
+into Cacti to view them.)
+
+Devices will be deleted from Cacti when the service is unprovisioned in Freeside, 
+and they will be deleted and re-added if the ip address changes.
+
+Currently, graphs themselves must still be added in cacti by hand or some
+other form of automation tailored to your specific graph inputs and data sources.
+
+=head1 AUTHOR
+
+Jonathan Prykop 
+jonathan@freeside.biz
+
+=head1 LICENSE AND COPYRIGHT
+
+Copyright 2015 Freeside Internet Services      
+
+This program is free software; you can redistribute it and/or           |
+modify it under the terms of the GNU General Public License             |
+as published by the Free Software Foundation.
+
+=cut
+
+1;
+
+
diff --git a/bin/freeside_cacti.php b/bin/freeside_cacti.php
new file mode 100755 (executable)
index 0000000..22fb0f0
--- /dev/null
@@ -0,0 +1,175 @@
+#!/usr/bin/php -q
+<?php
+/*
+ +-------------------------------------------------------------------------+
+ | Copyright (C) 2015 Freeside Internet Services                           |
+ |                                                                         |
+ | This program is free software; you can redistribute it and/or           |
+ | modify it under the terms of the GNU General Public License             |
+ | as published by the Free Software Foundation; either version 2          |
+ | of the License, or (at your option) any later version.                  |
+ |                                                                         |
+ | This program is distributed in the hope that it will be useful,         |
+ | but WITHOUT ANY WARRANTY; without even the implied warranty of          |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           |
+ | GNU General Public License for more details.                            |
+ +-------------------------------------------------------------------------+
+ | Copy this file to the cli directory of your Cacti installation, which   |
+ | should also contain an add_device.php script.  Give this file the same  |
+ | permissions as add_device.php, and configure your Freeside installation |
+ | with the location of that directory and the name of a user who has      |
+ | permission to read these files.  See the FS::part_export::cacti docs    |
+ | for more details.                                                       |
+ +-------------------------------------------------------------------------+
+*/
+
+/* do NOT run this script through a web browser */
+if (!isset($_SERVER["argv"][0]) || isset($_SERVER['REQUEST_METHOD'])  || isset($_SERVER['REMOTE_ADDR'])) {
+       die("<br><strong>This script is only meant to run at the command line.</strong>");
+}
+
+/* We are not talking to the browser */
+$no_http_headers = true;
+
+/* 
+Currently, only drop-device is actually being used by Freeside integration,
+but keeping commented out code for potential future development.
+*/
+
+include(dirname(__FILE__)."/../site/include/global.php");
+include_once($config["base_path"]."/lib/api_device.php");
+
+/*
+include_once($config["base_path"]."/lib/api_automation_tools.php");
+include_once($config["base_path"]."/lib/api_data_source.php");
+include_once($config["base_path"]."/lib/api_graph.php");
+include_once($config["base_path"]."/lib/functions.php");
+*/
+
+/* process calling arguments */
+$action = '';
+$ip = '';
+$host_template = '';
+// $delete_graphs = FALSE;
+$parms = $_SERVER["argv"];
+array_shift($parms);
+if (sizeof($parms)) {
+       foreach($parms as $parameter) {
+               @list($arg, $value) = @explode("=", $parameter);
+               switch ($arg) {
+        case "--drop-device":
+                       $action = 'drop-device';
+            break;
+/*
+        case "--get-device":
+                       $action = 'get-device';
+            break;
+        case "--get-graph-templates":
+                       $action = 'get-graph-templates';
+            break;
+*/
+               case "--ip":
+                       $ip = trim($value);
+                       break;
+               case "--host-template":
+                       $host_template = trim($value);
+                       break;
+/*
+               case "--delete-graphs":
+                       $delete_graphs = TRUE;
+                       break;
+*/
+               case "--version":
+               case "-V":
+               case "-H":
+               case "--help":
+                       die(default_die());
+               default:
+                       die("ERROR: Invalid Argument: ($arg)");
+               }
+       }
+} else {
+  die(default_die());
+}
+
+/* Now take an action */
+switch ($action) {
+case "drop-device":
+       $host_id = host_id($ip);
+/*
+       if ($delete_graphs) {
+               // code copied & pasted from version 0.8.8a
+        // cacti/site/lib/host.php and cacti/site/graphs.php 
+               // unfortunately no api function for this yet
+               $graphs = db_fetch_assoc("select
+                       graph_local.id as local_graph_id
+                       from graph_local
+                       where graph_local.host_id=" . $host_id);
+               if (sizeof($graphs) > 0) {
+                       foreach ($graphs as $graph) {
+                               $data_sources = array_rekey(db_fetch_assoc("SELECT data_template_data.local_data_id
+                                       FROM (data_template_rrd, data_template_data, graph_templates_item)
+                                       WHERE graph_templates_item.task_item_id=data_template_rrd.id
+                                       AND data_template_rrd.local_data_id=data_template_data.local_data_id
+                                       AND graph_templates_item.local_graph_id=" . $graph["local_graph_id"] . "
+                                       AND data_template_data.local_data_id > 0"), "local_data_id", "local_data_id");
+                               if (sizeof($data_sources)) {
+                                       api_data_source_remove_multi($data_sources);
+                               }
+                               api_graph_remove($graph["local_graph_id"]);
+                       }
+               }
+       }
+*/
+       api_device_remove($host_id);
+       if (host_id($ip,1)) {
+               die("Failed to remove hostname $ip");
+       }
+       exit(0);
+/*
+case "get-device":
+       echo host_id($ip);
+       exit(0);
+case "get-graph-templates":
+       if (!$host_template) {
+               die("No host template specified");
+       }
+       $graphs = getGraphTemplatesByHostTemplate($host_template);
+       if (sizeof($graphs)) {
+               foreach (array_keys($graphs) as $gtid) {
+                       echo $gtid . "\n";
+               }
+               exit(0);
+       }
+       die("No graph templates associated with this host template");
+*/
+default:
+       die("Specified action not found, contact a developer");
+}
+
+function default_die() {
+  return "Cacti interface for freeside.  Do not use for anything else.";
+}
+
+function host_id($ip_address, $nodie=0) {
+       if (!$ip_address) {
+               die("No hostname specified");
+       }
+       $devices = array();
+       $query = "select id from host";
+       $query .= " where hostname='$ip_address'";
+       $devices = db_fetch_assoc($query);
+       if (sizeof($devices) > 1) {
+        // This should never happen, just being thorough
+               die("Multiple devices found for hostname $ip_address");
+       } else if (!sizeof($devices)) {
+               if ($nodie) {
+                       return '';
+               } else {
+                       die("Could not find hostname $ip_address");
+               }
+       }
+       return $devices[0]['id'];
+}
+
+?>
index 70c0b53..9fe10bd 100644 (file)
@@ -72,6 +72,17 @@ sub ip_addr {
   my $out = $ip_addr;
   $out .= ' (' . include('/elements/popup_link-ping.html', ip => $ip_addr) . ')'
     if $ip_addr;
+  if ($svc->cacti_leaf_id) {
+    # should only ever be one, but not sure if that is enforced
+    my ($cacti) = $svc->cust_svc->part_svc->part_export('cacti');
+    $out .= ' (<A HREF="' 
+         .  $cacti->option('base_url')
+         .  'graph_view.php?action=tree&tree_id='
+         .  $cacti->option('tree_id')
+         .  '&leaf_id='
+         .  $svc->cacti_leaf_id
+         .  '">cacti</A>)';
+  }
   if ( my $addr_block = $svc->addr_block ) {
     $out .= '<br>Netmask: ' . $addr_block->NetAddr->mask .
             '<br>Gateway: ' . $addr_block->ip_gateway;