1package App::Netdisco::Util::DeviceAuth; 2 3use Dancer qw/:syntax :script/; 4use App::Netdisco::Util::DNS 'hostname_from_ip'; 5 6use Storable 'dclone'; 7use Try::Tiny; 8 9use base 'Exporter'; 10our @EXPORT = (); 11our @EXPORT_OK = qw/ 12 fixup_device_auth get_external_credentials 13/; 14our %EXPORT_TAGS = (all => \@EXPORT_OK); 15 16=head1 NAME 17 18App::Netdisco::Util::DeviceAuth 19 20=head1 DESCRIPTION 21 22Helper functions for device authentication. 23 24There are no default exports, however the C<:all> tag will export all 25subroutines. 26 27=head1 EXPORT_OK 28 29=head2 fixup_device_auth 30 31Rebuilds the C<device_auth> config with missing defaults and other fixups for 32config changes over time. Returns a list which can replace C<device_auth>. 33 34=cut 35 36sub fixup_device_auth { 37 my $da = dclone (setting('device_auth') || []); 38 my $sa = dclone (setting('snmp_auth') || []); 39 40 die "error: both snmp_auth and device_auth are defined!\n" 41 . "move snmp_auth config into device_auth and remove snmp_auth.\n" 42 if scalar @$da and scalar @$sa; 43 44 my $config = ((scalar @$da) ? $da : $sa); 45 my @new_stanzas = (); 46 47 # new style snmp config 48 foreach my $stanza (@$config) { 49 # user tagged 50 my $tag = ''; 51 if (1 == scalar keys %$stanza) { 52 $tag = (keys %$stanza)[0]; 53 $stanza = $stanza->{$tag}; 54 55 # corner case: untagged lone community 56 if ($tag eq 'community') { 57 $tag = $stanza; 58 $stanza = {community => $tag}; 59 } 60 } 61 62 # defaults 63 $stanza->{tag} ||= $tag; 64 $stanza->{read} = 1 if !exists $stanza->{read}; 65 $stanza->{no} ||= []; 66 $stanza->{only} ||= ['group:__ANY__']; 67 68 die "error: config: snmpv2 community in device_auth must be single item, not list\n" 69 if ref $stanza->{community}; 70 71 die "error: config: stanza in device_auth must have a tag\n" 72 if not $stanza->{tag} and exists $stanza->{user}; 73 74 push @new_stanzas, $stanza; 75 } 76 77 # import legacy sshcollector configuration 78 my @sshcollector = @{ dclone (setting('sshcollector') || []) }; 79 foreach my $stanza (@sshcollector) { 80 # defaults 81 $stanza->{driver} = 'cli'; 82 $stanza->{read} = 1; 83 $stanza->{no} ||= []; 84 85 # fixups 86 $stanza->{only} ||= [ scalar delete $stanza->{ip} || 87 scalar delete $stanza->{hostname} ]; 88 $stanza->{username} = scalar delete $stanza->{user}; 89 90 push @new_stanzas, $stanza; 91 } 92 93 # legacy config 94 # note: read strings tried before write 95 # note: read-write is no longer used for read operations 96 97 push @new_stanzas, map {{ 98 read => 1, write => 0, 99 no => [], only => ['any'], 100 community => $_, 101 }} @{setting('community') || []}; 102 103 push @new_stanzas, map {{ 104 write => 1, read => 0, 105 no => [], only => ['any'], 106 community => $_, 107 }} @{setting('community_rw') || []}; 108 109 foreach my $stanza (@new_stanzas) { 110 $stanza->{driver} ||= 'snmp' 111 if exists $stanza->{community} 112 or exists $stanza->{user}; 113 } 114 115 return @new_stanzas; 116} 117 118=head2 get_external_credentials( $device, $mode ) 119 120Runs a command to gather SNMP credentials or a C<device_auth> stanza. 121 122Mode can be C<read> or C<write> and defaults to 'read'. 123 124=cut 125 126sub get_external_credentials { 127 my ($device, $mode) = @_; 128 my $cmd = (setting('get_credentials') || setting('get_community')); 129 my $ip = $device->ip; 130 my $host = ($device->dns || hostname_from_ip($ip) || $ip); 131 $mode ||= 'read'; 132 133 if (defined $cmd and length $cmd) { 134 # replace variables 135 $cmd =~ s/\%MODE\%/$mode/egi; 136 $cmd =~ s/\%HOST\%/$host/egi; 137 $cmd =~ s/\%IP\%/$ip/egi; 138 139 my $result = `$cmd`; # BACKTICKS 140 return () unless defined $result and length $result; 141 142 my @lines = split (m/\n/, $result); 143 foreach my $line (@lines) { 144 if ($line =~ m/^community\s*=\s*(.*)\s*$/i) { 145 if (length $1 and $mode eq 'read') { 146 debug sprintf '[%s] external read credentials added', 147 $device->ip; 148 149 return map {{ 150 read => 1, 151 only => [$device->ip], 152 community => $_, 153 }} split(m/\s*,\s*/,$1); 154 } 155 } 156 elsif ($line =~ m/^setCommunity\s*=\s*(.*)\s*$/i) { 157 if (length $1 and $mode eq 'write') { 158 debug sprintf '[%s] external write credentials added', 159 $device->ip; 160 161 return map {{ 162 write => 1, 163 only => [$device->ip], 164 community => $_, 165 }} split(m/\s*,\s*/,$1); 166 } 167 } 168 else { 169 my $stanza = undef; 170 try { 171 $stanza = from_json( $line ); 172 debug sprintf '[%s] external credentials stanza added', 173 $device->ip; 174 } 175 catch { 176 info sprintf '[%s] error! failed to parse external credentials stanza', 177 $device->ip; 178 }; 179 return $stanza if ref $stanza; 180 } 181 } 182 } 183 184 return (); 185} 186 187true; 188