1package Netdot::Model::Device; 2 3use base 'Netdot::Model'; 4use warnings; 5use strict; 6use SNMP::Info; 7use Netdot::Util::DNS; 8use Netdot::Topology; 9use Parallel::ForkManager; 10use Data::Dumper; 11 12=head1 NAME 13 14Netdot::Model::Device - Network Device Class 15 16=head1 SYNOPSIS 17 18 my $device = Device->retrieve($id); 19 $device->snmp_update(); 20 21=cut 22 23# Timeout seconds for SNMP queries 24# (different from SNMP connections) 25my $TIMEOUT = Netdot->config->get('SNMP_QUERY_TIMEOUT'); 26 27# Define some signal handlers 28$SIG{ALRM} = sub{ die "timeout" }; 29 30# Some regular expressions 31my $IPV4 = Netdot->get_ipv4_regex(); 32 33# Other fixed variables 34my $MAXPROCS = Netdot->config->get('SNMP_MAX_PROCS'); 35 36# Objects we need here 37my $logger = Netdot->log->get_logger('Netdot::Model::Device'); 38 39my %IGNOREDVLANS; 40map { $IGNOREDVLANS{$_}++ } @{ Netdot->config->get('IGNOREVLANS') }; 41 42my @MIBDIRS; 43foreach my $md ( @{ Netdot->config->get('SNMP_MIB_DIRS') } ){ 44 push @MIBDIRS, Netdot->config->get('SNMP_MIBS_PATH')."/".$md; 45} 46 47# Netdot Interface field name to SNMP::Info method conversion table 48my %IFFIELDS = ( 49 type => 'i_type', 50 description => 'i_alias', speed => 'i_speed', 51 admin_status => 'i_up_admin', oper_status => 'i_up', 52 physaddr => 'i_mac', oper_duplex => 'i_duplex', 53 admin_duplex => 'i_duplex_admin', stp_id => 'i_stp_id', 54 dp_remote_id => 'c_id', dp_remote_ip => 'c_ip', 55 dp_remote_port => 'c_port', dp_remote_type => 'c_platform', 56 ); 57 58# DeviceModule field name to SNMP::Info method conversion table 59my %MFIELDS = ( 60 name => 'e_name', type => 'e_type', 61 contained_in => 'e_parent', class => 'e_class', 62 pos => 'e_pos', hw_rev => 'e_hwver', 63 fw_rev => 'e_fwver', sw_rev => 'e_swver', 64 model => 'e_model', serial_number => 'e_serial', 65 fru => 'e_fru', description => 'e_descr', 66 ); 67 68# SNMP::Info methods that return hash refs 69my @SMETHODS = qw( 70 hasCDP e_descr i_type i_alias i_speed i_up 71 i_up_admin i_duplex i_duplex_admin 72 ip_index ip_netmask i_mac ip_table 73 i_vlan_membership qb_v_name v_name v_state 74 hasLLDP 75); 76 77 78=head1 CLASS METHODS 79=cut 80 81############################################################################ 82 83=head2 search - Search Devices 84 85 Overrides base method to extend functionality: 86 87 Arguments: 88 Hash with the following keys: 89 name - Can be either a string, RR object RR id or IP address, 90 String can be FQDN or hostname. 91 producttype - Search for all Devices of a certain product type 92 93 The rest of the fields in the Device table. 94 Returns: 95 Array of Device objects or iterator (depending on context) 96 97 Exampless: 98 my @devs = Device->search(name=>'localhost.localdomain'); 99 100=cut 101 102sub search { 103 my ($class, @args) = @_; 104 $class->isa_class_method('search'); 105 106 # Class::DBI::search() might include an extra 'options' hash ref 107 # at the end. In that case, we want to extract the 108 # field/value hash first. 109 my $opts = @args % 2 ? pop @args : {}; 110 my %argv = @args; 111 112 my $dev; 113 114 if ( defined $argv{name} ){ 115 my $foundname = 0; 116 if ( ref($argv{name}) =~ /RR$/o ){ 117 # We were passed a RR object. 118 # Proceed as regular search 119 }elsif ( Ipblock->matches_ip($argv{name}) ){ 120 # Looks like an IP address 121 if ( my $ip = Ipblock->search(address=>$argv{name})->first ){ 122 if ( $ip->interface && ($dev = $ip->interface->device) ){ 123 $argv{name} = $dev->name; 124 $foundname = 1; 125 }else{ 126 $logger->debug(sub{"Device::search: Address $argv{name} exists but ". 127 "no Device associated"}); 128 } 129 }else{ 130 $logger->debug(sub{"Device::search: $argv{name} not found in DB"}); 131 } 132 } 133 if ( !$foundname ){ 134 # Notice that we could be looking for a RR with an IP address as name 135 # So go on. 136 # name is either a string or a RR id 137 if ( $argv{name} =~ /\D+/ ){ 138 # argument has non-digits, so it's not an id. Look up RR name 139 if ( my @rrs = RR->search(name=>$argv{name}) ){ 140 if ( scalar @rrs == 1 ){ 141 $argv{name} = $rrs[0]; 142 }else{ 143 # This means we have the same RR name on different zones 144 # Try to resolve the name and look up IP address 145 if ( my @ips = Netdot->dns->resolve_name($argv{name}) ){ 146 foreach my $ip ( @ips ){ 147 $logger->debug(sub{"Device::search: $argv{name} resolves to $ip"}); 148 if ( my $ip = Ipblock->search(address=>$ip)->first ){ 149 if ( $ip->interface && ($dev = $ip->interface->device) ){ 150 $argv{name} = $dev->name; 151 }elsif ( my @a_records = $ip->a_records ){ 152 # The IP is not assigned to any device interfaces 153 # but there might be a device with a name and A record 154 # associated with this IP 155 $argv{name} = $a_records[0]->rr; 156 }else{ 157 $argv{name} = 0; 158 } 159 } 160 } 161 } 162 } 163 }else{ 164 # No use searching for a non-digit string in the name field 165 $argv{name} = 0; 166 } 167 } 168 } 169 }elsif ( exists $argv{producttype} ){ 170 return $class->search_by_type($argv{producttype}); 171 } 172 173 # Proceed as a regular search 174 return $class->SUPER::search(%argv, $opts); 175} 176 177############################################################################ 178 179=head2 search_address_live 180 181 Query relevant devices for ARP and FWT entries in order to locate 182 a particular address in the netwrok. 183 184 Arguments: 185 mac - MAC address (required unless ip is given) 186 ip - IP address (required unless mac is given) 187 vlan - VLAN id (optional but useful) 188 Returns: 189 Hashref with following keys: 190 edge - Edge port interface id 191 arp - Hashref with key=interface, value=hashref with key=ip, value=mac 192 fwt - Hashref with key=interface, value=number of macs 193 Examples: 194 my $info = Device->search_address_live(mac=>'DEADDEADBEEF', vlan=60); 195 196=cut 197 198sub search_address_live { 199 my ($class, %argv) = @_; 200 $class->isa_class_method('search_address_live'); 201 $class->throw_fatal("Device::search_address_live: Cannot proceed without a MAC or IP") 202 unless ( $argv{mac} || $argv{ip} ); 203 204 my (@fwt_devs, @arp_devs); 205 my ($vlan, $subnet); 206 my %results; 207 208 if ( $argv{vlan} ){ 209 $vlan = Vlan->search(vid=>$argv{vlan})->first; 210 $class->throw_user("Cannot find VLAN id $argv{vlan}\n") 211 unless $vlan; 212 } 213 214 if ( $argv{ip} ){ 215 my $ipblock = Ipblock->search(address=>$argv{ip})->first; 216 if ( $ipblock ){ 217 if ( $ipblock->is_address ){ 218 $subnet = $ipblock->parent; 219 }else{ 220 $subnet = $ipblock; 221 } 222 if ( !$vlan ){ 223 $vlan = $subnet->vlan if $subnet->vlan; 224 } 225 @arp_devs = @{$subnet->get_devices()}; 226 }else{ 227 $subnet = Ipblock->get_covering_block(address=>$argv{ip}); 228 if ( $subnet ){ 229 if ( !$vlan ){ 230 $vlan = $subnet->vlan if ( $subnet && $subnet->vlan ); 231 } 232 @arp_devs = @{$subnet->get_devices()}; 233 } 234 } 235 } 236 237 if ( $vlan ){ 238 my @ivlans = $vlan->interfaces; 239 my %fwt_devs; 240 foreach my $iface ( map { $_->interface } @ivlans ){ 241 $fwt_devs{$iface->device->id} = $iface->device; 242 } 243 @fwt_devs = values %fwt_devs; 244 245 if ( !@arp_devs && !$subnet ){ 246 foreach my $subnet ( $vlan->subnets ){ 247 @arp_devs = @{$subnet->get_devices()}; 248 } 249 } 250 }else{ 251 if ( $subnet && !@fwt_devs ){ 252 @fwt_devs = @{$subnet->get_devices()}; 253 }else{ 254 $class->throw_user("Device::search_address_live: ". 255 "Cannot proceed without VLAN or IP information\n"); 256 } 257 } 258 259 my (@fwts); 260 my %routerports; 261 foreach my $dev ( @arp_devs ){ 262 my $cache; 263 $dev->_netdot_rebless(); 264 eval { 265 $cache = $class->_exec_timeout($dev->fqdn, sub{ return $dev->get_arp() }); 266 }; 267 $logger->debug($@) if $@; 268 if ( $cache ){ 269 foreach my $intid ( keys %$cache ){ 270 foreach my $ip ( keys %{$cache->{$intid}} ){ 271 next if ( $argv{ip} && ($ip ne $argv{ip}) ); 272 my $mac = $cache->{$intid}->{$ip}; 273 next if ( $argv{mac} && ($mac ne $argv{mac}) ); 274 # We now have a mac address if we didn't have it yet 275 unless ( $argv{mac} ){ 276 $argv{mac} = $mac; 277 } 278 # Keep record of all the interfaces where this ip was seen 279 $routerports{$intid}{$ip} = $mac; 280 $results{mac} = $mac; 281 $results{ip} = $ip; 282 } 283 } 284 } 285 # Do not query all the routers unless needed. 286 if ( %routerports ){ 287 $results{routerports} = \%routerports; 288 last; 289 } 290 } 291 292 # If we do not have a MAC at this point, there's no point in getting FWTs 293 if ( $argv{mac} ){ 294 $results{mac} = $argv{mac}; 295 foreach my $dev ( @fwt_devs ){ 296 my $fwt; 297 my %args; 298 $args{vlan} = $vlan->vid if ( $vlan ); 299 eval { 300 $fwt = $class->_exec_timeout($dev->fqdn, sub{ return $dev->get_fwt(%args) } ); 301 }; 302 $logger->debug($@) if $@; 303 push @fwts, $fwt if $fwt; 304 } 305 306 my %switchports; 307 foreach my $fwt ( @fwts ){ 308 foreach my $intid ( keys %{$fwt} ){ 309 foreach my $mac ( keys %{$fwt->{$intid}} ){ 310 next if ( $mac ne $argv{mac} ); 311 # Keep record of all the interfaces where it was seen 312 # How many MACs were on that interface is essential to 313 # determine the edge port 314 $switchports{$intid} = scalar(keys %{$fwt->{$intid}}); 315 } 316 } 317 } 318 $results{switchports} = \%switchports if %switchports; 319 320 # Now look for the port with the least addresses 321 my @ordered = sort { $switchports{$a} <=> $switchports{$b} } keys %switchports; 322 $results{edge} = $ordered[0] if $ordered[0]; 323 } 324 325 if ( %results ){ 326 if ( $results{mac} ){ 327 if ( my $vendor = PhysAddr->vendor($results{mac}) ){ 328 $results{vendor} = $vendor; 329 } 330 } 331 if ( $results{ip} ){ 332 if ( my $name = Netdot->dns->resolve_ip($results{ip}) ){ 333 $results{dns} = $name; 334 } 335 } 336 return \%results; 337 } 338} 339 340 341############################################################################ 342 343=head2 search_like - Search for device objects. Allow substrings 344 345 We override the base class to: 346 - allow 'name' to be searched as part of a hostname 347 - search 'name' in aliases field if a RR is not found 348 - add 'zone' to the list of arguments 349 350 Arguments: 351 Hash with key/value pairs 352 353 Returns: 354 Array of Device objects or iterator 355 356 Examples: 357 my @switches = Device->search_like(name=>'-sw'); 358 359=cut 360 361sub search_like { 362 my ($class, %argv) = @_; 363 $class->isa_class_method('search_like'); 364 365 if ( exists $argv{name} ){ 366 my %args = (name=>$argv{name}); 367 $args{zone} = $argv{zone} if $argv{zone}; 368 if ( my @rrs = RR->search_like(%args) ){ 369 return map { $class->search(name=>$_) } @rrs; 370 }elsif ( $class->SUPER::search_like(aliases=>$argv{name}) ){ 371 return $class->SUPER::search_like(aliases=>$argv{name}); 372 } 373 $logger->debug(sub{"Device::search_like: $argv{name} not found"}); 374 return; 375 376 }elsif ( exists $argv{producttype} ){ 377 return $class->search_by_type($argv{producttype}); 378 379 }elsif ( exists $argv{product} ){ 380 my $dbh = $class->db_Main; 381 my $p_bind = defined($argv{product})? "= $argv{product}" : 'IS NULL'; 382 my $crit; 383 if ( exists $argv{site} ){ 384 my $s_bind = defined($argv{site})? "= ".$dbh->quote($argv{site}) : 'IS NULL'; 385 $crit = " AND d.site $s_bind"; 386 }elsif ( exists $argv{os} ){ 387 my $o_bind = defined($argv{os})? "= ".$dbh->quote($argv{os}) : 'IS NULL'; 388 $crit = " AND d.os $o_bind"; 389 } 390 my @objs; 391 my $rows = Netdot::Model->raw_sql(" 392 SELECT d.id 393 FROM device d, asset a, product p, site s 394 WHERE d.asset_id = a.id 395 AND a.product_id $p_bind 396 $crit 397 GROUP BY d.id 398 ")->{rows}; 399 map { push(@objs, $class->retrieve($_->[0])) } @$rows; 400 return @objs; 401 402 }else{ 403 return $class->SUPER::search_like(%argv); 404 } 405} 406 407 408############################################################################ 409 410=head2 assign_name - Determine and assign correct name to device 411 412 This method will try to find or create an appropriate 413 Resource Record for a Device, given a hostname or ip address, 414 and optionally a sysname as a backup in case that name or IP do 415 not resolve. 416 417 Arguments: 418 hash with following keys: 419 host - hostname or IP address (string) 420 sysname - System name value from SNMP 421 Returns: 422 RR object if successful 423 Examples: 424 my $rr = Device->assign_name($host) 425=cut 426 427sub assign_name { 428 my ($class, %argv) = @_; 429 $class->isa_class_method('assign_name'); 430 my $host = $argv{host}; 431 my $sysname = $argv{sysname}; 432 $class->throw_fatal("Model::Device::assign_name: Missing arguments: host") 433 unless $host; 434 435 # An RR record might already exist 436 if ( defined $host && (my $rr = RR->search(name=>$host)->first) ){ 437 $logger->debug(sub{"Name $host exists in DB"}); 438 return $rr; 439 } 440 # An RR matching $host does not exist, or the name exists in 441 # multiple zones 442 my @ips; 443 if ( Ipblock->matches_ip($host) ){ 444 # we were given an IP address 445 if ( my $ipb = Ipblock->search(address=>$host)->first ){ 446 if ( $ipb->interface && ( my $dev = $ipb->interface->device ) ){ 447 $logger->debug("Device::assign_name: A Device with IP $host already exists: ". 448 $dev->get_label); 449 return $dev->name; 450 } 451 } 452 push @ips, $host; 453 }else{ 454 # We were given a name (not an address) 455 # Resolve to an IP address 456 if ( defined $host && (@ips = Netdot->dns->resolve_name($host)) ){ 457 my $str = join ', ', @ips; 458 $logger->debug(sub{"Device::assign_name: $host resolves to $str"}); 459 }else{ 460 $logger->debug(sub{"Device::assign_name: $host does not resolve"}); 461 } 462 } 463 my $fqdn; 464 my %args; 465 my $ip; 466 if ( @ips ){ 467 # At this point, we were either passed an IP 468 # or we got one or more from DNS. The idea is to obtain a FQDN 469 # We'll use the first IP that resolves 470 foreach my $i ( @ips ){ 471 if ( $fqdn = Netdot->dns->resolve_ip($i) ){ 472 $logger->debug(sub{"Device::assign_name: $i resolves to $fqdn"}); 473 if ( my $rr = RR->search(name=>$fqdn)->first ){ 474 $logger->debug(sub{"Device::assign_name: RR $fqdn already exists in DB"}); 475 return $rr; 476 } 477 $ip = $i; 478 last; 479 }else{ 480 $logger->debug(sub{"Device::assign_name: $i does not resolve"} ); 481 } 482 } 483 } 484 # At this point, if we haven't got a fqdn, then use the sysname argument, and if not given, 485 # use the name or IP address passed as the host argument 486 $fqdn ||= $sysname; 487 $fqdn ||= $host; 488 $fqdn = lc($fqdn); 489 490 # Check if we have a matching domain 491 if ( $fqdn =~ /\./ && !Ipblock->matches_ip($fqdn) ){ 492 # Notice that we search the whole string. That's because 493 # the hostname part might have dots. The Zone search method 494 # will take care of that. 495 if ( my $zone = (Zone->search(name=>$fqdn))[0] ){ 496 $args{zone} = $zone->name; 497 $args{name} = $fqdn; 498 $args{name} =~ s/\.$args{zone}//; 499 }else{ 500 $logger->debug(sub{"Device::assign_name: $fqdn not found" }); 501 # Assume the zone to be everything to the right 502 # of the first dot. This might be a wrong guess 503 # but it is as close as I can get. 504 my @sections = split '\.', $fqdn; 505 $args{name} = shift @sections; 506 $args{zone} = join '.', @sections; 507 } 508 }else{ 509 $args{name} = $fqdn; 510 } 511 512 # Try to create the RR object 513 # This will also create the Zone object if necessary 514 my $rr = RR->find_or_create(\%args); 515 $logger->info(sprintf("Inserted new RR: %s", $rr->get_label)); 516 # Make sure name has an associated IP and A record 517 if ( $ip ){ 518 my $ipb = Ipblock->search(address=>$ip)->first || 519 Ipblock->insert({address=>$ip, status=>'Static'}); 520 my $rraddr = RRADDR->find_or_create({ipblock=>$ipb, rr=>$rr}); 521 } 522 return $rr; 523} 524 525############################################################################ 526 527=head2 insert - Insert new Device 528 529 We override the insert method for extra functionality: 530 - More intelligent name assignment 531 - Assign defaults 532 - Assign contact lists 533 534 Arguments: 535 Hashref with Device fields and values, plus: 536 contacts - ContactList object(s) (array or scalar) 537 Returns: 538 New Device object 539 540 Examples: 541 my $newdevice = Device->insert(\%args); 542 543=cut 544 545sub insert { 546 my ($class, $argv) = @_; 547 $class->isa_class_method('insert'); 548 549 # name is required 550 if ( !exists($argv->{name}) ){ 551 $class->throw_fatal('Model::Device::insert: Missing required arguments: name'); 552 } 553 554 # Get the default owner entity from config 555 my $config_owner = Netdot->config->get('DEFAULT_DEV_OWNER'); 556 my $default_owner = Entity->search(name=>$config_owner)->first || 557 Entity->search(name=>'Unknown')->first; 558 559 # Assign defaults 560 # These will be overridden by given arguments 561 562 my %devtmp = ( 563 community => 'public', 564 customer_managed => 0, 565 collect_arp => 0, 566 collect_fwt => 0, 567 canautoupdate => 0, 568 date_installed => $class->timestamp, 569 monitor_config => 0, 570 monitored => 0, 571 owner => $default_owner, 572 snmp_bulk => $class->config->get('DEFAULT_SNMPBULK'), 573 snmp_managed => 0, 574 snmp_polling => 0, 575 snmp_down => 0, 576 snmp_conn_attempts => 0, 577 auto_dns => $class->config->get('UPDATE_DEVICE_IP_NAMES'), 578 ); 579 580 # Add given args (overrides defaults). 581 # Extract special arguments that affect the inserted device 582 my (@contacts, $info); 583 foreach my $key ( keys %{$argv} ){ 584 if ( $key eq 'contacts' ){ 585 @contacts = $argv->{contacts}; 586 }elsif ( $key eq 'info' ){ 587 $info = $argv->{info}; 588 $devtmp{snmp_managed} = 1; 589 }else{ 590 $devtmp{$key} = $argv->{$key}; 591 } 592 } 593 if ( exists $devtmp{snmp_managed} ){ 594 if ( !$devtmp{snmp_managed} ){ 595 # Means it's being set to 0 or undef 596 # Turn off other flags 597 $devtmp{canautoupdate} = 0; 598 $devtmp{snmp_polling} = 0; 599 $devtmp{collect_arp} = 0; 600 $devtmp{collect_fwt} = 0; 601 } 602 } 603 604 605 ############################################### 606 # Assign the correct name 607 # argument 'name' can be passed either as a RR object 608 # or as a string. 609 if ( ref($argv->{name}) =~ /RR/ ){ 610 # We are being passed the RR object 611 $devtmp{name} = $argv->{name}; 612 }else{ 613 # A string hostname was passed 614 if ( my $rr = RR->search(name=>$argv->{name})->first ){ 615 $devtmp{name} = $rr; 616 }else{ 617 $devtmp{name} = RR->insert({name=>$argv->{name}}); 618 } 619 } 620 if ( my $dbdev = $class->search(name=>$devtmp{name})->first ){ 621 $logger->debug(sprintf("Device::insert: Device %s already exists in DB as %s", 622 $argv->{name}, $dbdev->fqdn)); 623 return $dbdev; 624 } 625 626 $class->_validate_args(\%devtmp); 627 my $self = $class->SUPER::insert( \%devtmp ); 628 629 if ( @contacts ){ 630 $self->add_contact_lists(@contacts); 631 }else{ 632 $self->add_contact_lists(); 633 } 634 635 return $self; 636} 637 638############################################################################ 639 640=head2 manual_add - Add a device manually 641 642 Sets enough information so it can be monitored: 643 - Creates an interface 644 - Assigns IP to that interface 645 - Sets neighbor relationship if possible 646 647 Arguments: 648 host - Name or IP address. Either one will be resolved 649 Returns: 650 New Device object 651 Examples: 652 my $newdevice = Device->manual_add(host=>"myhost"); 653 654=cut 655 656sub manual_add { 657 my ($class, %argv) = @_; 658 $class->isa_class_method('manual_add'); 659 660 # host is required 661 if ( !exists($argv{host}) ){ 662 $class->throw_fatal('Model::Device::manual_add: Missing required argument: host'); 663 } 664 # We will try to get both the IP and the name 665 my ($ip, $name) = Netdot->dns->resolve_any($argv{host}); 666 $name ||= $ip; 667 my $dev = Device->insert({name=>$name, monitored=>1, snmp_managed=>0, 668 canautoupdate=>0, auto_dns=>0}); 669 my $ints = $dev->add_interfaces(1); 670 my $int = $ints->[0]; 671 if ( $ip ){ 672 my $ipb = Ipblock->search(address=>$ip)->first || Ipblock->insert({address=>$ip}); 673 $ipb->update({status=>"Static", interface=>$int, monitored=>1}); 674 $dev->update({snmp_target=>$ipb}); 675 # Try to set the interface neighbor 676 my $mac = $ipb->get_last_arp_mac(); 677 my $neighbor = $mac->find_edge_port if $mac; 678 $int->add_neighbor(id=>$neighbor, fixed=>1) if $neighbor; 679 } 680 return $dev; 681} 682 683############################################################################ 684 685=head2 get_snmp_info - SNMP-query a Device for general information 686 687 This method can either be called on an existing object, or as a 688 class method. 689 690 Arguments: 691 Arrayref with the following keys: 692 host - hostname or IP address (required unless called as object method) 693 session - SNMP Session (optional) 694 communities - SNMP communities 695 version - SNMP version 696 sec_name - SNMP Security Name 697 sec_level - SNMP Security Level 698 auth_proto - SNMP Authentication Protocol 699 auth_pass - SNMP Auth Key 700 priv_proto - SNMP Privacy Protocol 701 priv_pass - SNMP Privacy Key 702 timeout - SNMP timeout 703 retries - SNMP retries 704 bgp_peers - (flag) Retrieve bgp peer info 705 Returns: 706 Hash reference containing SNMP information 707 Examples: 708 709 Instance call: 710 my $info = $device->get_snmp_info(); 711 712 Class call: 713 my $info = Device->get_snmp_info(host=>$hostname, communities=>['public']); 714 715=cut 716 717sub get_snmp_info { 718 my ($self, %args) = @_; 719 my $class = ref($self) || $self; 720 721 my %dev; 722 723 my $sinfo = $args{session}; 724 if ( $sinfo ){ 725 $args{host} = $sinfo->{args}->{DestHost}; 726 }else{ 727 if ( ref($self) ){ 728 if ( $self->snmp_target ){ 729 $args{host} = $self->snmp_target->address; 730 $logger->debug(sub{"Device::get_snmp_info: Using configured target address: $args{host}"}); 731 }else{ 732 $args{host} = $self->fqdn; 733 } 734 }else { 735 $self->throw_fatal('Model::Device::get_snmp_info: Missing required parameters: host') 736 unless $args{host}; 737 } 738 # Get SNMP session 739 my %sess_args; 740 $sess_args{host} = $args{host}; 741 foreach my $arg ( qw( communities version timeout retries sec_name sec_level 742 auth_proto auth_pass priv_proto priv_pass) ){ 743 $sess_args{$arg} = $args{$arg} if defined $args{$arg}; 744 } 745 $sinfo = $self->_get_snmp_session(%sess_args); 746 return unless $sinfo; 747 } 748 749 750 $dev{_sclass} = $sinfo->class(); 751 752 # Assign the snmp_target by resolving the name 753 # SNMP::Info still does not support IPv6, so for now... 754 my @ips = Netdot->dns->resolve_name($args{host}, {v4_only=>1}); 755 $dev{snmp_target} = $ips[0] if defined $ips[0]; 756 757 $logger->debug("Device::get_snmp_info: SNMP target is $dev{snmp_target}"); 758 759 $dev{snmp_version} = $sinfo->snmp_ver; 760 $dev{community} = $sinfo->snmp_comm if ( defined $sinfo->snmp_ver && $sinfo->snmp_ver != 3 ); 761 762 my $name_src = ( $self->config->get('IFNAME_SHORT') eq '1' )? 763 'orig_i_name' : 'i_description'; 764 push @SMETHODS, $name_src; 765 766 if ( $sinfo->can('ipv6_addr_prefix') ){ 767 # This version of SNMP::Info supports fetching IPv6 addresses 768 push @SMETHODS, qw(ipv6_addr_prefix); 769 } 770 771 if ( $self->config->get('GET_DEVICE_MODULE_INFO') ){ 772 push @SMETHODS, qw( e_index e_type e_parent e_name e_class e_pos e_descr 773 e_hwver e_fwver e_swver e_model e_serial e_fru); 774 } 775 776 if ( $args{bgp_peers} || $self->config->get('ADD_BGP_PEERS')) { 777 push @SMETHODS, qw( bgp_peers bgp_peer_id bgp_peer_as bgp_peer_state ); 778 } 779 780 my %hashes; 781 foreach my $method ( @SMETHODS ){ 782 $hashes{$method} = $sinfo->$method; 783 } 784 785 ################################################################ 786 # Device's global vars 787 $dev{layers} = $sinfo->layers; 788 my $ipf = $sinfo->ipforwarding || 'unknown'; 789 $dev{ipforwarding} = ( $ipf eq 'forwarding') ? 1 : 0; 790 $dev{sysobjectid} = $sinfo->id; 791 if ( defined $dev{sysobjectid} ){ 792 $dev{sysobjectid} =~ s/^\.(.*)/$1/; # Remove unwanted first dot 793 my %IGNORED; 794 map { $IGNORED{$_}++ } @{ $self->config->get('IGNOREDEVS') }; 795 if ( exists($IGNORED{$dev{sysobjectid}}) ){ 796 $logger->info(sprintf("%s Product id %s ignored per configuration option (IGNOREDEVS)", 797 $args{host}, $dev{sysobjectid})); 798 return; 799 } 800 } 801 802 $dev{model} = $sinfo->model(); 803 $dev{os} = $sinfo->os_ver(); 804 $dev{physaddr} = $sinfo->b_mac() || $sinfo->mac(); 805 $dev{sysname} = $sinfo->name(); 806 $dev{router_id} = $sinfo->root_ip(); 807 $dev{sysdescription} = $sinfo->description(); 808 $dev{syscontact} = $sinfo->contact(); 809 if ( $hashes{'e_descr'} ){ 810 my $first_idx ; 811 # SNMP::Info::Airespace has stuff like "0.19.26.48.21.32" 812 # instead of integers. Ideally we would override this 813 # method in the Netdot::Model::Device::Airespace, but 814 # for now... 815 if ( defined $dev{_sclass} && $dev{_sclass} =~ /Airespace/o ){ 816 $first_idx = 1; 817 }else{ 818 $first_idx = (sort { $a <=> $b } values %{$hashes{'e_index'}})[0]; 819 } 820 $dev{productname} = $hashes{'e_descr'}->{$first_idx} ; 821 $dev{part_number} = $hashes{'e_model'}->{$first_idx}; 822 } 823 $dev{manufacturer} = $sinfo->vendor(); 824 $dev{serial_number} = $sinfo->serial(); 825 826 $dev{syslocation} = $sinfo->location(); 827 $dev{syslocation} = $class->rem_lt_sp($dev{syslocation}) # Remove white space 828 if ( $dev{syslocation} ); 829 830 ################################################################ 831 # Get STP (Spanning Tree Protocol) stuff 832 833 my $collect_stp = 1; 834 if ( ref($self) && !$self->collect_stp ){ 835 $collect_stp = 0; 836 } 837 838 if ( $self->config->get('GET_DEVICE_STP_INFO') && $collect_stp ){ 839 if ( defined $dev{physaddr} ){ 840 $dev{stp_type} = $sinfo->stp_ver(); 841 842 if ( defined $dev{stp_type} && $dev{stp_type} ne 'unknown' ){ 843 # Get STP port id 844 $hashes{'i_stp_id'} = $sinfo->i_stp_id; 845 846 # Store the vlan status 847 my %vlan_status; 848 foreach my $vlan ( keys %{$hashes{v_state}} ){ 849 my $vid = $vlan; 850 $vid =~ s/^1\.//; 851 $vlan_status{$vid} = $hashes{v_state}->{$vlan}; 852 } 853 854 if ( $dev{stp_type} eq 'ieee8021d' || $dev{stp_type} eq 'mst' ){ 855 856 # Standard values (make it instance 0) 857 my $stp_p_info = $self->_get_stp_info(sinfo=>$sinfo); 858 foreach my $method ( keys %$stp_p_info ){ 859 $dev{stp_instances}{0}{$method} = $stp_p_info->{$method}; 860 } 861 862 # MST-specific 863 if ( $dev{stp_type} eq 'mst' ){ 864 # Get MST-specific values 865 $dev{stp_mst_region} = $sinfo->mst_region_name(); 866 $dev{stp_mst_rev} = $sinfo->mst_region_rev(); 867 $dev{stp_mst_digest} = $sinfo->mst_config_digest(); 868 869 # Get the mapping of vlans to STP instance 870 $dev{stp_vlan2inst} = $sinfo->mst_vlan2instance(); 871 my $mapping = join ', ', 872 map { sprintf("%s=>%s", $_, $dev{stp_vlan2inst}->{$_}) } keys %{$dev{stp_vlan2inst}}; 873 $logger->debug(sub{"Device::get_snmp_info: $args{host} MST VLAN mapping: $mapping"}); 874 875 # Now, if there's more than one instance, we need to get 876 # the STP standard info for at least one vlan on that instance. 877 my %seen_inst; 878 if ( $sinfo->cisco_comm_indexing() ){ 879 while ( my($vid, $mst_inst) = each %{$dev{stp_vlan2inst}} ){ 880 next if ( exists $seen_inst{$mst_inst} ); 881 next unless $vlan_status{$vid} eq 'operational'; 882 next if ( exists $IGNOREDVLANS{$vid} ); 883 my $stp_p_info; 884 my $i_stp_info; 885 eval { 886 my $vsinfo = $self->_get_cisco_snmp_context_session(sinfo => $sinfo, 887 vlan => $vid); 888 889 return unless $vsinfo; 890 891 $stp_p_info = $class->_exec_timeout( 892 $args{host}, 893 sub{ return $self->_get_stp_info(sinfo=>$vsinfo) } 894 ); 895 896 $i_stp_info = $class->_exec_timeout( 897 $args{host}, 898 sub{ return $self->_get_i_stp_info(sinfo=>$vsinfo) } ); 899 }; 900 if ( my $e = $@ ){ 901 $logger->error("$args{host}: SNMP error for VLAN $vid: $e"); 902 next; 903 } 904 foreach my $method ( keys %$stp_p_info ){ 905 $dev{stp_instances}{$mst_inst}{$method} = $stp_p_info->{$method}; 906 } 907 foreach my $field ( keys %$i_stp_info ){ 908 foreach my $i ( keys %{$i_stp_info->{$field}} ){ 909 $dev{interface}{$i}{$field} = $i_stp_info->{$field}->{$i}; 910 } 911 } 912 $seen_inst{$mst_inst} = 1; 913 } 914 } 915 } 916 }elsif ( $dev{stp_type} =~ /pvst/i ){ 917 # Get stp info for each vlan 918 # STPInstance numbers match vlan id's 919 if ( $sinfo->cisco_comm_indexing() ){ 920 my %vlans; 921 foreach my $p ( keys %{$hashes{'i_vlan_membership'}} ){ 922 my $vlans = $hashes{'i_vlan_membership'}->{$p}; 923 map { $vlans{$_}++ } @$vlans; 924 } 925 foreach my $vid ( keys %vlans ){ 926 next if ( exists $IGNOREDVLANS{$vid} ); 927 next unless $vlan_status{$vid} eq 'operational'; 928 eval { 929 my $vsinfo = $self->_get_cisco_snmp_context_session(sinfo => $sinfo, 930 vlan => $vid); 931 932 return unless $vsinfo; 933 934 my $stp_p_info = $class->_exec_timeout( 935 $args{host}, 936 sub{ return $self->_get_stp_info(sinfo=>$vsinfo) } ); 937 938 foreach my $method ( keys %$stp_p_info ){ 939 $dev{stp_instances}{$vid}{$method} = $stp_p_info->{$method}; 940 } 941 942 my $i_stp_info = $class->_exec_timeout( 943 $args{host}, 944 sub{ return $self->_get_i_stp_info(sinfo=>$vsinfo) } ); 945 946 foreach my $field ( keys %$i_stp_info ){ 947 foreach my $i ( keys %{$i_stp_info->{$field}} ){ 948 $dev{interface}{$i}{$field} = $i_stp_info->{$field}->{$i}; 949 } 950 } 951 }; 952 if ( my $e = $@ ){ 953 $logger->error("$args{host}: SNMP error for VLAN $vid: $e"); 954 next; 955 } 956 } 957 } 958 } 959 } 960 } 961 } 962 963 # Set some values specific to device types 964 if ( $dev{ipforwarding} ){ 965 if ( my $local_as = $sinfo->bgp_local_as() ){ 966 my $asn = ASN->find_or_create({number=>$local_as}); 967 $dev{bgplocalas} = $asn; 968 } 969 $dev{bgpid} = $sinfo->bgp_id(); 970 } 971 972 ################################################################ 973 # CDP/LLDP stuff 974 if (( $hashes{hasCDP} ) or ($hashes{hasLLDP})){ 975 # Call all the relevant methods 976 my %dp_hashes; 977 my @dp_methods = qw ( c_id c_ip c_port c_platform ); 978 foreach my $m ( @dp_methods ){ 979 $dp_hashes{$m} = $sinfo->$m; 980 } 981 # Translate keys into iids 982 my $c_ifs = $sinfo->c_if(); 983 while ( my ($key, $iid) = each %$c_ifs ){ 984 next unless $iid; 985 foreach my $m ( @dp_methods ){ 986 next if !exists $dp_hashes{$m}->{$key}; 987 my $v = $dp_hashes{$m}->{$key}; 988 # Ignore values with non-ascii chars 989 next unless $self->is_ascii($v); 990 991 # SNMP::Info can include values from both LLDP and CDP 992 # which means that for each port, we can have different 993 # values. We save them all in a comma-separated list 994 if ( exists $hashes{$m}->{$iid} ){ 995 # Use a hash for fast lookup 996 my %vals; 997 map { $vals{$_} = 1 } split ';', $hashes{$m}->{$iid}; 998 if ( ! exists $vals{$dp_hashes{$m}->{$key}} ){ 999 # Append new value to list 1000 $vals{$v} = 1; 1001 } 1002 $hashes{$m}->{$iid} = join ';', keys %vals; 1003 }else{ 1004 $hashes{$m}->{$iid} = $dp_hashes{$m}->{$key}; 1005 } 1006 } 1007 } 1008 } 1009 1010 ################################################################ 1011 # Modules 1012 1013 if ( $self->config->get('GET_DEVICE_MODULE_INFO') ){ 1014 foreach my $key ( keys %{ $hashes{e_class} } ){ 1015 # Notice that we use int() to avoid duplicate errors 1016 # in DB when number is like 01000000 1017 $dev{module}{$key}{number} = int($hashes{e_index}->{$key}); 1018 foreach my $field ( keys %MFIELDS ){ 1019 my $method = $MFIELDS{$field}; 1020 my $v = $hashes{$method}->{$key}; 1021 if ( defined($v) && $self->is_ascii($v) ){ 1022 if ( $field eq 'fru' ){ 1023 # This is boolean 1024 $dev{module}{$key}{$field} = ( $v eq 'true' )? 1 : 0; 1025 }else{ 1026 $dev{module}{$key}{$field} = $v; 1027 } 1028 } 1029 } 1030 } 1031 } 1032 1033 1034 ################################################################ 1035 # Interface stuff 1036 1037 ############################################## 1038 # for each interface discovered... 1039 1040 foreach my $iid ( keys %{ $hashes{$name_src} } ){ 1041 my $name = $hashes{$name_src}->{$iid}; 1042 # check whether it should be ignored 1043 if ( $name ){ 1044 if ( my $ifreserved = $self->config->get('IFRESERVED') ){ 1045 if ( $name =~ /$ifreserved/i ){ 1046 $logger->debug(sub{"Device::get_snmp_info: $args{host}: Interface $name ". 1047 "ignored per config option (IFRESERVED)"}); 1048 delete $dev{interface}{$iid}; # Could have been added above 1049 next; 1050 } 1051 } 1052 $dev{interface}{$iid}{name} = $name; 1053 }else{ 1054 $dev{interface}{$iid}{name} = $iid; 1055 } 1056 $dev{interface}{$iid}{number} = $iid; 1057 1058 foreach my $field ( keys %IFFIELDS ){ 1059 my $method = $IFFIELDS{$field}; 1060 if ( exists $hashes{$method}->{$iid} ){ 1061 if ( $field =~ /_enabled/o ){ 1062 # These are all booleans 1063 $dev{interface}{$iid}{$field} = ( $hashes{$method}->{$iid} eq 'true' )? 1 : 0; 1064 }else{ 1065 $dev{interface}{$iid}{$field} = $hashes{$method}->{$iid}; 1066 } 1067 }elsif ( $field =~ /^dp_/o ) { 1068 # Make sure we erase any old discovery protocol values 1069 $dev{interface}{$iid}{$field} = ""; 1070 } 1071 } 1072 1073 ################################################################ 1074 # Vlan info 1075 # 1076 my ($vid, $vname); 1077 # These are all the vlans that are enabled on this port. 1078 if ( my $vm = $hashes{'i_vlan_membership'}->{$iid} ){ 1079 foreach my $vid ( @$vm ){ 1080 if ( exists $IGNOREDVLANS{$vid} ){ 1081 $logger->debug(sub{"Device::get_snmp_info: $args{host} VLAN $vid ignored ". 1082 "per configuration option (IGNOREVLANS)"}); 1083 next; 1084 } 1085 $dev{interface}{$iid}{vlans}{$vid}{vid} = $vid; 1086 } 1087 } 1088 foreach my $vid ( keys %{$dev{interface}{$iid}{vlans}} ){ 1089 # Get VLAN names 1090 $vname = $hashes{'qb_v_name'}->{$vid}; # Standard MIB 1091 unless ( $vname ){ 1092 # We didn't get a vlan name in the standard place 1093 # Try Cisco location 1094 # SNMP::Info should be doing this for me :-( 1095 if ( $sinfo->cisco_comm_indexing ){ 1096 my $hvname = $hashes{'v_name'}; 1097 foreach my $key ( keys %$hvname ){ 1098 if ( $key =~ /^(\d+\.$vid)$/ ){ 1099 $vname = $hvname->{$key}; 1100 last; 1101 } 1102 } 1103 } 1104 } 1105 $dev{interface}{$iid}{vlans}{$vid}{vname} = $vname if defined ($vname); 1106 1107 if ( $dev{stp_type} ){ 1108 if ( $dev{stp_type} eq 'mst' ){ 1109 # Get STP instance where this VLAN belongs 1110 # If there is no mapping, make it instance 0 1111 $dev{interface}{$iid}{vlans}{$vid}{stp_instance} = $dev{stp_vlan2inst}->{$vid} || 0; 1112 }elsif ( $dev{stp_type} =~ /pvst/i ){ 1113 # In PVST, we number the instances the same as VLANs 1114 $dev{interface}{$iid}{vlans}{$vid}{stp_instance} = $vid; 1115 }elsif ( $dev{stp_type} eq 'ieee8021d' ){ 1116 $dev{interface}{$iid}{vlans}{$vid}{stp_instance} = 0; 1117 } 1118 } 1119 } 1120 } 1121 1122 sub _check_if_status_down{ 1123 my ($dev, $iid) = @_; 1124 return 1 if ( (defined $dev->{interface}{$iid}{admin_status}) && 1125 ($dev->{interface}{$iid}{admin_status} eq 'down') ); 1126 return 0; 1127 } 1128 1129 ################################################################ 1130 # IPv4 addresses and masks 1131 # 1132 1133 # This table is critical, so if we didn't get anything, ask again 1134 if ( !defined($hashes{'ip_index'}) || !(keys %{ $hashes{'ip_index'} }) ){ 1135 $hashes{'ip_index'} = $sinfo->ip_index(); 1136 } 1137 if ( !defined($hashes{'ip_table'}) || !(keys %{ $hashes{'ip_table'} }) ){ 1138 $hashes{'ip_table'} = $sinfo->ip_table(); 1139 } 1140 while ( my($ipt,$iid) = each %{ $hashes{'ip_index'} } ){ 1141 my $ip; 1142 if ($iid > 150000000) { #nx-os has id > 150000000 1143 $ip=$hashes{'ip_table'}{$ipt}; 1144 } else { 1145 $ip=$ipt; 1146 } 1147 next unless (defined $dev{interface}{$iid}); 1148 next if &_check_if_status_down(\%dev, $iid); 1149 next if Ipblock->is_loopback($ip); 1150 next if ( $ip eq '0.0.0.0' || $ip eq '255.255.255.255' ); 1151 $dev{interface}{$iid}{ips}{$ip}{address} = $ip; 1152 $dev{interface}{$iid}{ips}{$ip}{version} = 4; 1153 if ( my $mask = $hashes{'ip_netmask'}->{$ip} ){ 1154 my ($subnet, $len) = Ipblock->get_subnet_addr(address => $ip, 1155 prefix => $mask ); 1156 $dev{interface}{$iid}{ips}{$ip}{subnet} = "$subnet/$len"; 1157 } 1158 } 1159 1160 ################################################################ 1161 # IPv6 addresses and prefixes 1162 1163 my $ignore_link_local = $self->config->get('IGNORE_IPV6_LINK_LOCAL'); 1164 1165 # Stuff in this hash looks like this: 1166 # 1167 # CISCO-IETF-IP-MIB: 1168 # 2.16.32.1.4.104.13.1.0.2.0.0.0.0.0.0.0.9 => 1169 # 49.2.16.32.1.4.104.13.1.0.2.0.0.0.0.0.0.0.0.64 1170 # 1171 # IP-MIB: 1172 # 2.32.1.4.104.13.1.0.2.0.0.0.0.0.0.0.41 => 1173 # 1.151126018.2.32.1.4.104.13.1.0.2.0.0.0.0.0.0.0.0.64 1174 # 1175 while ( my($key,$val) = each %{$hashes{'ipv6_addr_prefix'}} ){ 1176 my ($iid, $addr, $pfx, $len); 1177 # We only care about the last 16 octets in decimal 1178 # so, that's an digit and a dot, 15 times, plus another digit 1179 if ( $key =~ /^.+\.((?:\d+\.){15}\d+)$/o ) { 1180 $addr = $self->_octet_string_to_v6($1); 1181 } 1182 if ( $val =~ /^(\d+)\.(\d+)\.\d+\.([\d\.]+)\.(\d+)$/o ) { 1183 # ifIndex, type, size, prefix length 1184 if ( ($1 == 1) && ($2 > 150000000)) { 1185 # It seems that for nexus the ifIndex id is always greater than 150000000 1186 $iid=$2; 1187 } else { 1188 $iid=$1; 1189 } 1190 $len = $4; 1191 $pfx = $self->_octet_string_to_v6($3); 1192 } 1193 if ( $iid && $addr && $pfx && $len ){ 1194 next unless (defined $dev{interface}{$iid}); 1195 next if &_check_if_status_down(\%dev, $iid); 1196 next if ( Ipblock->is_link_local($addr) && $ignore_link_local ); 1197 $dev{interface}{$iid}{ips}{$addr}{address} = $addr; 1198 $dev{interface}{$iid}{ips}{$addr}{version} = 6; 1199 $dev{interface}{$iid}{ips}{$addr}{subnet} = "$pfx/$len"; 1200 }else{ 1201 # What happened? 1202 $logger->warn("$args{host}: Unrecognized ipv6_addr_prefix entry: $key => $val") 1203 } 1204 } 1205 1206 ################################################################ 1207 # IPv6 link-local addresses 1208 # It looks like in Cisco 'ipv6_index' contains all the addresses from 1209 # 'ipv6_addr_prefix', plus link locals, so we won't query it 1210 # unless we want those. 1211 unless ( $ignore_link_local ){ 1212 my $ipv6_index = $sinfo->ipv6_index(); 1213 my ($iid, $addr); 1214 while ( my($key,$val) = each %$ipv6_index ){ 1215 if ( $key =~ /^.+\.((?:\d+\.){15}\d+)$/o ) { 1216 $addr = $self->_octet_string_to_v6($1); 1217 next unless Ipblock->is_link_local($addr); 1218 $iid = $val; 1219 next unless $iid; # Sometimes this can be 0 1220 $dev{interface}{$iid}{ips}{$addr}{address} = $addr; 1221 $dev{interface}{$iid}{ips}{$addr}{version} = 6; 1222 }else{ 1223 # What happened? 1224 $logger->warn("$args{host}: Unrecognized ipv6_index entry: $key => $val") 1225 } 1226 } 1227 } 1228 1229 1230 1231 ############################################## 1232 # Deal with BGP Peers 1233 # only proceed if we were told to discover peers, either directly or in the config file 1234 if ( $args{bgp_peers} || $self->config->get('ADD_BGP_PEERS')) { 1235 $logger->debug(sub{"Device::get_snmp_info: Checking for BGPPeers"}); 1236 1237 ############################################## 1238 # for each BGP Peer discovered... 1239 foreach my $peer ( keys %{$hashes{'bgp_peers'}} ) { 1240 $dev{bgp_peer}{$peer}{address} = $peer; 1241 unless ( $dev{bgp_peer}{$peer}{bgppeerid} = $hashes{'bgp_peer_id'}->{$peer} ){ 1242 $logger->warn("Could not determine BGP peer id of peer $peer"); 1243 } 1244 if ( my $asn = $hashes{'bgp_peer_as'}->{$peer} ){ 1245 $dev{bgp_peer}{$peer}{asnumber} = $asn; 1246 $dev{bgp_peer}{$peer}{asname} = "AS $asn"; 1247 $dev{bgp_peer}{$peer}{orgname} = "AS $asn"; 1248 1249 if ( Netdot->config->get('DO_WHOISQ') ){ 1250 # We enabled whois queries in config 1251 if ( my $as_info = $self->_get_as_info($asn) ){ 1252 $dev{bgp_peer}{$peer}{asname} = $as_info->{asname}; 1253 $dev{bgp_peer}{$peer}{orgname} = $as_info->{orgname}; 1254 } 1255 } 1256 }else{ 1257 $logger->warn("Could not determine AS number of peer $peer"); 1258 } 1259 if ( my $state = $hashes{'bgp_peer_state'}->{$peer} ){ 1260 $dev{bgp_peer}{$peer}{state} = $state; 1261 } 1262 } 1263 } 1264 1265 # Remove whitespace at beginning and end 1266 while ( my ($key, $val) = each %dev){ 1267 next unless defined $val; 1268 $class->rem_lt_sp($val); 1269 $dev{$key} = $val; 1270 } 1271 1272 $logger->debug(sub{"Device::get_snmp_info: Finished getting SNMP info from $args{host}"}); 1273 return \%dev; 1274} 1275 1276 1277######################################################################### 1278 1279=head2 snmp_update_all - Update SNMP info for every device in DB 1280 1281 Arguments: 1282 communities Arrayref of SNMP communities 1283 version SNMP version 1284 timeout SNMP timeout 1285 retries SNMP retries 1286 do_info Update Device Info 1287 do_fwt Update Forwarding Tables 1288 do_arp Update ARP caches 1289 add_subnets Flag. When discovering routers, add subnets to database if they do not exist 1290 subs_inherit Flag. When adding subnets, have them inherit information from the Device 1291 bgp_peers Flag. When discovering routers, update bgp_peers 1292 pretend Flag. Do not commit changes to the database 1293 matching Regex. Only update devices whose names match regex 1294 Returns: 1295 True if successful 1296 1297 Examples: 1298 Device->snmp_update_all(); 1299 1300=cut 1301 1302sub snmp_update_all { 1303 my ($class, %argv) = @_; 1304 $class->isa_class_method('snmp_update_all'); 1305 my $start = time; 1306 1307 my @devs = $class->retrieve_all(); 1308 my $device_count = $class->_snmp_update_parallel(devs=>\@devs, %argv); 1309 my $end = time; 1310 $logger->info(sprintf("All Devices updated. %d devices in %s", 1311 $device_count, $class->sec2dhms($end-$start) )); 1312 1313} 1314 1315#################################################################################### 1316 1317=head2 snmp_update_block - Discover and/or update all devices in given IP blocks 1318 1319 Arguments: 1320 Hash with the following keys: 1321 blocks Arrayref of IP block addresses in CIDR or dotted mask notation 1322 communities Arrayref of SNMP communities 1323 version SNMP version 1324 timeout SNMP timeout 1325 retries SNMP retries 1326 do_info Update Device Info 1327 do_fwt Update Forwarding Tables 1328 do_arp Update ARP caches 1329 add_subnets Flag. When discovering routers, add subnets to database if they do not exist 1330 subs_inherit Flag. When adding subnets, have them inherit information from the Device 1331 bgp_peers Flag. When discovering routers, update bgp_peers 1332 pretend Flag. Do not commit changes to the database 1333 matching Regex. Only update devices whose names match regex 1334 1335 Returns: 1336 True if successful 1337 1338 Examples: 1339 Device->snmp_update_block(blocks=>"192.168.0.0/24"); 1340 1341=cut 1342 1343sub snmp_update_block { 1344 my ($class, %argv) = @_; 1345 $class->isa_class_method('snmp_update_block'); 1346 1347 my $blocks; 1348 $class->throw_fatal("Model::Device::snmp_update_block: Missing or invalid required argument: blocks") 1349 unless ( defined($blocks = $argv{blocks}) && ref($blocks) eq 'ARRAY' ); 1350 delete $argv{blocks}; 1351 1352 # Just for logging 1353 my $blist = join ', ', @$blocks; 1354 1355 my %h; 1356 foreach my $block ( @$blocks ){ 1357 # Get a list of host addresses for the given block 1358 my $hosts = Ipblock->get_host_addrs($block); 1359 foreach my $host ( @$hosts ){ 1360 $h{$host} = ""; 1361 } 1362 } 1363 $logger->debug(sub{"SNMP-discovering all devices in $blist"}); 1364 my $start = time; 1365 1366 # Call the more generic method 1367 $argv{hosts} = \%h; 1368 my $device_count = $class->_snmp_update_parallel(%argv); 1369 1370 my $end = time; 1371 $logger->info(sprintf("Devices in $blist updated. %d devices in %s", 1372 $device_count, $class->sec2dhms($end-$start) )); 1373 1374} 1375 1376#################################################################################### 1377 1378=head2 snmp_update_from_file - Discover and/or update all devices in a given file 1379 1380 Arguments: 1381 Hash with the following keys: 1382 file Path to file with list of hosts (IPs or hostnames) one per line 1383 communities Arrayref of SNMP communities 1384 version SNMP version 1385 timeout SNMP timeout 1386 retries SNMP retries 1387 add_subnets Flag. When discovering routers, add subnets to database if they do not exist 1388 subs_inherit Flag. When adding subnets, have them inherit information from the Device 1389 bgp_peers Flag. When discovering routers, update bgp_peers 1390 pretend Flag. Do not commit changes to the database 1391 matching Regex. Only update devices whose names match regex 1392 1393 Returns: 1394 True if successful 1395 1396 Examples: 1397 Device->snmp_update_from_file("/path/to/file"); 1398 1399=cut 1400 1401sub snmp_update_from_file { 1402 my ($class, %argv) = @_; 1403 $class->isa_class_method('snmp_update_from_file'); 1404 1405 my $file; 1406 $class->throw_fatal("Model::Device::snmp_update_from_file:Missing required argument: file") 1407 unless defined( $file = $argv{file} ); 1408 delete $argv{file}; 1409 1410 # Get a list of hosts from given file 1411 my $hosts = $class->_get_hosts_from_file($file); 1412 1413 $logger->debug(sub{"SNMP-discovering all devices in $file"}); 1414 my $start = time; 1415 1416 # Call the more generic method 1417 $argv{hosts} = $hosts; 1418 my $device_count = $class->_snmp_update_parallel(%argv); 1419 1420 my $end = time; 1421 $logger->info(sprintf("Devices in %s updated. %d devices in %s", 1422 $file, $device_count, $class->sec2dhms($end-$start))); 1423 1424} 1425 1426 1427######################################################################### 1428 1429=head2 discover - Insert or update a device after getting its SNMP info. 1430 1431 Adjusts a number of settings when inserting, based on certain 1432 info obtained via SNMP. 1433 1434 Arguments: 1435 Hash containing the following keys: 1436 name Host name or ip address (required) 1437 main_ip Main IP address (optional) 1438 session SNMP Session (optional) 1439 communities Arrayref of SNMP communities 1440 version SNMP version 1441 timeout SNMP timeout 1442 retries SNMP retries 1443 sec_name SNMP Security Name 1444 sec_level SNMP Security Level 1445 auth_proto SNMP Authentication Protocol 1446 auth_pass SNMP Auth Key 1447 priv_proto SNMP Privacy Protocol 1448 priv_pass SNMP Privacy Key 1449 do_info Update device info 1450 do_fwt Update forwarding tables 1451 do_arp Update ARP cache 1452 add_subnets Flag. When discovering routers, add subnets to database if they do not exist 1453 subs_inherit Flag. When adding subnets, have them inherit information from the Device 1454 bgp_peers Flag. When discovering routers, update bgp_peers 1455 pretend Flag. Do not commit changes to the database 1456 info Hashref with SNMP info (optional) 1457 timestamp Time Stamp (optional) 1458 Returns: 1459 New or existing Device object 1460 Examples: 1461 Device->discover(name=>$hostname, communities=>["public"]); 1462 Device->discover(name=>$hostname, info=>$info); 1463 1464=cut 1465 1466sub discover { 1467 my ($class, %argv) = @_; 1468 1469 $class->isa_class_method('discover'); 1470 1471 my $name = $argv{name} || 1472 $class->throw_fatal("Model::Device::discover: Missing required arguments: name"); 1473 1474 my $info = $argv{info} || 0; 1475 my $sinfo = $argv{session} || 0; 1476 my $dev; 1477 my $ip; 1478 1479 if ( $dev = Device->search(name=>$name)->first ){ 1480 $logger->debug(sub{"Device::discover: Device $name already exists in DB"}); 1481 1482 }else{ 1483 # Device not found by that name 1484 # We need to check and make sure there isn't a device in 1485 # the database with the same phyiscal address and/or serial number 1486 1487 unless ( $info ){ 1488 unless ( $sinfo ){ 1489 $sinfo = $class->_get_snmp_session(host => $name, 1490 communities => $argv{communities}, 1491 version => $argv{version}, 1492 timeout => $argv{timeout}, 1493 retries => $argv{retries}, 1494 sec_name => $argv{sec_name}, 1495 sec_level => $argv{sec_level}, 1496 auth_proto => $argv{auth_proto}, 1497 auth_pass => $argv{auth_pass}, 1498 priv_proto => $argv{priv_proto}, 1499 priv_pass => $argv{priv_pass}, 1500 sqe => $argv{sqe}, 1501 ); 1502 1503 return unless $sinfo; 1504 } 1505 $info = $class->_exec_timeout($name, 1506 sub{ return $class->get_snmp_info(session => $sinfo, 1507 bgp_peers => $argv{bgp_peers}, 1508 ) }); 1509 } 1510 } 1511 1512 my $device_is_new = 0; 1513 1514 unless ( $dev ){ #still no dev! guess we better make it! 1515 $logger->debug(sub{"Device::discover: Device $name does not yet exist"}); 1516 # Set some values in the new Device based on the SNMP info obtained 1517 my $main_ip = $argv{main_ip} || $class->_get_main_ip($info); 1518 my $host = $main_ip || $name; 1519 my $newname = $class->assign_name(host=>$host, sysname=>$info->{sysname}); 1520 1521 1522 my %devtmp; # Store new Device values here 1523 1524 if ( $info->{snmp_version} ) { 1525 # Means we probably discovered this using SNMP 1526 1527 # Set some default stuff 1528 %devtmp = (snmp_managed => 1, 1529 snmp_polling => 1, 1530 canautoupdate => 1, 1531 # These following two could have changed in get_snmp_session 1532 # so we grab them from %info instead of directly from %argv 1533 community => $info->{community}, 1534 snmp_version => $info->{snmp_version}, 1535 ); 1536 1537 if ( defined $devtmp{snmp_version} && $devtmp{snmp_version} == 3 ){ 1538 my %arg2field = (sec_name => 'snmp_securityname', 1539 sec_level => 'snmp_securitylevel', 1540 auth_proto => 'snmp_authprotocol', 1541 auth_pass => 'snmp_authkey', 1542 priv_proto => 'snmp_privprotocol', 1543 priv_pass => 'snmp_privkey', 1544 ); 1545 1546 foreach my $arg ( keys %arg2field ){ 1547 $devtmp{$arg2field{$arg}} = $argv{$arg} if defined $argv{$arg}; 1548 } 1549 } 1550 } 1551 1552 ################################################################ 1553 # Try to guess product type based on name 1554 if ( my $NAME2TYPE = $class->config->get('DEV_NAME2TYPE') ){ 1555 foreach my $str ( keys %$NAME2TYPE ){ 1556 if ( $newname->get_label =~ /$str/ ){ 1557 $info->{type} = $NAME2TYPE->{$str}; 1558 last; 1559 } 1560 } 1561 } 1562 # If not, assign type based on layers 1563 if ( !$info->{type} && $info->{layers} ){ 1564 $info->{type} = "Hub" if ( $class->_layer_active($info->{layers}, 1) ); 1565 $info->{type} = "Switch" if ( $class->_layer_active($info->{layers}, 2) ); 1566 $info->{type} = "Router" if ( $class->_layer_active($info->{layers}, 3) && 1567 $info->{ipforwarding} ); 1568 $info->{type} = "Server" if ( $class->_layer_active($info->{layers}, 7) ); 1569 $info->{type} = "Unknown" unless defined $info->{type}; 1570 } 1571 $logger->debug(sub{sprintf("%s type is: %s", $newname->get_label, $info->{type})}); 1572 1573 if ( $info->{layers} ){ 1574 # We collect rptrAddrTrackNewLastSrcAddress from hubs 1575 if ( $class->_layer_active($info->{layers}, 1) ){ 1576 $devtmp{collect_fwt} = 1; 1577 } 1578 if ( $class->_layer_active($info->{layers}, 2) ){ 1579 $devtmp{collect_fwt} = 1; 1580 $devtmp{collect_stp} = 1; 1581 } 1582 if ( $class->_layer_active($info->{layers}, 3) 1583 && $info->{ipforwarding} ){ 1584 $devtmp{collect_arp} = 1; 1585 } 1586 } 1587 # Catch any other Device fields passed to us 1588 # This will override the previous default values 1589 foreach my $field ( $class->meta_data->get_column_names ){ 1590 if ( defined $argv{$field} ){ 1591 $devtmp{$field} = $argv{$field}; 1592 } 1593 } 1594 1595 # Add contacts argument if passed 1596 $devtmp{contacts} = $argv{contacts} if defined $argv{contacts}; 1597 1598 # Try to assign a Site based on syslocation 1599 if ( !$devtmp{site} && (my $loc = $info->{syslocation}) ){ 1600 if ( my $site = Site->search_like(name=>"%$loc%")->first ){ 1601 $devtmp{site} = $site; 1602 } 1603 } 1604 # Insert the new Device 1605 $devtmp{name} = $newname; 1606 $logger->info(sprintf("Inserting new Device: %s", $name)); 1607 $dev = $class->insert(\%devtmp); 1608 $device_is_new = 1; 1609 } 1610 1611 # Get relevant snmp_update args 1612 my %uargs; 1613 foreach my $field ( qw(communities version timeout retries 1614 sec_name sec_level auth_proto auth_pass priv_proto priv_pass 1615 add_subnets subs_inherit bgp_peers pretend do_info do_fwt do_arp timestamp 1616 sqe) ){ 1617 $uargs{$field} = $argv{$field} if defined ($argv{$field}); 1618 } 1619 $uargs{session} = $sinfo if $sinfo; 1620 $uargs{info} = $info; 1621 $uargs{device_is_new} = $device_is_new; 1622 1623 # Update Device with SNMP info obtained 1624 $dev->snmp_update(%uargs); 1625 1626 return $dev; 1627} 1628 1629######################################################################### 1630 1631=head2 get_all_from_block - Retrieve devices with addresses within an IP block 1632 1633 Arguments: 1634 block - IP block in CIDR notation 1635 Returns: 1636 Array ref of Device objects 1637 Examples: 1638 my $devs = Device->get_all_from_block('192.168.1.0/24'); 1639 1640=cut 1641 1642sub get_all_from_block { 1643 my ($class, $block) = @_; 1644 $class->isa_class_method('get_all_from_block'); 1645 1646 defined $block || 1647 $class->throw_fatal("Model::Device::get_all_from_block: Missing required arguments: block"); 1648 1649 my $devs; 1650 if ( my $ipb = Ipblock->search(address=>$block)->first ){ 1651 $devs = $ipb->get_devices(); 1652 }else{ 1653 # Get a list of host addresses for the given block 1654 # This is highly inefficient 1655 my $hosts = Ipblock->get_host_addrs($block); 1656 my %devs; #index by id to avoid duplicates 1657 foreach my $ip ( @$hosts ){ 1658 if ( my $ipb = Ipblock->search(address=>$ip)->first ){ 1659 if ( $ipb->interface && $ipb->interface->device ){ 1660 my $dev = $ipb->interface->device; 1661 $devs->{$dev->id} = $dev; 1662 } 1663 } 1664 } 1665 $devs = \values %{$devs}; 1666 } 1667 return $devs; 1668} 1669 1670################################################################# 1671 1672=head2 get_base_macs_from_all - Retrieve base MACs from all devices 1673 1674 Arguments: 1675 None 1676 Returns: 1677 Hashref with key=address, value=device 1678 Examples: 1679 my $devmacs = Device->get_base_macs_from_all(); 1680 1681=cut 1682 1683sub get_base_macs_from_all { 1684 my ($class) = @_; 1685 $class->isa_class_method('get_base_macs_from_all'); 1686 1687 # Build the SQL query 1688 $logger->debug(sub{ "Device::get_base_macs_from_all: Retrieving all Device MACs..." }); 1689 1690 my $dbh = $class->db_Main; 1691 my $aref = $dbh->selectall_arrayref("SELECT p.address, d.id 1692 FROM physaddr p, device d, asset a 1693 WHERE a.physaddr=p.id 1694 AND d.asset_id=a.id 1695 "); 1696 # Build a hash of mac addresses to device ids 1697 my %dev_macs; 1698 foreach my $row ( @$aref ){ 1699 my ($address, $id) = @$row; 1700 $dev_macs{$address} = $id; 1701 } 1702 return \%dev_macs; 1703} 1704 1705################################################################# 1706 1707=head2 get_if_macs_from_all - Retrieve MACs from all device interfaces 1708 1709 Arguments: 1710 None 1711 Returns: 1712 Hashref with key=address, value=device 1713 Examples: 1714 my $devmacs = Device->get_if_macs_from_all(); 1715 1716=cut 1717 1718sub get_if_macs_from_all { 1719 my ($class) = @_; 1720 $class->isa_class_method('get_if_macs_from_all'); 1721 1722 # Build the SQL query 1723 $logger->debug(sub{ "Device::get_if_macs_from_all: Retrieving all Interface MACs..." }); 1724 1725 my $dbh = $class->db_Main; 1726 my $aref = $dbh->selectall_arrayref("SELECT p.address, d.id 1727 FROM physaddr p, device d, interface i 1728 WHERE i.device=d.id AND i.physaddr=p.id 1729 "); 1730 # Build a hash of mac addresses to device ids 1731 my %dev_macs; 1732 foreach my $row ( @$aref ){ 1733 my ($address, $id) = @$row; 1734 $dev_macs{$address} = $id; 1735 } 1736 return \%dev_macs; 1737} 1738 1739################################################################# 1740 1741=head2 get_macs_from_all 1742 1743 Retrieve all MAC addresses that belong to Devices 1744 1745 Arguments: 1746 None 1747 Returns: 1748 Hashref with key=address, value=device 1749 Examples: 1750 my $devmacs = Device->get_macs_from_all(); 1751 1752=cut 1753 1754sub get_macs_from_all { 1755 my ($class) = @_; 1756 $class->isa_class_method('get_macs_from_all'); 1757 1758 my $dev_macs = $class->get_base_macs_from_all(); 1759 my $if_macs = $class->get_if_macs_from_all(); 1760 1761 while ( my($k, $v) = each %$if_macs ){ 1762 $dev_macs->{$k} = $v; 1763 } 1764 return $dev_macs; 1765} 1766 1767################################################################# 1768 1769=head2 get_within_downtime 1770 1771 Get the devices within downtime. 1772 1773 Arguments: 1774 None 1775 Returns: 1776 Array of Device objects 1777 Examples: 1778 my @devices = Device->get_within_downtime(); 1779 1780=cut 1781 1782sub get_within_downtime { 1783 my ($class) = @_; 1784 $class->isa_class_method('get_within_downtime'); 1785 1786 my $now = $class->time2sqldate(time); 1787 my @devices = Device->retrieve_from_sql(qq{ 1788 down_from <= '$now' 1789 AND down_until >= '$now' 1790 }); 1791 return @devices; 1792} 1793 1794################################################################# 1795 1796=head2 get_ips_from_all 1797 1798 Retrieve all IP addresses that belong to Devices 1799 1800 Arguments: 1801 None 1802 Returns: 1803 Hashref with key=address (Decimal), value=device id 1804 Examples: 1805 my $devips = Device->get_ips_from_all(); 1806 1807=cut 1808 1809sub get_ips_from_all { 1810 my ($class) = @_; 1811 $class->isa_class_method('get_ips_from_all'); 1812 1813 # Build the SQL query 1814 $logger->debug(sub{ "Device::get_ips_from_all: Retrieving all Device IPs..." }); 1815 1816 my $dbh = $class->db_Main; 1817 my $aref1 = $dbh->selectall_arrayref("SELECT ip.address, d.id 1818 FROM ipblock ip, device d, interface i 1819 WHERE i.device=d.id AND ip.interface=i.id 1820 "); 1821 1822 my $aref2 = $dbh->selectall_arrayref("SELECT ip.address, d.id 1823 FROM ipblock ip, device d 1824 WHERE d.snmp_target=ip.id;"); 1825 1826 # Build a hash of mac addresses to device ids 1827 my %dev_ips; 1828 foreach my $row ( @$aref1, @$aref2 ){ 1829 my ($address, $id) = @$row; 1830 $dev_ips{$address} = $id; 1831 } 1832 return \%dev_ips; 1833 1834} 1835 1836################################################################################################### 1837 1838=head2 get_device_graph - Returns the graph of devices - Maps Device IDs 1839 1840 Arguments: 1841 None 1842 Returns: 1843 Hashref of hashrefs, where keys are Device IDs 1844 Example: 1845 my $graph = Device::get_device_graph() 1846 1847=cut 1848 1849sub get_device_graph { 1850 my ($class) = @_; 1851 $class->isa_class_method('get_device_graph'); 1852 1853 $logger->debug("Netdot::Device::get_device_graph: querying database"); 1854 1855 my $dbh = $class->db_Main; 1856 1857 my $graph = {}; 1858 my $links = $dbh->selectall_arrayref(" 1859 SELECT d1.id, d2.id 1860 FROM device d1, device d2, interface i1, interface i2 1861 WHERE i1.device = d1.id AND i2.device = d2.id 1862 AND i2.neighbor = i1.id AND i1.neighbor = i2.id 1863 "); 1864 1865 foreach my $link (@$links) { 1866 my ($fr, $to) = @$link; 1867 $graph->{$fr}{$to} = 1; 1868 $graph->{$to}{$fr} = 1; 1869 } 1870 1871 return $graph; 1872} 1873 1874################################################################################################### 1875 1876=head2 get_device_i_graph - Returns the graph of devices - Maps device and interface IDs 1877 1878 Arguments: 1879 None 1880 Returns: 1881 Hashref of hashrefs where: 1882 key1 = device id, key2 = interface id, value = interface id 1883 Example: 1884 my $graph = Device::get_device_i_graph() 1885 1886=cut 1887 1888sub get_device_i_graph { 1889 my ($class) = @_; 1890 $class->isa_class_method('get_device_i_graph'); 1891 1892 $logger->debug("Netdot::Device::get_device_i_graph: querying database"); 1893 1894 my $dbh = $class->db_Main; 1895 1896 my $graph = {}; 1897 my $links = $dbh->selectall_arrayref(" 1898 SELECT d1.id, i1.id, d2.id, i2.id 1899 FROM device d1, device d2, interface i1, interface i2 1900 WHERE i1.device = d1.id AND i2.device = d2.id 1901 AND i2.neighbor = i1.id AND i1.neighbor = i2.id 1902 "); 1903 1904 foreach my $link (@$links) { 1905 my ($d1, $i1, $d2, $i2) = @$link; 1906 $graph->{$d1}{$i1} = $i2; 1907 $graph->{$d2}{$i2} = $i1; 1908 } 1909 1910 return $graph; 1911} 1912 1913 1914################################################################################################### 1915 1916=head2 shortest_path_parents - A variation of Dijkstra's single-source shortest paths algorithm 1917 1918 Determines all the possible parents of each node that are in the shortest paths between 1919 that node and the given source. 1920 1921 Arguments: 1922 Source Vertex (Device ID) 1923 Returns: 1924 Hash ref where key = Device.id, value = Hashref where keys = parent Device.id's 1925 Example: 1926 $parents = Device::shortest_path_parents($s) 1927=cut 1928 1929sub shortest_path_parents { 1930 my ($class, $s) = @_; 1931 $class->isa_class_method('shortest_path_parents'); 1932 1933 $class->throw_fatal("Missing required arguments") unless ( $s ); 1934 1935 # This is can be a heavy query, and we may need to call this 1936 # method often, so use caching 1937 my $graph; 1938 unless ( $graph = $class->cache('device_graph') ){ 1939 $graph = $class->get_device_graph(); 1940 $class->cache('device_graph', $graph); 1941 } 1942 1943 $logger->debug("Netdot::Device::shortest_path_parents: Determining all shortest paths to Device id $s"); 1944 1945 my %cost; 1946 my %parents; 1947 my %dist; 1948 my $infinity = 1000000; 1949 my @nodes = keys %$graph; 1950 my @q = @nodes; 1951 1952 # Set all distances to infinity, except the source 1953 # Set default cost to 1 1954 foreach my $n ( @nodes ) { 1955 $dist{$n} = $infinity; 1956 $cost{$n} = 1; 1957 } 1958 $dist{$s} = 0; 1959 1960 # Get path costs 1961 # Use caching too 1962 my $q; 1963 unless ( $q = $class->cache('device_path_costs') ){ 1964 my $dbh = $class->db_Main; 1965 $q = $dbh->selectall_arrayref(" 1966 SELECT device.id, device.monitoring_path_cost 1967 FROM device 1968 WHERE device.monitoring_path_cost > 1" 1969 ); 1970 $class->cache('device_path_costs', $q); 1971 } 1972 foreach my $row ( @$q ){ 1973 my ($id, $cost) = @$row; 1974 $cost{$id} = $cost; 1975 } 1976 1977 while ( @q ) { 1978 # sort unsolved by distance from root 1979 @q = sort { $dist{$a} <=> $dist{$b} } @q; 1980 1981 # we'll solve the closest node. 1982 my $n = shift @q; 1983 1984 # now, look at all the nodes connected to n 1985 foreach my $n2 ( keys %{$graph->{$n}} ) { 1986 1987 # .. and find out if any of their estimated distances 1988 # can be improved if we go through n 1989 if ( $dist{$n2} >= ($dist{$n} + $cost{$n}) ) { 1990 $dist{$n2} = $dist{$n} + $cost{$n}; 1991 # Make sure all our parents have same shortest distance 1992 foreach my $p ( keys %{$parents{$n2}} ){ 1993 delete $parents{$n2}{$p} if ( $dist{$p}+$cost{$p} > $dist{$n}+$cost{$n} ); 1994 } 1995 $parents{$n2}{$n} = 1; 1996 } 1997 } 1998 } 1999 return \%parents; 2000} 2001 2002 2003 2004=head1 INSTANCE METHODS 2005 2006=cut 2007 2008############################################################################ 2009########################## INSTANCE METHODS ################################ 2010############################################################################ 2011 2012 2013############################################################################ 2014 2015=head2 add_contact_lists - Add Contact Lists to Device 2016 2017 Arguments: 2018 Array reference of ContactList objects or single ContactList object. 2019 If called with no arguments, it assigns the default contact list. 2020 Returns: 2021 Array of DeviceContacts objects 2022 Examples: 2023 2024 $self->add_contact_lists(\@cl); 2025 2026=cut 2027 2028sub add_contact_lists{ 2029 my ($self, $argv) = @_; 2030 $self->isa_object_method('add_contact_lists'); 2031 2032 my @cls; 2033 if ( ! $argv ){ 2034 my $confcl = $self->config->get('DEFAULT_CONTACTLIST'); 2035 if ( my $default_cl = ContactList->search(name=>$confcl)->first ){ 2036 push @cls, $default_cl; 2037 }else{ 2038 $logger->warn("add_contact_lists: Default Contact List not found: $confcl"); 2039 return; 2040 } 2041 }else{ 2042 if( ref($argv) eq "ARRAY" ){ 2043 @cls = @{ $argv }; 2044 }else{ 2045 push @cls, $argv; 2046 } 2047 } 2048 my @ret; 2049 foreach my $cl ( @cls ){ 2050 my $n = DeviceContacts->insert({ device => $self, contactlist => $cl }); 2051 push @ret, $n; 2052 } 2053 return @ret; 2054} 2055 2056############################################################################ 2057 2058=head2 has_layer - Determine if Device performs a given OSI layer function 2059 2060 2061 Arguments: 2062 Layer number 2063 Returns: 2064 True/False 2065 2066 Examples: 2067 $device->has_layer(2); 2068 2069=cut 2070 2071sub has_layer { 2072 my ($self, $layer) = @_; 2073 $self->isa_object_method('has_layer'); 2074 2075 my $layers = $self->layers(); 2076 return undef unless defined($layers); 2077 return undef unless length($layers); 2078 return substr($layers,8-$layer, 1); 2079} 2080 2081############################################################################ 2082 2083=head2 list_layers - Return a list of active OSI layers 2084 2085 Arguments: 2086 None 2087 Returns: 2088 Array of scalars 2089 Examples: 2090 $device->list_layers(); 2091 2092=cut 2093 2094sub list_layers { 2095 my ($self) = @_; 2096 $self->isa_object_method('list_layers'); 2097 my @layers; 2098 for ( my($i)=1; $i<=8; $i++ ){ 2099 push @layers, $i if ( $self->has_layer($i) ); 2100 } 2101 return @layers; 2102} 2103 2104######################################################################### 2105 2106=head2 arp_update - Update ARP cache in DB 2107 2108 Arguments: 2109 Hash with the following keys: 2110 session - SNMP Session 2111 cache - hash reference with arp cache info (optional) 2112 timestamp - Time Stamp (optional) 2113 no_update_tree - Do not update IP tree 2114 atomic - Flag. Perform atomic updates. 2115 Returns: 2116 True if successful 2117 2118 Examples: 2119 $self->arp_update(); 2120 2121=cut 2122 2123sub arp_update { 2124 my ($self, %argv) = @_; 2125 $self->isa_object_method('arp_update'); 2126 my $class = ref($self); 2127 2128 my $host = $self->fqdn; 2129 my $timestamp = $argv{timestamp} || $self->timestamp; 2130 2131 unless ( $self->collect_arp ){ 2132 $logger->debug(sub{"Device::arp_update: $host: Collect ARP option off. Skipping"}); 2133 return; 2134 } 2135 if ( $self->is_in_downtime ){ 2136 $logger->debug(sub{"Device::arp_update: $host in downtime. Skipping"}); 2137 return; 2138 } 2139 2140 # Fetch from SNMP if necessary 2141 my $cache = $argv{cache} || $class->_exec_timeout( 2142 $host, sub{ return $self->get_arp(session=>$argv{session}) } 2143 ); 2144 2145 unless ( keys %$cache ){ 2146 $logger->debug("$host: ARP cache empty"); 2147 return; 2148 } 2149 2150 # Measure only db update time 2151 my $start = time; 2152 $logger->debug(sub{"$host: Updating ARP cache"}); 2153 2154 # Create ArpCache object 2155 2156 my $ac; 2157 eval { 2158 $ac = ArpCache->insert({device=>$self->id, tstamp=>$timestamp}); 2159 }; 2160 if ( my $e = $@ ){ 2161 $logger->warn(sprintf("Device %s: Could not insert ArpCache at %s: %s", $self->fqdn, 2162 $timestamp, $e)); 2163 return; 2164 } 2165 2166 $self->_update_macs_from_arp_cache(caches => [$cache], 2167 timestamp => $timestamp, 2168 atomic => $argv{atomic}, 2169 ); 2170 2171 $self->_update_ips_from_arp_cache(caches => [$cache], 2172 timestamp => $timestamp, 2173 no_update_tree => $argv{no_update_tree}, 2174 atomic => $argv{atomic}, 2175 ); 2176 2177 my ($arp_count, @ce_updates); 2178 2179 foreach my $version ( keys %$cache ){ 2180 foreach my $intid ( keys %{$cache->{$version}} ){ 2181 foreach my $ip ( keys %{$cache->{$version}{$intid}} ){ 2182 my $mac = $cache->{$version}{$intid}{$ip}; 2183 $arp_count++; 2184 push @ce_updates, { 2185 arpcache => $ac->id, 2186 interface => $intid, 2187 ipaddr => Ipblock->ip2int($ip), 2188 version => $version, 2189 physaddr => $mac, 2190 }; 2191 } 2192 } 2193 } 2194 if ( $argv{atomic} ){ 2195 Netdot::Model->do_transaction( sub{ return ArpCacheEntry->fast_insert(list=>\@ce_updates) } ); 2196 }else{ 2197 ArpCacheEntry->fast_insert(list=>\@ce_updates); 2198 } 2199 2200 # Set the last_arp timestamp 2201 $self->update({last_arp=>$timestamp}); 2202 2203 my $end = time; 2204 $logger->debug(sub{ sprintf("$host: ARP cache updated. %s entries in %s", 2205 $arp_count, $self->sec2dhms($end-$start) )}); 2206 2207 return 1; 2208} 2209 2210############################################################################ 2211 2212=head2 get_arp - Fetch ARP and IPv6 ND tables 2213 2214 Arguments: 2215 session - SNMP session (optional) 2216 Returns: 2217 Hashref of hashrefs containing: 2218 ip version -> interface id -> mac address = ip address 2219 Examples: 2220 my $cache = $self->get_arp(%args) 2221=cut 2222 2223sub get_arp { 2224 my ($self, %argv) = @_; 2225 $self->isa_object_method('get_arp'); 2226 my $host = $self->fqdn; 2227 2228 unless ( $self->collect_arp ){ 2229 $logger->debug(sub{"Device::get_arp: $host: Collect ARP option off. Skipping"}); 2230 return; 2231 } 2232 if ( $self->is_in_downtime ){ 2233 $logger->debug(sub{"Device::get_arp: $host in downtime. Skipping"}); 2234 return; 2235 } 2236 2237 # This will hold both ARP and v6 ND caches 2238 my %cache; 2239 2240 ### v4 ARP 2241 my $start = time; 2242 my $arp_count = 0; 2243 my $arp_cache = $self->_get_arp_from_snmp(session=>$argv{session}); 2244 foreach ( keys %$arp_cache ){ 2245 $cache{'4'}{$_} = $arp_cache->{$_}; 2246 $arp_count+= scalar(keys %{$arp_cache->{$_}}) 2247 } 2248 my $end = time; 2249 $logger->info(sub{ sprintf("$host: ARP cache fetched. %s entries in %s", 2250 $arp_count, $self->sec2dhms($end-$start) ) }); 2251 2252 ### v6 ND 2253 if ( $self->config->get('GET_IPV6_ND') ){ 2254 $start = time; 2255 my $nd_count = 0; 2256 my $nd_cache = $self->_get_v6_nd_from_snmp(session=>$argv{session}); 2257 # Here we have to go one level deeper in order to 2258 # avoid losing the previous entries 2259 foreach ( keys %$nd_cache ){ 2260 foreach my $ip ( keys %{$nd_cache->{$_}} ){ 2261 $cache{'6'}{$_}{$ip} = $nd_cache->{$_}->{$ip}; 2262 $nd_count++; 2263 } 2264 } 2265 $end = time; 2266 $logger->info(sub{ sprintf("$host: IPv6 ND cache fetched. %s entries in %s", 2267 $nd_count, $self->sec2dhms($end-$start) ) }); 2268 } 2269 2270 return \%cache; 2271} 2272 2273 2274 2275######################################################################### 2276 2277=head2 fwt_update - Update Forwarding Table in DB 2278 2279 Arguments: 2280 Hash with the following keys: 2281 session - SNMP Session (optional) 2282 fwt - hash reference with FWT info (optional) 2283 timestamp - Time Stamp (optional) 2284 atomic - Flag. Perform atomic updates. 2285 Returns: 2286 True if successful 2287 2288 Examples: 2289 $self->fwt_update(); 2290 2291=cut 2292 2293sub fwt_update { 2294 my ($self, %argv) = @_; 2295 $self->isa_object_method('fwt_update'); 2296 my $class = ref($self); 2297 2298 my $host = $self->fqdn; 2299 my $timestamp = $argv{timestamp} || $self->timestamp; 2300 2301 unless ( $self->collect_fwt ){ 2302 $logger->debug(sub{"Device::fwt_update: $host: Collect FWT option off. Skipping"}); 2303 return; 2304 } 2305 if ( $self->is_in_downtime ){ 2306 $logger->debug(sub{"Device::fwt_update: $host in downtime. Skipping"}); 2307 return; 2308 } 2309 2310 # Fetch from SNMP if necessary 2311 my $fwt = $argv{fwt} || 2312 $class->_exec_timeout($host, sub{ return $self->get_fwt(session=>$argv{session}) } ); 2313 2314 unless ( keys %$fwt ){ 2315 $logger->debug("$host: FWT empty"); 2316 return; 2317 } 2318 2319 # Measure only db update time 2320 my $start = time; 2321 2322 $logger->debug(sub{"$host: Updating Forwarding Table (FWT)"}); 2323 2324 # Create FWTable object 2325 my $fw; 2326 eval { 2327 $fw = FWTable->insert({device => $self->id, 2328 tstamp => $timestamp}); 2329 }; 2330 if ( my $e = $@ ){ 2331 $logger->warn(sprintf("Device %s: Could not insert FWTable at %s: %s", $self->fqdn, $timestamp, $e)); 2332 return; 2333 } 2334 $self->_update_macs_from_fwt(caches => [$fwt], 2335 timestamp => $timestamp, 2336 atomic => $argv{atomic}, 2337 ); 2338 2339 my @fw_updates; 2340 foreach my $intid ( keys %{$fwt} ){ 2341 foreach my $mac ( keys %{$fwt->{$intid}} ){ 2342 push @fw_updates, { 2343 fwtable => $fw->id, 2344 interface => $intid, 2345 physaddr => $mac, 2346 }; 2347 } 2348 } 2349 2350 if ( $argv{atomic} ){ 2351 Netdot::Model->do_transaction( sub{ return FWTableEntry->fast_insert(list=>\@fw_updates) } ); 2352 }else{ 2353 FWTableEntry->fast_insert(list=>\@fw_updates); 2354 } 2355 2356 ############################################################## 2357 # Set the last_fwt timestamp 2358 $self->update({last_fwt=>$timestamp}); 2359 2360 my $end = time; 2361 $logger->debug(sub{ sprintf("$host: FWT updated. %s entries in %s", 2362 scalar @fw_updates, $self->sec2dhms($end-$start) )}); 2363 2364 return 1; 2365} 2366 2367 2368############################################################################ 2369 2370=head2 get_fwt - Fetch forwarding tables 2371 2372 Arguments: 2373 session - SNMP session (optional) 2374 Returns: 2375 Hashref 2376 Examples: 2377 my $fwt = $self->get_fwt(%args) 2378=cut 2379 2380sub get_fwt { 2381 my ($self, %argv) = @_; 2382 $self->isa_object_method('get_fwt'); 2383 my $class = ref($self); 2384 my $host = $self->fqdn; 2385 my $fwt = {}; 2386 2387 unless ( $self->collect_fwt ){ 2388 $logger->debug(sub{"Device::get_fwt: $host: Collect FWT option off. Skipping"}); 2389 return; 2390 } 2391 if ( $self->is_in_downtime ){ 2392 $logger->debug(sub{"Device::get_fwt: $host in downtime. Skipping"}); 2393 return; 2394 } 2395 2396 my $start = time; 2397 my $fwt_count = 0; 2398 $fwt = $self->_get_fwt_from_snmp(session=>$argv{session}); 2399 map { $fwt_count+= scalar(keys %{$fwt->{$_}}) } keys %$fwt; 2400 my $end = time; 2401 $logger->info(sub{ sprintf("$host: FWT fetched. %s entries in %s", 2402 $fwt_count, $self->sec2dhms($end-$start) ) }); 2403 return $fwt; 2404} 2405 2406 2407############################################################################ 2408 2409=head2 delete - Delete Device object 2410 2411 We override the insert method for extra functionality: 2412 - Remove orphaned Resource Records if necessary 2413 - Remove orphaned Asset records if necessary 2414 2415 Arguments: 2416 None 2417 Returns: 2418 True if successful 2419 2420 Examples: 2421 $device->delete(); 2422 2423=cut 2424 2425sub delete { 2426 my ($self) = @_; 2427 $self->isa_object_method('delete'); 2428 2429 # We don't want to delete dynamic addresses 2430 if ( my $ips = $self->get_ips ){ 2431 foreach my $ip ( @$ips ) { 2432 if ( $ip->status && $ip->status->name eq 'Dynamic' ){ 2433 $ip->update({interface=>undef}); 2434 } 2435 } 2436 } 2437 2438 # If the RR had a RRADDR, it was probably deleted. 2439 # Otherwise, we do it here. 2440 my $rrid = ( $self->name )? $self->name->id : ""; 2441 2442 $self->SUPER::delete(); 2443 2444 if ( my $rr = RR->retrieve($rrid) ){ 2445 $rr->delete() unless $rr->a_records; 2446 } 2447 2448 return 1; 2449} 2450 2451############################################################################ 2452 2453=head2 short_name - Get/Set name of Device 2454 2455 The device name is actually a pointer to the Resorce Record (RR) table 2456 2457 Arguments: 2458 name string (optional) 2459 Returns: 2460 Short name of Device (Resource Record Name) 2461 Examples: 2462 $device->short_name('switch1'); 2463 2464=cut 2465 2466sub short_name { 2467 my ($self, $name) = @_; 2468 $self->isa_object_method('short_name'); 2469 2470 my $rr; 2471 $self->throw_fatal("Model::Device::short_name: Device id ". $self->id ." has no RR defined") 2472 unless ( $rr = $self->name ); 2473 if ( $name ){ 2474 $rr->name($name); 2475 $rr->update; 2476 } 2477 return $rr->name; 2478} 2479 2480############################################################################ 2481 2482=head2 product - Get Device Product 2483 2484 Arguments: 2485 None 2486 Returns: 2487 Product object 2488 Examples: 2489 my $product_object = $device->product; 2490 2491=cut 2492 2493sub product { 2494 my ($self) = @_; 2495 $self->isa_object_method('product'); 2496 2497 my $p = Product->search_by_device($self->id)->first; 2498 return $p 2499} 2500 2501############################################################################ 2502 2503=head2 fqdn - Get Fully Qualified Domain Name 2504 2505 Arguments: 2506 None 2507 Returns: 2508 FQDN string 2509 Examples: 2510 print $device->fqdn(), "\n"; 2511 2512=cut 2513 2514sub fqdn { 2515 my $self = shift; 2516 $self->isa_object_method('fqdn'); 2517 $self->name && return $self->name->get_label; 2518} 2519 2520############################################################################ 2521 2522=head2 get_label - Overrides label method 2523 2524 Arguments: 2525 None 2526 Returns: 2527 FQDN string 2528 Examples: 2529 print $device->get_label(), "\n"; 2530 2531=cut 2532 2533sub get_label { 2534 my $self = shift; 2535 $self->isa_object_method('get_label'); 2536 return $self->fqdn; 2537} 2538 2539############################################################################ 2540 2541=head2 is_in_downtime - Is this device within downtime period? 2542 2543 Arguments: 2544 None 2545 Returns: 2546 True or false 2547 Examples: 2548 if ( $device->is_in_downtime ) ... 2549 2550=cut 2551 2552sub is_in_downtime { 2553 my ($self) = @_; 2554 2555 if ( $self->down_from && $self->down_until && 2556 $self->down_from ne '0000-00-00' && $self->down_until ne '0000-00-00' ){ 2557 my $time1 = $self->sqldate2time($self->down_from); 2558 my $time2 = $self->sqldate2time($self->down_until); 2559 my $now = time; 2560 if ( $time1 < $now && $now < $time2 ){ 2561 return 1; 2562 } 2563 } 2564 return 0; 2565} 2566 2567############################################################################ 2568 2569=head2 update - Update Device in Database 2570 2571 We override the update method for extra functionality: 2572 - Update 'last_updated' field with current timestamp 2573 - snmp_managed flag turns off all other snmp access flags 2574 - Validate various arguments 2575 2576 Arguments: 2577 Hash ref with Device fields 2578 Returns: 2579 See Class::DBI update() 2580 Example: 2581 $device->update( \%data ); 2582 2583=cut 2584 2585sub update { 2586 my ($self, $argv) = @_; 2587 2588 # Update the timestamp 2589 $argv->{last_updated} = $self->timestamp; 2590 2591 if ( exists $argv->{snmp_managed} && !($argv->{snmp_managed}) ){ 2592 # Means it's being set to 0 or undef 2593 # Turn off other flags 2594 $argv->{canautoupdate} = 0; 2595 $argv->{snmp_polling} = 0; 2596 $argv->{collect_arp} = 0; 2597 $argv->{collect_fwt} = 0; 2598 } 2599 $self->_validate_args($argv); 2600 2601 return $self->SUPER::update($argv); 2602} 2603 2604############################################################################ 2605 2606=head2 update_bgp_peering - Update/Insert BGP Peering information using SNMP info 2607 2608 2609 Arguments: 2610 Hash with the following keys 2611 peer - Hashref containing Peer SNMP info: 2612 address 2613 asname 2614 asnumber 2615 orgname 2616 bgppeerid 2617 old_peerings - Hash ref containing old peering objects 2618 Returns: 2619 BGPPeering object or undef if error 2620 Example: 2621 foreach my $peer ( keys %{$info->{bgp_peer}} ){ 2622 $self->update_bgp_peering(peer => $info->{bgp_peer}->{$peer}, 2623 old_peerings => \%old_peerings); 2624 } 2625 2626=cut 2627 2628sub update_bgp_peering { 2629 my ($self, %argv) = @_; 2630 my ($peer, $old_peerings) = @argv{"peer", "old_peerings"}; 2631 $self->isa_object_method('update_bgp_peering'); 2632 2633 $self->throw_fatal('Model::Device::update_bgp_peering: '. 2634 'Missing required arguments: peer, old_peerings') 2635 unless ( $peer && $old_peerings ); 2636 2637 my $host = $self->fqdn; 2638 my $p; # bgppeering object 2639 2640 # Check if we have basic Entity info 2641 my $entity; 2642 if ( $peer->{asname} || $peer->{orgname} || $peer->{asnumber} ){ 2643 2644 my $entityname = $peer->{orgname} || $peer->{asname}; 2645 $entityname .= " ($peer->{asnumber})" if $peer->{asnumber}; 2646 2647 # Check if Entity exists (notice it's an OR search) 2648 my @where; 2649 push @where, { asnumber => $peer->{asnumber} } if $peer->{asnumber}; 2650 push @where, { asname => $peer->{asname} } if $peer->{asname}; 2651 push @where, { name => $entityname }; 2652 2653 if ( $entity = Entity->search_where(\@where)->first ){ 2654 # Update AS stuff 2655 $entity->update({asname => $peer->{asname}, 2656 asnumber => $peer->{asnumber}}); 2657 }else{ 2658 # Doesn't exist. Create Entity 2659 # Build Entity info 2660 my %etmp = ( name => $entityname, 2661 asname => $peer->{asname}, 2662 asnumber => $peer->{asnumber}, 2663 ); 2664 2665 $logger->info(sprintf("%s: Peer Entity %s not found. Inserting", 2666 $host, $entityname )); 2667 2668 $entity = Entity->insert( \%etmp ); 2669 $logger->info(sprintf("%s: Created Peer Entity %s.", $host, $entityname)); 2670 } 2671 2672 # Make sure Entity has role "peer" 2673 if ( my $type = (EntityType->search(name => "Peer"))[0] ){ 2674 my %eroletmp = ( entity => $entity, type => $type ); 2675 my $erole; 2676 if ( $erole = EntityRole->search(%eroletmp)->first ){ 2677 $logger->debug(sub{ sprintf("%s: Entity %s already has 'Peer' role", 2678 $host, $entityname )}); 2679 }else{ 2680 EntityRole->insert(\%eroletmp); 2681 $logger->info(sprintf("%s: Added 'Peer' role to Entity %s", 2682 $host, $entityname )); 2683 } 2684 } 2685 2686 }else{ 2687 $logger->warn( sprintf("%s: Missing peer info. Cannot associate peering %s with an entity", 2688 $host, $peer->{address}) ); 2689 $entity = Entity->search(name=>"Unknown")->first; 2690 } 2691 2692 # Create a hash with the peering's info for update or insert 2693 my %pstate = (device => $self, 2694 entity => $entity, 2695 bgppeerid => $peer->{bgppeerid}, 2696 bgppeeraddr => $peer->{address}, 2697 state => $peer->{state}, 2698 ); 2699 2700 # Check if peering exists 2701 foreach my $peerid ( keys %{ $old_peerings } ){ 2702 2703 my $old_peer = $old_peerings->{$peerid}; 2704 if ( $old_peer->bgppeeraddr eq $peer->{address} ){ 2705 2706 # Peering Exists. 2707 $p = $old_peer; 2708 2709 # Delete from list of old peerings 2710 delete $old_peerings->{$peerid}; 2711 last; 2712 } 2713 } 2714 if ( $p ){ 2715 # Update in case anything has changed 2716 # Only change last_changed if the state has changed 2717 if ( defined $p->state && defined $pstate{state} && 2718 $p->state ne $pstate{state} ){ 2719 $pstate{last_changed} = $self->timestamp; 2720 } 2721 my $r = $p->update(\%pstate); 2722 $logger->debug(sub{ sprintf("%s: Updated Peering with: %s. ", $host, $entity->name)}) if $r; 2723 2724 }else{ 2725 # Peering doesn't exist. Create. 2726 # 2727 if ( $self->config->get('MONITOR_BGP_PEERINGS') ){ 2728 $pstate{monitored} = 1; 2729 }else{ 2730 $pstate{monitored} = 0; 2731 } 2732 $pstate{last_changed} = $self->timestamp; 2733 2734 # Assign the first available contactlist from the device list 2735 $pstate{contactlist} = $self->contacts->first->contactlist; 2736 2737 $p = BGPPeering->insert(\%pstate); 2738 my $peer_label; 2739 $peer_label = $entity->name if ($entity && ref($entity)) ; 2740 $peer_label = $peer->{address} if ($peer && ref($peer)); 2741 $peer_label ||= "n\a"; 2742 $logger->info(sprintf("%s: Inserted new Peering with: %s. ", $host, $peer_label)); 2743 } 2744 return $p; 2745} 2746 2747 2748############################################################################ 2749 2750=head2 snmp_update - Update Devices using SNMP information 2751 2752 2753 Arguments: 2754 Hash with the following keys: 2755 do_info Update device info (default) 2756 do_fwt Update forwarding tables 2757 do_arp Update ARP cache 2758 info Hashref with device info (optional) 2759 communities Arrayref of SNMP Community strings 2760 version SNMP Version [1|2|3] 2761 timeout SNMP Timeout 2762 retries SNMP Retries 2763 session SNMP Session 2764 add_subnets Flag. When discovering routers, add subnets to database if they do not exist 2765 subs_inherit Flag. When adding subnets, have them inherit information from the Device 2766 bgp_peers Flag. When discovering routers, update bgp_peers 2767 pretend Flag. Do not commit changes to the database 2768 timestamp Time Stamp (optional) 2769 no_update_tree Flag. Do not update IP tree. 2770 atomic Flag. Perform atomic updates. 2771 device_is_new Flag. Specifies that device was just created. 2772 2773 Returns: 2774 Updated Device object 2775 2776 Example: 2777 my $device = $device->snmp_update(do_info=>1, do_fwt=>1); 2778 2779=cut 2780 2781sub snmp_update { 2782 my ($self, %argv) = @_; 2783 $self->isa_object_method('snmp_update'); 2784 my $class = ref($self); 2785 unless ( $argv{do_info} || $argv{do_fwt} || $argv{do_arp} ){ 2786 $argv{do_info} = 1; 2787 } 2788 my $atomic = defined $argv{atomic} ? $argv{atomic} : $self->config->get('ATOMIC_DEVICE_UPDATES'); 2789 2790 my $host = $self->fqdn; 2791 my $timestamp = $argv{timestamp} || $self->timestamp; 2792 2793 my $sinfo = $argv{session}; 2794 unless ( $argv{info} || $sinfo ){ 2795 $sinfo = $self->_get_snmp_session(communities => $argv{communities}, 2796 version => $argv{version}, 2797 timeout => $argv{timeout}, 2798 retries => $argv{retries}, 2799 sec_name => $argv{sec_name}, 2800 sec_level => $argv{sec_level}, 2801 auth_proto => $argv{auth_proto}, 2802 auth_pass => $argv{auth_pass}, 2803 priv_proto => $argv{priv_proto}, 2804 priv_pass => $argv{priv_pass}, 2805 sqe => $argv{sqe}, 2806 ); 2807 2808 return unless $sinfo; 2809 2810 } 2811 2812 # Re-bless into appropriate sub-class if needed 2813 my %r_args; 2814 if ( $sinfo ){ 2815 $r_args{sclass} = $sinfo->class; 2816 $r_args{sysobjectid} = $sinfo->id; 2817 }elsif ( $argv{info} ){ 2818 $r_args{sclass} ||= $argv{info}->{_sclass}; 2819 $r_args{sysobjectid} = $argv{info}->{sysobjectid}; 2820 } 2821 $self->_netdot_rebless(%r_args); 2822 2823 if ( $argv{do_info} ){ 2824 my $info = $argv{info} || 2825 $class->_exec_timeout($host, 2826 sub{ return $self->get_snmp_info(session => $sinfo, 2827 bgp_peers => $argv{bgp_peers}) }); 2828 2829 if ( $atomic && !$argv{pretend} ){ 2830 Netdot::Model->do_transaction( 2831 sub{ return $self->info_update(add_subnets => $argv{add_subnets}, 2832 subs_inherit => $argv{subs_inherit}, 2833 bgp_peers => $argv{bgp_peers}, 2834 session => $sinfo, 2835 info => $info, 2836 device_is_new => $argv{device_is_new}, 2837 ) } ); 2838 }else{ 2839 $self->info_update(add_subnets => $argv{add_subnets}, 2840 subs_inherit => $argv{subs_inherit}, 2841 bgp_peers => $argv{bgp_peers}, 2842 pretend => $argv{pretend}, 2843 session => $sinfo, 2844 info => $info, 2845 device_is_new => $argv{device_is_new}, 2846 ); 2847 } 2848 } 2849 if ( $argv{do_fwt} ){ 2850 if ( $self->collect_fwt ){ 2851 $self->fwt_update(session => $sinfo, 2852 timestamp => $timestamp, 2853 atomic => $atomic, 2854 ); 2855 }else{ 2856 $logger->debug(sub{"Device::snmp_update: $host: Collect FWT option off. Skipping"}); 2857 } 2858 } 2859 if ( $argv{do_arp} ){ 2860 if ( $self->collect_arp ){ 2861 $self->arp_update(session => $sinfo, 2862 timestamp => $timestamp, 2863 no_update_tree => $argv{no_update_tree}, 2864 atomic => $atomic, 2865 ); 2866 }else{ 2867 $logger->debug(sub{"Device::snmp_update: $host: Collect ARP option off. Skipping"}); 2868 } 2869 } 2870 $logger->info("Device::snmp_update: $host: Finished updating"); 2871} 2872 2873 2874############################################################################ 2875 2876=head2 info_update - Update Device in Database using SNMP info 2877 2878 Updates an existing Device based on information gathered via SNMP. 2879 This is exclusively an object method. 2880 2881 Arguments: 2882 Hash with the following keys: 2883 session SNMP session (optional) 2884 info Hashref with Device SNMP information. 2885 If not passed, this method will try to get it. 2886 communities Arrayref of SNMP Community strings 2887 version SNMP Version [1|2|3] 2888 timeout SNMP Timeout 2889 retries SNMP Retries 2890 sec_name SNMP Security Name 2891 sec_level SNMP Security Level 2892 auth_proto SNMP Authentication Protocol 2893 auth_pass SNMP Auth Key 2894 priv_proto SNMP Privacy Protocol 2895 priv_pass SNMP Privacy Key 2896 add_subnets Flag. When discovering routers, add subnets to database if they do not exist 2897 subs_inherit Flag. When adding subnets, have them inherit information from the Device 2898 bgp_peers Flag. When discovering routers, update bgp_peers 2899 pretend Flag. Do not commit changes to the database 2900 device_is_new Flag. Specifies that device was just created. 2901 2902 Returns: 2903 Updated Device object 2904 2905 Example: 2906 my $device = $device->info_update(); 2907 2908=cut 2909 2910sub info_update { 2911 my ($self, %argv) = @_; 2912 $self->isa_object_method('info_update'); 2913 2914 my $class = ref $self; 2915 my $start = time; 2916 2917 # Show full name in output 2918 my $host = $self->fqdn; 2919 2920 my $info = $argv{info}; 2921 unless ( $info ){ 2922 # Get SNMP info 2923 if ( $argv{session} ){ 2924 $info = $class->_exec_timeout($host, 2925 sub{ return $self->get_snmp_info(bgp_peers => $argv{bgp_peers}, 2926 session => $argv{session}, 2927 ) }); 2928 2929 }else{ 2930 my $version = $argv{snmp_version} || $self->snmp_version 2931 || $self->config->get('DEFAULT_SNMPVERSION'); 2932 2933 my $timeout = $argv{timeout} || $self->config->get('DEFAULT_SNMPTIMEOUT'); 2934 my $retries = $argv{retries} || $self->config->get('DEFAULT_SNMPRETRIES'); 2935 my $communities = $argv{communities} || [$self->community] 2936 || $self->config->get('DEFAULT_SNMPCOMMUNITIES'); 2937 my $sec_name = $argv{sec_name} || $self->snmp_securityname 2938 || $self->config->get('DEFAULT_SNMP_SECNAME'); 2939 my $sec_level = $argv{sec_level} || $self->snmp_securitylevel 2940 || $self->config->get('DEFAULT_SNMP_SECLEVEL'); 2941 my $auth_proto = $argv{auth_proto} || $self->snmp_authprotocol 2942 || $self->config->get('DEFAULT_SNMP_AUTHPROTO'); 2943 my $auth_pass = $argv{auth_pass} || $self->snmp_authkey 2944 || $self->config->get('DEFAULT_SNMP_AUTHPASS'); 2945 my $priv_proto = $argv{priv_proto} || $self->snmp_privprotocol 2946 || $self->config->get('DEFAULT_SNMP_PRIVPROTO'); 2947 my $priv_pass = $argv{priv_pass} || $self->snmp_privkey 2948 || $self->config->get('DEFAULT_SNMP_PRIVPASS'); 2949 $info = $class->_exec_timeout($host, 2950 sub{ 2951 return $self->get_snmp_info(communities => $communities, 2952 version => $version, 2953 timeout => $timeout, 2954 retries => $retries, 2955 sec_name => $sec_name, 2956 sec_level => $sec_level, 2957 auth_proto => $auth_proto, 2958 auth_pass => $auth_pass, 2959 priv_proto => $priv_proto, 2960 priv_pass => $priv_pass, 2961 bgp_peers => $argv{bgp_peers}, 2962 ) }); 2963 } 2964 } 2965 unless ( $info ){ 2966 $logger->error("$host: No SNMP info received"); 2967 return; 2968 } 2969 unless ( ref($info) eq 'HASH' ){ 2970 $self->throw_fatal("Model::Device::info_update: Invalid info data structure"); 2971 } 2972 2973 # Pretend works by turning off autocommit in the DB handle and rolling back 2974 # all changes at the end 2975 if ( $argv{pretend} ){ 2976 $logger->info("$host: Performing a dry-run"); 2977 unless ( Netdot::Model->db_auto_commit(0) == 0 ){ 2978 $self->throw_fatal("Model::Device::info_update: Unable to set AutoCommit off"); 2979 } 2980 } 2981 2982 # Data that will be passed to the update method 2983 my %devtmp; 2984 2985 ############################################################## 2986 # Fill in some basic device info 2987 foreach my $field ( qw( community layers ipforwarding sysname 2988 sysdescription syslocation os collect_arp collect_fwt ) ){ 2989 $devtmp{$field} = $info->{$field} if exists $info->{$field}; 2990 } 2991 2992 ############################################################## 2993 if ( my $ipb = $self->_assign_snmp_target($info) ){ 2994 $devtmp{snmp_target} = $ipb; 2995 } 2996 2997 ############################################################## 2998 # Asset 2999 my %asset_args; 3000 my $base_mac = $self->_assign_base_mac($info); 3001 $asset_args{physaddr} = $base_mac if defined $base_mac; 3002 $asset_args{reserved_for} = ""; # needs to be cleared when device gets installed 3003 3004 # Make sure S/N contains something 3005 if (defined $info->{serial_number} && $info->{serial_number} =~ /\S+/ ){ 3006 $asset_args{serial_number} = $info->{serial_number}; 3007 } 3008 3009 # Search for an asset based on either serial number or base MAC 3010 # If two different assets are found, we will have to pick one and 3011 # delete the other, as this leads to errors 3012 my $asset_sn = Asset->search(serial_number=>$asset_args{serial_number})->first 3013 if $asset_args{serial_number}; 3014 my $asset_phy = Asset->search(physaddr=>$asset_args{physaddr})->first 3015 if $asset_args{physaddr}; 3016 3017 my $asset; 3018 if ( $asset_sn && $asset_phy ){ 3019 if ($asset_sn->id != $asset_phy->id ){ 3020 # Not the same. We'll just choose the one 3021 # with the serial number 3022 # Reassign any devices or device_modules' assets 3023 foreach my $dev ( $asset_phy->devices ){ 3024 $dev->update({asset_id=>$asset_sn}); 3025 } 3026 foreach my $mod ( $asset_phy->device_modules ){ 3027 $mod->update({asset_id=>$asset_sn}); 3028 } 3029 $logger->debug(sub{ sprintf("%s: Deleting duplicate asset %s", 3030 $host, $asset_phy->get_label)}); 3031 $asset_phy->delete(); 3032 } 3033 $asset = $asset_sn; 3034 }elsif ( $asset_sn ){ 3035 $asset = $asset_sn; 3036 }elsif ( $asset_phy ){ 3037 $asset = $asset_phy; 3038 } 3039 3040 my $dev_product; 3041 if ( $asset ){ 3042 # Make sure that the data is complete with the latest info we got 3043 $asset->update(\%asset_args); 3044 $dev_product = $asset->product_id; 3045 }elsif ( $asset_args{serial_number} || $asset_args{physaddr} ){ 3046 $dev_product = $self->_assign_product($info); 3047 $asset_args{product_id} = $dev_product->id; 3048 $asset = Asset->insert(\%asset_args); 3049 }else{ 3050 $dev_product = $self->_assign_product($info); 3051 } 3052 $devtmp{asset_id} = $asset->id if $asset; 3053 3054 3055 ############################################################## 3056 # Things to do only when creating the device 3057 if ( $argv{device_is_new} ){ 3058 3059 $devtmp{snmp_version} = $info->{snmp_version} if exists $info->{snmp_version}; 3060 3061 if ( $asset && $asset->product_id ){ 3062 my $val = $self->_assign_device_monitored($asset->product_id); 3063 $devtmp{monitored} = $val; 3064 $devtmp{snmp_polling} = $val; 3065 if ( my $st = $devtmp{snmp_target} ){ 3066 $st->update({monitored => $val}); 3067 } 3068 } 3069 3070 if ( my $g = $self->_assign_monitor_config_group($info) ){ 3071 $devtmp{monitor_config} = 1; 3072 $devtmp{monitor_config_group} = $g; 3073 } 3074 } 3075 3076 ############################################################## 3077 # Spanning Tree 3078 $self->_update_stp_info($info, \%devtmp); 3079 3080 ############################################################## 3081 # Modules 3082 $self->_update_modules( 3083 info => $info->{module}, 3084 manufacturer => $dev_product->manufacturer, 3085 ); 3086 3087 ############################################## 3088 $self->_update_interfaces(info => $info, 3089 add_subnets => $argv{add_subnets}, 3090 subs_inherit => $argv{subs_inherit}, 3091 overwrite_descr => ($info->{ipforwarding})? 1 : 0, 3092 ); 3093 3094 ############################################################### 3095 # Update BGP info 3096 # 3097 my $update_bgp = defined($argv{bgp_peers}) ? 3098 $argv{bgp_peers} : $self->config->get('ADD_BGP_PEERS'); 3099 3100 if ( $update_bgp ){ 3101 $self->_update_bgp_info(bgp_local_as => $info->{bgplocalas}, 3102 bgp_id => $info->{bgpid}, 3103 peers => $info->{bgp_peer}, 3104 newdev => \%devtmp); 3105 } 3106 3107 ############################################################## 3108 # Update Device object 3109 $self->update(\%devtmp); 3110 3111 my $end = time; 3112 $logger->debug(sub{ sprintf("%s: SNMP update completed in %s", 3113 $host, $self->sec2dhms($end-$start))}); 3114 3115 if ( $argv{pretend} ){ 3116 $logger->debug(sub{"$host: Rolling back changes"}); 3117 eval { 3118 $self->dbi_rollback; 3119 }; 3120 if ( my $e = $@ ){ 3121 $self->throw_fatal("Model::Device::info_update: Rollback Failed!: $e"); 3122 } 3123 $logger->debug(sub{"Model::Device::info_update: Turning AutoCommit back on"}); 3124 unless ( Netdot::Model->db_auto_commit(1) == 1 ){ 3125 $self->throw_fatal("Model::Device::info_update: Unable to set AutoCommit on"); 3126 } 3127 } 3128 3129 return $self; 3130} 3131 3132############################################################################ 3133 3134=head2 add_ip - Add an IP address 3135 3136 Arguments: 3137 IP address in dotted-quad notation 3138 Interface (Optional. Will use first interface if not passed) 3139 Returns: 3140 Ipblock object 3141 Examples: 3142 $device->add_ip('10.0.0.1', $int); 3143 3144=cut 3145 3146sub add_ip { 3147 my ($self, $address, $int) = @_; 3148 $self->isa_object_method('add_ip'); 3149 $self->throw_user("Missing required IP address argument") 3150 unless $address; 3151 my ($prefix, $version); 3152 if ( Ipblock->matches_v4($address) ){ 3153 $version = 4; $prefix = 32; 3154 }elsif ( Ipblock->matches_v6($address) ){ 3155 $version = 6; $prefix = 128; 3156 }else{ 3157 $self->throw_user("Invalid IP address: $address"); 3158 } 3159 $int ||= $self->interfaces->first; 3160 $self->throw_user("Need an interface to add this IP to") 3161 unless $int; 3162 my $ipb; 3163 if ( $ipb = Ipblock->search(address=>$address, 3164 version=>$version, prefix=>$prefix)->first ){ 3165 $ipb->update({interface=>$int, status=>'Static'}); 3166 3167 }else{ 3168 $ipb = Ipblock->insert({address=>$address, prefix=>$prefix, 3169 version=>$version, interface=>$int, 3170 status=>'Static'}); 3171 } 3172 return $ipb; 3173} 3174############################################################################ 3175 3176=head2 get_ips - Get all IP addresses from a device 3177 3178 Arguments: 3179 Hash with the following keys: 3180 sort_by [address|interface] 3181 Returns: 3182 Arrayref of Ipblock objects 3183 Examples: 3184 print $_->address, "\n" foreach $device->get_ips( sort_by => 'address' ); 3185 3186=cut 3187 3188sub get_ips { 3189 my ($self, %argv) = @_; 3190 $self->isa_object_method('get_ips'); 3191 3192 $argv{sort_by} ||= "address"; 3193 3194 my @ips; 3195 if ( $argv{sort_by} eq "address" ){ 3196 @ips = Ipblock->search_devipsbyaddr($self->id); 3197 }elsif ( $argv{sort_by} eq "interface" ){ 3198 @ips = Ipblock->search_devipsbyint($self->id); 3199 }else{ 3200 $self->throw_fatal("Model::Device::get_ips: Invalid sort criteria: $argv{sort_by}"); 3201 } 3202 return \@ips; 3203} 3204 3205############################################################################ 3206 3207=head2 get_neighbors - Get all Interface neighbors 3208 3209 Arguments: 3210 None 3211 Returns: 3212 Hash ref with key = local int id, value = remote int id 3213 Examples: 3214 my $neighbors = $device->get_neighbors(); 3215=cut 3216 3217sub get_neighbors { 3218 my ($self, $devs) = @_; 3219 $self->isa_object_method('get_neighbors'); 3220 3221 my %res; 3222 foreach my $int ( $self->interfaces ){ 3223 my $n = $int->neighbor(); 3224 $res{$int->id} = $n if $n; 3225 } 3226 return \%res; 3227} 3228 3229############################################################################ 3230 3231=head2 get_circuits - Get all Interface circuits 3232 3233 Arguments: 3234 None 3235 Returns: 3236 Array of circuit objects 3237 Examples: 3238 my @circuits = $device->get_circuits(); 3239=cut 3240 3241sub get_circuits { 3242 my ($self) = @_; 3243 $self->isa_object_method('get_circuits'); 3244 3245 my @res; 3246 foreach my $int ( $self->interfaces ){ 3247 my $c = $int->circuit(); 3248 push @res, $c if ( $c ); 3249 } 3250 return @res if @res; 3251 return; 3252} 3253 3254############################################################################ 3255 3256=head2 remove_neighbors - Remove neighbors from all interfaces 3257 3258 Arguments: 3259 None 3260 Returns: 3261 True 3262 Examples: 3263 $device->remove_neighbors(); 3264=cut 3265 3266sub remove_neighbors { 3267 my ($self) = @_; 3268 foreach my $int ( $self->interfaces ){ 3269 $int->remove_neighbor(); 3270 } 3271} 3272 3273############################################################################ 3274 3275=head2 get_subnets - Get all the subnets in which this device has any addresses 3276 3277 Arguments: 3278 None 3279 Returns: 3280 hashref of Ipblock objects, keyed by id 3281 Examples: 3282 my %s = $device->get_subnets(); 3283 print $s{$_}->address, "\n" foreach keys %s; 3284 3285=cut 3286 3287sub get_subnets { 3288 my $self = shift; 3289 $self->isa_object_method('get_subnets'); 3290 3291 my %subnets; 3292 foreach my $ip ( @{ $self->get_ips() } ){ 3293 my $subnet; 3294 if ( ($subnet = $ip->parent) 3295 && $subnet->status 3296 && $subnet->status->name eq "Subnet" ){ 3297 $subnets{$subnet->id} = $subnet; 3298 } 3299 } 3300 return \%subnets; 3301} 3302 3303############################################################################ 3304 3305=head2 add_interfaces - Manually add a number of interfaces to an existing device 3306 3307 The new interfaces will be added with numbers starting after the highest existing 3308 interface number 3309 3310 Arguments: 3311 Number of interfaces 3312 Returns: 3313 Arrayref of new interface objects 3314 Examples: 3315 $device->add_interfaces(2); 3316 3317=cut 3318 3319sub add_interfaces { 3320 my ($self, $num) = @_; 3321 $self->isa_object_method('add_interfaces'); 3322 3323 unless ( $num > 0 ){ 3324 $self->throw_user("Invalid number of Interfaces to add: $num"); 3325 } 3326 # Determine highest numbered interface in this device 3327 my @ints; 3328 my $start; 3329 my $ints = $self->ints_by_number; 3330 if ( defined $ints && scalar @$ints ){ 3331 my $lastidx = @$ints - 1; 3332 $start = int ( $ints->[$lastidx]->number ); 3333 }else{ 3334 $start = 0; 3335 } 3336 my %tmp = ( device => $self->id, number => $start ); 3337 my $i; 3338 my @newints; 3339 for ( $i = 0; $i < $num; $i++ ){ 3340 $tmp{number}++; 3341 push @newints, Interface->insert( \%tmp ); 3342 } 3343 return \@newints; 3344} 3345 3346############################################################################ 3347 3348=head2 ints_by_number - Retrieve interfaces from a Device and sort by number. 3349 3350 The number field can actually contain alpha characters. If so, 3351 sort alphanumerically, removing any non-alpha characters. Otherwise, 3352 sort numerically. 3353 3354 Arguments: 3355 None 3356 Returns: 3357 Sorted arrayref of interface objects 3358 Examples: 3359 print $_->number, "\n" foreach @{ $device->ints_by_number() }; 3360 3361=cut 3362 3363sub ints_by_number { 3364 my $self = shift; 3365 $self->isa_object_method('ints_by_number'); 3366 3367 my @ifs = $self->interfaces(); 3368 3369 my ($nondigit, $letters); 3370 for ( @ifs ) { 3371 if ($_->number =~ /\D/){ 3372 $nondigit = 1; 3373 if ($_->number =~ /[A-Za-z]/){ 3374 $letters = 1; 3375 } 3376 } 3377 } 3378 3379 if ( $nondigit ){ 3380 my @tmp; 3381 foreach my $if ( @ifs ){ 3382 my $num = $if->number; 3383 $num =~ s/\W+//g; 3384 push @tmp, [$num, $if]; 3385 } 3386 if ( $letters ){ 3387 @ifs = map { $_->[1] } sort { $a->[0] cmp $b->[0] } @tmp; 3388 }else{ 3389 @ifs = map { $_->[1] } sort { $a->[0] <=> $b->[0] } @tmp; 3390 } 3391 }else{ 3392 @ifs = sort { $a->number <=> $b->number } @ifs; 3393 } 3394 3395 return \@ifs; 3396} 3397 3398############################################################################ 3399 3400=head2 ints_by_name - Retrieve interfaces from a Device and sort by name. 3401 3402 This method deals with the problem of sorting Interface names that contain numbers. 3403 Simple alphabetical sorting does not yield useful results. 3404 3405 Arguments: 3406 None 3407 Returns: 3408 Sorted arrayref of interface objects 3409 Exampless: 3410 3411=cut 3412 3413sub ints_by_name { 3414 my $self = shift; 3415 $self->isa_object_method('ints_by_name'); 3416 3417 my @ifs = $self->interfaces; 3418 3419 # The following was borrowed from Netviewer 3420 # and was slightly modified to handle Netdot Interface objects. 3421 # Yes. It is ugly. 3422 @ifs = ( map { $_->[0] } sort { 3423 $a->[1] cmp $b->[1] 3424 || $a->[2] <=> $b->[2] 3425 || $a->[3] <=> $b->[3] 3426 || $a->[4] <=> $b->[4] 3427 || $a->[5] <=> $b->[5] 3428 || $a->[6] <=> $b->[6] 3429 || $a->[7] <=> $b->[7] 3430 || $a->[8] <=> $b->[8] 3431 || $a->[0]->name cmp $b->[0]->name } 3432 map{ [ $_, $_->name =~ /^(\D+)\d/, 3433 ( split( /\D+/, $_->name ))[0,1,2,3,4,5,6,7,8] ] } @ifs); 3434 3435 return \@ifs; 3436} 3437 3438############################################################################ 3439 3440=head2 ints_by_speed - Retrieve interfaces from a Device and sort by speed. 3441 3442 Arguments: 3443 None 3444 Returns: 3445 Sorted array of interface objects 3446 3447=cut 3448 3449sub ints_by_speed { 3450 my $self = shift; 3451 $self->isa_object_method('ints_by_speed'); 3452 3453 my @ifs = Interface->search( device => $self->id, {order_by => 'speed'}); 3454 3455 return \@ifs; 3456} 3457 3458############################################################################ 3459 3460=head2 ints_by_vlan - Retrieve interfaces from a Device and sort by vlan ID 3461 3462Arguments: None 3463Returns: Sorted arrayref of interface objects 3464 3465Note: If the interface has/belongs to more than one vlan, sort function will only 3466use one of the values. 3467 3468=cut 3469 3470sub ints_by_vlan { 3471 my $self = shift; 3472 $self->isa_object_method('ints_by_vlan'); 3473 3474 my @ifs = $self->interfaces(); 3475 my @tmp = map { [ ($_->vlans) ? ($_->vlans)[0]->vlan->vid : 0, $_] } @ifs; 3476 @ifs = map { $_->[1] } sort { $a->[0] <=> $b->[0] } @tmp; 3477 3478 return \@ifs; 3479} 3480 3481############################################################################ 3482 3483=head2 ints_by_jack - Retrieve interfaces from a Device and sort by Jack id 3484 3485Arguments: None 3486Returns: Sorted arrayref of interface objects 3487 3488=cut 3489 3490sub ints_by_jack { 3491 my ( $self ) = @_; 3492 3493 $self->isa_object_method('ints_by_jack'); 3494 3495 my @ifs = $self->interfaces(); 3496 3497 my @tmp = map { [ ($_->jack) ? $_->jack->jackid : 0, $_] } @ifs; 3498 @ifs = map { $_->[1] } sort { $a->[0] cmp $b->[0] } @tmp; 3499 3500 return \@ifs; 3501} 3502 3503############################################################################ 3504 3505=head2 ints_by_descr - Retrieve interfaces from a Device and sort by description 3506 3507Arguments: None 3508Returns: Sorted arrayref of interface objects 3509 3510=cut 3511 3512sub ints_by_descr { 3513 my ( $self, $o ) = @_; 3514 $self->isa_object_method('ints_by_descr'); 3515 3516 my @ifs = Interface->search( device => $self->id, {order_by => 'description'}); 3517 3518 return \@ifs; 3519} 3520 3521############################################################################ 3522 3523=head2 ints_by_monitored - Retrieve interfaces from a Device and sort by 'monitored' field 3524 3525Arguments: None 3526Returns: Sorted arrayref of interface objects 3527 3528=cut 3529 3530sub ints_by_monitored { 3531 my ( $self, $o ) = @_; 3532 $self->isa_object_method('ints_by_monitored'); 3533 3534 my @ifs = Interface->search( device => $self->id, {order_by => 'monitored DESC'}); 3535 3536 return \@ifs; 3537} 3538 3539############################################################################ 3540 3541=head2 ints_by_status - Retrieve interfaces from a Device and sort by 'status' field 3542 3543Arguments: None 3544Returns: Sorted arrayref of interface objects 3545 3546=cut 3547 3548sub ints_by_status { 3549 my ( $self, $o ) = @_; 3550 $self->isa_object_method('ints_by_status'); 3551 3552 my @ifs = Interface->search( device => $self->id, {order_by => 'oper_status'}); 3553 3554 return \@ifs; 3555} 3556 3557############################################################################ 3558 3559=head2 ints_by_snmp - Retrieve interfaces from a Device and sort by 'snmp_managed' field 3560 3561Arguments: None 3562Returns: Sorted arrayref of interface objects 3563 3564=cut 3565 3566sub ints_by_snmp { 3567 my ( $self, $o ) = @_; 3568 $self->isa_object_method('ints_by_snmp'); 3569 3570 my @ifs = Interface->search( device => $self->id, {order_by => 'snmp_managed DESC'}); 3571 3572 return \@ifs; 3573} 3574 3575############################################################################ 3576 3577=head2 interfaces_by - Retrieve sorted list of interfaces from a Device 3578 3579 This will call different methods depending on the sort field specified 3580 3581 Arguments: 3582 Hash with the following keys: 3583 sort_by [number|name|speed|vlan|jack|descr|monitored|snmp] 3584 Returns: 3585 Sorted arrayref of interface objects 3586 Examples: 3587 print $_->name, "\n" foreach $device->interfaces_by('name'); 3588 3589=cut 3590 3591sub interfaces_by { 3592 my ( $self, $sort ) = @_; 3593 $self->isa_object_method('interfaces_by'); 3594 3595 $sort ||= "number"; 3596 3597 if ( $sort eq "number" ){ 3598 return $self->ints_by_number; 3599 }elsif ( $sort eq "name" ){ 3600 return $self->ints_by_name; 3601 }elsif( $sort eq "speed" ){ 3602 return $self->ints_by_speed; 3603 }elsif( $sort eq "vlan" ){ 3604 return $self->ints_by_vlan; 3605 }elsif( $sort eq "jack" ){ 3606 return $self->ints_by_jack; 3607 }elsif( $sort eq "descr"){ 3608 return $self->ints_by_descr; 3609 }elsif( $sort eq "monitored"){ 3610 return $self->ints_by_monitored; 3611 }elsif( $sort eq "snmp"){ 3612 return $self->ints_by_snmp; 3613 }elsif( $sort eq "oper_status"){ 3614 return $self->ints_by_status; 3615 }else{ 3616 $self->throw_fatal("Model::Device::interfaces_by: Unknown sort field: $sort"); 3617 } 3618} 3619 3620############################################################################ 3621 3622=head2 bgppeers_by_ip - Sort by remote IP 3623 3624 Arguments: 3625 Array ref of BGPPeering objects 3626 Returns: 3627 Sorted arrayref of BGPPeering objects 3628=cut 3629 3630sub bgppeers_by_ip { 3631 my ( $self, $peers ) = @_; 3632 $self->isa_object_method('bgppeers_by_ip'); 3633 3634 my @peers = map { $_->[1] } 3635 sort { pack("C4"=>$a->[0] =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/) 3636 cmp pack("C4"=>$b->[0] =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/); } 3637 map { [$_->bgppeeraddr, $_] } @$peers ; 3638 3639 return unless scalar @peers; 3640 return \@peers; 3641} 3642 3643############################################################################ 3644 3645=head2 bgppeers_by_id - Sort by BGP ID 3646 3647 Arguments: 3648 Array ref of BGPPeering objects 3649 Returns: 3650 Sorted arrayref of BGPPeering objects 3651 3652=cut 3653sub bgppeers_by_id { 3654 my ( $self, $peers ) = @_; 3655 $self->isa_object_method('bgppeers_by_id'); 3656 3657 my @peers = map { $_->[1] } 3658 sort { pack("C4"=>$a->[0] =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/) 3659 cmp pack("C4"=>$b->[0] =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/); } 3660 map { [$_->bgppeerid, $_] } @$peers ; 3661 3662 return unless scalar @peers; 3663 return \@peers; 3664} 3665 3666############################################################################ 3667 3668=head2 bgppeers_by_entity - Sort by Entity name, AS number or AS Name 3669 3670 Arguments: 3671 Array ref of BGPPeering objects, 3672 Entity table field to sort by [name|asnumber|asname] 3673 Returns: 3674 Sorted array of BGPPeering objects 3675 3676=cut 3677 3678sub bgppeers_by_entity { 3679 my ( $self, $peers, $sort ) = @_; 3680 $self->isa_object_method('bgppeers_by_entity'); 3681 3682 $sort ||= "name"; 3683 unless ( $sort =~ /^name|asnumber|asname$/o ){ 3684 $self->throw_fatal("Model::Device::bgppeers_by_entity: Invalid Entity field: $sort"); 3685 } 3686 my $sortsub = ($sort eq "asnumber") ? 3687 sub{$a->entity->$sort <=> $b->entity->$sort} : 3688 sub{$a->entity->$sort cmp $b->entity->$sort}; 3689 my @peers = sort $sortsub grep { defined $_->entity } @$peers; 3690 3691 return unless scalar @peers; 3692 return \@peers; 3693} 3694 3695 3696############################################################################ 3697 3698=head2 get_bgp_peers - Retrieve BGP peers that match certain criteria and sort them 3699 3700 This overrides the method auto-generated by Class::DBI 3701 3702 Arguments: 3703 Hash with the following keys: 3704 entity <string> Return peers whose entity name matches <string> 3705 id <integer> Return peers whose ID matches <integer> 3706 ip <address> Return peers whose Remote IP matches <address> 3707 as <integer> Return peers whose AS matches <integer> 3708 type <string> Return peers of type [internal|external|all*] 3709 sort <string> Sort by [entity*|asnumber|asname|id|ip|state] 3710 3711 (*) default 3712 3713 Returns: 3714 Sorted arrayref of BGPPeering objects 3715 Examples: 3716 print $_->entity->name, "\n" foreach @{ $device->get_bgp_peers() }; 3717 3718=cut 3719 3720sub get_bgp_peers { 3721 my ( $self, %argv ) = @_; 3722 $self->isa_object_method('get_bgp_peers'); 3723 3724 $argv{type} ||= "all"; 3725 $argv{sort} ||= "entity"; 3726 my @peers; 3727 if ( $argv{entity} ){ 3728 @peers = grep { $_->entity->name eq $argv{entity} } $self->bgppeers; 3729 }elsif ( $argv{id} ){ 3730 @peers = grep { $_->bgppeerid eq $argv{id} } $self->bgppeers; 3731 }elsif ( $argv{ip} ){ 3732 @peers = grep { $_->bgppeeraddr eq $argv{id} } $self->bgppeers; 3733 }elsif ( $argv{as} ){ 3734 @peers = grep { $_->asnumber eq $argv{as} } $self->bgppeers; 3735 }elsif ( $argv{type} ){ 3736 if ( $argv{type} eq "internal" ){ 3737 @peers = grep { defined $_->entity && defined $self->bgplocalas && 3738 $_->entity->asnumber == $self->bgplocalas->number } $self->bgppeers; 3739 }elsif ( $argv{type} eq "external" ){ 3740 @peers = grep { defined $_->entity && defined $self->bgplocalas && 3741 $_->entity->asnumber != $self->bgplocalas->number } $self->bgppeers; 3742 }elsif ( $argv{type} eq "all" ){ 3743 @peers = $self->bgppeers(); 3744 }else{ 3745 $self->throw_fatal("Model::Device::get_bgp_peers: Invalid type: $argv{type}"); 3746 } 3747 }elsif ( ! $argv{sort} ){ 3748 $self->throw_fatal("Model::Device::get_bgp_peers: Missing or invalid search criteria"); 3749 } 3750 if ( $argv{sort} =~ /entity|asnumber|asname/ ){ 3751 $argv{sort} =~ s/entity/name/; 3752 return $self->bgppeers_by_entity(\@peers, $argv{sort}); 3753 }elsif( $argv{sort} eq "ip" ){ 3754 return $self->bgppeers_by_ip(\@peers); 3755 }elsif( $argv{sort} eq "id" ){ 3756 return $self->bgppeers_by_id(\@peers); 3757 }elsif( $argv{sort} eq "state" ){ 3758 @peers = sort { $a->state cmp $b->state } @peers; 3759 }else{ 3760 $self->throw_fatal("Model::Device::get_bgp_peers: Invalid sort argument: $argv{sort}"); 3761 } 3762 3763 return \@peers if scalar @peers; 3764 return; 3765} 3766 3767################################################################################################### 3768 3769=head2 set_overwrite_if_descr - Set the overwrite_description flag in all interfaces of this device 3770 3771 This flag controls whether the ifAlias value returned from the Device should 3772 overwrite the value of the Interface description field in the database. 3773 Some devices return a null string, which would erase any user-entered descriptions in Netdot. 3774 This method sets that value of this flag for each interface in the device. 3775 3776 Arguments: 3777 0 or 1 (true or false) 3778 Returns: 3779 True if successful 3780 Example: 3781 $device->set_overwrite_if_descr(1); 3782 3783=cut 3784 3785sub set_overwrite_if_descr { 3786 my ($self, $value) = @_; 3787 $self->isa_object_method("set_overwrite_if_descr"); 3788 3789 $self->throw_fatal("Model::Device::set_overwrite_if_descr: Invalid value: $value. Should be 0|1") 3790 unless ( $value =~ /^0|1$/ ); 3791 3792 foreach my $int ( $self->interfaces ){ 3793 $int->update({overwrite_descr=>$value}); 3794 } 3795 3796 return 1; 3797} 3798 3799################################################################################################### 3800 3801=head2 set_interfaces_auto_dns - Sets auto_dns flag on all IP interfaces of this device 3802 3803 Arguments: 3804 0 or 1 (true or false) 3805 Returns: 3806 True if successful 3807 Example: 3808 $device->set_interfaces_auto_dns(1); 3809 3810=cut 3811 3812sub set_interfaces_auto_dns { 3813 my ($self, $value) = @_; 3814 $self->isa_object_method("set_interfaces_auto_dns"); 3815 3816 $self->throw_fatal("Model::Device::set_interfaces_auto_dns: Invalid value: $value. Should be 0|1") 3817 unless ( $value =~ /^0|1$/ ); 3818 3819 foreach my $int ( $self->interfaces ){ 3820 # Ony interfaces with IP addresses 3821 next unless $int->ips; 3822 $int->update({auto_dns=>$value}); 3823 } 3824 3825 return 1; 3826} 3827 3828 3829 3830 3831 3832##################################################################### 3833# 3834# Private methods 3835# 3836##################################################################### 3837 3838############################################################################ 3839# Validate arguments to insert and update 3840# 3841# Arguments: 3842# hash reference with field/value pairs for Device 3843# Returns: 3844# True or throws exceptions 3845# 3846sub _validate_args { 3847 my ($proto, $args) = @_; 3848 my ($self, $class); 3849 if ( $class = ref $proto ){ 3850 $self = $proto; 3851 }else{ 3852 $class = $proto; 3853 } 3854 3855 # We need a name always 3856 $args->{name} ||= $self->name if ( defined $self ); 3857 unless ( $args->{name} ){ 3858 $class->throw_user("Name cannot be null"); 3859 } 3860 3861 # SNMP Version 3862 if ( defined $args->{snmp_version} ){ 3863 if ( $args->{snmp_version} !~ /^1|2|3$/ ){ 3864 $class->throw_user("Invalid SNMP version. It must be either 1, 2 or 3"); 3865 } 3866 } 3867 3868 # Asset 3869 if ( $args->{asset_id} && (my $asset = Asset->retrieve(int($args->{asset_id}))) ){ 3870 # is there a device associated with this asset we're given? 3871 if ( my $otherdev = $asset->devices->first ){ 3872 if ( defined $self ){ 3873 if ( $self->id != $otherdev->id ){ 3874 my $msg = sprintf("%s: Existing device: %s uses S/N %s, MAC %s", 3875 $self->fqdn, $otherdev->fqdn, $asset->serial_number, 3876 $asset->physaddr); 3877 if ( Netdot->config->get('ENFORCE_DEVICE_UNIQUENESS') ){ 3878 $self->throw_user($msg); 3879 }else{ 3880 $logger->warn($msg); 3881 } 3882 } 3883 }else{ 3884 my $msg = sprintf("Existing device: %s uses S/N %s, MAC %s ", 3885 $asset->serial_number, $asset->physaddr, $otherdev->fqdn); 3886 if ( Netdot->config->get('ENFORCE_DEVICE_UNIQUENESS') ){ 3887 $class->throw_user($msg); 3888 }else{ 3889 $logger->warn($msg); 3890 } 3891 } 3892 } 3893 } 3894 3895 # Host Device 3896 if ( $self && $args->{host_device} ){ 3897 my $hd = scalar($args->{host_device}); 3898 if ( $hd == $self->id ){ 3899 $class->throw_user("Device cannot be a host of itself"); 3900 } 3901 } 3902 3903 return 1; 3904} 3905 3906######################################################################################## 3907# _layer_active - Determine if a particular layer is active in the layers bit string 3908# 3909# 3910# Arguments: 3911# layers bit string 3912# layer number 3913# Returns: 3914# True/False 3915# 3916# Examples: 3917# $class->_layer_active(2); 3918# 3919sub _layer_active { 3920 my ($class, $layers, $layer) = @_; 3921 $class->isa_class_method('_layer_active'); 3922 3923 $class->throw_fatal("Model::Device::_layer_active: Missing required arguments: layers && layer") 3924 unless ( $layers && $layer ); 3925 3926 return substr($layers,8-$layer, 1); 3927} 3928 3929############################################################################# 3930sub _make_sinfo_object 3931{ 3932 my ($self, $sclass, %sinfoargs) = @_; 3933 if ( $sinfoargs{sqe} ) { 3934 $sinfoargs{Session} = Netdot::FakeSNMPSession->new(%sinfoargs); 3935 $logger->debug(sub{"Device::_make_sinfo_object: with SQE" }); 3936 } 3937 return $sclass->new( %sinfoargs ); 3938} 3939 3940############################################################################# 3941# _get_snmp_session - Establish a SNMP session. Tries to reuse sessions. 3942# 3943# Arguments: 3944# Arrayref with the following keys (mostly optional): 3945# host IP or hostname (required unless called as instance method) 3946# communities Arrayref of SNMP Community strings 3947# version SNMP version 3948# sec_name SNMP Security Name 3949# sec_level SNMP Security Level 3950# auth_proto SNMP Authentication Protocol 3951# auth_pass SNMP Auth Key 3952# priv_proto SNMP Privacy Protocol 3953# priv_pass SNMP Privacy Key 3954# bulkwalk Whether to use SNMP BULK 3955# timeout SNMP Timeout 3956# retries Number of retries after Timeout 3957# sclass SNMP::Info class 3958# 3959# Returns: 3960# SNMP::Info object if successful 3961# 3962# Examples: 3963# 3964# Instance call: 3965# my $session = $device->_get_snmp_session(); 3966# 3967# Class call: 3968# my $session = Device->_get_snmp_session(host=>$hostname, communities=>['public']); 3969# 3970 3971sub _get_snmp_session { 3972 my ($self, %argv) = @_; 3973 3974 my $class; 3975 my $sclass = $argv{sclass} if defined ( $argv{sclass} ); 3976 3977 if ( $class = ref($self) ){ 3978 # Being called as an instance method 3979 3980 # Do not continue unless snmp_managed flag is on 3981 unless ( $self->snmp_managed ){ 3982 $logger->debug(sprintf("Device %s not SNMP-managed. Aborting.", $self->fqdn)); 3983 return; 3984 } 3985 3986 # Do not continue if we've exceeded the connection attempts threshold 3987 if ( $self->snmp_down == 1 ){ 3988 $logger->info(sprintf("Device %s has been marked as snmp_down. Aborting.", $self->fqdn)); 3989 return; 3990 } 3991 3992 # Fill up SNMP arguments from object if it wasn't passed to us 3993 if ( !defined $argv{communities} && $self->community ){ 3994 push @{$argv{communities}}, $self->community; 3995 } 3996 $argv{bulkwalk} ||= $self->snmp_bulk; 3997 $argv{version} ||= $self->snmp_version; 3998 if ( $argv{version} == 3 ){ 3999 $argv{sec_name} ||= $self->snmp_securityname; 4000 $argv{sec_level} ||= $self->snmp_securitylevel; 4001 $argv{auth_proto} ||= $self->snmp_authprotocol; 4002 $argv{auth_pass} ||= $self->snmp_authkey; 4003 $argv{priv_proto} ||= $self->snmp_privprotocol; 4004 $argv{priv_pass} ||= $self->snmp_privkey; 4005 } 4006 4007 # We might already have a SNMP::Info class 4008 $sclass ||= $self->{_sclass} if defined $self->{_sclass}; 4009 4010 # Fill out some arguments if not given explicitly 4011 unless ( $argv{host} ){ 4012 if ( $self->snmp_target ){ 4013 $argv{host} = $self->snmp_target->address; 4014 }else{ 4015 $argv{host} = $self->fqdn; 4016 } 4017 } 4018 $self->throw_user(sprintf("Could not determine IP nor hostname for Device id: %d", $self->id)) 4019 unless $argv{host}; 4020 4021 }else{ 4022 $self->throw_fatal("Model::Device::_get_snmp_session: Missing required arguments: host") 4023 unless $argv{host}; 4024 } 4025 4026 # Set defaults 4027 my %sinfoargs = ( 4028 DestHost => $argv{host}, 4029 Version => $argv{version} || $self->config->get('DEFAULT_SNMPVERSION'), 4030 Timeout => (defined $argv{timeout})? $argv{timeout} : $self->config->get('DEFAULT_SNMPTIMEOUT'), 4031 Retries => (defined $argv{retries}) ? $argv{retries} : $self->config->get('DEFAULT_SNMPRETRIES'), 4032 Debug => ( $logger->is_debug() )? 1 : 0, 4033 BulkWalk => (defined $argv{bulkwalk}) ? $argv{bulkwalk} : $self->config->get('DEFAULT_SNMPBULK'), 4034 BulkRepeaters => $self->config->get('DEFAULT_SNMPBULK_MAX_REPEATERS'), 4035 MibDirs => \@MIBDIRS, 4036 sqe => $argv{sqe}, 4037 ); 4038 4039 if ( defined $sclass && $sclass ne 'SNMP::Info' ) { 4040 $sinfoargs{AutoSpecify} = 0; 4041 }else{ 4042 $sinfoargs{AutoSpecify} = 1; 4043 $sclass = 'SNMP::Info'; 4044 } 4045 4046 4047 # Turn off bulkwalk if we're using Net-SNMP 5.2.3 or 5.3.1. 4048 if ( !$sinfoargs{sqe} && $sinfoargs{BulkWalk} == 1 && ($SNMP::VERSION eq '5.0203' || $SNMP::VERSION eq '5.0301') 4049 && !$self->config->get('IGNORE_BUGGY_SNMP_CHECK')) { 4050 $logger->info("Turning off bulkwalk due to buggy Net-SNMP $SNMP::VERSION"); 4051 $sinfoargs{BulkWalk} = 0; 4052 } 4053 4054 my $sinfo; 4055 4056 # Deal with the number of connection attempts and the snmp_down flag 4057 # We need to do this in a couple of places 4058 sub _check_max_attempts { 4059 my ($self, $host) = @_; 4060 return unless ref($self); # only applies to instance calls 4061 my $max = $self->config->get('MAX_SNMP_CONNECTION_ATTEMPTS'); 4062 my $count = $self->snmp_conn_attempts || 0; 4063 $count++; 4064 $self->update({snmp_conn_attempts=>$count}); 4065 $logger->info(sprintf("Device::_get_snmp_session: %s: Failed connection attempts: %d", 4066 $host, $count)); 4067 if ( $max == 0 ){ 4068 # This setting implies that we're told not to limit maximum connections 4069 return; 4070 } 4071 if ( $count >= $max ){ 4072 $self->update({snmp_down=>1}); 4073 } 4074 } 4075 4076 if ( $sinfoargs{Version} == 3 ){ 4077 $sinfoargs{SecName} = $argv{sec_name} if $argv{sec_name}; 4078 $sinfoargs{SecLevel} = $argv{sec_level} if $argv{sec_level}; 4079 $sinfoargs{AuthProto} = $argv{auth_proto} if $argv{auth_proto}; 4080 $sinfoargs{AuthPass} = $argv{auth_pass} if $argv{auth_pass}; 4081 $sinfoargs{PrivProto} = $argv{priv_proto} if $argv{priv_proto}; 4082 $sinfoargs{PrivPass} = $argv{priv_pass} if $argv{priv_pass}; 4083 $sinfoargs{Context} = $argv{context} if $argv{context}; 4084 4085 $logger->debug(sub{ sprintf("Device::get_snmp_session: Trying SNMPv%d session with %s", 4086 $sinfoargs{Version}, $argv{host})}); 4087 4088 $sinfo = $self->_make_sinfo_object($sclass, %sinfoargs); 4089 4090 if ( defined $sinfo ){ 4091 # Check for errors 4092 if ( my $err = $sinfo->error ){ 4093 $self->throw_user(sprintf("Device::_get_snmp_session: SNMPv%d error: device %s: %s", 4094 $sinfoargs{Version}, $argv{host}, $err)); 4095 } 4096 4097 }else { 4098 &_check_max_attempts($self, $argv{host}); 4099 $self->throw_user(sprintf("Device::get_snmp_session: %s: SNMPv%d failed", 4100 $argv{host}, $sinfoargs{Version})); 4101 } 4102 4103 }else{ 4104 # If we still don't have any communities, get defaults from config file 4105 $argv{communities} = $self->config->get('DEFAULT_SNMPCOMMUNITIES') 4106 unless defined $argv{communities}; 4107 4108 # Try each community 4109 foreach my $community ( @{$argv{communities}} ){ 4110 4111 $sinfoargs{Community} = $community; 4112 $logger->debug(sub{ sprintf("Device::_get_snmp_session: Trying SNMPv%d session with %s, ". 4113 "community %s", 4114 $sinfoargs{Version}, $argv{host}, $sinfoargs{Community})}); 4115 $sinfo = $self->_make_sinfo_object($sclass, %sinfoargs); 4116 4117 # If v2 failed, try v1 4118 if ( !defined $sinfo && $sinfoargs{Version} == 2 ){ 4119 $logger->debug(sub{ sprintf("Device::_get_snmp_session: %s: SNMPv%d failed. Trying SNMPv1", 4120 $argv{host}, $sinfoargs{Version})}); 4121 $sinfoargs{Version} = 1; 4122 $sinfo = $self->_make_sinfo_object($sclass, %sinfoargs); 4123 } 4124 4125 if ( defined $sinfo ){ 4126 # Check for errors 4127 if ( my $err = $sinfo->error ){ 4128 $self->throw_user(sprintf("Device::_get_snmp_session: SNMPv%d error: device %s, ". 4129 "community '%s': %s", 4130 $sinfoargs{Version}, $argv{host}, $sinfoargs{Community}, $err)); 4131 } 4132 last; # If we made it here, we are fine. Stop trying communities 4133 4134 }else{ 4135 $logger->debug(sub{ sprintf("Device::_get_snmp_session: Failed SNMPv%s session with ". 4136 "%s community '%s'", 4137 $sinfoargs{Version}, $argv{host}, $sinfoargs{Community})}); 4138 } 4139 4140 } #end foreach community 4141 4142 unless ( defined $sinfo ){ 4143 &_check_max_attempts($self, $argv{host}); 4144 $self->throw_user(sprintf("Device::_get_snmp_session: Cannot connect to %s. ". 4145 "Tried communities: %s", 4146 $argv{host}, (join ', ', @{$argv{communities}}) )); 4147 } 4148 } 4149 4150 4151 if ( $class ){ 4152 # We're called as an instance method 4153 4154 # Save SNMP::Info class 4155 $logger->debug(sub{"Device::get_snmp_session: $argv{host} is: ", $sinfo->class() }); 4156 $self->{_sclass} = $sinfo->class(); 4157 4158 my %uargs; 4159 4160 # Reset dead counter and snmp_down flag 4161 $uargs{snmp_conn_attempts} = 0; $uargs{snmp_down} = 0; 4162 4163 # Fill out some SNMP parameters if they are not set 4164 $uargs{snmp_version} = $sinfoargs{Version} unless defined($self->snmp_version); 4165 $uargs{snmp_bulk} = $sinfoargs{BulkWalk} unless defined($self->snmp_bulk); 4166 4167 if ( $sinfoargs{Version} == 3 ){ 4168 # Store v3 parameters 4169 $uargs{snmp_securityname} = $sinfoargs{SecName} unless defined($self->snmp_securityname); 4170 $uargs{snmp_securitylevel} = $sinfoargs{SecLevel} unless defined($self->snmp_securitylevel); 4171 $uargs{snmp_authprotocol} = $sinfoargs{AuthProto} unless defined($self->snmp_authprotocol); 4172 $uargs{snmp_authkey} = $sinfoargs{AuthPass} unless defined($self->snmp_authkey); 4173 $uargs{snmp_privprotocol} = $sinfoargs{PrivProto} unless defined($self->snmp_privprotocol); 4174 $uargs{snmp_privkey} = $sinfoargs{PrivPass} unless defined($self->snmp_privkey); 4175 4176 }else{ 4177 $uargs{community} = $sinfoargs{Community} unless defined($self->community); 4178 } 4179 $self->update(\%uargs) if ( keys %uargs ); 4180 } 4181 $logger->debug(sub{ sprintf("SNMPv%d session with host %s established", 4182 $sinfoargs{Version}, $argv{host}) }); 4183 4184 # We want to do our own 'munging' for certain things 4185 my $munge = $sinfo->munge(); 4186 delete $munge->{'i_speed'}; # We store these as integers in the db. Munge at display 4187 $munge->{'i_speed_high'} = sub{ return $self->_munge_speed_high(@_) }; 4188 $munge->{'stp_root'} = sub{ return $self->_stp2mac(@_) }; 4189 $munge->{'stp_p_bridge'} = sub{ return $self->_stp2mac(@_) }; 4190 foreach my $m ('i_mac', 'fw_mac', 'mac', 'b_mac', 'at_paddr', 'rptrAddrTrackNewLastSrcAddress', 4191 'stp_p_port'){ 4192 $munge->{$m} = sub{ return $self->_oct2hex(@_) }; 4193 } 4194 return $sinfo; 4195} 4196 4197######################################################################### 4198# Return device's main IP, which will determine device's main name 4199# 4200sub _get_main_ip { 4201 my ($class, $info) = @_; 4202 4203 $class->throw_fatal("Model::Device::_get_main_ip: Missing required argument (info)") 4204 unless $info; 4205 my @methods = @{$class->config->get('DEVICE_NAMING_METHOD_ORDER')}; 4206 $class->throw_fatal("Model::Device::_get_main_ip: Missing or invalid configuration variable: ". 4207 "DEVICE_NAMING_METHOD_ORDER") 4208 unless scalar @methods; 4209 4210 my %allips; 4211 my @allints = keys %{$info->{interface}}; 4212 map { map { $allips{$_} = '' } keys %{$info->{interface}->{$_}->{ips}} } @allints; 4213 4214 my $ip; 4215 if ( scalar(keys %allips) == 1 ){ 4216 $ip = (keys %allips)[0]; 4217 $logger->debug(sub{"Device::_get_main_ip: Device has one IP: $ip" }); 4218 } 4219 foreach my $method ( @methods ){ 4220 $logger->debug(sub{"Device::_get_main_ip: Trying method $method" }); 4221 if ( $method eq 'sysname' && $info->{sysname} ){ 4222 my @resips = Netdot->dns->resolve_name($info->{sysname}); 4223 foreach my $resip ( @resips ){ 4224 if ( defined $resip && exists $allips{$resip} ){ 4225 $ip = $resip; 4226 last; 4227 } 4228 } 4229 }elsif ( $method eq 'highest_ip' ){ 4230 my %dec; 4231 foreach my $int ( @allints ){ 4232 map { $dec{$_} = Ipblock->ip2int($_) } keys %allips; 4233 } 4234 my @ordered = sort { $dec{$b} <=> $dec{$a} } keys %dec; 4235 $ip = $ordered[0]; 4236 }elsif ( $method =~ /loopback/ ){ 4237 my %loopbacks; 4238 foreach my $int ( @allints ){ 4239 my $name = $info->{interface}->{$int}->{name}; 4240 if ( $name && $name =~ /^loopback(\d+)/i ){ 4241 $loopbacks{$int} = $1; 4242 } 4243 } 4244 my @ordered = sort { $loopbacks{$a} <=> $loopbacks{$b} } keys %loopbacks; 4245 my $main_int; 4246 if ( $method eq 'lowest_loopback' ){ 4247 $main_int = shift @ordered; 4248 }elsif ( $method eq 'highest_loopback' ){ 4249 $main_int = pop @ordered; 4250 } 4251 if ( $main_int ){ 4252 $ip = (keys %{$info->{interface}->{$main_int}->{ips}})[0]; 4253 } 4254 }elsif ( $method eq 'router_id' ){ 4255 $ip = $info->{router_id} if defined $info->{router_id}; 4256 }elsif ( $method eq 'snmp_target' ){ 4257 $ip = $info->{snmp_target}; 4258 } 4259 4260 if ( defined $ip ){ 4261 if ( Ipblock->matches_v4($ip) && Ipblock->validate($ip) ){ 4262 $logger->debug(sub{"Device::_get_main_ip: Chose $ip using naming method: $method" }); 4263 return $ip ; 4264 }else{ 4265 $logger->debug(sub{"Device::_get_main_ip: $ip not valid. Ignoring"}); 4266 # Keep trying 4267 undef($ip); 4268 } 4269 4270 } 4271 } 4272 $logger->debug(sub{"Device::_get_main_ip: Could not determine the main IP for this device"}); 4273 return; 4274} 4275 4276######################################################################### 4277# Retrieve STP info 4278sub _get_stp_info { 4279 my ($self, %argv) = @_; 4280 4281 my $sinfo = $argv{sinfo}; 4282 my %res; 4283 foreach my $method ( 4284 'stp_root', 'stp_root_port', 'stp_priority', 4285 'i_stp_bridge', 'i_stp_port', 'i_stp_state', 4286 ){ 4287 $res{$method} = $sinfo->$method; 4288 } 4289 return \%res; 4290} 4291 4292######################################################################### 4293# Retrieve STP interface info 4294sub _get_i_stp_info { 4295 my ($self, %argv) = @_; 4296 4297 # Map method name to interface field name 4298 my %STPFIELDS = ( 4299 'i_bpdufilter_enabled' => 'bpdu_filter_enabled', 4300 'i_bpduguard_enabled' => 'bpdu_guard_enabled', 4301 'i_rootguard_enabled' => 'root_guard_enabled', 4302 'i_loopguard_enabled' => 'loop_guard_enabled', 4303 ); 4304 4305 my $sinfo = $argv{sinfo}; 4306 my %res; 4307 foreach my $method ( 4308 'i_rootguard_enabled', 'i_loopguard_enabled', 4309 'i_bpduguard_enabled', 'i_bpdufilter_enabled' 4310 ){ 4311 $res{$STPFIELDS{$method}} = $sinfo->$method; 4312 foreach my $i ( keys %{$res{$STPFIELDS{$method}}} ){ 4313 $res{$STPFIELDS{$method}}->{$i} = ($res{$STPFIELDS{$method}}->{$i} eq 'true' || 4314 $res{$STPFIELDS{$method}}->{$i} eq 'enable' )? 1 : 0; 4315 } 4316 } 4317 return \%res; 4318} 4319 4320######################################################################### 4321# Retrieve a list of device objects from given file (one per line) 4322# 4323# Arguments: File path 4324# Returns : Arrayref of device objects 4325# 4326sub _get_devs_from_file { 4327 my ($class, $file) = @_; 4328 $class->isa_class_method('get_devs_from_file'); 4329 4330 my $hosts = $class->_get_hosts_from_file($file); 4331 my @devs; 4332 foreach my $host ( keys %$hosts ){ 4333 if ( my $dev = $class->search(name=>$host)->first ){ 4334 push @devs, $dev; 4335 }else{ 4336 $logger->info("Device $host does not yet exist in the Database."); 4337 } 4338 } 4339 $class->throw_user("Device::_get_devs_from_file: No existing devices in list. ". 4340 "You might need to run a discover first.") 4341 unless ( scalar @devs ); 4342 4343 return \@devs; 4344} 4345######################################################################### 4346# Retrieve a list of hostnames/communities from given file (one per line) 4347# 4348# Arguments: File path 4349# Returns : Hashref with hostnames (or IP addresses) as key 4350# and, optionally, SNMP community as value 4351# 4352sub _get_hosts_from_file { 4353 my ($class, $file) = @_; 4354 $class->isa_class_method('get_hosts_from_file'); 4355 4356 $class->throw_user("Device::_get_hosts_from_file: Missing or invalid file: $file") 4357 unless ( defined($file) && -r $file ); 4358 4359 open(FILE, "<$file") or 4360 $class->throw_user("Can't open file $file for reading: $!"); 4361 4362 $logger->debug(sub{"Device::_get_hosts_from_file: Retrieving host list from $file" }); 4363 4364 my %hosts; 4365 while (<FILE>){ 4366 chomp($_); 4367 next if ( /^#/ ); 4368 if ( /\S+\s+\S+/ ){ 4369 my ($host, $comm) = split /\s+/, $_; 4370 $hosts{$host} = $comm; 4371 }else{ 4372 if ( /\S+/ ){ # allow for only host on line 4373 $hosts{$_} = ''; 4374 } 4375 } 4376 } 4377 close(FILE); 4378 4379 $class->throw_user("Host list is empty!") 4380 unless ( scalar keys %hosts ); 4381 4382 return \%hosts; 4383} 4384 4385######################################################################### 4386# Initialize ForkManager 4387# Arguments: None 4388# Returns : Parallel::ForkManager object 4389# 4390sub _fork_init { 4391 my ($class) = @_; 4392 $class->isa_class_method('_fork_init'); 4393 4394 # Tell DBI that we don't want to disconnect the server's DB handle 4395 my $dbh = $class->db_Main; 4396 unless ( $dbh->{InactiveDestroy} = 1 ) { 4397 $class->throw_fatal("Model::Device::_fork_init: Cannot set InactiveDestroy: ", $dbh->errstr); 4398 } 4399 4400 # MAXPROCS processes for parallel updates 4401 $logger->debug(sub{"Device::_fork_init: Launching up to $MAXPROCS children processes" }); 4402 my $pm = Parallel::ForkManager->new($MAXPROCS); 4403 4404 # Prevent SNMP::Info from loading mib-init in each forked process 4405 $logger->debug("Device::_fork_init: Loading dummy SNMP::Info object"); 4406 my $dummy = SNMP::Info->new( DestHost => 'localhost', 4407 Version => 1, 4408 AutoSpecify => 0, 4409 Debug => ( $logger->is_debug() )? 1 : 0, 4410 MibDirs => \@MIBDIRS, 4411 ); 4412 4413 return $pm; 4414} 4415 4416######################################################################### 4417# _fork_end - Wrap up ForkManager 4418# Wait for all children 4419# Set InactiveDestroy back to default 4420# 4421# Arguments: Parallel::ForkManager object 4422# Returns : True if successful 4423# 4424sub _fork_end { 4425 my ($class, $pm) = @_; 4426 $class->isa_class_method('_fork_end'); 4427 4428 # Wait for all children to finish 4429 $logger->debug(sub{"Device::_fork_end: Waiting for children..." }); 4430 $pm->wait_all_children; 4431 $logger->debug(sub{"Device::_fork_end: All children finished" }); 4432 4433 # Return DBI to its normal DESTROY behavior 4434 my $dbh = $class->db_Main; 4435 $dbh->{InactiveDestroy} = 0; 4436 return 1; 4437} 4438 4439#################################################################################### 4440# _snmp_update_parallel - Discover and/or update all devices in given list concurrently 4441# 4442# Arguments: 4443# Hash with the following keys: 4444# hosts Hashref of host names and their communities 4445# devs Arrayref of Device objects 4446# communities Arrayref of SNMP communities 4447# version SNMP version 4448# timeout SNMP timeout 4449# retries SNMP retries 4450# do_info Update Device Info 4451# do_fwt Update Forwarding Tables 4452# do_arp Update ARP caches 4453# add_subnets Flag. When discovering routers, add subnets to database if they do not exist 4454# subs_inherit Flag. When adding subnets, have them inherit information from the Device 4455# bgp_peers Flag. When discovering routers, update bgp_peers 4456# pretend Flag. Do not commit changes to the database 4457# matching Regex. Only update devices whose names match regex 4458# Returns: 4459# Device count 4460# 4461sub async (&@); 4462sub _snmp_update_parallel { 4463 my ($class, %argv) = @_; 4464 $class->isa_class_method('_snmp_update_parallel'); 4465 my $use_sqe = Netdot->config->get('USE_SNMP_QUERY_ENGINE'); 4466 my $sqe; 4467 if ($use_sqe && !$Netdot::Model::Device::_sqe_module_loaded) { 4468 $logger->info("SQE is requested, trying to load relevant modules"); 4469 eval { 4470 require Coro; Coro->import; 4471 require AnyEvent; AnyEvent->import; 4472 require Coro::AnyEvent; Coro::AnyEvent->import; 4473 require Netdot::FakeSNMPSession; Netdot::FakeSNMPSession->import; 4474 require Net::SNMP::QueryEngine::AnyEvent; Net::SNMP::QueryEngine::AnyEvent->import; 4475 $Netdot::Model::Device::_sqe_module_loaded = 1; 4476 $logger->info("SQE-related modules loaded succesfully"); 4477 }; 4478 unless ($Netdot::Model::Device::_sqe_module_loaded) { 4479 $logger->info("Failure loading SQE-related modules, disabling SQE: $@"); 4480 $use_sqe = ""; 4481 } 4482 } 4483 if ($use_sqe) { 4484 my @conn = split /:/, $use_sqe; 4485 my $check_done = AnyEvent->condvar; 4486 eval { 4487 $sqe = Net::SNMP::QueryEngine::AnyEvent->new(connect => \@conn, 4488 on_connect => sub { $check_done->send }, 4489 on_error => sub { $sqe = undef; $check_done->send }, 4490 ); 4491 $check_done->recv; 4492 if ($sqe) { 4493 $sqe->info(sub { my ($h,$ok,$r) = @_; $sqe = undef unless $ok }); 4494 $sqe->wait; 4495 } 4496 }; 4497 } 4498 if ($sqe) { 4499 $logger->info("SQE is requested and available, using it for SNMP collection"); 4500 $logger->debug("$class\::_snmp_update_parallel: Loading dummy SNMP::Info object"); 4501 my $dummy = SNMP::Info->new( DestHost => 'localhost', 4502 Version => 1, 4503 AutoSpecify => 0, 4504 Debug => ( $logger->is_debug() )? 1 : 0, 4505 MibDirs => \@MIBDIRS, 4506 ); 4507 $class->_snmp_update_parallel_sqe(%argv, sqe => $sqe); 4508 } else { 4509 if ($use_sqe) { 4510 $logger->info("SQE daemon is NOT available, using traditional method for SNMP collection"); 4511 } else { 4512 $logger->info("Using traditional method for SNMP collection"); 4513 } 4514 $class->_snmp_update_parallel_traditional(%argv); 4515 } 4516} 4517 4518sub _snmp_update_parallel_args_check { 4519 my ($class, %argv) = @_; 4520 4521 my ($hosts, $devs); 4522 if ( defined $argv{hosts} ){ 4523 $class->throw_fatal("Model::Device::_snmp_update_parallel: Invalid hosts hash") 4524 if ( ref($argv{hosts}) ne "HASH" ); 4525 $hosts = $argv{hosts}; 4526 }elsif ( defined $argv{devs} ){ 4527 $class->throw_fatal("Model::Device::_snmp_update_parallel: Invalid devs array") 4528 if ( ref($argv{devs}) ne "ARRAY" ); 4529 $devs = $argv{devs}; 4530 }else{ 4531 $class->throw_fatal("Model::Device::_snmp_update_parallel: Missing required parameters: ". 4532 "hosts or devs"); 4533 } 4534 4535 my %uargs; 4536 foreach my $field ( qw(version timeout retries sec_name sec_level auth_proto auth_pass 4537 priv_proto priv_pass add_subnets subs_inherit bgp_peers pretend 4538 do_info do_fwt do_arp sqe) ){ 4539 $uargs{$field} = $argv{$field} if defined ($argv{$field}); 4540 } 4541 4542 $uargs{no_update_tree} = 1; 4543 $uargs{timestamp} = $class->timestamp; 4544 4545 return ($hosts, $devs, %uargs); 4546} 4547 4548sub _snmp_update_get_device_args { 4549 my ($class, $dev, %args) = @_; 4550 4551 if ( $args{do_info} ){ 4552 unless ( $dev->canautoupdate ){ 4553 $logger->debug(sub{ sprintf("%s: Auto Update option off", $dev->fqdn) }); 4554 $args{do_info} = 0; 4555 } 4556 } 4557 if ( $args{do_fwt} ){ 4558 unless ( $dev->collect_fwt ){ 4559 $logger->debug(sub{ sprintf("%s: Collect FWT option off", $dev->fqdn) }); 4560 $args{do_fwt} = 0; 4561 } 4562 } 4563 if ( $args{do_arp} ){ 4564 unless ( $dev->collect_arp ){ 4565 $logger->debug(sub{ sprintf("%s: Collect ARP option off", $dev->fqdn) }); 4566 $args{do_arp} = 0; 4567 } 4568 } 4569 4570 return %args; 4571} 4572 4573sub _snmp_update_parallel_sqe { 4574 my ($class, %argv) = @_; 4575 $class->isa_class_method('_snmp_update_parallel_sqe'); 4576 4577 my ($hosts, $devs, %uargs) = $class->_snmp_update_parallel_args_check(%argv); 4578 4579 my %do_devs; 4580 4581 my $device_count = 0; 4582 my $n_polling_devices = 0; 4583 my $start = time; 4584 4585 if ( $devs ){ 4586 foreach my $dev ( @$devs ){ 4587 # Put in list 4588 $do_devs{$dev->id} = $dev; 4589 } 4590 }elsif ( $hosts ){ 4591 foreach my $host ( keys %$hosts ){ 4592 # Give preference to the community associated with the host 4593 if ( my $commstr = $hosts->{$host} ){ 4594 $uargs{communities} = [$commstr]; 4595 }else{ 4596 $uargs{communities} = $argv{communities}; 4597 } 4598 # If the device exists in the DB, we add it to the list 4599 my $dev; 4600 if ( $dev = $class->search(name=>$host)->first ){ 4601 $do_devs{$dev->id} = $dev; 4602 $logger->debug(sub{ sprintf("%s exists in DB.", $dev->fqdn) }); 4603 }else{ 4604 $device_count++; 4605 $n_polling_devices++; 4606 async { 4607 eval { $class->discover(name=> $host, %uargs); }; 4608 if ( my $e = $@ ){ 4609 $logger->error($e); 4610 exit 1; 4611 } 4612 $n_polling_devices--; 4613 }; 4614 } 4615 } 4616 } 4617 4618 # Go over list of existing devices 4619 while ( my ($id, $dev) = each %do_devs ){ 4620 4621 if ( my $regex = $argv{matching} ){ 4622 unless ( $dev->fqdn =~ /$regex/o ){ 4623 next; 4624 } 4625 } 4626 # Make sure we don't launch a process unless necessary 4627 if ( $dev->is_in_downtime() ){ 4628 $logger->debug(sub{ sprintf("Model::Device::_snmp_update_parallel_sqe: %s in downtime. Skipping", $dev->fqdn) }); 4629 next; 4630 } 4631 4632 my %args = $class->_snmp_update_get_device_args($dev, %uargs); 4633 unless ( $args{do_info} || $args{do_fwt} || $args{do_arp} ){ 4634 next; 4635 } 4636 4637 $device_count++; 4638 $n_polling_devices++; 4639 async { 4640 eval { $dev->snmp_update(%args); }; 4641 if ( my $e = $@ ){ 4642 $logger->error($e); 4643 exit 1; 4644 } 4645 $n_polling_devices--; 4646 }; 4647 } 4648 4649 while ($n_polling_devices > 0) { 4650 Coro::AnyEvent::idle_upto(5); 4651 Coro::AnyEvent::sleep(0.05); 4652 } 4653 4654 # Rebuild the IP tree if ARP caches were updated 4655 if ( $argv{do_arp} ){ 4656 Ipblock->build_tree(4); 4657 Ipblock->build_tree(6); 4658 } 4659 my $runtime = time - $start; 4660 $class->_update_poll_stats($uargs{timestamp}, $runtime); 4661 4662 return $device_count; 4663} 4664 4665sub _snmp_update_parallel_traditional { 4666 my ($class, %argv) = @_; 4667 $class->isa_class_method('_snmp_update_parallel_traditional'); 4668 4669 my ($hosts, $devs, %uargs) = $class->_snmp_update_parallel_args_check(%argv); 4670 4671 my %do_devs; 4672 4673 my $device_count = 0; 4674 my $start = time; 4675 # Init ForkManager 4676 my $pm = $class->_fork_init(); 4677 4678 if ( $devs ){ 4679 foreach my $dev ( @$devs ){ 4680 # Put in list 4681 $do_devs{$dev->id} = $dev; 4682 } 4683 }elsif ( $hosts ){ 4684 foreach my $host ( keys %$hosts ){ 4685 # Give preference to the community associated with the host 4686 if ( my $commstr = $hosts->{$host} ){ 4687 $uargs{communities} = [$commstr]; 4688 }else{ 4689 $uargs{communities} = $argv{communities}; 4690 } 4691 # If the device exists in the DB, we add it to the list 4692 my $dev; 4693 if ( $dev = $class->search(name=>$host)->first ){ 4694 $do_devs{$dev->id} = $dev; 4695 $logger->debug(sub{ sprintf("%s exists in DB.", $dev->fqdn) }); 4696 }else{ 4697 $device_count++; 4698 # FORK 4699 $pm->start and next; 4700 eval { 4701 $class->_launch_child(pm => $pm, 4702 code => sub{ return $class->discover(name=>$host, %uargs) } ); 4703 }; 4704 if ( my $e = $@ ){ 4705 $logger->error($e); 4706 exit 1; 4707 } 4708 # Make sure the child process ends 4709 return; 4710 } 4711 } 4712 } 4713 4714 # Go over list of existing devices 4715 while ( my ($id, $dev) = each %do_devs ){ 4716 4717 if ( my $regex = $argv{matching} ){ 4718 unless ( $dev->fqdn =~ /$regex/o ){ 4719 next; 4720 } 4721 } 4722 # Make sure we don't launch a process unless necessary 4723 if ( $dev->is_in_downtime() ){ 4724 $logger->debug(sub{ sprintf("Model::Device::_snmp_update_parallel_traditional: %s in downtime. Skipping", 4725 $dev->fqdn) }); 4726 next; 4727 } 4728 4729 my %args = $class->_snmp_update_get_device_args($dev, %uargs); 4730 unless ( $args{do_info} || $args{do_fwt} || $args{do_arp} ){ 4731 next; 4732 } 4733 4734 $device_count++; 4735 # FORK 4736 $pm->start and next; 4737 eval { 4738 $class->_launch_child(pm => $pm, 4739 code => sub{ return $dev->snmp_update(%args) } ); 4740 }; 4741 if ( my $e = $@ ){ 4742 $logger->error($e); 4743 exit 1; 4744 } 4745 # Make sure the child process ends 4746 return; 4747 } 4748 4749 # End forking state 4750 $class->_fork_end($pm); 4751 4752 # Rebuild the IP tree if ARP caches were updated 4753 if ( $argv{do_arp} ){ 4754 Ipblock->build_tree(4); 4755 Ipblock->build_tree(6); 4756 } 4757 my $runtime = time - $start; 4758 $class->_update_poll_stats($uargs{timestamp}, $runtime); 4759 4760 return $device_count; 4761} 4762 4763############################################################################ 4764#_update_poll_stats 4765# 4766# Arguments: 4767# timestamp 4768# Returns: 4769# True if successful 4770# Examples: 4771# $class->_update_poll_stats($timestamp); 4772# 4773# 4774sub _update_poll_stats { 4775 my ($class, $timestamp, $runtime) = @_; 4776 my $relpath = Netdot->config->get('POLL_STATS_FILE_PATH'); 4777 my $file = Netdot->config->get('NETDOT_PATH')."/".$relpath; 4778 $class->isa_class_method('_update_poll_stats'); 4779 my $stats = $class->_get_poll_stats($timestamp); 4780 $class->throw_fatal("Device::_update_poll_stats: Error getting stats") 4781 unless ($stats && ref($stats) eq "HASH"); 4782 my @vals = ($stats->{ips}, $stats->{macs}, $stats->{arp_devices}, 4783 $stats->{fwt_devices}, $runtime); 4784 my $valstr = 'N:'; 4785 $valstr .= join ':', @vals; 4786 my $template = "ips:macs:arp_devs:fwt_devs:poll_time"; 4787 $logger->debug("Updating Poll Stats for $timestamp: $template, $valstr"); 4788 RRDs::update($file, "-t", $template, $valstr); 4789 if ( my $e = RRDs::error ){ 4790 $logger->error("_update_poll_stats: Could not update RRD: $e"); 4791 return; 4792 } 4793 return 1; 4794} 4795 4796############################################################################ 4797#_get_poll_stats 4798# 4799# Arguments: 4800# timestamp 4801# Returns: 4802# True if successful 4803# Examples: 4804# $class->_get_poll_stats($timestamp); 4805# 4806# 4807sub _get_poll_stats { 4808 my ($class, $timestamp) = @_; 4809 $class->isa_class_method('_update_poll_stats'); 4810 $logger->debug("Getting Poll Stats for $timestamp"); 4811 my $dbh = $class->db_Main; 4812 4813 my %res; # Store results here 4814 4815 ############################################## 4816 # IP addresses 4817 my $sth1 = $dbh->prepare('SELECT COUNT(id) 4818 FROM ipblock 4819 WHERE version=4 AND 4820 prefix=32 AND 4821 last_seen=? 4822 '); 4823 4824 $sth1->execute($timestamp); 4825 my $total_ips= $sth1->fetchrow_array() || 0; 4826 4827 my $sth2 = $dbh->prepare('SELECT COUNT(ip.id) 4828 FROM ipblock ip, interface i 4829 WHERE ip.interface=i.id AND 4830 ip.last_seen=? 4831 '); 4832 $sth2->execute($timestamp); 4833 my $dev_ips= $sth2->fetchrow_array() || 0; 4834 4835 $res{ips} = $total_ips - $dev_ips; 4836 4837 ############################################## 4838 # MAC addresses 4839 my $sth3 = $dbh->prepare('SELECT COUNT(DISTINCT i.physaddr) 4840 FROM physaddr p, interface i 4841 WHERE i.physaddr=p.id AND 4842 p.last_seen=? 4843 '); 4844 $sth3->execute($timestamp); 4845 my $num_int_macs = $sth3->fetchrow_array() || 0; 4846 4847 my $sth4 = $dbh->prepare('SELECT COUNT(p.id) 4848 FROM physaddr p, device d, asset a 4849 WHERE a.physaddr=p.id AND 4850 a.id=d.asset_id AND 4851 p.last_seen=? 4852 '); 4853 $sth4->execute($timestamp); 4854 my $num_dev_macs = $sth4->fetchrow_array() || 0; 4855 4856 my $sth5 = $dbh->prepare('SELECT COUNT(id) 4857 FROM physaddr 4858 WHERE last_seen=? 4859 '); 4860 $sth5->execute($timestamp); 4861 my $total_macs = $sth5->fetchrow_array() || 0; 4862 4863 $res{macs} = $total_macs - ($num_int_macs + $num_dev_macs); 4864 4865 ############################################## 4866 # ARP Devices 4867 my $sth6 = $dbh->prepare('SELECT COUNT(id) 4868 FROM device 4869 WHERE last_arp=? 4870 '); 4871 $sth6->execute($timestamp); 4872 $res{arp_devices} = $sth6->fetchrow_array() || 0; 4873 4874 ############################################## 4875 # FWT Devices 4876 my $sth7 = $dbh->prepare('SELECT COUNT(id) 4877 FROM device 4878 WHERE last_fwt=? 4879 '); 4880 $sth7->execute($timestamp); 4881 $res{fwt_devices} = $sth7->fetchrow_array() || 0; 4882 4883 return \%res; 4884 4885} 4886 4887############################################################################ 4888#_get_arp_from_snmp - Fetch ARP tables via SNMP 4889# 4890# Performs some validation and abstracts SNMP::Info logic 4891# 4892# Arguments: 4893# session - SNMP session (optional) 4894# Returns: 4895# Hash ref. 4896# Examples: 4897# $self->_get_arp_from_snmp(); 4898# 4899sub _get_arp_from_snmp { 4900 my ($self, %argv) = @_; 4901 $self->isa_object_method('_get_arp_from_snmp'); 4902 my $host = $self->fqdn; 4903 4904 my %cache; 4905 my $sinfo = $argv{session} || $self->_get_snmp_session(); 4906 4907 return unless $sinfo; 4908 4909 my %devints; my %devsubnets; 4910 foreach my $int ( $self->interfaces ){ 4911 $devints{$int->number} = $int->id; 4912 } 4913 $logger->debug(sub{"$host: Fetching ARP cache via SNMP" }); 4914 my ( $at_paddr, $atnetaddr, $at_index ); 4915 $at_paddr = $sinfo->at_paddr(); 4916 4917 # With the following checks we are trying to query only one 4918 # OID instead of three, which is a significant performance 4919 # improvement with very large caches. 4920 # The optional .1 in the middle is for cases where the old 4921 # atPhysAddress is used. 4922 my $use_shortcut = 1; 4923 my @paddr_keys = keys %$at_paddr; 4924 if ( $paddr_keys[0] && $paddr_keys[0] =~ /^(\d+)(\.1|\.4)?\.($IPV4)$/ ){ 4925 my $idx = $1; 4926 if ( !exists $devints{$idx} ){ 4927 $use_shortcut = 0; 4928 $logger->debug(sub{"Device::_get_arp_from_snmp: $host: Not using ARP query shortcut"}); 4929 $atnetaddr = $sinfo->atnetaddr(); 4930 $at_index = $sinfo->at_index(); 4931 } 4932 } 4933 foreach my $key ( @paddr_keys ){ 4934 my ($ip, $idx, $mac); 4935 $mac = $at_paddr->{$key}; 4936 if ( $use_shortcut ){ 4937 if ( $key =~ /^(\d+)(\.1|\.4)?\.($IPV4)$/ ){ 4938 $idx = $1; 4939 $ip = $3; 4940 }else{ 4941 $logger->debug(sub{"Device::_get_arp_from_snmp: $host: Unrecognized hash key: $key" }); 4942 next; 4943 } 4944 }else{ 4945 $idx = $at_index->{$key}; 4946 $ip = $atnetaddr->{$key}; 4947 } 4948 unless ( $ip && $idx && $mac ){ 4949 $logger->debug(sub{"Device::_get_arp_from_snmp: $host: Missing information at row: $key" }); 4950 next; 4951 } 4952 # Store in hash 4953 $cache{$idx}{$ip} = $mac; 4954 } 4955 return $self->_validate_arp(\%cache, 4); 4956} 4957 4958############################################################################ 4959#_get_v6_nd_from_snmp - Fetch IPv6 Neighbor Discovery tables via SNMP 4960# 4961# Abstracts SNMP::Info logic 4962# 4963# Arguments: 4964# session - SNMP session (optional) 4965# Returns: 4966# Hash ref. 4967# Examples: 4968# $self->_get_v6_nd_from_snmp(); 4969# 4970sub _get_v6_nd_from_snmp { 4971 my ($self, %argv) = @_; 4972 $self->isa_object_method('_get_v6_nd_from_snmp'); 4973 my $host = $self->fqdn; 4974 my %cache; 4975 my $sinfo = $argv{session} || $self->_get_snmp_session(); 4976 4977 return unless $sinfo; 4978 4979 unless ( $sinfo->can('ipv6_n2p_mac') ){ 4980 $logger->debug("Device::_get_v6_nd_from_snmp: This version of SNMP::Info ". 4981 "does not support fetching Ipv6 addresses"); 4982 return; 4983 } 4984 $logger->debug(sub{"$host: Fetching IPv6 ND cache via SNMP" }); 4985 my $n2p_mac = $sinfo->ipv6_n2p_mac(); 4986 my $n2p_addr = $sinfo->ipv6_n2p_addr(); 4987 my $n2p_if = $sinfo->ipv6_n2p_if(); 4988 4989 unless ( $n2p_mac && $n2p_addr && $n2p_if && 4990 %$n2p_mac && %$n2p_addr && %$n2p_if ){ 4991 $logger->debug(sub{"Device::_get_v6_nd_from_snmp: $host: No IPv6 ND information" }); 4992 return; 4993 } 4994 while ( my($row,$val) = each %$n2p_mac ){ 4995 my $idx = $n2p_if->{$row}; 4996 my $ip = $n2p_addr->{$row}; 4997 my $mac = $val; 4998 unless ( $idx && $ip && $mac ){ 4999 $logger->debug(sub{"Device::_get_v6_nd_from_snmp: $host: Missing information in row: $row" }); 5000 next; 5001 } 5002 $cache{$idx}{$ip} = $mac; 5003 } 5004 return $self->_validate_arp(\%cache, 6); 5005} 5006 5007############################################################################ 5008# _validate_arp - Validate contents of ARP and v6 ND structures 5009# 5010# Arguments: 5011# hashref of hashrefs containing ifIndex, IP address and Mac 5012# IP version 5013# Returns: 5014# Hash ref. 5015# Examples: 5016# $self->_validate_arp(\%cache, 6); 5017# 5018# 5019sub _validate_arp { 5020 my ($self, $cache, $version) = @_; 5021 5022 $self->throw_fatal("Device::_validate_arp: Missing required arguments") 5023 unless ($cache && $version); 5024 5025 my $host = $self->fqdn(); 5026 5027 my $ign_non_subnet = Netdot->config->get('IGNORE_IPS_FROM_ARP_NOT_WITHIN_SUBNET'); 5028 5029 # Get all interfaces and IPs 5030 my %devints; my %devsubnets; 5031 foreach my $int ( $self->interfaces ){ 5032 $devints{$int->number} = $int->id; 5033 if ( $ign_non_subnet ){ 5034 foreach my $ip ( $int->ips ){ 5035 next unless ($ip->version == $version); 5036 push @{$devsubnets{$int->id}}, $ip->parent->netaddr 5037 if $ip->parent; 5038 } 5039 } 5040 } 5041 5042 my %valid; 5043 foreach my $idx ( keys %$cache ){ 5044 my $intid = $devints{$idx} if exists $devints{$idx}; 5045 unless ( $intid ){ 5046 $logger->warn("Device::_validate_arp: $host: Interface $idx not in database. Skipping"); 5047 next; 5048 } 5049 foreach my $ip ( keys %{$cache->{$idx}} ){ 5050 if ( $version == 6 && Ipblock->is_link_local($ip) && 5051 Netdot->config->get('IGNORE_IPV6_LINK_LOCAL') ){ 5052 next; 5053 } 5054 my $mac = $cache->{$idx}->{$ip}; 5055 my $validmac = PhysAddr->validate($mac); 5056 unless ( $validmac ){ 5057 $logger->debug(sub{"Device::_validate_arp: $host: Invalid MAC: $mac" }); 5058 next; 5059 } 5060 $mac = $validmac; 5061 if ( $ign_non_subnet ){ 5062 # This check does not work with link-local, so if user wants those 5063 # just validate them 5064 if ( $version == 6 && Ipblock->is_link_local($ip) ){ 5065 $valid{$intid}{$ip} = $mac; 5066 next; 5067 } 5068 my $nip; 5069 unless ( $nip = NetAddr::IP->new($ip) ){ 5070 $logger->error("Device::_validate_arp: Cannot create NetAddr::IP object from $ip"); 5071 next; 5072 } 5073 foreach my $nsub ( @{$devsubnets{$intid}} ){ 5074 if ( $nip->within($nsub) ){ 5075 $valid{$intid}{$ip} = $mac; 5076 last; 5077 } 5078 } 5079 }else{ 5080 $valid{$intid}{$ip} = $mac; 5081 } 5082 $logger->debug(sub{"Device::_validate_arp: $host: valid: $idx -> $ip -> $mac" }); 5083 } 5084 } 5085 return \%valid; 5086} 5087 5088############################################################################ 5089# _get_cisco_snmp_context_session 5090# 5091# Use the session information to connect to the VLAN-based "context" 5092# that provides forwarding table and STP information for that VLAN 5093# In SNMPv2, the context is specified by adding @vlan to the community 5094# whereas in SNMPv3, the "Context" parameter is used as "vlan-$vlan" 5095sub _get_cisco_snmp_context_session { 5096 my ($self, %argv) = @_; 5097 my $class = ref($self) || $self; 5098 5099 my $sinfo = $argv{sinfo}; 5100 my $vlan = $argv{vlan}; 5101 5102 # Grab the existing SNMP session parameters 5103 my $sargs = $sinfo->args(); 5104 my %args = ('host' => $sargs->{DestHost}, 5105 'version' => $sargs->{Version}, 5106 'retries' => $sargs->{Retries}, 5107 'snmpbulk' => $sargs->{BulkWalk}, 5108 'sclass' => $sinfo->class, 5109 ); 5110 5111 if ( $args{version} == 3 ){ 5112 $args{sec_name} = $sargs->{SecName}; 5113 $args{sec_level} = $sargs->{SecLevel}; 5114 $args{auth_proto} = $sargs->{AuthProto}; 5115 $args{auth_pass} = $sargs->{AuthPass}; 5116 $args{priv_proto} = $sargs->{PrivProto}; 5117 $args{priv_pass} = $sargs->{PrivPass}; 5118 $args{context} = "vlan-$vlan"; 5119 }else{ 5120 $args{communities} = [$self->community . '@' . $vlan]; 5121 } 5122 5123 return $class->_get_snmp_session(%args); 5124} 5125 5126############################################################################ 5127# Get list of all active VLANS on this device 5128sub _get_active_vlans { 5129 my ($self, %argv) = @_; 5130 5131 my $sinfo = $argv{sinfo}; 5132 my %vlans; 5133 map { $vlans{$_}++ } values %{$sinfo->i_vlan()}; 5134 my $v_state = $sinfo->v_state(); 5135 foreach my $vid ( keys %vlans ){ 5136 my $key = '1.'.$vid; 5137 delete $vlans{$vid} unless ( exists $v_state->{$key} 5138 && $v_state->{$key} eq 'operational' ); 5139 } 5140 my @res; 5141 foreach my $vlan ( sort { $a <=> $b } keys %vlans ){ 5142 next if ( exists $IGNOREDVLANS{$vlan} ); 5143 push @res, $vlan; 5144 } 5145 return @res; 5146} 5147 5148 5149############################################################################ 5150#_get_fwt_from_snmp - Fetch fowarding tables via SNMP 5151# 5152# Performs some validation and abstracts snmp::info logic 5153# Some logic borrowed from netdisco's macksuck() 5154# 5155# Arguments: 5156# session - SNMP Session (optional) 5157# vlan - Only query table for this VLAN (optional) 5158# Returns: 5159# Hash ref. 5160# 5161# Examples: 5162# $self->get_fwt_from_snmp; 5163# 5164# 5165sub _get_fwt_from_snmp { 5166 my ($self, %argv) = @_; 5167 $self->isa_object_method('get_fwt_from_snmp'); 5168 my $class = ref($self); 5169 5170 my $host = $self->fqdn; 5171 5172 unless ( $self->collect_fwt ){ 5173 $logger->debug(sub{"Device::_get_fwt_from_snmp: $host: Collect FWT option off. Skipping"}); 5174 return; 5175 } 5176 5177 if ( $self->is_in_downtime ){ 5178 $logger->debug(sub{"Device::_get_fwt_from_snmp: $host in downtime. Skipping"}); 5179 return; 5180 } 5181 5182 my $start = time; 5183 my $sinfo = $argv{session} || $self->_get_snmp_session(); 5184 return unless $sinfo; 5185 5186 my $sints = $sinfo->interfaces(); 5187 5188 # Build a hash with device's interfaces, indexed by ifIndex 5189 my %devints; 5190 foreach my $int ( $self->interfaces ){ 5191 $devints{$int->number} = $int->id; 5192 } 5193 5194 # Fetch FWT. 5195 # Notice that we pass the result variable as a parameter since that's the 5196 # easiest way to append more info later using the same function (see below). 5197 my %fwt; 5198 $logger->debug(sub{"$host: Fetching forwarding table via SNMP" }); 5199 $class->_exec_timeout($host, sub{ return $self->_walk_fwt(sinfo => $sinfo, 5200 sints => $sints, 5201 devints => \%devints, 5202 fwt => \%fwt, 5203 ); 5204 }); 5205 5206 # On most Cisco switches you have to connect to each 5207 # VLAN to get the forwarding table 5208 my $cisco_comm_indexing = $sinfo->cisco_comm_indexing(); 5209 if ( $cisco_comm_indexing ){ 5210 $logger->debug(sub{"$host supports Cisco community string indexing. Connecting to each VLAN" }); 5211 5212 foreach my $vlan ( $self->_get_active_vlans(sinfo=>$sinfo) ){ 5213 next if ( $argv{vlan} && $argv{vlan} ne $vlan ); 5214 my $vlan_sinfo; 5215 eval { 5216 $vlan_sinfo = $self->_get_cisco_snmp_context_session(sinfo=>$sinfo, vlan=>$vlan); 5217 }; 5218 if ( my $e = $@ ){ 5219 $logger->error("$host: SNMP error for VLAN $vlan: $e"); 5220 next; 5221 } 5222 5223 return unless $vlan_sinfo; 5224 5225 $class->_exec_timeout($host, sub{ return $self->_walk_fwt(sinfo => $vlan_sinfo, 5226 sints => $sints, 5227 devints => \%devints, 5228 fwt => \%fwt); 5229 }); 5230 } 5231 } 5232 5233 my $end = time; 5234 my $fwt_count = 0; 5235 map { $fwt_count+= scalar keys %{ $fwt{$_} } } keys %fwt; 5236 $logger->debug(sub{ sprintf("$host: FWT fetched. %d entries in %s", 5237 $fwt_count, $self->sec2dhms($end-$start) ) }); 5238 5239 return \%fwt; 5240} 5241 5242######################################################################### 5243sub _walk_fwt { 5244 my ($self, %argv) = @_; 5245 $self->isa_object_method('_walk_fwt'); 5246 5247 my ($sinfo, $sints, $devints, $fwt) = @argv{"sinfo", "sints", "devints", "fwt"}; 5248 5249 my $host = $self->fqdn; 5250 5251 $self->throw_fatal("Model::Device::_walk_fwt: Missing required arguments") 5252 unless ( $sinfo && $sints && $devints && $fwt ); 5253 5254 5255 my %tmp; 5256 5257 # Try BRIDGE mib stuff first, then REPEATER mib 5258 if ( my $fw_mac = $sinfo->fw_mac() ){ 5259 5260 my $fw_port = $sinfo->fw_port(); 5261 my $bp_index = $sinfo->bp_index(); 5262 5263 # To map the port in the forwarding table to the 5264 # physical device port we have this triple indirection: 5265 # fw_port -> bp_index -> interfaces 5266 5267 foreach my $fw_index ( keys %$fw_mac ){ 5268 5269 my $mac = $fw_mac->{$fw_index}; 5270 unless ( defined $mac ) { 5271 $logger->debug( 5272 sub{"Device::_walk_fwt: $host: MAC not defined at index $fw_index. Skipping" }); 5273 next; 5274 } 5275 5276 my $bp_id = $fw_port->{$fw_index}; 5277 unless ( defined $bp_id ) { 5278 $logger->debug( 5279 sub{"Device::_walk_fwt: $host: Port $fw_index has no fw_port mapping. Skipping" }); 5280 next; 5281 } 5282 5283 my $iid = $bp_index->{$bp_id}; 5284 unless ( defined $iid ) { 5285 $logger->debug( 5286 sub{"Device::_walk_fwt: $host: Interface $bp_id has no bp_index mapping. Skipping" }); 5287 next; 5288 } 5289 5290 $tmp{$iid}{$mac} = 1; 5291 } 5292 5293 }elsif ( my $last_src = $sinfo->rptrAddrTrackNewLastSrcAddress() ){ 5294 5295 foreach my $iid ( keys %{ $last_src } ){ 5296 my $mac = $last_src->{$iid}; 5297 unless ( defined $mac ) { 5298 $logger->debug( 5299 sub{"Device::_walk_fwt: $host: MAC not defined at rptr index $iid. Skipping" }); 5300 next; 5301 } 5302 5303 $tmp{$iid}{$mac} = 1; 5304 } 5305 5306 } 5307 5308 # Clean up here to avoid repeating these checks in each loop above 5309 foreach my $iid ( keys %tmp ){ 5310 my $descr = $sints->{$iid}; 5311 unless ( defined $descr ) { 5312 $logger->debug( 5313 sub{"Device::_walk_fwt: $host: SNMP iid $iid has no physical port matching. Skipping" }); 5314 next; 5315 } 5316 5317 my $intid = $devints->{$iid} if exists $devints->{$iid}; 5318 unless ( $intid ){ 5319 $logger->warn("Device::_walk_fwt: $host: Interface $iid ($descr) is not in database. Skipping"); 5320 next; 5321 } 5322 5323 foreach my $mac ( keys %{ $tmp{$iid} } ){ 5324 next unless $mac; 5325 my $validmac = PhysAddr->validate($mac); 5326 if ( $validmac ){ 5327 $mac = $validmac; 5328 }else{ 5329 $logger->debug(sub{"Device::_walk_fwt: $host: Invalid MAC: $mac" }); 5330 next; 5331 } 5332 $fwt->{$intid}->{$mac} = 1; 5333 $logger->debug(sub{"Device::_walk_fwt: $host: $iid ($descr) -> $mac" }); 5334 } 5335 5336 } 5337 5338 return 1; 5339} 5340 5341######################################################################### 5342# Run given code within TIMEOUT time 5343# Uses ALRM signal to tell process to throw an exception 5344# 5345# Rationale: 5346# An SNMP connection is established but the agent never replies to a query. 5347# In those cases, the standard Timeout parameter for the SNMP session 5348# does not help. 5349# 5350# Arguments: 5351# hostname 5352# code reference 5353# Returns: 5354# Array with results 5355# 5356sub _exec_timeout { 5357 my ($class, $host, $code) = @_; 5358 $class->isa_class_method("_exec_timeout"); 5359 5360 $class->throw_fatal("Model::Device::_exec_timeout: Missing required argument: code") unless $code; 5361 $class->throw_fatal("Model::Device::_exec_timeout: Invalid code reference") 5362 unless ( ref($code) eq 'CODE' ); 5363 my @result; 5364 eval { 5365 alarm($TIMEOUT); 5366 @result = $code->(); 5367 alarm(0); 5368 }; 5369 if ( my $e = $@ ){ 5370 my $msg; 5371 if ( $e =~ /timeout/ ){ 5372 $class->throw_user("Device $host timed out ($TIMEOUT sec)"); 5373 }else{ 5374 $class->throw_user("$e"); 5375 } 5376 } 5377 wantarray ? @result : $result[0]; 5378} 5379 5380######################################################################### 5381# Executes given code as a child process. Makes sure DBI handle does 5382# not disconnect 5383# 5384# Arguments: 5385# hash with following keys: 5386# code - Code reference to execute 5387# pm - Parallel::ForkManager object 5388# 5389# 5390sub _launch_child { 5391 my ($class, %argv) = @_; 5392 $class->isa_class_method("_launch_child"); 5393 5394 my ($code, $pm) = @argv{"code", "pm"}; 5395 5396 $class->throw_fatal("Model::Device::_launch_child: Missing required arguments") 5397 unless ( defined $pm && defined $code ); 5398 5399 # Tell DBI that we don't want to disconnect the server's DB handle 5400 my $dbh = $class->db_Main; 5401 unless ( $dbh->{InactiveDestroy} = 1 ) { 5402 $class->throw_fatal("Model::Device::_launch_child: Cannot set InactiveDestroy: ", $dbh->errstr); 5403 } 5404 # Run given code 5405 $code->(); 5406 $dbh->disconnect(); 5407 $pm->finish; # exit the child process 5408} 5409 5410##################################################################### 5411# _get_as_info - Retrieve info about given Autonomous System Number 5412# 5413# Arguments: 5414# asn: Autonomous System Number 5415# Returns: 5416# Hash with keys: 5417# asname: Short name for this AS 5418# orgname: Short description for this AS 5419 5420sub _get_as_info{ 5421 my ($self, $asn) = @_; 5422 5423 return unless $asn; 5424 5425 if ( $asn >= 64512 && $asn <= 65535 ){ 5426 $logger->debug("Device::_get_as_info: $asn is IANA reserved"); 5427 return; 5428 } 5429 5430 # For some reason, use'ing this module at the top of this 5431 # file causes mod_perl to raise hell. 5432 # Loading it this way seems to avoid that problem 5433 eval "use Net::IRR"; 5434 if ( my $e = $@ ){ 5435 $self->throw_fatal("Model::Device::_get_as_info: Error loading module: $e"); 5436 } 5437 5438 my %results; 5439 5440 my $server = $self->config->get('WHOIS_SERVER'); 5441 $logger->debug(sub{"Device::_get_as_info: Querying $server"}); 5442 my $i = Net::IRR->connect(host => $server); 5443 unless ( $i ){ 5444 $logger->error("Device::_get_as_info: Cannot connect to $server"); 5445 return; 5446 } 5447 my $obj = $i->match("aut-num","as$asn"); 5448 unless ( $obj ){ 5449 $logger->debug("Device::_get_as_info: Can't find AS $asn in $server"); 5450 return; 5451 } 5452 $i->disconnect(); 5453 5454 if ( $obj =~ /as-name:\s+(.*)\n/o ){ 5455 my $as_name = $1; 5456 $as_name = substr($as_name, 0, 32); 5457 $results{asname} = $as_name; 5458 $logger->debug(sub{"Device::_get_as_info:: $server: Found asname: $as_name"}); 5459 } 5460 if ( $obj =~ /descr:\s+(.*)\n/o ){ 5461 my $descr = $1; 5462 $descr = substr($descr, 0, 128); 5463 $results{orgname} = $descr; 5464 $logger->debug(sub{"Device::_get_as_info:: $server: Found orgname: $descr"}); 5465 } 5466 $results{orgname} ||= $asn; 5467 $results{asname} ||= $results{orgname}; 5468 return \%results if %results; 5469 return; 5470} 5471 5472##################################################################### 5473# _update_macs_from_fwt - Update MAC addresses 5474# 5475# Arguments: 5476# hash with following keys: 5477# caches - Arrayref with FWT info 5478# timestamp - Time Stamp 5479# atomic - Perform atomic updates 5480sub _update_macs_from_fwt { 5481 my ($class, %argv) = @_; 5482 my ($caches, $timestamp, $atomic) = @argv{'caches', 'timestamp', 'atomic'}; 5483 5484 my %mac_updates; 5485 foreach my $cache ( @$caches ){ 5486 foreach my $idx ( keys %{$cache} ){ 5487 foreach my $mac ( keys %{$cache->{$idx}} ){ 5488 $mac_updates{$mac} = 1; 5489 } 5490 } 5491 } 5492 if ( $atomic ){ 5493 Netdot::Model->do_transaction( sub{ return PhysAddr->fast_update(\%mac_updates, $timestamp) } ); 5494 }else{ 5495 PhysAddr->fast_update(\%mac_updates, $timestamp); 5496 } 5497 return 1; 5498} 5499 5500 5501##################################################################### 5502# _update_macs_from_arp_cache - Update MAC addresses from ARP cache 5503# 5504# Arguments: 5505# hash with following keys: 5506# caches - Arrayref with ARP cache 5507# timestamp - Time Stamp 5508# atomic - Perform atomic updates 5509sub _update_macs_from_arp_cache { 5510 my ($class, %argv) = @_; 5511 my ($caches, $timestamp, $atomic) = @argv{'caches', 'timestamp', 'atomic'}; 5512 5513 my %mac_updates; 5514 foreach my $cache ( @$caches ){ 5515 foreach my $version ( keys %{$cache} ){ 5516 foreach my $idx ( keys %{$cache->{$version}} ){ 5517 foreach my $mac ( values %{$cache->{$version}->{$idx}} ){ 5518 $mac_updates{$mac} = 1; 5519 } 5520 } 5521 } 5522 } 5523 if ( $atomic ){ 5524 Netdot::Model->do_transaction( sub{ return PhysAddr->fast_update(\%mac_updates, $timestamp) } ); 5525 }else{ 5526 PhysAddr->fast_update(\%mac_updates, $timestamp); 5527 } 5528 return 1; 5529} 5530 5531##################################################################### 5532# _update_ips_from_arp_cache - Update IP addresses from ARP cache 5533# 5534# Arguments: 5535# hash with following keys: 5536# caches - Arrayref of hashrefs with ARP Cache info 5537# timestamp - Time Stamp 5538# no_update_tree - Boolean 5539# atomic - Perform atomic updates 5540sub _update_ips_from_arp_cache { 5541 my ($class, %argv) = @_; 5542 my ($caches, $timestamp, 5543 $no_update_tree, $atomic) = @argv{'caches', 'timestamp', 5544 'no_update_tree', 'atomic'}; 5545 5546 my %ip_updates; 5547 5548 my $ip_status = (IpblockStatus->search(name=>'Discovered'))[0]; 5549 $class->throw_fatal("Model::Device::_update_ips_from_cache: IpblockStatus 'Discovered' not found?") 5550 unless $ip_status; 5551 5552 my %build_tree; 5553 foreach my $cache ( @$caches ){ 5554 foreach my $version ( keys %{$cache} ){ 5555 $build_tree{$version} = 1; 5556 foreach my $idx ( keys %{$cache->{$version}} ){ 5557 foreach my $ip ( keys %{$cache->{$version}{$idx}} ){ 5558 my $mac = $cache->{$version}->{$idx}->{$ip}; 5559 my $prefix = ($version == 4)? 32 : 128; 5560 $ip_updates{$ip} = { 5561 prefix => $prefix, 5562 version => $version, 5563 timestamp => $timestamp, 5564 physaddr => $mac, 5565 status => $ip_status, 5566 }; 5567 } 5568 } 5569 } 5570 } 5571 5572 if ( $atomic ){ 5573 Netdot::Model->do_transaction( sub{ return Ipblock->fast_update(\%ip_updates) } ); 5574 }else{ 5575 Ipblock->fast_update(\%ip_updates); 5576 } 5577 5578 unless ( $no_update_tree ){ 5579 foreach my $version ( sort keys %build_tree ){ 5580 Ipblock->build_tree($version); 5581 } 5582 } 5583 5584 return 1; 5585} 5586 5587 5588##################################################################### 5589# Convert octet stream values returned from SNMP into an ASCII HEX string 5590# 5591sub _oct2hex { 5592 my ($self, $v) = @_; 5593 return uc( sprintf('%s', unpack('H*', $v)) ); 5594} 5595 5596##################################################################### 5597# Takes an 8-byte octet stream (HEX-STRING) containing priority+MAC 5598# (from do1dStp MIBs) and returns a ASCII hex string containing the 5599# MAC address only (6 bytes). 5600# 5601sub _stp2mac { 5602 my ($self, $mac) = @_; 5603 return undef unless $mac; 5604 $mac = $self->_oct2hex($mac); 5605 $mac = substr($mac, 4, 12); 5606 return $mac if length $mac; 5607 return undef; 5608} 5609 5610##################################################################### 5611# ifHighSpeed is an estimate of the interface's current bandwidth in units 5612# of 1,000,000 bits per second. 5613# We store interface speed as bps (integer format) 5614# 5615sub _munge_speed_high { 5616 my ($self, $v) = @_; 5617 return $v * 1000000; 5618} 5619 5620############################################################## 5621# Assign Base MAC 5622# 5623# Ideally this is the base MAC address from BRIDGE-MIB 5624# but if that's not available, we'll assign the first good 5625# MAC address from interfaces. This might be necessary 5626# to create a unique asset 5627# 5628# Arguments 5629# snmp info hashref 5630# Returns 5631# PhysAddr object if successful, undef otherwise 5632# 5633sub _assign_base_mac { 5634 my ($self, $info) = @_; 5635 5636 my $host = $self->fqdn; 5637 my $address = delete $info->{physaddr}; 5638 if ( $address && ($address = PhysAddr->validate($address)) ) { 5639 # OK 5640 }else{ 5641 $logger->debug(sub{"$host does not provide a valid base MAC.". 5642 " Using first available interface MAC."}); 5643 foreach my $iid ( sort { $a <=> $b} keys %{$info->{interface}} ){ 5644 if ( my $addr = $info->{interface}->{$iid}->{physaddr} ){ 5645 next unless ($address = PhysAddr->validate($addr)); 5646 last; 5647 } 5648 } 5649 } 5650 unless ( $address ){ 5651 $logger->debug("$host: No suitable MAC address found"); 5652 return; 5653 } 5654 # Look it up 5655 my $mac; 5656 if ( $mac = PhysAddr->search(address=>$address)->first ){ 5657 # The address exists 5658 # (may have been discovered in fw tables/arp cache) 5659 $mac->update({static=>1, last_seen=>$self->timestamp}); 5660 $logger->debug(sub{"$host: Using existing $address as base bridge address"}); 5661 return $mac; 5662 }else{ 5663 # address is new. Add it 5664 eval { 5665 $mac = PhysAddr->insert({address=>$address, static=>1}); 5666 }; 5667 if ( my $e = $@ ){ 5668 $logger->debug(sprintf("%s: Could not insert base MAC: %s: %s", 5669 $host, $address, $e)); 5670 }else{ 5671 $logger->info(sprintf("%s: Inserted new base MAC: %s", $host, $mac->address)); 5672 return $mac; 5673 } 5674 } 5675 $logger->debug("$host: No suitable base MAC found"); 5676} 5677 5678############################################################## 5679# Assign the snmp_target address if it's not there yet 5680# 5681# Arguments 5682# snmp info hashref 5683# Returns 5684# Ipblock object 5685# 5686sub _assign_snmp_target { 5687 my ($self, $info) = @_; 5688 my $host = $self->fqdn; 5689 if ( $self->snmp_managed && !$self->snmp_target && $info->{snmp_target} ){ 5690 my $ipb = Ipblock->search(address=>$info->{snmp_target})->first; 5691 unless ( $ipb ){ 5692 eval { 5693 $ipb = Ipblock->insert({address=>$info->{snmp_target}, status=>'Static'}); 5694 }; 5695 if ( my $e = $@ ){ 5696 $logger->warn("Device::assign_snmp_target: $host: Could not insert snmp_target address: ". 5697 $info->{snmp_target} .": ", $e); 5698 } 5699 } 5700 if ( $ipb ){ 5701 $logger->info(sprintf("%s: SNMP target address set to %s", 5702 $host, $ipb->address)); 5703 return $ipb; 5704 } 5705 } 5706} 5707 5708############################################################## 5709# Assign Product 5710# 5711# Arguments 5712# snmp info hashref 5713# Returns 5714# Product object 5715# 5716sub _assign_product { 5717 my ($self, $info) = @_; 5718 $self->throw_fatal("Invalid info hashref") 5719 unless ( $info && ref($info) eq 'HASH' ); 5720 5721 # Build a query that tries to find any of these 5722 # Notice that we use an array to mean "OR" 5723 my @where; 5724 push @where, { sysobjectid => $info->{sysobjectid} } if $info->{sysobjectid}; 5725 push @where, { part_number => $info->{model} } if $info->{model}; 5726 my @names; 5727 push @names, $info->{model} if $info->{model}; 5728 push @names, $info->{productname} if $info->{productname}; 5729 push @where, { name => \@names } if @names; 5730 my $prod = Product->search_where(\@where)->first if @where; 5731 5732 return $prod if $prod; 5733 5734 # Try to create it then 5735 my %args; 5736 $args{name} = $info->{model} || $info->{productname}; 5737 $args{sysobjectid} = $info->{sysobjectid}; 5738 $args{description} = $info->{productname}; 5739 $args{part_number} = $info->{part_number}; 5740 5741 if ( defined $info->{type} ){ 5742 my $ptype = ProductType->find_or_create({name=>$info->{type}}); 5743 $args{type} = $ptype; 5744 } 5745 5746 my $manuf_name = ($info->{manufacturer})? $info->{manufacturer} : 'Unknown'; 5747 $args{manufacturer} = Entity->find_or_create({name=>$manuf_name}); 5748 5749 $args{hostname} = $self->fqdn; 5750 5751 if ( $args{name} ){ 5752 return Product->insert(\%args); 5753 }else{ 5754 return Product->find_or_create({name=>'Unknown'}); 5755 } 5756} 5757 5758############################################################## 5759# Assign monitored flag based on device type 5760# 5761# Arguments 5762# Product object 5763# Returns 5764# 1 or 0 5765# 5766sub _assign_device_monitored { 5767 my ($self, $product) = @_; 5768 return unless $product; 5769 if ( my $ptype = $product->type->name ){ 5770 if ( my $default = $self->config->get('DEFAULT_DEV_MONITORED') ){ 5771 if ( exists $default->{$ptype} ){ 5772 return $default->{$ptype}; 5773 } 5774 } 5775 } 5776} 5777 5778 5779############################################################## 5780# Assign monitor_config_group 5781# 5782# Arguments 5783# snmp info hashref 5784# Returns 5785# String 5786# 5787sub _assign_monitor_config_group{ 5788 my ($self, $info) = @_; 5789 if ( $self->config->get('DEV_MONITOR_CONFIG') && 5790 (!$self->monitor_config_group || $self->monitor_config_group eq "") ){ 5791 my $monitor_config_map = $self->config->get('DEV_MONITOR_CONFIG_GROUP_MAP') || {}; 5792 if ( my $type = $info->{type} ){ 5793 if ( exists $monitor_config_map->{$type} ){ 5794 return $monitor_config_map->{$type}; 5795 } 5796 } 5797 } 5798} 5799 5800 5801############################################################## 5802# Update Spanning Tree information 5803# 5804# Arguments 5805# snmp info hashref 5806# Device object arguments hashref 5807# Returns 5808# Nothing 5809# 5810sub _update_stp_info { 5811 my ($self, $info, $devtmp) = @_; 5812 my $host = $self->fqdn; 5813 5814 ############################################################## 5815 # Global Spanning Tree Info 5816 $devtmp->{stp_type} = $info->{stp_type}; 5817 $devtmp->{stp_enabled} = 1 if ( defined $info->{stp_type} && $info->{stp_type} ne 'unknown' ); 5818 5819 5820 ############################################################## 5821 # Assign MST-specific values 5822 foreach my $field ( qw( stp_mst_region stp_mst_rev stp_mst_digest ) ){ 5823 if ( exists $info->{$field} ){ 5824 $devtmp->{$field} = $info->{$field}; 5825 # Notify if these have changed 5826 if ( $field eq 'stp_mst_region' || $field eq 'stp_mst_digest' ){ 5827 if ( defined($self->$field) && ($self->$field ne $devtmp->{$field}) ){ 5828 $logger->warn(sprintf("%s: $field has changed: %s -> %s", 5829 $host, $self->$field, $devtmp->{$field})); 5830 } 5831 } 5832 } 5833 } 5834 5835 ############################################################## 5836 # Deal with STP instances 5837 if ( $devtmp->{stp_enabled} ){ 5838 $logger->debug(sub{ sprintf("%s: STP is enabled", $host)}); 5839 $logger->debug(sub{ sprintf("%s: STP type is: %s", $host, $devtmp->{stp_type})}); 5840 5841 # Get all current instances, hash by number 5842 my %old_instances; 5843 map { $old_instances{$_->number} = $_ } $self->stp_instances(); 5844 5845 # Go over all STP instances 5846 foreach my $instn ( keys %{$info->{stp_instances}} ){ 5847 my $stpinst; 5848 my %args = (device=>$self->id, number=>$instn); 5849 # Create if it does not exist 5850 unless ( $stpinst = STPInstance->search(%args)->first ){ 5851 $stpinst = STPInstance->insert(\%args); 5852 $logger->info("$host: STP Instance $instn created"); 5853 } 5854 # update arguments for this instance 5855 my %uargs; 5856 if ( my $root_bridge = $info->{stp_instances}->{$instn}->{stp_root} ){ 5857 if ( defined $stpinst->root_bridge && ($root_bridge ne $stpinst->root_bridge) ){ 5858 $logger->warn(sprintf("%s: STP instance %s: Root Bridge changed: %s -> %s", 5859 $host, $stpinst->number, $stpinst->root_bridge, $root_bridge)); 5860 } 5861 $uargs{root_bridge} = $root_bridge; 5862 }else{ 5863 $logger->debug(sub{ "$host: STP Designated Root not defined for instance $instn"}); 5864 } 5865 5866 if ( my $root_p = $info->{stp_instances}->{$instn}->{stp_root_port} ){ 5867 if ( defined $stpinst->root_port && $stpinst->root_port != 0 && 5868 ( $root_p != $stpinst->root_port) ){ 5869 # Do not notify if this is the first time it's set 5870 $logger->warn(sprintf("%s: STP instance %s: Root Port changed: %s -> %s", 5871 $host, $stpinst->number, $stpinst->root_port, $root_p)); 5872 } 5873 $uargs{root_port} = $root_p; 5874 }else{ 5875 $logger->debug(sub{"$host: STP Root Port not defined for instance $instn"}); 5876 } 5877 # Finally, just get the priority 5878 $uargs{bridge_priority} = $info->{stp_instances}->{$instn}->{stp_priority}; 5879 if ( defined $stpinst->bridge_priority && defined $uargs{bridge_priority} && 5880 $stpinst->bridge_priority ne $uargs{bridge_priority} ){ 5881 $logger->warn(sprintf("%s: STP instance %s: Bridge Priority Changed: %s -> %s", 5882 $host, $stpinst->number, $stpinst->bridge_priority, 5883 $uargs{bridge_priority})); 5884 } 5885 5886 # Update the instance 5887 $stpinst->update(\%uargs); 5888 5889 # Remove this one from the old list 5890 delete $old_instances{$instn}; 5891 } 5892 # Remove any non-existing STP instances 5893 foreach my $i ( keys %old_instances ){ 5894 $logger->info("$host: Removing STP instance $i"); 5895 $old_instances{$i}->delete; 5896 } 5897 }else{ 5898 if ( my @instances = $self->stp_instances() ){ 5899 $logger->debug(sub{"$host: STP appears disabled. Removing all existing STP instances"}); 5900 foreach my $i ( @instances ){ 5901 $i->delete(); 5902 } 5903 } 5904 } 5905} 5906 5907 5908############################################## 5909# Add/Update Modules 5910# 5911# Arguments 5912# Hashref with following keys: 5913# info => modules hashref from SNMP 5914# manufacturer => (Entity) from Device Product 5915# Returns 5916# True 5917# 5918sub _update_modules { 5919 my ($self, %argv) = @_; 5920 my ($modules, $mf) = @argv{'info', 'manufacturer'}; 5921 my $host = $self->fqdn; 5922 5923 # Get old modules (if any) 5924 my %oldmodules; 5925 map { $oldmodules{$_->number} = $_ } $self->modules(); 5926 5927 foreach my $key ( keys %{$modules} ){ 5928 my $number = $modules->{$key}->{number}; 5929 my %mod_args = %{$modules->{$key}}; 5930 $mod_args{device} = $self->id; 5931 my $show_name = $mod_args{name} || $number; 5932 # find or create asset object for given serial number and product 5933 my $asset; 5934 if ( my $serial = delete $mod_args{serial_number} ){ 5935 if ( $serial =~ /^fill in/io ){ 5936 # We've seen HP switches with "Fill in this information" 5937 # as value for S/N and model. Annoying. 5938 next; 5939 } 5940 # Try to find the asset based on the serial and the 5941 # manufacturer first. The reason is that model names 5942 # learned from device info can vary slightly 5943 # from the name in the module information 5944 $asset = Asset->search_sn_mf($serial, $mf)->first; 5945 5946 # The asset can unfortunately be from a different 5947 # manufacturer. We run the risk of assigning the 5948 # wrong asset, but the alternative may be worse 5949 $asset = Asset->search(serial_number=>$serial)->first 5950 unless $asset; 5951 5952 if ( !$asset && (my $model = $mod_args{model}) ){ 5953 # Now, search for the asset based on the match 5954 # of both the product and either the name or 5955 # the part number 5956 5957 if ( $model =~ /^fill in/io ){ 5958 next; 5959 } 5960 5961 # Find or create product 5962 my $product; 5963 $product = Product->search_where({ 5964 manufacturer => $mf, 5965 -or => [part_number => $model, name => $model], 5966 })->first; 5967 5968 my $type = ProductType->find_or_create({name=>'Module'}); 5969 5970 if ( $product ){ 5971 if ( !$product->type || $product->type->name eq 'Unknown' ){ 5972 $product->update({type => $type}); 5973 } 5974 }else{ 5975 $product = Product->insert({part_number => $model, 5976 name => $model, 5977 manufacturer => $mf, 5978 type => $type, 5979 }); 5980 } 5981 5982 # Find or create asset 5983 $asset = Asset->find_or_create({product_id => $product, 5984 serial_number => $serial, 5985 }); 5986 5987 $logger->debug("$host: module $number ($show_name) has asset: ". 5988 $asset->get_label); 5989 } 5990 } 5991 5992 # At this point we should have an asset object, but check 5993 if ( $asset ){ 5994 # Clear reservation comment as soon as hardware gets installed 5995 $asset->update({reserved_for => ""}); 5996 $mod_args{asset_id} = $asset->id; 5997 }else{ 5998 # If there's an asset then we probably need to remove it 5999 $mod_args{asset_id} = undef; 6000 } 6001 6002 # See if the module exists 6003 my $module; 6004 if ( exists $oldmodules{$number} ){ 6005 $module = $oldmodules{$number}; 6006 # Update 6007 $module->update(\%mod_args); 6008 }else{ 6009 # Create new object 6010 $logger->info("$host: New module $number ($show_name) found. Inserting."); 6011 $module = DeviceModule->insert(\%mod_args); 6012 } 6013 delete $oldmodules{$number}; 6014 } 6015 # Remove modules that no longer exist 6016 foreach my $number ( keys %oldmodules ){ 6017 my $module = $oldmodules{$number}; 6018 $logger->info("$host: Module no longer exists: $number. Removing."); 6019 $module->delete(); 6020 } 6021 1; 6022} 6023 6024############################################## 6025# Add/Update Interfaces 6026# 6027# Arguments 6028# Hahref with following keys: 6029# info - snmp information 6030# add_subnets - Flag. Whether to add subnets if layer3 and ipforwarding 6031# subs_inherit - Flag. Whether subnets inherit parent info. 6032# overwrite_descr - Flag. What to set this field to by default. 6033# Returns 6034# PhysAddr 6035# 6036sub _update_interfaces { 6037 my ($self, %argv) = @_; 6038 6039 my $host = $self->fqdn; 6040 my $info = $argv{info}; 6041 6042 # Do not update interfaces for these devices 6043 # (specified in config file) 6044 my %IGNORED; 6045 map { $IGNORED{$_}++ } @{ $self->config->get('IGNOREPORTS') }; 6046 if ( defined $info->{sysobjectid} && exists $IGNORED{$info->{sysobjectid}} ){ 6047 $logger->debug( 6048 sub{"Device::_update_interfaces: $host ports ignored per configuration option (IGNOREPORTS)"}); 6049 return; 6050 } 6051 6052 # How to deal with new subnets 6053 # We'll only do this for stuff that routes packets 6054 my ($add_subnets, $subs_inherit); 6055 if ( $info->{ipforwarding} ){ 6056 # Given arguments take precedence over configuration 6057 $add_subnets = (defined($argv{add_subnets}))? 6058 $argv{add_subnets} : $self->config->get('ADDSUBNETS'); 6059 $subs_inherit = ( $add_subnets && defined($argv{subs_inherit}) ) ? 6060 $argv{subs_inherit} : $self->config->get('SUBNET_INHERIT_DEV_INFO'); 6061 } 6062 6063 # Get old IPs (if any) 6064 my %old_ips; 6065 if ( my $devips = $self->get_ips ){ 6066 foreach ( @$devips ){ 6067 # Use decimal address in the index to avoid ambiguities with notation 6068 my $numip = $_->address_numeric; 6069 $old_ips{$numip} = $_; 6070 } 6071 } 6072 6073 ############################################## 6074 # Try to solve the problem with devices that change ifIndex 6075 # We use the name as the most stable key to identify interfaces 6076 # If names are not unique, use number 6077 6078 # Get old Interfaces (if any). 6079 my ( %oldifs, %oldifsbynumber, %oldifsbyname ); 6080 6081 # Index by object id. 6082 map { $oldifs{$_->id} = $_ } $self->interfaces(); 6083 6084 if ( $ENV{REMOTE_USER} eq 'netdot' ){ 6085 6086 # Avoid a situation in which the SNMP query fails or data 6087 # is truncated, resulting in too many interfaces being 6088 # incorrectly removed. This in turn causes IP addresses 6089 # and A/AAAA and PTR records to be removed, which then 6090 # causes all sorts of other problems 6091 6092 # Cron jobs run as user 'netdot' 6093 # This would not work if there is an actual user (person) 6094 # whose username is netdot, running the update 6095 6096 my $int_thold = $self->config->get('IF_COUNT_THRESHOLD'); 6097 if ( $int_thold <= 0 || $int_thold >= 1 ){ 6098 $self->throw_fatal('Incorrect value for IF_COUNT_THRESHOLD in config file'); 6099 } 6100 6101 my %old_snmp_ifs; 6102 map { $old_snmp_ifs{$_->id} = $_ } 6103 grep { $_->doc_status eq 'snmp' } values %oldifs; 6104 6105 my $ifs_old = scalar(keys(%old_snmp_ifs)); 6106 my $ifs_new = scalar(keys(%{$info->{interface}})); 6107 6108 $logger->debug("$host: Old Ifs: $ifs_old, New Ifs: $ifs_new"); 6109 6110 if ( ($ifs_old && !$ifs_new) || ($ifs_new && ($ifs_new < $ifs_old) && 6111 ($ifs_new / $ifs_old) <= $int_thold) ){ 6112 $logger->warn(sprintf("%s: new/old interface ratio: %.2f is below INT_COUNT_THRESHOLD". 6113 " Skipping interface update. Re-discover manually if needed.", 6114 $host, $ifs_new/$ifs_old)); 6115 return; 6116 } 6117 6118 # Do the same for IP addresses 6119 my $ips_old = scalar(keys(%old_ips)); 6120 my $ips_new = 0; 6121 foreach my $i ( values %{ $info->{interface} } ){ 6122 foreach my $ip ( values %{ $i->{ips} } ){ 6123 my $address = $ip->{address}; 6124 next if Ipblock->is_loopback($address); 6125 $ips_new++; 6126 } 6127 } 6128 6129 $logger->debug("$host: Old IPs: $ips_old, New IPs: $ips_new"); 6130 6131 if ( ($ips_old && !$ips_new) || ($ips_new && ($ips_new < $ips_old) && 6132 ($ips_new / $ips_old) <= $int_thold) ){ 6133 $logger->warn(sprintf("%s: new/old IP ratio: %.2f is below INT_COUNT_THRESHOLD". 6134 "Skipping interface update. Re-discover manually if needed.", 6135 $host, $ips_new/$ips_old)); 6136 return; 6137 } 6138 } 6139 6140 # Index by interface name (ifDescr) and number (ifIndex) 6141 foreach my $id ( keys %oldifs ){ 6142 $oldifsbynumber{$oldifs{$id}->number} = $oldifs{$id} 6143 if ( defined($oldifs{$id}->number) ); 6144 6145 $oldifsbyname{$oldifs{$id}->name} = $oldifs{$id} 6146 if ( defined($oldifs{$id}->name) ); 6147 } 6148 6149 # Index new interfaces by name to check if any names are repeated 6150 my $ifkey = 'name'; 6151 my %newifsbyname; 6152 foreach my $int ( keys %{$info->{interface}} ){ 6153 if ( defined $info->{interface}->{$int}->{name} ){ 6154 my $n = $info->{interface}->{$int}->{name}; 6155 $newifsbyname{$n}++; 6156 if ( $newifsbyname{$n} > 1 ){ 6157 $ifkey = 'number'; 6158 } 6159 } 6160 } 6161 foreach my $newif ( sort keys %{ $info->{interface} } ) { 6162 6163 # Remove the new interface's ip addresses from list to delete 6164 foreach my $newaddr ( keys %{$info->{interface}->{$newif}->{ips}} ){ 6165 my $numip = Ipblock->ip2int($newaddr); 6166 delete $old_ips{$numip} if exists $old_ips{$numip}; 6167 } 6168 6169 my $newname = $info->{interface}->{$newif}->{name}; 6170 my $newnumber = $info->{interface}->{$newif}->{number}; 6171 my $oldif; 6172 if ( $ifkey eq 'name' ){ 6173 if ( defined $newname && ($oldif = $oldifsbyname{$newname}) ){ 6174 # Found one with the same name 6175 $logger->debug(sub{ sprintf("%s: Interface with name %s found", 6176 $host, $oldif->name)}); 6177 6178 if ( $oldif->number ne $newnumber ){ 6179 # New and old numbers do not match for this name 6180 $logger->info(sprintf("%s: Interface %s had number: %s, now has: %s", 6181 $host, $oldif->name, $oldif->number, $newnumber)); 6182 } 6183 }elsif ( defined $newnumber && exists $oldifsbynumber{$newnumber} ){ 6184 # Name not found, but found one with the same number 6185 $oldif = $oldifsbynumber{$newnumber}; 6186 $logger->debug(sub{ sprintf("%s: Interface with number %s found", 6187 $host, $oldif->number)}); 6188 } 6189 }else{ 6190 # Using number as unique reference 6191 if ( defined $newnumber && exists $oldifsbynumber{$newnumber} ){ 6192 $oldif = $oldifsbynumber{$newnumber}; 6193 $logger->debug(sub{ sprintf("%s: Interface with number %s found", 6194 $host, $oldif->number)}); 6195 } 6196 } 6197 my $if; 6198 if ( $if = $oldif ){ 6199 $if->snmp_update(snmp_info => $info->{interface}->{$newif}, 6200 add_subnets => $add_subnets, 6201 subs_inherit => $subs_inherit, 6202 stp_instances => $info->{stp_instances}, 6203 ); 6204 }else{ 6205 # Interface does not exist. Add it. 6206 6207 my $ifname = $info->{interface}->{$newif}->{name} || $newnumber; 6208 my %args = (device => $self, 6209 number => $newif, 6210 name => $ifname, 6211 doc_status => 'snmp', 6212 auto_dns => $self->auto_dns, 6213 ); 6214 # Make sure we can write to the description field when 6215 # device is a router 6216 $args{overwrite_descr} = 1 if $argv{overwrite_descr}; 6217 6218 ############################################ 6219 # Determine Monitored flag value 6220 $args{monitored} = 0; 6221 my $IFM = $self->config->get('IF_MONITORED'); 6222 if ( defined $IFM ){ 6223 if ( $IFM == 0 ){ 6224 # do nothing 6225 }elsif ( $IFM == 1 ){ 6226 $args{monitored} = 1; 6227 }elsif ( $IFM == 2 ){ 6228 if ( scalar keys %{$info->{interface}->{$newif}->{ips}} ){ 6229 $args{monitored} = 1; 6230 } 6231 }elsif ( $IFM == 3 ){ 6232 if ( defined $info->{interface}->{$newif}->{ips} && 6233 (my $snmp_target = $self->snmp_target) ){ 6234 foreach my $address ( keys %{ $info->{interface}->{$newif}->{ips} } ){ 6235 if ( $address eq $snmp_target->address ){ 6236 $args{monitored} = 1; 6237 last; 6238 } 6239 } 6240 } 6241 }else{ 6242 $logger->warn("Configured IF_MONITORED value: $IFM not recognized"); 6243 } 6244 } 6245 6246 $if = Interface->snmp_update( 6247 %args, 6248 snmp_info => $info->{interface}->{$newif}, 6249 add_subnets => $add_subnets, 6250 subs_inherit => $subs_inherit, 6251 stp_instances => $info->{stp_instances}, 6252 ); 6253 6254 } 6255 6256 $self->throw_fatal("Model::Device::_update_interfaces: $host: ". 6257 "Could not find or create interface: $newnumber") 6258 unless $if; 6259 6260 # Remove this interface from list to delete 6261 delete $oldifs{$if->id} if exists $oldifs{$if->id}; 6262 6263 } #end foreach my newif 6264 6265 ############################################## 6266 # Mark each interface that no longer exists 6267 # 6268 foreach my $id ( keys %oldifs ) { 6269 my $iface = $oldifs{$id}; 6270 if ( $iface->doc_status eq "snmp" ){ 6271 $logger->info(sprintf("%s: Interface %s no longer exists. Marking as removed.", 6272 $host, $iface->get_label)); 6273 # Also, remove any cdp/lldp info from that interface to avoid confusion 6274 # while discovering topology 6275 $iface->update({doc_status=>'removed', 6276 dp_remote_id=>"", dp_remote_ip=>"", 6277 dp_remote_port=>"", dp_remote_type=>""}); 6278 $iface->remove_neighbor() if $iface->neighbor(); 6279 } 6280 } 6281 6282 ############################################## 6283 # remove ip addresses that no longer exist 6284 while ( my ($address, $obj) = each %old_ips ){ 6285 # Check that it still exists 6286 # (could have been deleted if its interface was deleted) 6287 next unless ( defined $obj ); 6288 next if ( ref($obj) =~ /deleted/i ); 6289 6290 # Don't delete if interface was added manually, which means that 6291 # the IP was probably manually added too. 6292 next if ($obj->interface && $obj->interface->doc_status eq 'manual'); 6293 6294 # Don't delete dynamic addresses, just unset the interface 6295 if ( $obj->status && $obj->status->name eq 'Dynamic' ){ 6296 $obj->update({interface=>undef}); 6297 next; 6298 } 6299 6300 # If interface is set to "Ignore IP", don't delete existing IP 6301 if ( $obj->interface && $obj->interface->ignore_ip ){ 6302 $logger->debug(sub{sprintf("%s: IP %s not deleted: %s set to Ignore IP", 6303 $host, $obj->address, $obj->interface->get_label)}); 6304 next; 6305 } 6306 6307 # Don't delete snmp_target address unless updating via UI 6308 if ( $ENV{REMOTE_USER} eq 'netdot' && $self->snmp_target && 6309 $self->snmp_target->id == $obj->id ){ 6310 $logger->debug(sub{sprintf("%s: IP %s is snmp target. Skipping delete", 6311 $host, $obj->address)}); 6312 next; 6313 } 6314 6315 $logger->info(sprintf("%s: IP %s no longer exists. Removing.", 6316 $host, $obj->address)); 6317 $obj->delete(no_update_tree=>1); 6318 } 6319 6320 ############################################################## 6321 # Update A records for each IP address 6322 6323 if ( $self->config->get('UPDATE_DEVICE_IP_NAMES') && $self->auto_dns ){ 6324 6325 # Get addresses that the main Device name resolves to 6326 my @hostnameips; 6327 if ( @hostnameips = Netdot->dns->resolve_name($host) ){ 6328 $logger->debug(sub{ sprintf("Device::_update_interfaces: %s resolves to: %s", 6329 $host, (join ", ", @hostnameips))}); 6330 } 6331 6332 my @my_ips; 6333 foreach my $ip ( @{ $self->get_ips() } ){ 6334 if ( $ip->version == 6 && $ip->is_link_local ){ 6335 # Do not create AAAA records for link-local addresses 6336 next; 6337 }else{ 6338 push @my_ips, $ip; 6339 } 6340 } 6341 my $num_ips = scalar(@my_ips); 6342 foreach my $ip ( @my_ips ){ 6343 # We do not want to stop the process if a name update fails 6344 eval { 6345 $ip->update_a_records(hostname_ips=>\@hostnameips, num_ips=>$num_ips); 6346 }; 6347 if ( my $e = $@ ){ 6348 $logger->error(sprintf("Error updating A record for IP %s: %s", 6349 $ip->address, $e)); 6350 } 6351 } 6352 } 6353} 6354 6355############################################################### 6356# Add/Update/Delete BGP Peerings 6357# 6358# Arguments 6359# Hash with following keys: 6360# bgp_local_as - Local AS to this device 6361# bgp_id - BGP ID of this device 6362# peers - Hashref of peering info 6363# newdev - Device object arguments hashref 6364# Returns 6365# Nothing 6366# 6367 6368sub _update_bgp_info { 6369 my ($self, %argv) = @_; 6370 6371 my $host = $self->fqdn; 6372 my $devtmp = $argv{newdev}; 6373 6374 # Set Local BGP info 6375 if( defined $argv{bgp_local_as} ){ 6376 $logger->debug(sub{ sprintf("%s: BGP Local AS is %s", $host, $argv{bgp_local_as}) }); 6377 $devtmp->{bgplocalas} = $argv{bgp_local_as}; 6378 } 6379 if( defined $argv{bgp_id} ){ 6380 $logger->debug(sub{ sprintf("%s: BGP ID is %s", $host, $argv{bgp_id})}); 6381 $devtmp->{bgpid} = $argv{bgp_id}; 6382 } 6383 6384 # Get current BGP peerings 6385 # 6386 my %old_peerings; 6387 map { $old_peerings{ $_->id } = $_ } $self->bgppeers(); 6388 6389 if ( $ENV{REMOTE_USER} eq 'netdot' ){ 6390 6391 # Avoid deleting all peerings in cases where we fail to fetch the SNMP data 6392 # We compare the number of old and the number of new, and stop if 6393 # the difference is > threshold. We only do this if the user is 'netdot' 6394 # to allow a real user to manually update it 6395 6396 my $p_old = scalar(keys(%old_peerings)); 6397 my $p_new = scalar(keys(%{$argv{peers}})); 6398 6399 $logger->debug("$host: Old peerings: $p_old, New peerings: $p_new"); 6400 6401 # Threshold hard set for now 6402 my $p_thold = 0.5; 6403 6404 if ( ($p_old && !$p_new) || ($p_new && ($p_new < $p_old) && 6405 ($p_new / $p_old) <= $p_thold) ){ 6406 $logger->warn(sprintf("%s: new/old interface ratio: %.2f is below threshold (%s) ". 6407 "Skipping BGP peerings update. Re-discover manually if needed.", 6408 $host, $p_new/$p_old, $p_thold)); 6409 return; 6410 } 6411 } 6412 6413 # Update BGP Peerings 6414 # 6415 foreach my $peer ( keys %{$argv{peers}} ){ 6416 $self->update_bgp_peering(peer => $argv{peers}->{$peer}, 6417 old_peerings => \%old_peerings); 6418 } 6419 # remove each BGP Peering that no longer exists 6420 # 6421 foreach my $peerid ( keys %old_peerings ) { 6422 my $p = $old_peerings{$peerid}; 6423 $logger->info(sprintf("%s: BGP Peering with %s (%s) no longer exists. Removing.", 6424 $host, $p->entity->name, $p->bgppeeraddr)); 6425 $p->delete(); 6426 } 6427} 6428 6429 6430############################################################### 6431# Takes an OID and returns the object name if the right MIB is loaded. 6432# 6433# Arguments: SNMP OID string 6434# Returns: Object name (scalar) 6435# 6436sub _snmp_translate { 6437 my ($self, $oid) = @_; 6438 my $name = &SNMP::translateObj($oid); 6439 return $name if defined($name); 6440} 6441 6442 6443##################################################################### 6444# Converts dotted-decimal octets from SNMP into IPv6 address format, 6445# i.e: 6446# 32.1.4.104.13.1.0.196.0.0.0.0.0.0.0.0 => 6447# 2001:0468:0d01:00c4:0000:0000:0000:0000 6448# 6449sub _octet_string_to_v6 { 6450 my ($self, $str) = @_; 6451 return unless $str; 6452 my $v6_packed = pack("C*", split(/\./, $str)); 6453 my @groups = map { sprintf("%04x", $_) } unpack("n*", $v6_packed); 6454 return unless ( scalar(@groups) == 8 ); 6455 my $v6addr = join(':', @groups); 6456 return $v6addr; 6457} 6458 6459################################################################ 6460# Subclass if needed 6461# 6462# Arguments 6463# Hash 6464# Returns 6465# Device subclass object 6466# Example 6467# $self->_netdot_rebless($sinfo); 6468# 6469sub _netdot_rebless { 6470 my ($self, %argv) = @_; 6471 my $class = ref($self); 6472 my $host = $self->fqdn; 6473 6474 my ($sclass, $sysobjectid) = @argv{'sclass', 'sysobjectid'}; 6475 6476 if ( $class ne 'Device' && $class ne __PACKAGE__ ){ 6477 # Looks like we were subclassed already 6478 $logger->debug("Device::_netdot_rebless: $host: Already reblessed as $class"); 6479 return $self; 6480 } 6481 6482 my $new_class = __PACKAGE__; 6483 6484 if ( defined $sclass && $sclass =~ /Airespace/o ){ 6485 $new_class .= '::Airespace'; 6486 $logger->debug("Device::_netdot_rebless: $host: changed class to $new_class"); 6487 bless $self, $new_class; 6488 return $self; 6489 } 6490 6491 # In the other cases, we have more flexibility by mapping OIDs to classes in 6492 # the config file. 6493 6494 my $oid = $sysobjectid; 6495 $oid ||= $self->asset_id->product_id->sysobjectid 6496 if ( $self->asset_id && $self->asset_id->product_id ); 6497 6498 unless ( $oid ){ 6499 $logger->debug("Device::_netdot_rebless: $host: sysObjectID not available"); 6500 return; 6501 } 6502 my $obj = $self->_snmp_translate($oid); 6503 unless ( $obj ){ 6504 $logger->debug("Device::_netdot_rebless: $host: Unknown SysObjectID $oid"); 6505 return; 6506 } 6507 6508 my %OID2CLASSMAP = %{ $self->config->get('FETCH_DEVICE_INFO_VIA_CLI') }; 6509 foreach my $pat ( keys %OID2CLASSMAP ){ 6510 if ( $obj =~ /$pat/ ){ 6511 my $subclass = $OID2CLASSMAP{$pat}; 6512 $new_class .= "::CLI::$subclass"; 6513 $logger->debug("Device::_netdot_rebless: $host: changed class to $new_class"); 6514 bless $self, $new_class; 6515 return $self; 6516 } 6517 } 6518} 6519 6520############################################################################ 6521# 6522# search_by_type - Get a list of Device objects by type 6523# 6524# 6525__PACKAGE__->set_sql(by_type => qq{ 6526 SELECT d.id 6527 FROM device d, product p, producttype t, rr, asset a 6528 WHERE a.product_id = p.id 6529 AND d.asset_id = a.id 6530 AND p.type = t.id 6531 AND rr.id = d.name 6532 AND t.id = ? 6533 ORDER BY rr.name 6534 }); 6535 6536__PACKAGE__->set_sql(no_type => qq{ 6537 SELECT p.name, p.id, COUNT(d.id) AS numdevs 6538 FROM device d, product p, asset a 6539 WHERE a.product_id = p.id 6540 AND d.asset_id = a.id 6541 AND p.type IS NULL 6542 GROUP BY p.name, p.id 6543 ORDER BY numdevs DESC 6544 }); 6545 6546__PACKAGE__->set_sql(by_product_os => qq{ 6547 SELECT d.id, a.product_id, d.os 6548 FROM device d, asset a 6549 WHERE d.asset_id = a.id 6550 AND d.os is NOT NULL 6551 AND d.os != '0' 6552 ORDER BY a.product_id,d.os 6553 }); 6554 6555__PACKAGE__->set_sql(for_os_mismatches => qq{ 6556 SELECT d.id,a.product_id,d.os,d.name 6557 FROM device d, product p, asset a 6558 WHERE a.product_id=p.id 6559 AND d.asset_id = a.id 6560 AND d.os IS NOT NULL 6561 AND p.latest_os IS NOT NULL 6562 AND d.os != p.latest_os 6563 ORDER BY a.product_id,d.os 6564 }); 6565 6566__PACKAGE__->set_sql(by_mac => qq{ 6567 SELECT d.id 6568 FROM device d, physaddr p, asset a 6569 WHERE a.physaddr = p.id 6570 AND d.asset_id = a.id 6571 AND p.address = ? 6572 }); 6573 6574=head1 AUTHOR 6575 6576Carlos Vicente, C<< <cvicente at ns.uoregon.edu> >> 6577 6578=head1 COPYRIGHT & LICENSE 6579 6580Copyright 2012 University of Oregon, all rights reserved. 6581 6582This program is free software; you can redistribute it and/or modify 6583it under the terms of the GNU General Public License as published by 6584the Free Software Foundation; either version 2 of the License, or 6585(at your option) any later version. 6586 6587This program is distributed in the hope that it will be useful, but 6588WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY 6589or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 6590License for more details. 6591 6592You should have received a copy of the GNU General Public License 6593along with this program; if not, write to the Free Software Foundation, 6594Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 6595 6596=cut 6597 6598#Be sure to return 1 65991; 6600 6601 6602