1package App::Netdisco::Worker::Plugin::Discover::Properties; 2 3use Dancer ':syntax'; 4use App::Netdisco::Worker::Plugin; 5use aliased 'App::Netdisco::Worker::Status'; 6 7use App::Netdisco::Transport::SNMP (); 8use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; 9use App::Netdisco::Util::FastResolver 'hostnames_resolve_async'; 10use App::Netdisco::Util::Device 'get_device'; 11use App::Netdisco::Util::DNS 'hostname_from_ip'; 12use App::Netdisco::Util::SNMP 'snmp_comm_reindex'; 13use Dancer::Plugin::DBIC 'schema'; 14use Scope::Guard 'guard'; 15use NetAddr::IP::Lite ':lower'; 16use Encode; 17 18register_worker({ phase => 'early', driver => 'snmp' }, sub { 19 my ($job, $workerconf) = @_; 20 21 my $device = $job->device; 22 my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) 23 or return Status->defer("discover failed: could not SNMP connect to $device"); 24 25 # VTP Management Domain -- assume only one. 26 my $vtpdomains = $snmp->vtp_d_name; 27 my $vtpdomain; 28 if (defined $vtpdomains and scalar values %$vtpdomains) { 29 $device->set_column( vtp_domain => (values %$vtpdomains)[-1] ); 30 } 31 32 my $hostname = hostname_from_ip($device->ip); 33 $device->set_column( dns => $hostname ) if $hostname; 34 35 my @properties = qw/ 36 snmp_ver 37 description uptime name 38 layers mac 39 ps1_type ps2_type ps1_status ps2_status 40 fan slots 41 vendor os os_ver 42 /; 43 44 foreach my $property (@properties) { 45 $device->set_column( $property => $snmp->$property ); 46 } 47 48 (my $model = Encode::decode('UTF-8', ($snmp->model || ''))) =~ s/\s+$//; 49 (my $serial = Encode::decode('UTF-8', ($snmp->serial || ''))) =~ s/\s+$//; 50 $device->set_column( model => $model ); 51 $device->set_column( serial => $serial ); 52 $device->set_column( contact => Encode::decode('UTF-8', $snmp->contact) ); 53 $device->set_column( location => Encode::decode('UTF-8', $snmp->location) ); 54 55 $device->set_column( num_ports => $snmp->ports ); 56 $device->set_column( snmp_class => $snmp->class ); 57 $device->set_column( snmp_engineid => unpack('H*', ($snmp->snmpEngineID || '')) ); 58 59 $device->set_column( last_discover => \'now()' ); 60 61 # protection for failed SNMP gather 62 if ($device->in_storage) { 63 my $ip = $device->ip; 64 my $protect = setting('snmp_field_protection')->{'device'} || {}; 65 my %dirty = $device->get_dirty_columns; 66 foreach my $field (keys %dirty) { 67 next unless check_acl_only($ip, $protect->{$field}); 68 if (!defined $dirty{$field} or $dirty{$field} eq '') { 69 return $job->cancel("discover cancelled: $ip failed to return valid $field"); 70 } 71 } 72 } 73 74 # support for Hooks 75 vars->{'hook_data'} = { $device->get_columns }; 76 delete vars->{'hook_data'}->{'snmp_comm'}; # for privacy 77 78 # support for new_device Hook 79 vars->{'new_device'} = 1 if not $device->in_storage; 80 81 schema('netdisco')->txn_do(sub { 82 $device->update_or_insert(undef, {for => 'update'}); 83 return Status->done("Ended discover for $device"); 84 }); 85}); 86 87register_worker({ phase => 'early', driver => 'snmp' }, sub { 88 my ($job, $workerconf) = @_; 89 90 my $device = $job->device; 91 return unless $device->in_storage; 92 return unless $job->subaction eq 'with-nodes'; 93 94 my $db_device = get_device($device->ip); 95 if ($device->ip ne $db_device->ip) { 96 return schema('netdisco')->txn_do(sub { 97 $device->delete; 98 return $job->cancel("fresh discover cancelled: $device already known as $db_device"); 99 }); 100 } 101 102 return Status->info(" [$device] device - OK to continue discover"); 103}); 104 105register_worker({ phase => 'early', driver => 'snmp' }, sub { 106 my ($job, $workerconf) = @_; 107 108 my $device = $job->device; 109 return unless $device->in_storage; 110 111 my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) 112 or return Status->defer("discover failed: could not SNMP connect to $device"); 113 114 my $pass = Status->info(" [$device] device - OK to continue discover"); 115 my $interfaces = $snmp->interfaces; 116 117 # OK if no interfaces 118 return $pass if 0 == scalar keys %$interfaces; 119 # OK if any value is not the same as key 120 return $pass if scalar grep {$_ ne $interfaces->{$_}} keys %$interfaces; 121 # OK if any non-digit in values 122 return $pass if scalar grep {$_ !~ m/^[0-9]+$/} values %$interfaces; 123 124 # gather ports 125 my $device_ports = {map {($_->port => $_)} 126 $device->ports(undef, {prefetch => 'properties'})->all}; 127 # OK if no ports 128 return $pass if 0 == scalar keys %$device_ports; 129 # OK if any interface value is a port name 130 foreach my $port (keys %$device_ports) { 131 return $pass if scalar grep {$port eq $_} values %$interfaces; 132 } 133 134 # else cancel 135 return $job->cancel("discover cancelled: $device failed to return valid interfaces"); 136}); 137 138register_worker({ phase => 'early', driver => 'snmp' }, sub { 139 my ($job, $workerconf) = @_; 140 141 my $device = $job->device; 142 return unless $device->in_storage; 143 my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) 144 or return Status->defer("discover failed: could not SNMP connect to $device"); 145 146 my @aliases = (); 147 push @aliases, _get_ipv4_aliases($device, $snmp); 148 push @aliases, _get_ipv6_aliases($device, $snmp); 149 150 debug sprintf ' resolving %d aliases with max %d outstanding requests', 151 scalar @aliases, $ENV{'PERL_ANYEVENT_MAX_OUTSTANDING_DNS'}; 152 my $resolved_aliases = hostnames_resolve_async(\@aliases); 153 154 # fake one aliases entry for devices not providing ip_index 155 # or if we're discovering on an IP not listed in ip_index 156 push @$resolved_aliases, { alias => $device->ip, dns => $device->dns } 157 if 0 == scalar grep {$_->{alias} eq $device->ip} @aliases; 158 159 # support for Hooks 160 vars->{'hook_data'}->{'device_ips'} = $resolved_aliases; 161 162 schema('netdisco')->txn_do(sub { 163 my $gone = $device->device_ips->delete; 164 debug sprintf ' [%s] device - removed %d aliases', 165 $device->ip, $gone; 166 $device->device_ips->populate($resolved_aliases); 167 168 return Status->info(sprintf ' [%s] aliases - added %d new aliases', 169 $device->ip, scalar @aliases); 170 }); 171}); 172 173register_worker({ phase => 'early', driver => 'snmp' }, sub { 174 my ($job, $workerconf) = @_; 175 176 my $device = $job->device; 177 return unless $device->in_storage; 178 my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) 179 or return Status->defer("discover failed: could not SNMP connect to $device"); 180 181 my $interfaces = $snmp->interfaces; 182 my $i_type = $snmp->i_type; 183 my $i_ignore = $snmp->i_ignore; 184 my $i_descr = $snmp->i_description; 185 my $i_mtu = $snmp->i_mtu; 186 my $i_speed = $snmp->i_speed; 187 my $i_speed_admin = $snmp->i_speed_admin; 188 my $i_mac = $snmp->i_mac; 189 my $i_up = $snmp->i_up; 190 my $i_up_admin = $snmp->i_up_admin; 191 my $i_name = $snmp->i_name; 192 my $i_duplex = $snmp->i_duplex; 193 my $i_duplex_admin = $snmp->i_duplex_admin; 194 my $i_stp_state = $snmp->i_stp_state; 195 my $i_vlan = $snmp->i_vlan; 196 my $i_lastchange = $snmp->i_lastchange; 197 my $agg_ports = $snmp->agg_ports; 198 199 # clear the cached uptime and get a new one 200 my $dev_uptime = $snmp->load_uptime; 201 if (!defined $dev_uptime) { 202 error sprintf ' [%s] interfaces - Error! Failed to get uptime from device!', 203 $device->ip; 204 return Status->error("discover failed: no uptime from device $device!"); 205 } 206 207 # used to track how many times the device uptime wrapped 208 my $dev_uptime_wrapped = 0; 209 210 # use SNMP-FRAMEWORK-MIB::snmpEngineTime if available to 211 # fix device uptime if wrapped 212 if (defined $snmp->snmpEngineTime) { 213 $dev_uptime_wrapped = int( $snmp->snmpEngineTime * 100 / 2**32 ); 214 if ($dev_uptime_wrapped > 0) { 215 debug sprintf ' [%s] interfaces - device uptime wrapped %d times - correcting', 216 $device->ip, $dev_uptime_wrapped; 217 $device->uptime( $dev_uptime + $dev_uptime_wrapped * 2**32 ); 218 } 219 } 220 221 # build device interfaces suitable for DBIC 222 my %interfaces; 223 foreach my $entry (keys %$interfaces) { 224 my $port = $interfaces->{$entry}; 225 226 if (not $port) { 227 debug sprintf ' [%s] interfaces - ignoring %s (no port mapping)', 228 $device->ip, $entry; 229 next; 230 } 231 232 if (scalar grep {$port =~ m/^$_$/} @{setting('ignore_interfaces') || []}) { 233 debug sprintf 234 ' [%s] interfaces - ignoring %s (%s) (config:ignore_interfaces)', 235 $device->ip, $entry, $port; 236 next; 237 } 238 239 if (exists $i_ignore->{$entry}) { 240 debug sprintf ' [%s] interfaces - ignoring %s (%s) (%s)', 241 $device->ip, $entry, $port, ($i_type->{$entry} || ''); 242 next; 243 } 244 245 # Skip interfaces which are 'notPresent' and match the notpresent type filter 246 if (defined $i_up->{$entry} and defined $i_type->{$entry} and $i_up->{$entry} eq 'notPresent' and (scalar grep {$i_type->{$entry} =~ m/^$_$/} @{setting('ignore_notpresent_types') || []}) ) { 247 debug sprintf ' [%s] interfaces - ignoring %s (%s) (%s) (config:ignore_notpresent_types)', 248 $device->ip, $entry, $port, $i_up->{$entry}; 249 next; 250 } 251 252 my $lc = $i_lastchange->{$entry} || 0; 253 if (not $dev_uptime_wrapped and $lc > $dev_uptime) { 254 debug sprintf ' [%s] interfaces - device uptime wrapped (%s) - correcting', 255 $device->ip, $port; 256 $device->uptime( $dev_uptime + 2**32 ); 257 $dev_uptime_wrapped = 1; 258 } 259 260 if ($device->is_column_changed('uptime') and $lc) { 261 if ($lc < $dev_uptime) { 262 # ambiguous: lastchange could be sysUptime before or after wrap 263 if ($dev_uptime > 30000 and $lc < 30000) { 264 # uptime wrap more than 5min ago but lastchange within 5min 265 # assume lastchange was directly after boot -> no action 266 } 267 else { 268 # uptime wrap less than 5min ago or lastchange > 5min ago 269 # to be on safe side, assume lastchange after counter wrap 270 debug sprintf 271 ' [%s] interfaces - correcting LastChange for %s, assuming sysUptime wrap', 272 $device->ip, $port; 273 $lc += $dev_uptime_wrapped * 2**32; 274 } 275 } 276 } 277 278 $interfaces{$port} = { 279 port => $port, 280 descr => $i_descr->{$entry}, 281 up => $i_up->{$entry}, 282 up_admin => $i_up_admin->{$entry}, 283 mac => $i_mac->{$entry}, 284 speed => $i_speed->{$entry}, 285 speed_admin => $i_speed_admin->{$entry}, 286 mtu => $i_mtu->{$entry}, 287 name => Encode::decode('UTF-8', $i_name->{$entry}), 288 duplex => $i_duplex->{$entry}, 289 duplex_admin => $i_duplex_admin->{$entry}, 290 stp => $i_stp_state->{$entry}, 291 type => $i_type->{$entry}, 292 vlan => $i_vlan->{$entry}, 293 pvid => $i_vlan->{$entry}, 294 is_master => 'false', 295 slave_of => undef, 296 lastchange => $lc, 297 }; 298 } 299 300 # must do this after building %interfaces so that we can set is_master 301 foreach my $sidx (keys %$agg_ports) { 302 my $slave = $interfaces->{$sidx} or next; 303 next unless defined $agg_ports->{$sidx}; # slave without a master?! 304 my $master = $interfaces->{ $agg_ports->{$sidx} } or next; 305 next unless exists $interfaces{$slave} and exists $interfaces{$master}; 306 307 $interfaces{$slave}->{slave_of} = $master; 308 $interfaces{$master}->{is_master} = 'true'; 309 } 310 311 # support for Hooks 312 vars->{'hook_data'}->{'ports'} = [values %interfaces]; 313 314 schema('netdisco')->resultset('DevicePort')->txn_do_locked(sub { 315 my $gone = $device->ports->delete({keep_nodes => 1}); 316 debug sprintf ' [%s] interfaces - removed %d interfaces', 317 $device->ip, $gone; 318 $device->update_or_insert(undef, {for => 'update'}); 319 $device->ports->populate([values %interfaces]); 320 321 return Status->info(sprintf ' [%s] interfaces - added %d new interfaces', 322 $device->ip, scalar values %interfaces); 323 }); 324}); 325 326# return a list of VRF which are OK to connect 327sub _get_vrf_list { 328 my ($device, $snmp) = @_; 329 330 return () if ! $snmp->cisco_comm_indexing; 331 332 my @ok_vrfs = (); 333 my $vrf_name = $snmp->vrf_name || {}; 334 335 while (my ($idx, $vrf) = each(%$vrf_name)) { 336 if ($vrf =~ /^\S+$/) { 337 my $ctx_name = pack("C*",split(/\./,$idx)); 338 $ctx_name =~ s/.*[^[:print:]]+//; 339 debug sprintf(' [%s] Discover VRF %s with SNMP Context %s', $device->ip, $vrf, $ctx_name); 340 push (@ok_vrfs, $ctx_name); 341 } 342 } 343 344 return @ok_vrfs; 345} 346 347sub _get_ipv4_aliases { 348 my ($device, $snmp) = @_; 349 my @aliases; 350 351 my $ip_index = $snmp->ip_index; 352 my $interfaces = $snmp->interfaces; 353 my $ip_netmask = $snmp->ip_netmask; 354 355 # Get IP Table per VRF if supported 356 my @vrf_list = _get_vrf_list($device, $snmp); 357 if (scalar @vrf_list) { 358 my $guard = guard { snmp_comm_reindex($snmp, $device, 0) }; 359 foreach my $vrf (@vrf_list) { 360 snmp_comm_reindex($snmp, $device, $vrf); 361 $ip_index = { %$ip_index, %{$snmp->ip_index} }; 362 $interfaces = { %$interfaces, %{$snmp->interfaces} }; 363 $ip_netmask = { %$ip_netmask, %{$snmp->ip_netmask} }; 364 } 365 } 366 367 # build device aliases suitable for DBIC 368 foreach my $entry (keys %$ip_index) { 369 my $ip = NetAddr::IP::Lite->new($entry) 370 or next; 371 my $addr = $ip->addr; 372 373 next if $addr eq '0.0.0.0'; 374 next if check_acl_no($ip, 'group:__LOCAL_ADDRESSES__'); 375 next if setting('ignore_private_nets') and $ip->is_rfc1918; 376 377 my $iid = $ip_index->{$addr}; 378 my $port = $interfaces->{$iid}; 379 my $subnet = $ip_netmask->{$addr} 380 ? NetAddr::IP::Lite->new($addr, $ip_netmask->{$addr})->network->cidr 381 : undef; 382 383 debug sprintf ' [%s] device - aliased as %s', $device->ip, $addr; 384 push @aliases, { 385 alias => $addr, 386 port => $port, 387 subnet => $subnet, 388 dns => undef, 389 }; 390 } 391 392 return @aliases; 393} 394 395sub _get_ipv6_aliases { 396 my ($device, $snmp) = @_; 397 my @aliases; 398 399 my $ipv6_index = $snmp->ipv6_index || {}; 400 my $ipv6_addr = $snmp->ipv6_addr || {}; 401 my $ipv6_type = $snmp->ipv6_type || {}; 402 my $ipv6_pfxlen = $snmp->ipv6_addr_prefixlength || {}; 403 my $interfaces = $snmp->interfaces || {}; 404 405 # Get IP Table per VRF if supported 406 my @vrf_list = _get_vrf_list($device, $snmp); 407 if (scalar @vrf_list) { 408 my $guard = guard { snmp_comm_reindex($snmp, $device, 0) }; 409 foreach my $vrf (@vrf_list) { 410 snmp_comm_reindex($snmp, $device, $vrf); 411 $ipv6_index = { %$ipv6_index, %{$snmp->ipv6_index || {}} }; 412 $ipv6_addr = { %$ipv6_addr, %{$snmp->ipv6_addr || {}} }; 413 $ipv6_type = { %$ipv6_type, %{$snmp->ipv6_type || {}} }; 414 $ipv6_pfxlen = { %$ipv6_pfxlen, %{$snmp->ipv6_addr_prefixlength || {}} }; 415 $interfaces = { %$interfaces, %{$snmp->interfaces} }; 416 } 417 } 418 419 # build device aliases suitable for DBIC 420 foreach my $iid (keys %$ipv6_index) { 421 next unless $ipv6_type->{$iid} and $ipv6_type->{$iid} eq 'unicast'; 422 my $entry = $ipv6_addr->{$iid} or next; 423 my $ip = NetAddr::IP::Lite->new($entry) or next; 424 my $addr = $ip->addr; 425 426 next if $addr eq '::0'; 427 next if check_acl_no($ip, 'group:__LOCAL_ADDRESSES__'); 428 429 my $port = $interfaces->{ $ipv6_index->{$iid} }; 430 my $subnet = $ipv6_pfxlen->{$iid} 431 ? NetAddr::IP::Lite->new($addr .'/'. $ipv6_pfxlen->{$iid})->network->cidr 432 : undef; 433 434 debug sprintf ' [%s] device - aliased as %s', $device->ip, $addr; 435 push @aliases, { 436 alias => $addr, 437 port => $port, 438 subnet => $subnet, 439 dns => undef, 440 }; 441 } 442 443 return @aliases; 444} 445 446true; 447