From 5f2da69697a1b18735b608a5bbd14e23c2c473a7 Mon Sep 17 00:00:00 2001 From: Oliver Gorwits Date: Sat, 9 Sep 2017 22:26:04 +0100 Subject: [PATCH] move discover and discoverall to worker plugins --- .../Netdisco/Backend/Worker/Poller/Device.pm | 100 -- lib/App/Netdisco/Core/Discover.pm | 996 ------------------ lib/App/Netdisco/DB/Result/Device.pm | 2 + lib/App/Netdisco/Worker/Plugin/Arpnip.pm | 9 +- .../Netdisco/Worker/Plugin/Arpnip/Nodes.pm | 5 +- .../Netdisco/Worker/Plugin/Arpnip/Subnets.pm | 5 +- lib/App/Netdisco/Worker/Plugin/Discover.pm | 30 + .../Worker/Plugin/Discover/CanonicalIP.pm | 82 ++ .../Worker/Plugin/Discover/Entities.pm | 96 ++ .../Worker/Plugin/Discover/Interfaces.pm | 151 +++ .../Worker/Plugin/Discover/Neighbors.pm | 335 ++++++ .../Worker/Plugin/Discover/PortPower.pm | 82 ++ .../Worker/Plugin/Discover/Properties.pm | 105 ++ .../Netdisco/Worker/Plugin/Discover/VLANs.pm | 95 ++ .../Worker/Plugin/Discover/Wireless.pm | 86 ++ .../Worker/Plugin/Discover/WithNodes.pm | 39 + lib/App/Netdisco/Worker/Plugin/DiscoverAll.pm | 31 + lib/App/Netdisco/Worker/Plugin/Macsuck.pm | 9 +- .../Netdisco/Worker/Plugin/Macsuck/Nodes.pm | 4 +- .../Worker/Plugin/Macsuck/WirelessNodes.pm | 4 +- 20 files changed, 1150 insertions(+), 1116 deletions(-) delete mode 100644 lib/App/Netdisco/Backend/Worker/Poller/Device.pm delete mode 100644 lib/App/Netdisco/Core/Discover.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/Discover.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/Discover/CanonicalIP.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/Discover/Entities.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/Discover/Interfaces.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/Discover/Neighbors.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/Discover/PortPower.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/Discover/Properties.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/Discover/VLANs.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/Discover/Wireless.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/Discover/WithNodes.pm create mode 100644 lib/App/Netdisco/Worker/Plugin/DiscoverAll.pm diff --git a/lib/App/Netdisco/Backend/Worker/Poller/Device.pm b/lib/App/Netdisco/Backend/Worker/Poller/Device.pm deleted file mode 100644 index 45861473..00000000 --- a/lib/App/Netdisco/Backend/Worker/Poller/Device.pm +++ /dev/null @@ -1,100 +0,0 @@ -package App::Netdisco::Backend::Worker::Poller::Device; - -use Dancer qw/:moose :syntax :script/; - -use App::Netdisco::Transport::SNMP; -use App::Netdisco::Util::Device qw/get_device is_discoverable_now/; -use App::Netdisco::Core::Discover ':all'; -use App::Netdisco::Backend::Util ':all'; -use App::Netdisco::JobQueue qw/jq_queued jq_insert/; - -use Dancer::Plugin::DBIC 'schema'; -use NetAddr::IP::Lite ':lower'; - -use Role::Tiny; -use namespace::clean; - -# queue a discover job for all devices known to Netdisco -sub discoverall { - my ($self, $job) = @_; - - my %queued = map {$_ => 1} jq_queued('discover'); - my @devices = schema('netdisco')->resultset('Device')->search({ - -or => [ 'vendor' => undef, 'vendor' => { '!=' => 'netdisco' }], - })->get_column('ip')->all; - my @filtered_devices = grep {!exists $queued{$_}} @devices; - - jq_insert([ - map {{ - device => $_, - action => 'discover', - username => $job->username, - userip => $job->userip, - }} (@filtered_devices) - ]); - - return job_done("Queued discover job for all devices"); -} - -# run a discover job for one device, and its *new* neighbors -sub discover { - my ($self, $job) = @_; - - my $device = get_device($job->device) - or return job_error( - "discover failed: unable to interpret device parameter: " - . ($job->device || "''")); - my $host = $device->ip; - - if ($device->ip eq '0.0.0.0') { - return job_error("discover failed: no device param (need -d ?)"); - } - - if ($device->in_storage - and $device->vendor and $device->vendor eq 'netdisco') { - return job_done("discover skipped: $host is pseudo-device"); - } - - unless (is_discoverable_now($device)) { - return job_defer("discover deferred: $host is not discoverable"); - } - - my $snmp = App::Netdisco::Transport::SNMP->reader_for($device); - if (!defined $snmp) { - return job_defer("discover failed: could not SNMP connect to $host"); - } - - store_device($device, $snmp); - set_canonical_ip($device, $snmp); # must come after store_device - store_interfaces($device, $snmp); - store_wireless($device, $snmp); - store_vlans($device, $snmp); - store_power($device, $snmp); - store_modules($device, $snmp) if setting('store_modules'); - discover_new_neighbors($device, $snmp); - - # if requested, and the device has not yet been arpniped/macsucked, queue now - if ($device->in_storage and $job->subaction and $job->subaction eq 'with-nodes') { - if (!defined $device->last_macsuck) { - jq_insert({ - device => $device->ip, - action => 'macsuck', - username => $job->username, - userip => $job->userip, - }); - } - - if (!defined $device->last_arpnip) { - jq_insert({ - device => $device->ip, - action => 'arpnip', - username => $job->username, - userip => $job->userip, - }); - } - } - - return job_done("Ended discover for $host"); -} - -1; diff --git a/lib/App/Netdisco/Core/Discover.pm b/lib/App/Netdisco/Core/Discover.pm deleted file mode 100644 index be7c7bcd..00000000 --- a/lib/App/Netdisco/Core/Discover.pm +++ /dev/null @@ -1,996 +0,0 @@ -package App::Netdisco::Core::Discover; - -use Dancer qw/:syntax :script/; -use Dancer::Plugin::DBIC 'schema'; - -use App::Netdisco::Util::Device - qw/get_device match_devicetype is_discoverable/; -use App::Netdisco::Util::Permission qw/check_acl_only check_acl_no/; -use App::Netdisco::Util::FastResolver 'hostnames_resolve_async'; -use App::Netdisco::Util::DNS ':all'; -use App::Netdisco::JobQueue qw/jq_queued jq_insert/; -use NetAddr::IP::Lite ':lower'; -use List::MoreUtils (); -use Scalar::Util 'blessed'; -use Encode; -use Try::Tiny; -use NetAddr::MAC; - -use base 'Exporter'; -our @EXPORT = (); -our @EXPORT_OK = qw/ - set_canonical_ip - store_device store_interfaces store_wireless - store_vlans store_power store_modules - store_neighbors discover_new_neighbors -/; -our %EXPORT_TAGS = (all => \@EXPORT_OK); - -=head1 NAME - -App::Netdisco::Core::Discover - -=head1 DESCRIPTION - -A set of helper subroutines to support parts of the Netdisco application. - -There are no default exports, however the C<:all> tag will export all -subroutines. - -=head1 EXPORT_OK - -=head2 set_canonical_ip( $device, $snmp ) - -Returns: C<$device> - -Given a Device database object, and a working SNMP connection, check whether -the database object's IP is the best choice for that device. If not, update -the IP and hostname in the device object for the canonical IP. - -=cut - -sub set_canonical_ip { - my ($device, $snmp) = @_; - - my $old_ip = $device->ip; - my $new_ip = $old_ip; - my $revofname = ipv4_from_hostname($snmp->name); - - if (setting('reverse_sysname') and $revofname) { - if ($snmp->snmp_connect_ip( $new_ip )) { - $new_ip = $revofname; - } - else { - debug sprintf ' [%s] device - cannot renumber to %s - SNMP connect failed', - $old_ip, $revofname; - } - } - - if (scalar @{ setting('device_identity') }) { - my @idmaps = @{ setting('device_identity') }; - my $devips = $device->device_ips->order_by('alias'); - - ALIAS: while (my $alias = $devips->next) { - next if $alias->alias eq $old_ip; - - foreach my $map (@idmaps) { - next unless ref {} eq ref $map; - - foreach my $key (sort keys %$map) { - # lhs matches device, rhs matches device_ip - if (check_acl_only($device, $key) - and check_acl_only($alias, $map->{$key})) { - - if ($snmp->snmp_connect_ip( $alias->alias )) { - $new_ip = $alias->alias; - last ALIAS; - } - else { - debug sprintf ' [%s] device - cannot renumber to %s - SNMP connect failed', - $old_ip, $alias->alias; - } - } - } - } - } # ALIAS - } - - return if $new_ip eq $old_ip; - - schema('netdisco')->txn_do(sub { - # delete target device with the same vendor and serial number - schema('netdisco')->resultset('Device')->search({ - ip => $new_ip, vendor => $device->vendor, serial => $device->serial, - })->delete; - - # if target device exists then this will die - $device->renumber($new_ip) - or die "cannot renumber to: $new_ip"; # rollback - - debug sprintf ' [%s] device - changed IP to %s (%s)', - $old_ip, $device->ip, ($device->dns || ''); - }); -} - -=head2 store_device( $device, $snmp ) - -Given a Device database object, and a working SNMP connection, discover and -store basic device information. - -The Device database object can be a fresh L object which is -not yet stored to the database. - -=cut - -sub store_device { - my ($device, $snmp) = @_; - - my $ip_index = $snmp->ip_index; - my $interfaces = $snmp->interfaces; - my $ip_netmask = $snmp->ip_netmask; - - # build device aliases suitable for DBIC - my @aliases; - foreach my $entry (keys %$ip_index) { - my $ip = NetAddr::IP::Lite->new($entry) - or next; - my $addr = $ip->addr; - - next if $addr eq '0.0.0.0'; - next if check_acl_no($ip, 'group:__LOCAL_ADDRESSES__'); - next if setting('ignore_private_nets') and $ip->is_rfc1918; - - my $iid = $ip_index->{$addr}; - my $port = $interfaces->{$iid}; - my $subnet = $ip_netmask->{$addr} - ? NetAddr::IP::Lite->new($addr, $ip_netmask->{$addr})->network->cidr - : undef; - - debug sprintf ' [%s] device - aliased as %s', $device->ip, $addr; - push @aliases, { - alias => $addr, - port => $port, - subnet => $subnet, - dns => undef, - }; - } - - debug sprintf ' resolving %d aliases with max %d outstanding requests', - scalar @aliases, $ENV{'PERL_ANYEVENT_MAX_OUTSTANDING_DNS'}; - my $resolved_aliases = hostnames_resolve_async(\@aliases); - - # fake one aliases entry for devices not providing ip_index - push @$resolved_aliases, { alias => $device->ip, dns => $device->dns } - if 0 == scalar @aliases; - - # VTP Management Domain -- assume only one. - my $vtpdomains = $snmp->vtp_d_name; - my $vtpdomain; - if (defined $vtpdomains and scalar values %$vtpdomains) { - $device->set_column( vtp_domain => (values %$vtpdomains)[-1] ); - } - - my $hostname = hostname_from_ip($device->ip); - $device->set_column( dns => $hostname ) if $hostname; - - my @properties = qw/ - snmp_ver - description uptime name - layers ports mac - ps1_type ps2_type ps1_status ps2_status - fan slots - vendor os os_ver - /; - - foreach my $property (@properties) { - $device->set_column( $property => $snmp->$property ); - } - - $device->set_column( model => Encode::decode('UTF-8', $snmp->model) ); - $device->set_column( serial => Encode::decode('UTF-8', $snmp->serial) ); - $device->set_column( contact => Encode::decode('UTF-8', $snmp->contact) ); - $device->set_column( location => Encode::decode('UTF-8', $snmp->location) ); - - - $device->set_column( snmp_class => $snmp->class ); - $device->set_column( last_discover => \'now()' ); - - schema('netdisco')->txn_do(sub { - my $gone = $device->device_ips->delete; - debug sprintf ' [%s] device - removed %d aliases', - $device->ip, $gone; - $device->update_or_insert(undef, {for => 'update'}); - $device->device_ips->populate($resolved_aliases); - debug sprintf ' [%s] device - added %d new aliases', - $device->ip, scalar @aliases; - }); -} - -=head2 store_interfaces( $device, $snmp ) - -Given a Device database object, and a working SNMP connection, discover and -store the device's interface/port information. - -The Device database object can be a fresh L object which is -not yet stored to the database. - -=cut - -sub store_interfaces { - my ($device, $snmp) = @_; - - my $interfaces = $snmp->interfaces; - my $i_type = $snmp->i_type; - my $i_ignore = $snmp->i_ignore; - my $i_descr = $snmp->i_description; - my $i_mtu = $snmp->i_mtu; - my $i_speed = $snmp->i_speed; - my $i_mac = $snmp->i_mac; - my $i_up = $snmp->i_up; - my $i_up_admin = $snmp->i_up_admin; - my $i_name = $snmp->i_name; - my $i_duplex = $snmp->i_duplex; - my $i_duplex_admin = $snmp->i_duplex_admin; - my $i_stp_state = $snmp->i_stp_state; - my $i_vlan = $snmp->i_vlan; - my $i_lastchange = $snmp->i_lastchange; - my $agg_ports = $snmp->agg_ports; - - # clear the cached uptime and get a new one - my $dev_uptime = $snmp->load_uptime; - if (!defined $dev_uptime) { - error sprintf ' [%s] interfaces - Error! Failed to get uptime from device!', - $device->ip; - return; - } - - # used to track how many times the device uptime wrapped - my $dev_uptime_wrapped = 0; - - # use SNMP-FRAMEWORK-MIB::snmpEngineTime if available to - # fix device uptime if wrapped - if (defined $snmp->snmpEngineTime) { - $dev_uptime_wrapped = int( $snmp->snmpEngineTime * 100 / 2**32 ); - if ($dev_uptime_wrapped > 0) { - info sprintf ' [%s] interface - device uptime wrapped %d times - correcting', - $device->ip, $dev_uptime_wrapped; - $device->uptime( $dev_uptime + $dev_uptime_wrapped * 2**32 ); - } - } - - # build device interfaces suitable for DBIC - my %interfaces; - foreach my $entry (keys %$interfaces) { - my $port = $interfaces->{$entry}; - - if (not $port) { - debug sprintf ' [%s] interfaces - ignoring %s (no port mapping)', - $device->ip, $entry; - next; - } - - if (scalar grep {$port =~ m/^$_$/} @{setting('ignore_interfaces') || []}) { - debug sprintf - ' [%s] interfaces - ignoring %s (%s) (config:ignore_interfaces)', - $device->ip, $entry, $port; - next; - } - - if (exists $i_ignore->{$entry}) { - debug sprintf ' [%s] interfaces - ignoring %s (%s) (%s)', - $device->ip, $entry, $port, $i_type->{$entry}; - next; - } - - my $lc = $i_lastchange->{$entry} || 0; - if (not $dev_uptime_wrapped and $lc > $dev_uptime) { - info sprintf ' [%s] interfaces - device uptime wrapped (%s) - correcting', - $device->ip, $port; - $device->uptime( $dev_uptime + 2**32 ); - $dev_uptime_wrapped = 1; - } - - if ($device->is_column_changed('uptime') and $lc) { - if ($lc < $dev_uptime) { - # ambiguous: lastchange could be sysUptime before or after wrap - if ($dev_uptime > 30000 and $lc < 30000) { - # uptime wrap more than 5min ago but lastchange within 5min - # assume lastchange was directly after boot -> no action - } - else { - # uptime wrap less than 5min ago or lastchange > 5min ago - # to be on safe side, assume lastchange after counter wrap - debug sprintf - ' [%s] interfaces - correcting LastChange for %s, assuming sysUptime wrap', - $device->ip, $port; - $lc += $dev_uptime_wrapped * 2**32; - } - } - } - - $interfaces{$port} = { - port => $port, - descr => $i_descr->{$entry}, - up => $i_up->{$entry}, - up_admin => $i_up_admin->{$entry}, - mac => $i_mac->{$entry}, - speed => $i_speed->{$entry}, - mtu => $i_mtu->{$entry}, - name => Encode::decode('UTF-8', $i_name->{$entry}), - duplex => $i_duplex->{$entry}, - duplex_admin => $i_duplex_admin->{$entry}, - stp => $i_stp_state->{$entry}, - type => $i_type->{$entry}, - vlan => $i_vlan->{$entry}, - pvid => $i_vlan->{$entry}, - is_master => 'false', - slave_of => undef, - lastchange => $lc, - }; - } - - # must do this after building %interfaces so that we can set is_master - foreach my $sidx (keys %$agg_ports) { - my $slave = $interfaces->{$sidx} or next; - my $master = $interfaces->{ $agg_ports->{$sidx} } or next; - next unless exists $interfaces{$slave} and exists $interfaces{$master}; - - $interfaces{$slave}->{slave_of} = $master; - $interfaces{$master}->{is_master} = 'true'; - } - - schema('netdisco')->resultset('DevicePort')->txn_do_locked(sub { - my $gone = $device->ports->delete({keep_nodes => 1}); - debug sprintf ' [%s] interfaces - removed %d interfaces', - $device->ip, $gone; - $device->update_or_insert(undef, {for => 'update'}); - $device->ports->populate([values %interfaces]); - debug sprintf ' [%s] interfaces - added %d new interfaces', - $device->ip, scalar values %interfaces; - }); -} - -=head2 store_wireless( $device, $snmp ) - -Given a Device database object, and a working SNMP connection, discover and -store the device's wireless interface information. - -The Device database object can be a fresh L object which is -not yet stored to the database. - -=cut - -sub store_wireless { - my ($device, $snmp) = @_; - - my $ssidlist = $snmp->i_ssidlist; - return unless scalar keys %$ssidlist; - - my $interfaces = $snmp->interfaces; - my $ssidbcast = $snmp->i_ssidbcast; - my $ssidmac = $snmp->i_ssidmac; - my $channel = $snmp->i_80211channel; - my $power = $snmp->dot11_cur_tx_pwr_mw; - - # build device ssid list suitable for DBIC - my @ssids; - foreach my $entry (keys %$ssidlist) { - (my $iid = $entry) =~ s/\.\d+$//; - my $port = $interfaces->{$iid}; - - if (not $port) { - debug sprintf ' [%s] wireless - ignoring %s (no port mapping)', - $device->ip, $iid; - next; - } - - push @ssids, { - port => $port, - ssid => $ssidlist->{$entry}, - broadcast => $ssidbcast->{$entry}, - bssid => $ssidmac->{$entry}, - }; - } - - schema('netdisco')->txn_do(sub { - my $gone = $device->ssids->delete; - debug sprintf ' [%s] wireless - removed %d SSIDs', - $device->ip, $gone; - $device->ssids->populate(\@ssids); - debug sprintf ' [%s] wireless - added %d new SSIDs', - $device->ip, scalar @ssids; - }); - - # build device channel list suitable for DBIC - my @channels; - foreach my $entry (keys %$channel) { - my $port = $interfaces->{$entry}; - - if (not $port) { - debug sprintf ' [%s] wireless - ignoring %s (no port mapping)', - $device->ip, $entry; - next; - } - - push @channels, { - port => $port, - channel => $channel->{$entry}, - power => $power->{$entry}, - }; - } - - schema('netdisco')->txn_do(sub { - my $gone = $device->wireless_ports->delete; - debug sprintf ' [%s] wireless - removed %d wireless channels', - $device->ip, $gone; - $device->wireless_ports->populate(\@channels); - debug sprintf ' [%s] wireless - added %d new wireless channels', - $device->ip, scalar @channels; - }); -} - -=head2 store_vlans( $device, $snmp ) - -Given a Device database object, and a working SNMP connection, discover and -store the device's vlan information. - -The Device database object can be a fresh L object which is -not yet stored to the database. - -=cut - -sub store_vlans { - my ($device, $snmp) = @_; - - my $v_name = $snmp->v_name; - my $v_index = $snmp->v_index; - - # build device vlans suitable for DBIC - my %v_seen = (); - my @devicevlans; - foreach my $entry (keys %$v_name) { - my $vlan = $v_index->{$entry}; - next unless defined $vlan and $vlan; - ++$v_seen{$vlan}; - - push @devicevlans, { - vlan => $vlan, - description => $v_name->{$entry}, - last_discover => \'now()', - }; - } - - my $i_vlan = $snmp->i_vlan; - my $i_vlan_membership = $snmp->i_vlan_membership; - my $i_vlan_type = $snmp->i_vlan_type; - my $interfaces = $snmp->interfaces; - - # build device port vlans suitable for DBIC - my @portvlans = (); - foreach my $entry (keys %$i_vlan_membership) { - my %port_vseen = (); - my $port = $interfaces->{$entry}; - next unless defined $port; - - my $type = $i_vlan_type->{$entry}; - - foreach my $vlan (@{ $i_vlan_membership->{$entry} }) { - next unless defined $vlan and $vlan; - next if ++$port_vseen{$vlan} > 1; - - my $native = ((defined $i_vlan->{$entry}) and ($vlan eq $i_vlan->{$entry})) ? "t" : "f"; - push @portvlans, { - port => $port, - vlan => $vlan, - native => $native, - vlantype => $type, - last_discover => \'now()', - }; - - next if $v_seen{$vlan}; - - # also add an unnamed vlan to the device - push @devicevlans, { - vlan => $vlan, - description => (sprintf "VLAN %d", $vlan), - last_discover => \'now()', - }; - ++$v_seen{$vlan}; - } - } - - schema('netdisco')->txn_do(sub { - my $gone = $device->vlans->delete; - debug sprintf ' [%s] vlans - removed %d device VLANs', - $device->ip, $gone; - $device->vlans->populate(\@devicevlans); - debug sprintf ' [%s] vlans - added %d new device VLANs', - $device->ip, scalar @devicevlans; - }); - - schema('netdisco')->txn_do(sub { - my $gone = $device->port_vlans->delete; - debug sprintf ' [%s] vlans - removed %d port VLANs', - $device->ip, $gone; - $device->port_vlans->populate(\@portvlans); - debug sprintf ' [%s] vlans - added %d new port VLANs', - $device->ip, scalar @portvlans; - }); -} - -=head2 store_power( $device, $snmp ) - -Given a Device database object, and a working SNMP connection, discover and -store the device's PoE information. - -The Device database object can be a fresh L object which is -not yet stored to the database. - -=cut - -sub store_power { - my ($device, $snmp) = @_; - - my $p_watts = $snmp->peth_power_watts; - my $p_status = $snmp->peth_power_status; - - if (!defined $p_watts) { - debug sprintf ' [%s] power - 0 power modules', $device->ip; - return; - } - - # build device module power info suitable for DBIC - my @devicepower; - foreach my $entry (keys %$p_watts) { - push @devicepower, { - module => $entry, - power => $p_watts->{$entry}, - status => $p_status->{$entry}, - }; - } - - my $interfaces = $snmp->interfaces; - my $p_ifindex = $snmp->peth_port_ifindex; - my $p_admin = $snmp->peth_port_admin; - my $p_pstatus = $snmp->peth_port_status; - my $p_class = $snmp->peth_port_class; - my $p_power = $snmp->peth_port_power; - - # build device port power info suitable for DBIC - my @portpower; - foreach my $entry (keys %$p_ifindex) { - my $port = $interfaces->{ $p_ifindex->{$entry} }; - next unless $port; - - my ($module) = split m/\./, $entry; - - push @portpower, { - port => $port, - module => $module, - admin => $p_admin->{$entry}, - status => $p_pstatus->{$entry}, - class => $p_class->{$entry}, - power => $p_power->{$entry}, - - }; - } - - schema('netdisco')->txn_do(sub { - my $gone = $device->power_modules->delete; - debug sprintf ' [%s] power - removed %d power modules', - $device->ip, $gone; - $device->power_modules->populate(\@devicepower); - debug sprintf ' [%s] power - added %d new power modules', - $device->ip, scalar @devicepower; - }); - - schema('netdisco')->txn_do(sub { - my $gone = $device->powered_ports->delete; - debug sprintf ' [%s] power - removed %d PoE capable ports', - $device->ip, $gone; - $device->powered_ports->populate(\@portpower); - debug sprintf ' [%s] power - added %d new PoE capable ports', - $device->ip, scalar @portpower; - }); -} - -=head2 store_modules( $device, $snmp ) - -Given a Device database object, and a working SNMP connection, discover and -store the device's module information. - -The Device database object can be a fresh L object which is -not yet stored to the database. - -=cut - -sub store_modules { - my ($device, $snmp) = @_; - - my $e_index = $snmp->e_index; - - if (!defined $e_index) { - schema('netdisco')->txn_do(sub { - my $gone = $device->modules->delete; - debug sprintf ' [%s] modules - removed %d chassis modules', - $device->ip, $gone; - - $device->modules->update_or_create({ - ip => $device->ip, - index => 1, - parent => 0, - name => 'chassis', - class => 'chassis', - pos => -1, - # too verbose and link doesn't work anyway - # description => $device->description, - sw_ver => $device->os_ver, - serial => $device->serial, - model => $device->model, - fru => \'false', - last_discover => \'now()', - }); - }); - - debug - sprintf ' [%s] modules - 0 chassis components (added one pseudo for chassis)', - $device->ip; - - return; - } - - my $e_descr = $snmp->e_descr; - my $e_type = $snmp->e_type; - my $e_parent = $snmp->e_parent; - my $e_name = $snmp->e_name; - my $e_class = $snmp->e_class; - my $e_pos = $snmp->e_pos; - my $e_hwver = $snmp->e_hwver; - my $e_fwver = $snmp->e_fwver; - my $e_swver = $snmp->e_swver; - my $e_model = $snmp->e_model; - my $e_serial = $snmp->e_serial; - my $e_fru = $snmp->e_fru; - - # build device modules list for DBIC - my @modules; - foreach my $entry (keys %$e_index) { - push @modules, { - index => $e_index->{$entry}, - type => $e_type->{$entry}, - parent => $e_parent->{$entry}, - name => Encode::decode('UTF-8', $e_name->{$entry}), - class => $e_class->{$entry}, - pos => $e_pos->{$entry}, - hw_ver => Encode::decode('UTF-8', $e_hwver->{$entry}), - fw_ver => Encode::decode('UTF-8', $e_fwver->{$entry}), - sw_ver => Encode::decode('UTF-8', $e_swver->{$entry}), - model => Encode::decode('UTF-8', $e_model->{$entry}), - serial => Encode::decode('UTF-8', $e_serial->{$entry}), - fru => $e_fru->{$entry}, - description => Encode::decode('UTF-8', $e_descr->{$entry}), - last_discover => \'now()', - }; - } - - schema('netdisco')->txn_do(sub { - my $gone = $device->modules->delete; - debug sprintf ' [%s] modules - removed %d chassis modules', - $device->ip, $gone; - $device->modules->populate(\@modules); - debug sprintf ' [%s] modules - added %d new chassis modules', - $device->ip, scalar @modules; - }); -} - -=head2 store_neighbors( $device, $snmp ) - -returns: C<@to_discover> - -Given a Device database object, and a working SNMP connection, discover and -store the device's port neighbors information. - -Entries in the Topology database table will override any discovered device -port relationships. - -The Device database object can be a fresh L object which is -not yet stored to the database. - -A list of discovererd neighbors will be returned as [C<$ip>, C<$type>] tuples. - -=cut - -sub store_neighbors { - my ($device, $snmp) = @_; - my @to_discover = (); - - # first allow any manually configured topology to be set - _set_manual_topology($device, $snmp); - - if (!defined $snmp->has_topo) { - debug sprintf ' [%s] neigh - neighbor protocols are not enabled', $device->ip; - return @to_discover; - } - - my $interfaces = $snmp->interfaces; - my $c_if = $snmp->c_if; - my $c_port = $snmp->c_port; - my $c_id = $snmp->c_id; - my $c_platform = $snmp->c_platform; - my $c_cap = $snmp->c_cap; - - # v4 and v6 neighbor tables - my $c_ip = ($snmp->c_ip || {}); - my %c_ipv6 = %{ ($snmp->can('hasLLDP') and $snmp->hasLLDP) - ? ($snmp->lldp_ipv6 || {}) : {} }; - - # remove keys with undef values, as c_ip does - delete @c_ipv6{ grep { not defined $c_ipv6{$_} } keys %c_ipv6 }; - # now combine them, v6 wins - $c_ip = { %$c_ip, %c_ipv6 }; - - foreach my $entry (sort (List::MoreUtils::uniq( (keys %$c_ip), (keys %$c_cap) ))) { - if (!defined $c_if->{$entry} or !defined $interfaces->{ $c_if->{$entry} }) { - debug sprintf ' [%s] neigh - port for IID:%s not resolved, skipping', - $device->ip, $entry; - next; - } - - my $port = $interfaces->{ $c_if->{$entry} }; - my $portrow = schema('netdisco')->resultset('DevicePort') - ->single({ip => $device->ip, port => $port}); - - if (!defined $portrow) { - info sprintf ' [%s] neigh - local port %s not in database!', - $device->ip, $port; - next; - } - - if (ref $c_ip->{$entry}) { - error sprintf ' [%s] neigh - Error! port %s has multiple neighbors - skipping', - $device->ip, $port; - next; - } - - my $remote_ip = $c_ip->{$entry}; - my $remote_port = undef; - my $remote_type = Encode::decode('UTF-8', $c_platform->{$entry} || ''); - my $remote_id = Encode::decode('UTF-8', $c_id->{$entry}); - my $remote_cap = $c_cap->{$entry} || []; - - # IP Phone and WAP detection type fixup - if (scalar @$remote_cap or $remote_type) { - my $phone_flag = grep {match_devicetype($_, 'phone_capabilities')} - @$remote_cap; - my $ap_flag = grep {match_devicetype($_, 'wap_capabilities')} - @$remote_cap; - - if ($phone_flag or match_devicetype($remote_type, 'phone_platforms')) { - $remote_type = 'IP Phone: '. $remote_type - if $remote_type !~ /ip.phone/i; - } - elsif ($ap_flag or match_devicetype($remote_type, 'wap_platforms')) { - $remote_type = 'AP: '. $remote_type; - } - - $portrow->update({remote_type => $remote_type}); - } - - if ($portrow->manual_topo) { - info sprintf ' [%s] neigh - %s has manually defined topology', - $device->ip, $port; - next; - } - - next unless $remote_ip; - - # a bunch of heuristics to search known devices if we don't have a - # useable remote IP... - - if ($remote_ip eq '0.0.0.0' or - check_acl_no($remote_ip, 'group:__LOCAL_ADDRESSES__')) { - - if ($remote_id) { - my $devices = schema('netdisco')->resultset('Device'); - my $neigh = $devices->single({name => $remote_id}); - info sprintf - ' [%s] neigh - bad address %s on port %s, searching for %s instead', - $device->ip, $remote_ip, $port, $remote_id; - - if (!defined $neigh) { - my $mac = NetAddr::MAC->new(mac => $remote_id); - if ($mac and not $mac->errstr) { - $neigh = $devices->single({mac => $mac->as_ieee}); - } - } - - # some HP switches send 127.0.0.1 as remote_ip if no ip address - # on default vlan for HP switches remote_ip looks like - # "myswitchname(012345-012345)" - if (!defined $neigh) { - (my $tmpid = $remote_id) =~ s/.*\(([0-9a-f]{6})-([0-9a-f]{6})\).*/$1$2/; - my $mac = NetAddr::MAC->new(mac => $tmpid); - if ($mac and not $mac->errstr) { - info sprintf - '[%s] neigh - found neighbor %s by MAC %s', - $device->ip, $remote_id, $mac->as_ieee; - $neigh = $devices->single({mac => $mac->as_ieee}); - } - } - - if (!defined $neigh) { - (my $shortid = $remote_id) =~ s/\..*//; - $neigh = $devices->single({name => { -ilike => "${shortid}%" }}); - } - - if ($neigh) { - $remote_ip = $neigh->ip; - info sprintf ' [%s] neigh - found %s with IP %s', - $device->ip, $remote_id, $remote_ip; - } - else { - info sprintf ' [%s] neigh - could not find %s, skipping', - $device->ip, $remote_id; - next; - } - } - else { - info sprintf ' [%s] neigh - skipping unuseable address %s on port %s', - $device->ip, $remote_ip, $port; - next; - } - } - - # what we came here to do.... discover the neighbor - debug sprintf - ' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue', - $device->ip, $remote_ip, ($remote_type || ''), $port; - push @to_discover, [$remote_ip, $remote_type]; - - $remote_port = $c_port->{$entry}; - if (defined $remote_port) { - # clean weird characters - $remote_port =~ s/[^\d\/\.,()\w:-]+//gi; - } - else { - info sprintf ' [%s] neigh - no remote port found for port %s at %s', - $device->ip, $port, $remote_ip; - } - - $portrow->update({ - remote_ip => $remote_ip, - remote_port => $remote_port, - remote_type => $remote_type, - remote_id => $remote_id, - is_uplink => \"true", - manual_topo => \"false", - }); - - # update master of our aggregate to be a neighbor of - # the master on our peer device (a lot of iffs to get there...). - # & cannot use ->neighbor prefetch because this is the port insert! - if (defined $portrow->slave_of) { - - my $peer_device = get_device($remote_ip); - my $master = schema('netdisco')->resultset('DevicePort')->single({ - ip => $device->ip, - port => $portrow->slave_of - }); - - if ($peer_device and $peer_device->in_storage and $master - and not ($portrow->is_master or defined $master->slave_of)) { - - my $peer_port = schema('netdisco')->resultset('DevicePort')->single({ - ip => $peer_device->ip, - port => $portrow->remote_port, - }); - - $master->update({ - remote_ip => ($peer_device->ip || $remote_ip), - remote_port => ($peer_port ? $peer_port->slave_of : undef ), - is_uplink => \"true", - is_master => \"true", - manual_topo => \"false", - }); - } - } - } - - return @to_discover; -} - -# take data from the topology table and update remote_ip and remote_port -# in the devices table. only use root_ips and skip any bad topo entries. -sub _set_manual_topology { - my ($device, $snmp) = @_; - - schema('netdisco')->txn_do(sub { - # clear manual topology flags - schema('netdisco')->resultset('DevicePort') - ->search({ip => $device->ip})->update({manual_topo => \'false'}); - - my $topo_links = schema('netdisco')->resultset('Topology') - ->search({-or => [dev1 => $device->ip, dev2 => $device->ip]}); - debug sprintf ' [%s] neigh - setting manual topology links', $device->ip; - - while (my $link = $topo_links->next) { - # could fail for broken topo, but we ignore to try the rest - try { - schema('netdisco')->txn_do(sub { - # only work on root_ips - my $left = get_device($link->dev1); - my $right = get_device($link->dev2); - - # skip bad entries - return unless ($left->in_storage and $right->in_storage); - - $left->ports - ->single({port => $link->port1}) - ->update({ - remote_ip => $right->ip, - remote_port => $link->port2, - remote_type => undef, - remote_id => undef, - is_uplink => \"true", - manual_topo => \"true", - }); - - $right->ports - ->single({port => $link->port2}) - ->update({ - remote_ip => $left->ip, - remote_port => $link->port1, - remote_type => undef, - remote_id => undef, - is_uplink => \"true", - manual_topo => \"true", - }); - }); - }; - } - }); -} - -=head2 discover_new_neighbors( $device, $snmp ) - -Given a Device database object, and a working SNMP connection, discover and -store the device's port neighbors information. - -Entries in the Topology database table will override any discovered device -port relationships. - -The Device database object can be a fresh L object which is -not yet stored to the database. - -Any discovered neighbor unknown to Netdisco will have a C job -immediately queued (subject to the filtering by the C settings). - -=cut - -sub discover_new_neighbors { - my @to_discover = store_neighbors(@_); - - # only enqueue if device is not already discovered, - # discover_* config permits the discovery - foreach my $neighbor (@to_discover) { - my ($ip, $remote_type) = @$neighbor; - - my $device = get_device($ip); - next if $device->in_storage; - - if (not is_discoverable($device, $remote_type)) { - debug sprintf - ' queue - %s, type [%s] excluded by discover_* config', - $ip, ($remote_type || ''); - next; - } - - jq_insert({ - device => $ip, - action => 'discover', - subaction => 'with-nodes', - }); - } -} - -1; diff --git a/lib/App/Netdisco/DB/Result/Device.pm b/lib/App/Netdisco/DB/Result/Device.pm index 820d77b1..63f43dbd 100644 --- a/lib/App/Netdisco/DB/Result/Device.pm +++ b/lib/App/Netdisco/DB/Result/Device.pm @@ -10,6 +10,8 @@ use warnings; use NetAddr::IP::Lite ':lower'; use App::Netdisco::Util::DNS 'hostname_from_ip'; +use overload '""' => sub { shift->ip }, fallback => 1; + use base 'DBIx::Class::Core'; __PACKAGE__->table("device"); __PACKAGE__->add_columns( diff --git a/lib/App/Netdisco/Worker/Plugin/Arpnip.pm b/lib/App/Netdisco/Worker/Plugin/Arpnip.pm index 74c70bfc..0bc3e7fa 100644 --- a/lib/App/Netdisco/Worker/Plugin/Arpnip.pm +++ b/lib/App/Netdisco/Worker/Plugin/Arpnip.pm @@ -5,7 +5,6 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Util::Device 'is_arpnipable_now'; -use App::Netdisco::Transport::SNMP (); register_worker({ primary => true }, sub { my ($job, $workerconf) = @_; @@ -14,15 +13,13 @@ register_worker({ primary => true }, sub { return Status->error('arpnip failed: unable to interpret device param') unless defined $device; - my $host = $device->ip; - - return Status->done("arpnip skipped: $host not yet discovered") + return Status->done("arpnip skipped: $device not yet discovered") unless $device->in_storage; - return Status->defer("arpnip skipped: $host is pseudo-device") + return Status->defer("arpnip skipped: $device is pseudo-device") if $device->vendor and $device->vendor eq 'netdisco'; - return Status->defer("arpnip deferred: $host is not arpnipable") + return Status->defer("arpnip deferred: $device is not arpnipable") unless is_arpnipable_now($device); return Status->done('Arpnip is able to run.'); diff --git a/lib/App/Netdisco/Worker/Plugin/Arpnip/Nodes.pm b/lib/App/Netdisco/Worker/Plugin/Arpnip/Nodes.pm index 13c98ded..cbae6786 100644 --- a/lib/App/Netdisco/Worker/Plugin/Arpnip/Nodes.pm +++ b/lib/App/Netdisco/Worker/Plugin/Arpnip/Nodes.pm @@ -4,6 +4,7 @@ use Dancer ':syntax'; use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; +use App::Netdisco::Transport::SNMP (); use App::Netdisco::Util::Node 'check_mac'; use App::Netdisco::Util::FastResolver 'hostnames_resolve_async'; use Dancer::Plugin::DBIC 'schema'; @@ -15,9 +16,9 @@ register_worker({ primary => true, driver => 'snmp' }, sub { my $device = $job->device; my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) - or return Status->defer("arpnip failed: could not SNMP connect to $host"); + or return Status->defer("arpnip failed: could not SNMP connect to $device"); - return Status->defer("Skipped arpnip for device $host without layer 3 capability") + return Status->defer("Skipped arpnip for device $device without layer 3 capability") unless $snmp->has_layer(3); # get v4 arp table diff --git a/lib/App/Netdisco/Worker/Plugin/Arpnip/Subnets.pm b/lib/App/Netdisco/Worker/Plugin/Arpnip/Subnets.pm index c523ecac..3c0cb43a 100644 --- a/lib/App/Netdisco/Worker/Plugin/Arpnip/Subnets.pm +++ b/lib/App/Netdisco/Worker/Plugin/Arpnip/Subnets.pm @@ -4,6 +4,7 @@ use Dancer ':syntax'; use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; +use App::Netdisco::Transport::SNMP (); use App::Netdisco::Util::Permission 'check_acl_no'; use Dancer::Plugin::DBIC 'schema'; use NetAddr::IP::Lite ':lower'; @@ -14,9 +15,9 @@ register_worker({ primary => false, driver => 'snmp' }, sub { my $device = $job->device; my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) - or return Status->defer("arpnip failed: could not SNMP connect to $host"); + or return Status->defer("arpnip failed: could not SNMP connect to $device"); - return Status->defer("Skipped arpnip for device $host without layer 3 capability") + return Status->defer("Skipped arpnip for device $device without layer 3 capability") unless $snmp->has_layer(3); # get directly connected networks diff --git a/lib/App/Netdisco/Worker/Plugin/Discover.pm b/lib/App/Netdisco/Worker/Plugin/Discover.pm new file mode 100644 index 00000000..5775f6b6 --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Discover.pm @@ -0,0 +1,30 @@ +package App::Netdisco::Worker::Plugin::Discover; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::Util::Device 'is_discoverable_now'; + +register_worker({ primary => true }, sub { + my ($job, $workerconf) = @_; + my $device = $job->device; + + return Status->error('discover failed: unable to interpret device param') + unless defined $device; + + my $host = $device->ip; + + return Status->error("discover failed: no device param (need -d ?)") + if $host eq '0.0.0.0'; + + return Status->defer("discover skipped: $host is pseudo-device") + if $device->vendor and $device->vendor eq 'netdisco'; + + return Status->defer("discover deferred: $host is not discoverable") + unless is_discoverable_now($device); + + return Status->done('discover is able to run.'); +}); + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/CanonicalIP.pm b/lib/App/Netdisco/Worker/Plugin/Discover/CanonicalIP.pm new file mode 100644 index 00000000..b597635d --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Discover/CanonicalIP.pm @@ -0,0 +1,82 @@ +package App::Netdisco::Worker::Plugin::Discover::CanonicalIP; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::Transport::SNMP (); +use App::Netdisco::Util::Permission 'check_acl_only'; +use App::Netdisco::Util::DNS 'ipv4_from_hostname'; +use Dancer::Plugin::DBIC 'schema'; + +register_worker({ primary => false, driver => 'snmp' }, sub { + my ($job, $workerconf) = @_; + + my $device = $job->device; + my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) + or return Status->defer("discover failed: could not SNMP connect to $device"); + + my $old_ip = $device->ip; + my $new_ip = $old_ip; + my $revofname = ipv4_from_hostname($snmp->name); + + if (setting('reverse_sysname') and $revofname) { + if ($snmp->snmp_connect_ip( $new_ip )) { + $new_ip = $revofname; + } + else { + debug sprintf ' [%s] device - cannot renumber to %s - SNMP connect failed', + $old_ip, $revofname; + } + } + + if (scalar @{ setting('device_identity') }) { + my @idmaps = @{ setting('device_identity') }; + my $devips = $device->device_ips->order_by('alias'); + + ALIAS: while (my $alias = $devips->next) { + next if $alias->alias eq $old_ip; + + foreach my $map (@idmaps) { + next unless ref {} eq ref $map; + + foreach my $key (sort keys %$map) { + # lhs matches device, rhs matches device_ip + if (check_acl_only($device, $key) + and check_acl_only($alias, $map->{$key})) { + + if ($snmp->snmp_connect_ip( $alias->alias )) { + $new_ip = $alias->alias; + last ALIAS; + } + else { + debug sprintf ' [%s] device - cannot renumber to %s - SNMP connect failed', + $old_ip, $alias->alias; + } + } + } + } + } # ALIAS + } + + return Status->done('Ended discover for '. $device->ip) + if $new_ip eq $old_ip; + + schema('netdisco')->txn_do(sub { + # delete target device with the same vendor and serial number + schema('netdisco')->resultset('Device')->search({ + ip => $new_ip, vendor => $device->vendor, serial => $device->serial, + })->delete; + + # if target device exists then this will die + $device->renumber($new_ip) + or die "cannot renumber to: $new_ip"; # rollback + + debug sprintf ' [%s] device - changed IP to %s (%s)', + $old_ip, $device->ip, ($device->dns || ''); + }); + + return Status->done('Ended discover for '. $device->ip); +}); + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/Entities.pm b/lib/App/Netdisco/Worker/Plugin/Discover/Entities.pm new file mode 100644 index 00000000..5b76e2aa --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Discover/Entities.pm @@ -0,0 +1,96 @@ +package App::Netdisco::Worker::Plugin::Discover::Entities; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::Transport::SNMP (); +use Dancer::Plugin::DBIC 'schema'; +use Encode; + +register_worker({ primary => false, driver => 'snmp' }, sub { + my ($job, $workerconf) = @_; + + my $device = $job->device; + my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) + or return Status->defer("discover failed: could not SNMP connect to $device"); + + my $e_index = $snmp->e_index; + + if (!defined $e_index) { + schema('netdisco')->txn_do(sub { + my $gone = $device->modules->delete; + debug sprintf ' [%s] modules - removed %d chassis modules', + $device->ip, $gone; + + $device->modules->update_or_create({ + ip => $device->ip, + index => 1, + parent => 0, + name => 'chassis', + class => 'chassis', + pos => -1, + # too verbose and link doesn't work anyway + # description => $device->description, + sw_ver => $device->os_ver, + serial => $device->serial, + model => $device->model, + fru => \'false', + last_discover => \'now()', + }); + }); + + debug + sprintf ' [%s] modules - 0 chassis components (added one pseudo for chassis)', + $device->ip; + + return Status->done("Ended discover for $device"); + } + + my $e_descr = $snmp->e_descr; + my $e_type = $snmp->e_type; + my $e_parent = $snmp->e_parent; + my $e_name = $snmp->e_name; + my $e_class = $snmp->e_class; + my $e_pos = $snmp->e_pos; + my $e_hwver = $snmp->e_hwver; + my $e_fwver = $snmp->e_fwver; + my $e_swver = $snmp->e_swver; + my $e_model = $snmp->e_model; + my $e_serial = $snmp->e_serial; + my $e_fru = $snmp->e_fru; + + # build device modules list for DBIC + my @modules; + foreach my $entry (keys %$e_index) { + push @modules, { + index => $e_index->{$entry}, + type => $e_type->{$entry}, + parent => $e_parent->{$entry}, + name => Encode::decode('UTF-8', $e_name->{$entry}), + class => $e_class->{$entry}, + pos => $e_pos->{$entry}, + hw_ver => Encode::decode('UTF-8', $e_hwver->{$entry}), + fw_ver => Encode::decode('UTF-8', $e_fwver->{$entry}), + sw_ver => Encode::decode('UTF-8', $e_swver->{$entry}), + model => Encode::decode('UTF-8', $e_model->{$entry}), + serial => Encode::decode('UTF-8', $e_serial->{$entry}), + fru => $e_fru->{$entry}, + description => Encode::decode('UTF-8', $e_descr->{$entry}), + last_discover => \'now()', + }; + } + + schema('netdisco')->txn_do(sub { + my $gone = $device->modules->delete; + debug sprintf ' [%s] modules - removed %d chassis modules', + $device->ip, $gone; + $device->modules->populate(\@modules); + debug sprintf ' [%s] modules - added %d new chassis modules', + $device->ip, scalar @modules; + }); + + return Status->done('Ended discover for '. $device->ip); +}); + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/Interfaces.pm b/lib/App/Netdisco/Worker/Plugin/Discover/Interfaces.pm new file mode 100644 index 00000000..9cca662f --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Discover/Interfaces.pm @@ -0,0 +1,151 @@ +package App::Netdisco::Worker::Plugin::Discover::Interfaces; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::Transport::SNMP (); +use Dancer::Plugin::DBIC 'schema'; +use Encode; + +register_worker({ primary => false, driver => 'snmp' }, sub { + my ($job, $workerconf) = @_; + + my $device = $job->device; + my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) + or return Status->defer("discover failed: could not SNMP connect to $device"); + + my $interfaces = $snmp->interfaces; + my $i_type = $snmp->i_type; + my $i_ignore = $snmp->i_ignore; + my $i_descr = $snmp->i_description; + my $i_mtu = $snmp->i_mtu; + my $i_speed = $snmp->i_speed; + my $i_mac = $snmp->i_mac; + my $i_up = $snmp->i_up; + my $i_up_admin = $snmp->i_up_admin; + my $i_name = $snmp->i_name; + my $i_duplex = $snmp->i_duplex; + my $i_duplex_admin = $snmp->i_duplex_admin; + my $i_stp_state = $snmp->i_stp_state; + my $i_vlan = $snmp->i_vlan; + my $i_lastchange = $snmp->i_lastchange; + my $agg_ports = $snmp->agg_ports; + + # clear the cached uptime and get a new one + my $dev_uptime = $snmp->load_uptime; + if (!defined $dev_uptime) { + error sprintf ' [%s] interfaces - Error! Failed to get uptime from device!', + $device->ip; + return Status->error("discover failed: no uptime from device $device!"); + } + + # used to track how many times the device uptime wrapped + my $dev_uptime_wrapped = 0; + + # use SNMP-FRAMEWORK-MIB::snmpEngineTime if available to + # fix device uptime if wrapped + if (defined $snmp->snmpEngineTime) { + $dev_uptime_wrapped = int( $snmp->snmpEngineTime * 100 / 2**32 ); + if ($dev_uptime_wrapped > 0) { + info sprintf ' [%s] interface - device uptime wrapped %d times - correcting', + $device->ip, $dev_uptime_wrapped; + $device->uptime( $dev_uptime + $dev_uptime_wrapped * 2**32 ); + } + } + + # build device interfaces suitable for DBIC + my %interfaces; + foreach my $entry (keys %$interfaces) { + my $port = $interfaces->{$entry}; + + if (not $port) { + debug sprintf ' [%s] interfaces - ignoring %s (no port mapping)', + $device->ip, $entry; + next; + } + + if (scalar grep {$port =~ m/^$_$/} @{setting('ignore_interfaces') || []}) { + debug sprintf + ' [%s] interfaces - ignoring %s (%s) (config:ignore_interfaces)', + $device->ip, $entry, $port; + next; + } + + if (exists $i_ignore->{$entry}) { + debug sprintf ' [%s] interfaces - ignoring %s (%s) (%s)', + $device->ip, $entry, $port, $i_type->{$entry}; + next; + } + + my $lc = $i_lastchange->{$entry} || 0; + if (not $dev_uptime_wrapped and $lc > $dev_uptime) { + info sprintf ' [%s] interfaces - device uptime wrapped (%s) - correcting', + $device->ip, $port; + $device->uptime( $dev_uptime + 2**32 ); + $dev_uptime_wrapped = 1; + } + + if ($device->is_column_changed('uptime') and $lc) { + if ($lc < $dev_uptime) { + # ambiguous: lastchange could be sysUptime before or after wrap + if ($dev_uptime > 30000 and $lc < 30000) { + # uptime wrap more than 5min ago but lastchange within 5min + # assume lastchange was directly after boot -> no action + } + else { + # uptime wrap less than 5min ago or lastchange > 5min ago + # to be on safe side, assume lastchange after counter wrap + debug sprintf + ' [%s] interfaces - correcting LastChange for %s, assuming sysUptime wrap', + $device->ip, $port; + $lc += $dev_uptime_wrapped * 2**32; + } + } + } + + $interfaces{$port} = { + port => $port, + descr => $i_descr->{$entry}, + up => $i_up->{$entry}, + up_admin => $i_up_admin->{$entry}, + mac => $i_mac->{$entry}, + speed => $i_speed->{$entry}, + mtu => $i_mtu->{$entry}, + name => Encode::decode('UTF-8', $i_name->{$entry}), + duplex => $i_duplex->{$entry}, + duplex_admin => $i_duplex_admin->{$entry}, + stp => $i_stp_state->{$entry}, + type => $i_type->{$entry}, + vlan => $i_vlan->{$entry}, + pvid => $i_vlan->{$entry}, + is_master => 'false', + slave_of => undef, + lastchange => $lc, + }; + } + + # must do this after building %interfaces so that we can set is_master + foreach my $sidx (keys %$agg_ports) { + my $slave = $interfaces->{$sidx} or next; + my $master = $interfaces->{ $agg_ports->{$sidx} } or next; + next unless exists $interfaces{$slave} and exists $interfaces{$master}; + + $interfaces{$slave}->{slave_of} = $master; + $interfaces{$master}->{is_master} = 'true'; + } + + schema('netdisco')->resultset('DevicePort')->txn_do_locked(sub { + my $gone = $device->ports->delete({keep_nodes => 1}); + debug sprintf ' [%s] interfaces - removed %d interfaces', + $device->ip, $gone; + $device->update_or_insert(undef, {for => 'update'}); + $device->ports->populate([values %interfaces]); + debug sprintf ' [%s] interfaces - added %d new interfaces', + $device->ip, scalar values %interfaces; + }); + + return Status->done('Ended discover for '. $device->ip); +}); + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors.pm b/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors.pm new file mode 100644 index 00000000..5fab7507 --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Discover/Neighbors.pm @@ -0,0 +1,335 @@ +package App::Netdisco::Worker::Plugin::Discover::Neighbors; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::Transport::SNMP (); +use App::Netdisco::Util::Device + qw/get_device match_devicetype is_discoverable/; +use App::Netdisco::Util::Permission 'check_acl_no'; +use App::Netdisco::JobQueue 'jq_insert'; +use Dancer::Plugin::DBIC 'schema'; +use List::MoreUtils (); +use NetAddr::MAC; +use Encode; + +=head2 discover_new_neighbors( $device, $snmp ) + +Given a Device database object, and a working SNMP connection, discover and +store the device's port neighbors information. + +Entries in the Topology database table will override any discovered device +port relationships. + +The Device database object can be a fresh L object which is +not yet stored to the database. + +Any discovered neighbor unknown to Netdisco will have a C job +immediately queued (subject to the filtering by the C settings). + +=cut + +register_worker({ primary => false, driver => 'snmp' }, sub { + my ($job, $workerconf) = @_; + + my $device = $job->device; + my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) + or return Status->defer("discover failed: could not SNMP connect to $device"); + + my @to_discover = store_neighbors($device, $snmp); + + # only enqueue if device is not already discovered, + # discover_* config permits the discovery + foreach my $neighbor (@to_discover) { + my ($ip, $remote_type) = @$neighbor; + + my $device = get_device($ip); + next if $device->in_storage; + + if (not is_discoverable($device, $remote_type)) { + debug sprintf + ' queue - %s, type [%s] excluded by discover_* config', + $ip, ($remote_type || ''); + next; + } + + jq_insert({ + device => $ip, + action => 'discover', + subaction => 'with-nodes', + }); + } + + return Status->done('Ended discover for '. $device->ip); +}); + +=head2 store_neighbors( $device, $snmp ) + +returns: C<@to_discover> + +Given a Device database object, and a working SNMP connection, discover and +store the device's port neighbors information. + +Entries in the Topology database table will override any discovered device +port relationships. + +The Device database object can be a fresh L object which is +not yet stored to the database. + +A list of discovererd neighbors will be returned as [C<$ip>, C<$type>] tuples. + +=cut + +sub store_neighbors { + my ($device, $snmp) = @_; + my @to_discover = (); + + # first allow any manually configured topology to be set + set_manual_topology($device, $snmp); + + if (!defined $snmp->has_topo) { + debug sprintf ' [%s] neigh - neighbor protocols are not enabled', $device->ip; + return @to_discover; + } + + my $interfaces = $snmp->interfaces; + my $c_if = $snmp->c_if; + my $c_port = $snmp->c_port; + my $c_id = $snmp->c_id; + my $c_platform = $snmp->c_platform; + my $c_cap = $snmp->c_cap; + + # v4 and v6 neighbor tables + my $c_ip = ($snmp->c_ip || {}); + my %c_ipv6 = %{ ($snmp->can('hasLLDP') and $snmp->hasLLDP) + ? ($snmp->lldp_ipv6 || {}) : {} }; + + # remove keys with undef values, as c_ip does + delete @c_ipv6{ grep { not defined $c_ipv6{$_} } keys %c_ipv6 }; + # now combine them, v6 wins + $c_ip = { %$c_ip, %c_ipv6 }; + + foreach my $entry (sort (List::MoreUtils::uniq( (keys %$c_ip), (keys %$c_cap) ))) { + if (!defined $c_if->{$entry} or !defined $interfaces->{ $c_if->{$entry} }) { + debug sprintf ' [%s] neigh - port for IID:%s not resolved, skipping', + $device->ip, $entry; + next; + } + + my $port = $interfaces->{ $c_if->{$entry} }; + my $portrow = schema('netdisco')->resultset('DevicePort') + ->single({ip => $device->ip, port => $port}); + + if (!defined $portrow) { + info sprintf ' [%s] neigh - local port %s not in database!', + $device->ip, $port; + next; + } + + if (ref $c_ip->{$entry}) { + error sprintf ' [%s] neigh - Error! port %s has multiple neighbors - skipping', + $device->ip, $port; + next; + } + + my $remote_ip = $c_ip->{$entry}; + my $remote_port = undef; + my $remote_type = Encode::decode('UTF-8', $c_platform->{$entry} || ''); + my $remote_id = Encode::decode('UTF-8', $c_id->{$entry}); + my $remote_cap = $c_cap->{$entry} || []; + + # IP Phone and WAP detection type fixup + if (scalar @$remote_cap or $remote_type) { + my $phone_flag = grep {match_devicetype($_, 'phone_capabilities')} + @$remote_cap; + my $ap_flag = grep {match_devicetype($_, 'wap_capabilities')} + @$remote_cap; + + if ($phone_flag or match_devicetype($remote_type, 'phone_platforms')) { + $remote_type = 'IP Phone: '. $remote_type + if $remote_type !~ /ip.phone/i; + } + elsif ($ap_flag or match_devicetype($remote_type, 'wap_platforms')) { + $remote_type = 'AP: '. $remote_type; + } + + $portrow->update({remote_type => $remote_type}); + } + + if ($portrow->manual_topo) { + info sprintf ' [%s] neigh - %s has manually defined topology', + $device->ip, $port; + next; + } + + next unless $remote_ip; + + # a bunch of heuristics to search known devices if we don't have a + # useable remote IP... + + if ($remote_ip eq '0.0.0.0' or + check_acl_no($remote_ip, 'group:__LOCAL_ADDRESSES__')) { + + if ($remote_id) { + my $devices = schema('netdisco')->resultset('Device'); + my $neigh = $devices->single({name => $remote_id}); + info sprintf + ' [%s] neigh - bad address %s on port %s, searching for %s instead', + $device->ip, $remote_ip, $port, $remote_id; + + if (!defined $neigh) { + my $mac = NetAddr::MAC->new(mac => $remote_id); + if ($mac and not $mac->errstr) { + $neigh = $devices->single({mac => $mac->as_ieee}); + } + } + + # some HP switches send 127.0.0.1 as remote_ip if no ip address + # on default vlan for HP switches remote_ip looks like + # "myswitchname(012345-012345)" + if (!defined $neigh) { + (my $tmpid = $remote_id) =~ s/.*\(([0-9a-f]{6})-([0-9a-f]{6})\).*/$1$2/; + my $mac = NetAddr::MAC->new(mac => $tmpid); + if ($mac and not $mac->errstr) { + info sprintf + '[%s] neigh - found neighbor %s by MAC %s', + $device->ip, $remote_id, $mac->as_ieee; + $neigh = $devices->single({mac => $mac->as_ieee}); + } + } + + if (!defined $neigh) { + (my $shortid = $remote_id) =~ s/\..*//; + $neigh = $devices->single({name => { -ilike => "${shortid}%" }}); + } + + if ($neigh) { + $remote_ip = $neigh->ip; + info sprintf ' [%s] neigh - found %s with IP %s', + $device->ip, $remote_id, $remote_ip; + } + else { + info sprintf ' [%s] neigh - could not find %s, skipping', + $device->ip, $remote_id; + next; + } + } + else { + info sprintf ' [%s] neigh - skipping unuseable address %s on port %s', + $device->ip, $remote_ip, $port; + next; + } + } + + # what we came here to do.... discover the neighbor + debug sprintf + ' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue', + $device->ip, $remote_ip, ($remote_type || ''), $port; + push @to_discover, [$remote_ip, $remote_type]; + + $remote_port = $c_port->{$entry}; + if (defined $remote_port) { + # clean weird characters + $remote_port =~ s/[^\d\/\.,()\w:-]+//gi; + } + else { + info sprintf ' [%s] neigh - no remote port found for port %s at %s', + $device->ip, $port, $remote_ip; + } + + $portrow->update({ + remote_ip => $remote_ip, + remote_port => $remote_port, + remote_type => $remote_type, + remote_id => $remote_id, + is_uplink => \"true", + manual_topo => \"false", + }); + + # update master of our aggregate to be a neighbor of + # the master on our peer device (a lot of iffs to get there...). + # & cannot use ->neighbor prefetch because this is the port insert! + if (defined $portrow->slave_of) { + + my $peer_device = get_device($remote_ip); + my $master = schema('netdisco')->resultset('DevicePort')->single({ + ip => $device->ip, + port => $portrow->slave_of + }); + + if ($peer_device and $peer_device->in_storage and $master + and not ($portrow->is_master or defined $master->slave_of)) { + + my $peer_port = schema('netdisco')->resultset('DevicePort')->single({ + ip => $peer_device->ip, + port => $portrow->remote_port, + }); + + $master->update({ + remote_ip => ($peer_device->ip || $remote_ip), + remote_port => ($peer_port ? $peer_port->slave_of : undef ), + is_uplink => \"true", + is_master => \"true", + manual_topo => \"false", + }); + } + } + } + + return @to_discover; +} + +# take data from the topology table and update remote_ip and remote_port +# in the devices table. only use root_ips and skip any bad topo entries. +sub set_manual_topology { + my ($device, $snmp) = @_; + + schema('netdisco')->txn_do(sub { + # clear manual topology flags + schema('netdisco')->resultset('DevicePort') + ->search({ip => $device->ip})->update({manual_topo => \'false'}); + + my $topo_links = schema('netdisco')->resultset('Topology') + ->search({-or => [dev1 => $device->ip, dev2 => $device->ip]}); + debug sprintf ' [%s] neigh - setting manual topology links', $device->ip; + + while (my $link = $topo_links->next) { + # could fail for broken topo, but we ignore to try the rest + try { + schema('netdisco')->txn_do(sub { + # only work on root_ips + my $left = get_device($link->dev1); + my $right = get_device($link->dev2); + + # skip bad entries + return unless ($left->in_storage and $right->in_storage); + + $left->ports + ->single({port => $link->port1}) + ->update({ + remote_ip => $right->ip, + remote_port => $link->port2, + remote_type => undef, + remote_id => undef, + is_uplink => \"true", + manual_topo => \"true", + }); + + $right->ports + ->single({port => $link->port2}) + ->update({ + remote_ip => $left->ip, + remote_port => $link->port1, + remote_type => undef, + remote_id => undef, + is_uplink => \"true", + manual_topo => \"true", + }); + }); + }; + } + }); +} + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/PortPower.pm b/lib/App/Netdisco/Worker/Plugin/Discover/PortPower.pm new file mode 100644 index 00000000..1ad2bf50 --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Discover/PortPower.pm @@ -0,0 +1,82 @@ +package App::Netdisco::Worker::Plugin::Discover::PortPower; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::Transport::SNMP (); +use Dancer::Plugin::DBIC 'schema'; + +register_worker({ primary => false, driver => 'snmp' }, sub { + my ($job, $workerconf) = @_; + + my $device = $job->device; + my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) + or return Status->defer("discover failed: could not SNMP connect to $device"); + + my $p_watts = $snmp->peth_power_watts; + my $p_status = $snmp->peth_power_status; + + if (!defined $p_watts) { + debug sprintf ' [%s] power - 0 power modules', $device->ip; + return Status->done("Ended discover for $device"); + } + + # build device module power info suitable for DBIC + my @devicepower; + foreach my $entry (keys %$p_watts) { + push @devicepower, { + module => $entry, + power => $p_watts->{$entry}, + status => $p_status->{$entry}, + }; + } + + my $interfaces = $snmp->interfaces; + my $p_ifindex = $snmp->peth_port_ifindex; + my $p_admin = $snmp->peth_port_admin; + my $p_pstatus = $snmp->peth_port_status; + my $p_class = $snmp->peth_port_class; + my $p_power = $snmp->peth_port_power; + + # build device port power info suitable for DBIC + my @portpower; + foreach my $entry (keys %$p_ifindex) { + my $port = $interfaces->{ $p_ifindex->{$entry} }; + next unless $port; + + my ($module) = split m/\./, $entry; + + push @portpower, { + port => $port, + module => $module, + admin => $p_admin->{$entry}, + status => $p_pstatus->{$entry}, + class => $p_class->{$entry}, + power => $p_power->{$entry}, + + }; + } + + schema('netdisco')->txn_do(sub { + my $gone = $device->power_modules->delete; + debug sprintf ' [%s] power - removed %d power modules', + $device->ip, $gone; + $device->power_modules->populate(\@devicepower); + debug sprintf ' [%s] power - added %d new power modules', + $device->ip, scalar @devicepower; + }); + + schema('netdisco')->txn_do(sub { + my $gone = $device->powered_ports->delete; + debug sprintf ' [%s] power - removed %d PoE capable ports', + $device->ip, $gone; + $device->powered_ports->populate(\@portpower); + debug sprintf ' [%s] power - added %d new PoE capable ports', + $device->ip, scalar @portpower; + }); + + return Status->done('Ended discover for '. $device->ip); +}); + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/Properties.pm b/lib/App/Netdisco/Worker/Plugin/Discover/Properties.pm new file mode 100644 index 00000000..8f19845e --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Discover/Properties.pm @@ -0,0 +1,105 @@ +package App::Netdisco::Worker::Plugin::Discover::Properties; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::Transport::SNMP (); +use App::Netdisco::Util::Permission 'check_acl_no'; +use App::Netdisco::Util::FastResolver 'hostnames_resolve_async'; +use App::Netdisco::Util::DNS 'hostname_from_ip'; +use Dancer::Plugin::DBIC 'schema'; +use NetAddr::IP::Lite ':lower'; +use Encode; + +register_worker({ primary => true, driver => 'snmp' }, sub { + my ($job, $workerconf) = @_; + + my $device = $job->device; + my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) + or return Status->defer("discover failed: could not SNMP connect to $device"); + + my $ip_index = $snmp->ip_index; + my $interfaces = $snmp->interfaces; + my $ip_netmask = $snmp->ip_netmask; + + # build device aliases suitable for DBIC + my @aliases; + foreach my $entry (keys %$ip_index) { + my $ip = NetAddr::IP::Lite->new($entry) + or next; + my $addr = $ip->addr; + + next if $addr eq '0.0.0.0'; + next if check_acl_no($ip, 'group:__LOCAL_ADDRESSES__'); + next if setting('ignore_private_nets') and $ip->is_rfc1918; + + my $iid = $ip_index->{$addr}; + my $port = $interfaces->{$iid}; + my $subnet = $ip_netmask->{$addr} + ? NetAddr::IP::Lite->new($addr, $ip_netmask->{$addr})->network->cidr + : undef; + + debug sprintf ' [%s] device - aliased as %s', $device->ip, $addr; + push @aliases, { + alias => $addr, + port => $port, + subnet => $subnet, + dns => undef, + }; + } + + debug sprintf ' resolving %d aliases with max %d outstanding requests', + scalar @aliases, $ENV{'PERL_ANYEVENT_MAX_OUTSTANDING_DNS'}; + my $resolved_aliases = hostnames_resolve_async(\@aliases); + + # fake one aliases entry for devices not providing ip_index + push @$resolved_aliases, { alias => $device->ip, dns => $device->dns } + if 0 == scalar @aliases; + + # VTP Management Domain -- assume only one. + my $vtpdomains = $snmp->vtp_d_name; + my $vtpdomain; + if (defined $vtpdomains and scalar values %$vtpdomains) { + $device->set_column( vtp_domain => (values %$vtpdomains)[-1] ); + } + + my $hostname = hostname_from_ip($device->ip); + $device->set_column( dns => $hostname ) if $hostname; + + my @properties = qw/ + snmp_ver + description uptime name + layers ports mac + ps1_type ps2_type ps1_status ps2_status + fan slots + vendor os os_ver + /; + + foreach my $property (@properties) { + $device->set_column( $property => $snmp->$property ); + } + + $device->set_column( model => Encode::decode('UTF-8', $snmp->model) ); + $device->set_column( serial => Encode::decode('UTF-8', $snmp->serial) ); + $device->set_column( contact => Encode::decode('UTF-8', $snmp->contact) ); + $device->set_column( location => Encode::decode('UTF-8', $snmp->location) ); + + + $device->set_column( snmp_class => $snmp->class ); + $device->set_column( last_discover => \'now()' ); + + schema('netdisco')->txn_do(sub { + my $gone = $device->device_ips->delete; + debug sprintf ' [%s] device - removed %d aliases', + $device->ip, $gone; + $device->update_or_insert(undef, {for => 'update'}); + $device->device_ips->populate($resolved_aliases); + debug sprintf ' [%s] device - added %d new aliases', + $device->ip, scalar @aliases; + }); + + return Status->done('Ended discover for '. $device->ip); +}); + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/VLANs.pm b/lib/App/Netdisco/Worker/Plugin/Discover/VLANs.pm new file mode 100644 index 00000000..191f8733 --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Discover/VLANs.pm @@ -0,0 +1,95 @@ +package App::Netdisco::Worker::Plugin::Discover::VLANs; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::Transport::SNMP (); +use Dancer::Plugin::DBIC 'schema'; + +register_worker({ primary => false, driver => 'snmp' }, sub { + my ($job, $workerconf) = @_; + + my $device = $job->device; + my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) + or return Status->defer("discover failed: could not SNMP connect to $device"); + + my $v_name = $snmp->v_name; + my $v_index = $snmp->v_index; + + # build device vlans suitable for DBIC + my %v_seen = (); + my @devicevlans; + foreach my $entry (keys %$v_name) { + my $vlan = $v_index->{$entry}; + next unless defined $vlan and $vlan; + ++$v_seen{$vlan}; + + push @devicevlans, { + vlan => $vlan, + description => $v_name->{$entry}, + last_discover => \'now()', + }; + } + + my $i_vlan = $snmp->i_vlan; + my $i_vlan_membership = $snmp->i_vlan_membership; + my $i_vlan_type = $snmp->i_vlan_type; + my $interfaces = $snmp->interfaces; + + # build device port vlans suitable for DBIC + my @portvlans = (); + foreach my $entry (keys %$i_vlan_membership) { + my %port_vseen = (); + my $port = $interfaces->{$entry}; + next unless defined $port; + + my $type = $i_vlan_type->{$entry}; + + foreach my $vlan (@{ $i_vlan_membership->{$entry} }) { + next unless defined $vlan and $vlan; + next if ++$port_vseen{$vlan} > 1; + + my $native = ((defined $i_vlan->{$entry}) and ($vlan eq $i_vlan->{$entry})) ? "t" : "f"; + push @portvlans, { + port => $port, + vlan => $vlan, + native => $native, + vlantype => $type, + last_discover => \'now()', + }; + + next if $v_seen{$vlan}; + + # also add an unnamed vlan to the device + push @devicevlans, { + vlan => $vlan, + description => (sprintf "VLAN %d", $vlan), + last_discover => \'now()', + }; + ++$v_seen{$vlan}; + } + } + + schema('netdisco')->txn_do(sub { + my $gone = $device->vlans->delete; + debug sprintf ' [%s] vlans - removed %d device VLANs', + $device->ip, $gone; + $device->vlans->populate(\@devicevlans); + debug sprintf ' [%s] vlans - added %d new device VLANs', + $device->ip, scalar @devicevlans; + }); + + schema('netdisco')->txn_do(sub { + my $gone = $device->port_vlans->delete; + debug sprintf ' [%s] vlans - removed %d port VLANs', + $device->ip, $gone; + $device->port_vlans->populate(\@portvlans); + debug sprintf ' [%s] vlans - added %d new port VLANs', + $device->ip, scalar @portvlans; + }); + + return Status->done('Ended discover for '. $device->ip); +}); + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/Wireless.pm b/lib/App/Netdisco/Worker/Plugin/Discover/Wireless.pm new file mode 100644 index 00000000..8869c8ff --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Discover/Wireless.pm @@ -0,0 +1,86 @@ +package App::Netdisco::Worker::Plugin::Discover::Wireless; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::Transport::SNMP (); +use Dancer::Plugin::DBIC 'schema'; + +register_worker({ primary => false, driver => 'snmp' }, sub { + my ($job, $workerconf) = @_; + + my $device = $job->device; + my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) + or return Status->defer("discover failed: could not SNMP connect to $device"); + + my $ssidlist = $snmp->i_ssidlist; + return Status->done("Ended discover for $device") + unless scalar keys %$ssidlist; + + my $interfaces = $snmp->interfaces; + my $ssidbcast = $snmp->i_ssidbcast; + my $ssidmac = $snmp->i_ssidmac; + my $channel = $snmp->i_80211channel; + my $power = $snmp->dot11_cur_tx_pwr_mw; + + # build device ssid list suitable for DBIC + my @ssids; + foreach my $entry (keys %$ssidlist) { + (my $iid = $entry) =~ s/\.\d+$//; + my $port = $interfaces->{$iid}; + + if (not $port) { + debug sprintf ' [%s] wireless - ignoring %s (no port mapping)', + $device->ip, $iid; + next; + } + + push @ssids, { + port => $port, + ssid => $ssidlist->{$entry}, + broadcast => $ssidbcast->{$entry}, + bssid => $ssidmac->{$entry}, + }; + } + + schema('netdisco')->txn_do(sub { + my $gone = $device->ssids->delete; + debug sprintf ' [%s] wireless - removed %d SSIDs', + $device->ip, $gone; + $device->ssids->populate(\@ssids); + debug sprintf ' [%s] wireless - added %d new SSIDs', + $device->ip, scalar @ssids; + }); + + # build device channel list suitable for DBIC + my @channels; + foreach my $entry (keys %$channel) { + my $port = $interfaces->{$entry}; + + if (not $port) { + debug sprintf ' [%s] wireless - ignoring %s (no port mapping)', + $device->ip, $entry; + next; + } + + push @channels, { + port => $port, + channel => $channel->{$entry}, + power => $power->{$entry}, + }; + } + + schema('netdisco')->txn_do(sub { + my $gone = $device->wireless_ports->delete; + debug sprintf ' [%s] wireless - removed %d wireless channels', + $device->ip, $gone; + $device->wireless_ports->populate(\@channels); + debug sprintf ' [%s] wireless - added %d new wireless channels', + $device->ip, scalar @channels; + }); + + return Status->done('Ended discover for '. $device->ip); +}); + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/Discover/WithNodes.pm b/lib/App/Netdisco/Worker/Plugin/Discover/WithNodes.pm new file mode 100644 index 00000000..37011e86 --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Discover/WithNodes.pm @@ -0,0 +1,39 @@ +package App::Netdisco::Worker::Plugin::Discover::WithNodes; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use Dancer::Plugin::DBIC 'schema'; + +register_worker({ primary => false }, sub { + my ($job, $workerconf) = @_; + my $device = $job->device; + + # if requested, and the device has not yet been + # arpniped/macsucked, queue those jobs now + if ($device->in_storage + and $job->subaction and $job->subaction eq 'with-nodes') { + if (!defined $device->last_macsuck) { + jq_insert({ + device => $device->ip, + action => 'macsuck', + username => $job->username, + userip => $job->userip, + }); + } + + if (!defined $device->last_arpnip) { + jq_insert({ + device => $device->ip, + action => 'arpnip', + username => $job->username, + userip => $job->userip, + }); + } + } + + return Status->done('Ended discover for '. $device->ip); +}); + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/DiscoverAll.pm b/lib/App/Netdisco/Worker/Plugin/DiscoverAll.pm new file mode 100644 index 00000000..db15db6e --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/DiscoverAll.pm @@ -0,0 +1,31 @@ +package App::Netdisco::Worker::Plugin::DiscoverAll; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::JobQueue qw/jq_queued jq_insert/; +use Dancer::Plugin::DBIC 'schema'; + +register_worker({ primary => true }, sub { + my ($job, $workerconf) = @_; + + my %queued = map {$_ => 1} jq_queued('discover'); + my @devices = schema('netdisco')->resultset('Device')->search({ + -or => [ 'vendor' => undef, 'vendor' => { '!=' => 'netdisco' }], + })->get_column('ip')->all; + my @filtered_devices = grep {!exists $queued{$_}} @devices; + + jq_insert([ + map {{ + device => $_, + action => 'discover', + username => $job->username, + userip => $job->userip, + }} (@filtered_devices) + ]); + + return Status->done('Queued discover job for all devices'); +}); + +true; diff --git a/lib/App/Netdisco/Worker/Plugin/Macsuck.pm b/lib/App/Netdisco/Worker/Plugin/Macsuck.pm index 7ac98cf9..933c397e 100644 --- a/lib/App/Netdisco/Worker/Plugin/Macsuck.pm +++ b/lib/App/Netdisco/Worker/Plugin/Macsuck.pm @@ -5,7 +5,6 @@ use App::Netdisco::Worker::Plugin; use aliased 'App::Netdisco::Worker::Status'; use App::Netdisco::Util::Device 'is_macsuckable_now'; -use App::Netdisco::Transport::SNMP (); register_worker({ primary => true }, sub { my ($job, $workerconf) = @_; @@ -14,15 +13,13 @@ register_worker({ primary => true }, sub { return Status->error('macsuck failed: unable to interpret device param') unless defined $device; - my $host = $device->ip; - - return Status->done("macsuck skipped: $host not yet discovered") + return Status->done("macsuck skipped: $device not yet discovered") unless $device->in_storage; - return Status->defer("macsuck skipped: $host is pseudo-device") + return Status->defer("macsuck skipped: $device is pseudo-device") if $device->vendor and $device->vendor eq 'netdisco'; - return Status->defer("macsuck deferred: $host is not macsuckable") + return Status->defer("macsuck deferred: $device is not macsuckable") unless is_macsuckable_now($device); return Status->done('Macsuck is able to run.'); diff --git a/lib/App/Netdisco/Worker/Plugin/Macsuck/Nodes.pm b/lib/App/Netdisco/Worker/Plugin/Macsuck/Nodes.pm index 6f996abe..5e24f2e8 100644 --- a/lib/App/Netdisco/Worker/Plugin/Macsuck/Nodes.pm +++ b/lib/App/Netdisco/Worker/Plugin/Macsuck/Nodes.pm @@ -19,9 +19,9 @@ register_worker({ primary => true, driver => 'snmp' }, sub { my $device = $job->device; my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) - or return Status->defer("macsuck failed: could not SNMP connect to $host"); + or return Status->defer("macsuck failed: could not SNMP connect to $device"); - return Status->defer("Skipped macsuck for device $host without layer 2 capability") + return Status->defer("Skipped macsuck for device $device without layer 2 capability") unless $snmp->has_layer(2); # would be possible just to use now() on updated records, but by using this diff --git a/lib/App/Netdisco/Worker/Plugin/Macsuck/WirelessNodes.pm b/lib/App/Netdisco/Worker/Plugin/Macsuck/WirelessNodes.pm index 6b5d7b78..cfcfc6d6 100644 --- a/lib/App/Netdisco/Worker/Plugin/Macsuck/WirelessNodes.pm +++ b/lib/App/Netdisco/Worker/Plugin/Macsuck/WirelessNodes.pm @@ -13,9 +13,9 @@ register_worker({ primary => false, driver => 'snmp' }, sub { my $device = $job->device; my $snmp = App::Netdisco::Transport::SNMP->reader_for($device) - or return Status->defer("macsuck failed: could not SNMP connect to $host"); + or return Status->defer("macsuck failed: could not SNMP connect to $device"); - return Status->defer("Skipped macsuck for device $host without layer 2 capability") + return Status->defer("Skipped macsuck for device $device without layer 2 capability") unless $snmp->has_layer(2); my $now = 'to_timestamp('. (join '.', gettimeofday) .')';