diff options
author | Kristian Lyngstol <kristian@bohemians.org> | 2016-04-12 18:01:32 +0200 |
---|---|---|
committer | Kristian Lyngstol <kristian@bohemians.org> | 2016-04-12 18:01:32 +0200 |
commit | 09ea945c3908fd42e90eb64c194d9af11d174206 (patch) | |
tree | 8bcf0f98113ea4a92effa1c39344730c7e6f50b0 /include | |
parent | c3ebdbda43c23ade1e49dfd9f546fba1d15f9941 (diff) |
Actual initial import
Fetched from tgmanage.
Diffstat (limited to 'include')
-rwxr-xr-x | include/config.pm.dist | 109 | ||||
-rwxr-xr-x | include/nms.pm | 186 | ||||
-rw-r--r-- | include/nms/snmp.pm | 91 | ||||
-rw-r--r-- | include/nms/util.pm | 141 | ||||
-rwxr-xr-x | include/nms/web.pm | 112 |
5 files changed, 639 insertions, 0 deletions
diff --git a/include/config.pm.dist b/include/config.pm.dist new file mode 100755 index 0000000..776e479 --- /dev/null +++ b/include/config.pm.dist @@ -0,0 +1,109 @@ +#! /usr/bin/perl +use strict; +use warnings; +use DBI; +package nms::config; + +# DB +our $db_name = "nms"; +our $db_host = "bb-8.tg16.gathering.org"; +our $db_username = "nms"; +our $db_password = "<removed>"; + +# NMS: What SNMP objects to fetch. +# Some basics +our @snmp_objects = [ +['ifIndex'], +['sysName'], +['sysDescr'], +['ifHighSpeed'], +['ifType'], +['ifName'], +['ifDescr'], +['ifAlias'], +['ifOperStatus'], +['ifAdminStatus'], +['ifLastChange'], +['ifHCInOctets'], +['ifHCOutOctets'], +['ifInDiscards'], +['ifOutDiscards'], +['ifInErrors'], +['ifOutErrors'], +['ifInUnknownProtos'], +['ifOutQLen'], +['sysUpTime'], +['jnxOperatingTemp'], +['jnxOperatingCPU'], +['jnxOperatingDescr'], +['jnxBoxSerialNo'] +]; +# Max SNMP polls to fire off at the same time. +our $snmp_max = 20; + +# DHCP-servers +our $dhcp_server1 = "185.110.149.2"; # primary +our $dhcp_server2 = "185.110.148.2"; # secondary + +# TACACS-login for NMS +our $tacacs_user = "<removed>"; +our $tacacs_pass = "<removed>"; + +# Telnet-timeout for smanagrun +our $telnet_timeout = 300; + +# IP/IPv6/DNS-info +our $tgname = "tg16"; +our $pri_hostname = "r2-d2"; +our $pri_v4 = "185.110.149.2"; +our $pri_v6 = "2a06:5841:149a::2"; +our $pri_net_v4 = "185.110.149.0/26"; +our $pri_net_v6 = "2a06:5841:149a::/64"; + +our $sec_hostname = "c-3po"; +our $sec_v4 = "185.110.148.2"; +our $sec_v6 = "2a06:5841:1337::2"; +our $sec_net_v4 = "185.110.148.0/26"; +our $sec_net_v6 = "2a06:5841:1337::/64"; + +# for RIPE to get reverse zones via DNS AXFR +# https://www.ripe.net/data-tools/dns/reverse-dns/how-to-set-up-reverse-delegation +our $ext_xfer = "193.0.0.0/22; 2001:610:240::/48; 2001:67c:2e8::/48"; + +# allow XFR from NOC +our $noc_net = "185.110.150.0/25; 2a06:5841:150a::1/64"; + +# To generate new dnssec-key for ddns: +# dnssec-keygen -a HMAC-MD5 -b 128 -n HOST DHCP_UPDATER +our $ddns_key = "<removed>"; +our $ddns_to = "127.0.0.1"; # just use localhost + +# Base networks +our $base_ipv4net = "88.92.0.0/17"; +our $base_ipv6net = "2a06:5840::/29"; +our $ipv6zone = "0.4.8.5.6.0.a.2.ip6.arpa"; + +# extra networks that are outside the normal ranges +# that should have recursive DNS access +our $rec_net = "185.110.148.0/22"; + +# extra networks that are outside the normal ranges +# that should be added to DNS +our @extra_nets = ( + '185.110.148.0/24', + '185.110.149.0/24', + '185.110.150.0/24', + '185.110.151.0/24', +); + +# add WLC's +our $wlc1 = "185.110.148.14"; + +# add VOIP-server +our $voip1 = "<removed>"; + +# PXE-server (rest of bootstrap assumes $sec_v4/$sec_v6) +our $pxe_server_v4 = $sec_v4; +our $pxe_server_v6 = $sec_v6; + +1; diff --git a/include/nms.pm b/include/nms.pm new file mode 100755 index 0000000..2ec922b --- /dev/null +++ b/include/nms.pm @@ -0,0 +1,186 @@ +#! /usr/bin/perl +use strict; +use warnings; +use DBI; +use Net::OpenSSH; +use Net::Telnet; +use Data::Dumper; +use FileHandle; +use JSON; +package nms; + +use base 'Exporter'; +our @EXPORT = qw(switch_disconnect switch_connect_ssh switch_connect_dlink switch_exec switch_exec_json switch_timeout db_connect); + +BEGIN { + require "config.pm"; + eval { + require "config.local.pm"; + }; +} + + +sub db_connect { + my $connstr = "dbi:Pg:dbname=" . $nms::config::db_name; + $connstr .= ";host=" . $nms::config::db_host unless (!defined($nms::config::db_host)); + + my $dbh = DBI->connect($connstr, + $nms::config::db_username, + $nms::config::db_password, {AutoCommit => 0}) + or die "Couldn't connect to database"; + return $dbh; +} + +sub switch_connect_ssh($) { + my ($ip) = @_; + my $ssh = Net::OpenSSH->new($ip, + user => $nms::config::tacacs_user, + password => $nms::config::tacacs_pass, + master_opts => [ "-o", "StrictHostKeyChecking=no" ]); + my ($pty, $pid) = $ssh->open2pty({stderr_to_stdout => 1}) + or die "unable to start remote shell: " . $ssh->error; + + my $dumplog = FileHandle->new; + $dumplog->open(">>/tmp/dumplog-queue") or die "/tmp/dumplog-queue: $!"; + #$dumplog->print("\n\nConnecting to " . $ip . "\n\n"); + + my $inputlog = FileHandle->new; + $inputlog->open(">>/tmp/inputlog-queue") or die "/tmp/inputlog-queue: $!"; + #$inputlog->print("\n\nConnecting to " . $ip . "\n\n"); + + my $telnet = Net::Telnet->new(-fhopen => $pty, + -timeout => $nms::config::telnet_timeout, + -dump_log => $dumplog, + -input_log => $inputlog, + -prompt => '/.*\@[a-z0-9-]+[>#] /', + -telnetmode => 0, + -cmd_remove_mode => 1, + -output_record_separator => "\r"); + $telnet->waitfor(-match => $telnet->prompt, + -errmode => "return") + or die "login failed: " . $telnet->lastline; + + $telnet->cmd("set cli screen-length 0"); + + return { telnet => $telnet, ssh => $ssh, pid => $pid, pty => $pty }; +} + +sub switch_connect_dlink($) { + my ($ip) = @_; + + my $dumplog = FileHandle->new; + $dumplog->open(">>/tmp/dumplog-queue") or die "/tmp/dumplog-queue: $!"; + $dumplog->print("\n\nConnecting to " . $ip . "\n\n"); + + my $inputlog = FileHandle->new; + $inputlog->open(">>/tmp/inputlog-queue") or die "/tmp/inputlog-queue: $!"; + $inputlog->print("\n\nConnecting to " . $ip . "\n\n"); + + my $conn = new Net::Telnet( Timeout => $nms::config::telnet_timeout, + Dump_Log => $dumplog, + Input_Log => $inputlog, + Errmode => 'return', + Prompt => '/[\S\-\_]+[#>]/'); + my $ret = $conn->open( Host => $ip); + if (!$ret || $ret != 1) { + return (undef); + } + # Handle login with and without password + print "Logging in without password\n"; + $conn->waitfor('/User ?Name:/'); + $conn->print('admin'); + my (undef, $match) = $conn->waitfor('/DGS-3100#|Password:/'); + die 'Unexpected prompt after login attempt' if (not defined $match); + if ($match eq 'Password:') { + $conn->print('gurbagurba'); # Dette passordet skal feile + $conn->waitfor('/User ?Name:/'); + $conn->print($nms::config::tacacs_user); + my (undef, $match) = $conn->waitfor('/DGS-3100#|Password:/'); + if ($match eq 'Password:') { + $conn->cmd($nms::config::tacacs_pass); + } + } + return { telnet => $conn }; +} + +# Send a command to switch and return the data recvied from the switch +sub switch_exec { + my ($cmd, $conn, $print) = @_; + + sleep 1; # don't overload the D-Link + + # Send the command and get data from switch + my @data; + if (defined($print)) { + $conn->print($cmd); + return; + } else { + @data = $conn->cmd($cmd); + print "ERROR: " . $conn->errmsg . "\n" if $conn->errmsg; + } + return @data; +} + +sub switch_exec_json($$) { + my ($cmd, $conn) = @_; + my @json = switch_exec("$cmd | display json", $conn); + pop @json; # Remove the banner at the end of the output + return ::decode_json(join("", @json)); +} + +sub switch_timeout { + my ($timeout, $conn) = @_; + + $conn->timeout($timeout); + return ('Set timeout to ' . $timeout); +} + +sub switch_disconnect($) { + my ($struct) = @_; + my $conn = $struct->{telnet}; + $conn->close(); + if ($struct->{pid}) { + waitpid($struct->{pid}, 0); + } +} +# A few utilities to convert from SNMP binary address format to human-readable. + +sub convert_mac { + return join(':', map { sprintf "%02x", $_ } unpack('C*', shift)); +} + +sub convert_ipv4 { + return join('.', map { sprintf "%d", $_ } unpack('C*', shift)); +} + +sub convert_ipv6 { + return join(':', map { sprintf "%x", $_ } unpack('n*', shift)); +} + +sub convert_addr { + my ($data, $type) = @_; + if ($type == 1) { + return convert_ipv4($data); + } elsif ($type == 2) { + return convert_ipv6($data); + } else { + die "Unknown address type $type"; + } +} + +# Convert raw binary SNMP data to list of bits. +sub convert_bytelist { + return split //, unpack("B*", shift); +} + +sub convert_lldp_caps { + my ($caps_data, $data) = @_; + + my @caps = convert_bytelist($caps_data); + my @caps_names = qw(other repeater bridge ap router telephone docsis stationonly); + for (my $i = 0; $i < scalar @caps && $i < scalar @caps_names; ++$i) { + $data->{'cap_enabled_' . $caps_names[$i]} = $caps[$i]; + } +} + +1; diff --git a/include/nms/snmp.pm b/include/nms/snmp.pm new file mode 100644 index 0000000..26ada44 --- /dev/null +++ b/include/nms/snmp.pm @@ -0,0 +1,91 @@ +#! /usr/bin/perl +use strict; +use warnings; +use SNMP; +use nms; +package nms::snmp; + +use base 'Exporter'; +our @EXPORT = qw(); + +BEGIN { + # $SNMP::debugging = 1; + + # sudo mkdir /usr/share/mibs/site + # cd /usr/share/mibs/site + # wget -O- ftp://ftp.cisco.com/pub/mibs/v2/v2.tar.gz | sudo tar --strip-components=3 -zxvvf - + SNMP::initMib(); + SNMP::addMibDirs("/srv/tgmanage/mibs/StandardMibs"); + SNMP::addMibDirs("/srv/tgmanage/mibs/JuniperMibs"); + + SNMP::loadModules('SNMPv2-MIB'); + SNMP::loadModules('ENTITY-MIB'); + SNMP::loadModules('IF-MIB'); + SNMP::loadModules('LLDP-MIB'); + SNMP::loadModules('IP-MIB'); + SNMP::loadModules('IP-FORWARD-MIB'); +} + +sub snmp_open_session { + my ($ip, $community, $async) = @_; + + $async //= 0; + + my %options = (UseEnums => 1); + if ($ip =~ /:/) { + $options{'DestHost'} = "udp6:$ip"; + } else { + $options{'DestHost'} = "udp:$ip"; + } + + if ($community =~ /^snmpv3:(.*)$/) { + my ($username, $authprotocol, $authpassword, $privprotocol, $privpassword) = split /\//, $1; + + $options{'SecName'} = $username; + $options{'SecLevel'} = 'authNoPriv'; + $options{'AuthProto'} = $authprotocol; + $options{'AuthPass'} = $authpassword; + + if (defined($privprotocol) && defined($privpassword)) { + $options{'SecLevel'} = 'authPriv'; + $options{'PrivProto'} = $privprotocol; + $options{'PrivPass'} = $privpassword; + } + + $options{'Version'} = 3; + } else { + $options{'Community'} = $community; + $options{'Version'} = 2; + } + + my $session = SNMP::Session->new(%options); + if (defined($session) && ($async || defined($session->getnext('sysDescr')))) { + return $session; + } else { + die 'Could not open SNMP session to ' . $ip; + } +} + +# Not currently in use; kept around for reference. +sub fetch_multi_snmp { + my ($session, @oids) = @_; + + my %results = (); + + # Do bulk reads of 40 and 40; seems to be about the right size for 1500-byte packets. + for (my $i = 0; $i < scalar @oids; $i += 40) { + my $end = $i + 39; + $end = $#oids if ($end > $#oids); + my @oid_slice = @oids[$i..$end]; + + my $localresults = $session->get_request(-varbindlist => \@oid_slice); + return undef if (!defined($localresults)); + + while (my ($key, $value) = each %$localresults) { + $results{$key} = $value; + } + } + + return \%results; +} + diff --git a/include/nms/util.pm b/include/nms/util.pm new file mode 100644 index 0000000..64637b8 --- /dev/null +++ b/include/nms/util.pm @@ -0,0 +1,141 @@ +#! /usr/bin/perl +use strict; +use warnings; +package nms::util; +use Data::Dumper; + +use base 'Exporter'; +our @EXPORT = qw(guess_placement parse_switches_txt parse_switches parse_switch); + +# Parse a single switches.txt-formatted switch +sub parse_switch { + my ($switch, $subnet4, $subnet6, $mgtmt4, $mgtmt6, $lolid, $distro) = split(/ /); + my %foo = guess_placement($switch); + my %ret = ( + 'sysname' => "$switch", + 'subnet4' => "$subnet4", + 'subnet6' => "$subnet6", + 'mgmt_v4_addr' => "$mgtmt4", + 'mgmt_v6_addr' => "$mgtmt6", + 'traffic_vlan' => "$lolid", + 'distro' => "$distro" + ); + %{$ret{'placement'}} = guess_placement($switch); + return %ret; +} + +# Parses a switches_txt given as a filehandle on $_[0] +# (e.g.: parse_switches_txt(*STDIN) or parse_switches_txt(whatever). +sub parse_switches_txt { + my $fh = $_[0]; + my @switches; + while(<$fh>) { + chomp; + my %switch = parse_switch($_); + push @switches, {%switch}; + } + return @switches; +} + +# Parses switches in switches.txt format given as $_[0]. +# E.g: parse_switches("e1-3 88.92.0.0/26 2a06:5840:0a::/64 88.92.54.2/26 2a06:5840:54a::2/64 1013 distro0") +sub parse_switches { + my @switches; + my $txt = $_[0]; + foreach (split("\n",$txt)) { + chomp; + my %switch = parse_switch($_); + push @switches, {%switch}; + } + return @switches; +} + +# Guesses placement from name to get a starting point +# Largely courtesy of Knuta +sub guess_placement { + my ($x, $y, $xx, $yy); + + my $name = $_[0]; + my $src = "unknown"; + if ($name =~ /^e\d+-\d+$/) { + $name =~ /e(\d+)-(\d+)/; + my ($e, $s) = ($1, $2); + $src = "main"; + + $x = int(292 + (($e-1)/2) * 31.1); + $y = undef; + + $x += 14 if ($e >= 13); + $x += 14 if ($e >= 25); + $x += 14 if ($e >= 41); + $x += 14 if ($e >= 59); + + if ($s > 2) { + $y = 405 - 120 * ($s-2); + } else { + $y = 689 - 120 * ($s); + } + + $xx = $x + 16; + $yy = $y + 120; + + # Justeringer + $y += 45 if $name eq "e1-4"; + $y += 20 if $name eq "e3-4"; + $y += 15 if $name eq "e5-4"; + $yy -= 25 if $name eq "e7-1"; + $y += 10 if $name eq "e5-2"; + $yy -= 25 if $name eq "e5-2"; + $y += 20 if ($e >= 81 and $s == 2); + $yy -= 20 if ($e >= 79 and $s == 1); + $yy -= 30 if ($e >= 81 and $s == 1); + + } elsif ($name =~ /^creativia(\d+)$/) { + my ($s) = ($1); + $src = "creativia"; + $x = 1535; + $y = int(160 + 32.2 * $s); + $yy = $y + 20; + if ($s == 1) { + $xx = $x + 70; + } elsif ($s == 2) { + $xx = $x + 90; + } elsif ($s == 3) { + $xx = $x + 102; + } else { + $xx = $x + 142; + } + + } elsif ($name =~ /^crew(\d+)-(\d+)$/) { + my ($s, $n) = ($1, $2); + $src = "crew"; + $x = 550 + 65 * $n; + $y = int(759 + 20.5 * $s); + $xx = $x + 65; + $yy = $y + 14; + } elsif ($name =~ /^distro(\d)/) { + my $d = ($1); + $src = "distro"; + $x = 292 + $d * 165; + $y = 415; + $xx = $x + 130; + $yy = $y + 20; + } else { + # Fallback to have _some_ position + $src = "random"; + $x = int(rand(500)); + $y = int(rand(500)); + $xx = $x + 20; + $yy = $y + 130; + }; + + + my %box = ( + 'src' => "$src", + 'x1' => $x, + 'y1' => $y, + 'xx' => $xx, + 'yy' => $yy + ); + return %box; +} diff --git a/include/nms/web.pm b/include/nms/web.pm new file mode 100755 index 0000000..7c9339e --- /dev/null +++ b/include/nms/web.pm @@ -0,0 +1,112 @@ +#! /usr/bin/perl +# vim:ts=8:sw=8 +use strict; +use warnings; +use utf8; +use DBI; +use Data::Dumper; +use JSON; +use nms; +use Digest::SHA; +use FreezeThaw; +use URI::Escape; +package nms::web; + +use base 'Exporter'; +our %get_params; +our %json; +our @EXPORT = qw(finalize_output now json $dbh db_safe_quote %get_params get_input %json); +our $dbh; +our $now; +our $when; +our %cc; + +sub get_input { + my $in = ""; + while(<STDIN>) { $in .= $_; } + return $in; +} +# Print cache-control from %cc +sub printcc { + my $line = ""; + my $first = ""; + foreach my $tmp (keys(%cc)) { + $line .= $first . $tmp . "=" . $cc{$tmp}; + $first = ", "; + } + print 'Cache-Control: ' . $line . "\n"; +} + +sub db_safe_quote { + my $word = $_[0]; + my $term = $get_params{$word}; + if (!defined($term)) { + if(defined($_[1])) { + $term = $_[1]; + } else { + die "Missing CGI param $word"; + } + } + return $dbh->quote($term) || die; +} + +# returns a valid $when statement +# Also sets cache-control headers if time is overridden +# This can be called explicitly to override the window of time we evaluate. +# Normally up to 15 minutes old data will be returned, but for some API +# endpoints it is better to return no data than old data (e.g.: ping). +sub setwhen { + $now = "now()"; + my $window = '8m'; + my $offset = '0s'; + if (@_ > 0) { + $window = $_[0]; + } + if (@_ > 1) { + $offset = $_[1]; + } + if (defined($get_params{'now'})) { + $now = db_safe_quote('now') . "::timestamp with time zone "; + $cc{'max-age'} = "3600"; + } + $now = "(" . $now . " - '" . $offset . "'::interval)"; + $when = " time > " . $now . " - '".$window."'::interval and time < " . $now . " "; +} + +sub finalize_output { + my $query; + my $hash = Digest::SHA::sha512_base64(FreezeThaw::freeze(%json)); + $dbh->commit; + $query = $dbh->prepare('select extract(epoch from date_trunc(\'seconds\', ' . $now . ')) as time;'); + $query->execute(); + + $json{'time'} = int($query->fetchrow_hashref()->{'time'}); + $json{'hash'} = $hash; + + printcc; + + print "Etag: $hash\n"; + print "Access-Control-Allow-Origin: *\n"; + print "Access-Control-Allow-Methods: HEAD, GET\n"; + print "Content-Type: text/json; charset=utf-8\n\n"; + print JSON::XS::encode_json(\%json); + print "\n"; +} + +sub populate_params { + my $querystring = $ENV{'QUERY_STRING'} || ""; + foreach my $hdr (split("&",$querystring)) { + my ($key, $value) = split("=",$hdr,"2"); + $get_params{$key} = URI::Escape::uri_unescape($value); + } +} + +BEGIN { + $cc{'stale-while-revalidate'} = "3600"; + $cc{'max-age'} = "20"; + + $dbh = nms::db_connect(); + populate_params(); + setwhen(); +} +1; |