230 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
			
		
		
	
	
			230 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
| package App::Netdisco::Util::SNMP;
 | ||
| 
 | ||
| use Dancer qw/:syntax :script/;
 | ||
| use App::Netdisco::Util::DNS 'hostname_from_ip';
 | ||
| use App::Netdisco::Util::Permission ':all';
 | ||
| 
 | ||
| use base 'Exporter';
 | ||
| our @EXPORT = ();
 | ||
| our @EXPORT_OK = qw/
 | ||
|   fixup_device_auth get_communities snmp_comm_reindex
 | ||
| /;
 | ||
| our %EXPORT_TAGS = (all => \@EXPORT_OK);
 | ||
| 
 | ||
| =head1 NAME
 | ||
| 
 | ||
| App::Netdisco::Util::SNMP
 | ||
| 
 | ||
| =head1 DESCRIPTION
 | ||
| 
 | ||
| Helper functions for L<SNMP::Info> instances.
 | ||
| 
 | ||
| There are no default exports, however the C<:all> tag will export all
 | ||
| subroutines.
 | ||
| 
 | ||
| =head1 EXPORT_OK
 | ||
| 
 | ||
| =head2 fixup_device_auth
 | ||
| 
 | ||
| Rebuilds the C<device_auth> config with missing defaults and other fixups for
 | ||
| config changes over time. Returns a list which can replace C<device_auth>.
 | ||
| 
 | ||
| =cut
 | ||
| 
 | ||
| sub fixup_device_auth {
 | ||
|   my $config = (setting('snmp_auth') || setting('device_auth'));
 | ||
|   my @new_stanzas = ();
 | ||
| 
 | ||
|   # new style snmp config
 | ||
|   foreach my $stanza (@$config) {
 | ||
|     # user tagged
 | ||
|     my $tag = '';
 | ||
|     if (1 == scalar keys %$stanza) {
 | ||
|       $tag = (keys %$stanza)[0];
 | ||
|       $stanza = $stanza->{$tag};
 | ||
| 
 | ||
|       # corner case: untagged lone community
 | ||
|       if ($tag eq 'community') {
 | ||
|           $tag = $stanza;
 | ||
|           $stanza = {community => $tag};
 | ||
|       }
 | ||
|     }
 | ||
| 
 | ||
|     # defaults
 | ||
|     $stanza->{tag} ||= $tag;
 | ||
|     $stanza->{read} = 1 if !exists $stanza->{read};
 | ||
|     $stanza->{no}   ||= [];
 | ||
|     $stanza->{only} ||= ['any'];
 | ||
| 
 | ||
|     die "error: config: snmpv2 community in device_auth must be single item, not list\n"
 | ||
|       if ref $stanza->{community};
 | ||
| 
 | ||
|     die "error: config: stanza in device_auth must have a tag\n"
 | ||
|       if not $stanza->{tag} and exists $stanza->{user};
 | ||
| 
 | ||
|     push @new_stanzas, $stanza
 | ||
|   }
 | ||
| 
 | ||
|   # legacy config 
 | ||
|   # note: read strings tried before write
 | ||
|   # note: read-write is no longer used for read operations
 | ||
| 
 | ||
|   push @new_stanzas, map {{
 | ||
|     read => 1, write => 0,
 | ||
|     no => [], only => ['any'],
 | ||
|     community => $_,
 | ||
|   }} @{setting('community') || []};
 | ||
| 
 | ||
|   push @new_stanzas, map {{
 | ||
|     write => 1, read => 0,
 | ||
|     no => [], only => ['any'],
 | ||
|     community => $_,
 | ||
|   }} @{setting('community_rw') || []};
 | ||
| 
 | ||
|   foreach my $stanza (@new_stanzas) {
 | ||
|     $stanza->{driver} ||= 'snmp'
 | ||
|       if exists $stanza->{community}
 | ||
|          or exists $stanza->{user};
 | ||
|   }
 | ||
| 
 | ||
|   return @new_stanzas;
 | ||
| }
 | ||
| 
 | ||
| =head2 get_communities( $device, $mode )
 | ||
| 
 | ||
| Takes the current C<device_auth> setting and pushes onto the front of the list
 | ||
| the last known good SNMP settings used for this mode (C<read> or C<write>).
 | ||
| 
 | ||
| =cut
 | ||
| 
 | ||
