From e05e4f1f0d6a704db4d3c8b3d9a851216569580c Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Fri, 20 Mar 2015 15:25:12 -0500 Subject: [PATCH] RT#18834 Cacti integration [phase one, simple but stable] --- FS/FS/Schema.pm | 1 + FS/FS/part_export/cacti.pm | 331 ++++++++++++++++++++++++++++++++++++++ bin/freeside_cacti.php | 175 ++++++++++++++++++++ httemplate/view/svc_broadband.cgi | 11 ++ 4 files changed, 518 insertions(+) create mode 100644 FS/FS/part_export/cacti.pm create mode 100755 bin/freeside_cacti.php diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index cd74a3854..3c90892e9 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -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 index 000000000..6877c8f5f --- /dev/null +++ b/FS/FS/part_export/cacti.pm @@ -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.
+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 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 index 000000000..22fb0f0f4 --- /dev/null +++ b/bin/freeside_cacti.php @@ -0,0 +1,175 @@ +#!/usr/bin/php -q +This script is only meant to run at the command line."); +} + +/* 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']; +} + +?> diff --git a/httemplate/view/svc_broadband.cgi b/httemplate/view/svc_broadband.cgi index 70c0b5300..9fe10bd3a 100644 --- a/httemplate/view/svc_broadband.cgi +++ b/httemplate/view/svc_broadband.cgi @@ -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 .= ' (cacti)'; + } if ( my $addr_block = $svc->addr_block ) { $out .= '
Netmask: ' . $addr_block->NetAddr->mask . '
Gateway: ' . $addr_block->ip_gateway; -- 2.11.0