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