| sub get_communities {
 | ||
|   my ($device, $mode) = @_;
 | ||
|   $mode ||= 'read';
 | ||
| 
 | ||
|   my $seen_tags = {}; # for cleaning community table
 | ||
|   my $config = (setting('device_auth') || []);
 | ||
|   my @communities = ();
 | ||
| 
 | ||
|   # first of all, use external command if configured
 | ||
|   push @communities, _get_external_community($device, $mode)
 | ||
|     if setting('get_community') and length setting('get_community');
 | ||
| 
 | ||
|   # last known-good by tag
 | ||
|   my $tag_name = 'snmp_auth_tag_'. $mode;
 | ||
|   my $stored_tag = eval { $device->community->$tag_name };
 | ||
| 
 | ||
|   if ($device->in_storage and $stored_tag) {
 | ||
|     foreach my $stanza (@$config) {
 | ||
|       if ($stanza->{tag} and $stored_tag eq $stanza->{tag}) {
 | ||
|         push @communities, {%$stanza, only => [$device->ip]};
 | ||
|         last;
 | ||
|       }
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   # try last-known-good v2 read
 | ||
|   push @communities, {
 | ||
|     read => 1, write => 0, driver => 'snmp',
 | ||
|     only => [$device->ip],
 | ||
|     community => $device->snmp_comm,
 | ||
|   } if defined $device->snmp_comm and $mode eq 'read';
 | ||
| 
 | ||
|   # try last-known-good v2 write
 | ||
|   my $snmp_comm_rw = eval { $device->community->snmp_comm_rw };
 | ||
|   push @communities, {
 | ||
|     write => 1, read => 0, driver => 'snmp',
 | ||
|     only => [$device->ip],
 | ||
|     community => $snmp_comm_rw,
 | ||
|   } if $snmp_comm_rw and $mode eq 'write';
 | ||
| 
 | ||
|   # clean the community table of obsolete tags
 | ||
|   eval { $device->community->update({$tag_name => undef}) }
 | ||
|     if $device->in_storage
 | ||
|        and (not $stored_tag or !exists $seen_tags->{ $stored_tag });
 | ||
| 
 | ||
|   return ( @communities, @$config );
 | ||
| }
 | ||
| 
 | ||
| sub _get_external_community {
 | ||
|   my ($device, $mode) = @_;
 | ||
|   my $cmd = setting('get_community');
 | ||
|   my $ip = $device->ip;
 | ||
|   my $host = ($device->dns || hostname_from_ip($ip) || $ip);
 | ||
| 
 | ||
|   if (defined $cmd and length $cmd) {
 | ||
|       # replace variables
 | ||
|       $cmd =~ s/\%HOST\%/$host/egi;
 | ||
|       $cmd =~ s/\%IP\%/$ip/egi;
 | ||
| 
 | ||
|       my $result = `$cmd`; # BACKTICKS
 | ||
|       return () unless defined $result and length $result;
 | ||
| 
 | ||
|       my @lines = split (m/\n/, $result);
 | ||
|       foreach my $line (@lines) {
 | ||
|           if ($line =~ m/^community\s*=\s*(.*)\s*$/i) {
 | ||
|               if (length $1 and $mode eq 'read') {
 | ||
|                   return map {{
 | ||
|                     read => 1,
 | ||
|                     only => [$device->ip],
 | ||
|                     community => $_,
 | ||
|                   }} split(m/\s*,\s*/,$1);
 | ||
|               }
 | ||
|           }
 | ||
|           elsif ($line =~ m/^setCommunity\s*=\s*(.*)\s*$/i) {
 | ||
|               if (length $1 and $mode eq 'write') {
 | ||
|                   return map {{
 | ||
|                     write => 1,
 | ||
|                     only => [$device->ip],
 | ||
|                     community => $_,
 | ||
|                   }} split(m/\s*,\s*/,$1);
 | ||
|               }
 | ||
|           }
 | ||
|       }
 | ||
|   }
 | ||
| 
 | ||
|   return ();
 | ||
| }
 | ||
| 
 | ||
| =head2 snmp_comm_reindex( $snmp, $device, $vlan )
 | ||
| 
 | ||
| Takes an established L<SNMP::Info> instance and makes a fresh connection using
 | ||
| community indexing, with the given C<$vlan> ID. Works for all SNMP versions.
 | ||
| 
 | ||
| Passing VLAN "C<0>" (zero) will reset the indexing to the basic v2 community
 | ||
| or v3 empty context.
 | ||
| 
 | ||
| =cut
 | ||
| 
 | ||
| sub snmp_comm_reindex {
 | ||
|   my ($snmp, $device, $vlan) = @_;
 | ||
|   my $ver = $snmp->snmp_ver;
 | ||
| 
 | ||
|   if ($ver == 3) {
 | ||
|       my $prefix = '';
 | ||
|       my @comms = get_communities($device, 'read');
 | ||
|       # find a context prefix configured by the user
 | ||
|       foreach my $c (@comms) {
 | ||
|           next unless $c->{tag}
 | ||
|             and $c->{tag} eq (eval { $device->community->snmp_auth_tag_read } || '');
 | ||
|           $prefix = $c->{context_prefix} and last;
 | ||
|       }
 | ||
|       $prefix ||= 'vlan-';
 | ||
| 
 | ||
|       debug
 | ||
|         sprintf '[%s] reindexing to "%s%s" (ver: %s, class: %s)',
 | ||
|         $device->ip, $prefix, $vlan, $ver, $snmp->class;
 | ||
|       $vlan ? $snmp->update(Context => ($prefix . $vlan))
 | ||
|             : $snmp->update(Context => '');
 | ||
|   }
 | ||
|   else {
 | ||
|       my $comm = $snmp->snmp_comm;
 | ||
| 
 | ||
|       debug sprintf '[%s] reindexing to vlan %s (ver: %s, class: %s)',
 | ||
|         $device->ip, $vlan, $ver, $snmp->class;
 | ||
|       $vlan ? $snmp->update(Community => $comm . '@' . $vlan)
 | ||
|             : $snmp->update(Community => $comm);
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| true;
 |