#!/usr/bin/env perl use strict; use warnings FATAL => 'all'; use Dancer ':script'; use Dancer::Plugin::DBIC 'schema'; use Netdisco::DB; use SNMP::Info; use Daemon::Generic::While1; use Config::Tiny; use File::Slurp; use Try::Tiny; use feature 'say'; newdaemon( progname => 'netdisco-daemon', ($> != 0 ? (pidbase => './') : ()), configfile => '/etc/netdisco/netdisco.conf', logpriority => 'daemon.info', ); sub gd_preconfig { my $self = shift; my $config = {}; if (-e $self->{configfile}) { # read file and alter line continuations to be single lines my $config_content = read_file($self->{configfile}); $config_content =~ s/\\\n//sg; # parse config naively as .ini $config = Config::Tiny->new()->read_string($config_content); $self->gd_error(Config::Tiny->errstr) unless defined $config; } $self->gd_error('No read-write community string has been set.') unless length $config->{_}->{community_rw}; # store for later access var(nd_config => $config); # add local settings $config->{loc} = { sleep_time => 5, }; return (); # important } sub get_device { my ($self, $device) = @_; my $alias = schema('netdisco')->resultset('DeviceIp') ->search({alias => $device})->first; return if not eval { $alias->ip }; return schema('netdisco')->resultset('Device') ->find({ip => $alias->ip}); } sub build_mibdirs { my $nd_config = var('nd_config'); my $mibhome = $nd_config->{_}->{mibhome}; (my $mibdirs = $nd_config->{_}->{mibdirs}) =~ s/\s+//g; $mibdirs =~ s/\$mibhome/$mibhome/g; return [ split /,/, $mibdirs ]; } sub snmp_connect { my ($self, $device) = @_; my $nd_config = var('nd_config'); # TODO: really only supporing v2c at the moment my %snmp_args = ( DestHost => $device->ip, Version => ($device->snmp_ver || $nd_config->{_}->{snmpver} || 2), Retries => ($nd_config->{_}->{snmpretries} || 2), Timeout => ($nd_config->{_}->{snmptimeout} || 1000000), MibDirs => build_mibdirs(), AutoSpecify => 1, Debug => ($ENV{INFO_TRACE} || 0), ); (my $comm = $nd_config->{_}->{community_rw}) =~ s/\s+//g; my @communities = split /,/, $comm; my $info = undef; COMMUNITY: foreach my $c (@communities) { try { $info = SNMP::Info->new(%snmp_args, Community => $c); last COMMUNITY if ( $info and (not defined $info->error) and length $info->uptime ); }; } return $info; } sub set_location { my ($self, $job, $device, $info) = @_; my $location = ($job->subaction || ''); try { my $rv = $info->set_location($location); if (!defined $rv) { my $log = sprintf 'Failed to set location on [%s]: %s', $job->device, ($info->error || ''); return ('error', $log); } # double check $info->clear_cache; my $new_location = ($info->location || ''); if ($new_location ne $location) { my $log = sprintf 'Failed to update location on [%s] to [%s]', $job->device, ($location); return ('error', $log); } # update netdisco DB $device->update({location => $location}); my $log = sprintf 'Updated location on [%s] to [%s]', $job->device, $location; return ('done', $log); } catch { return( 'error', (sprintf 'Failed to update location on [%s]: %s', $job->device, $_) ); }; } sub do_job { my ($self, $job) = @_; my $nd_config = var('nd_config'); # get device details from db my $device = $self->get_device($job->device) or return (); # snmp connect using rw community my $info = $self->snmp_connect($device) or return (); # do update my %dispatch = ( location => 'set_location', ); my $target = $dispatch{$job->action} or return (); # return results return $self->$target($job, $device, $info); } sub gd_run_body { my $self = shift; my $nd_config = var('nd_config'); # get all pending jobs my $rs = schema('netdisco')->resultset('Admin')->search({ action => [qw/location contact portcontrol portname vlan/], status => 'queued', }); JOB: while (my $job = $rs->next) { # filter for discover_* my $device = NetAddr::IP::Lite->new($job->device) or next JOB; if (length $nd_config->{_}->{discover_no}) { my @d_no = split /,\s*/, $nd_config->{_}->{discover_no}; foreach my $item (@d_no) { my $ip = NetAddr::IP::Lite->new($item) or next JOB; next JOB if $ip->contains($device); } } if (length $nd_config->{_}->{discover_only}) { my $okay = 0; my @d_only = split /,\s*/, $nd_config->{_}->{discover_only}; foreach my $item (@d_only) { my $ip = NetAddr::IP::Lite->new($item) or next JOB; ++$okay if $ip->contains($device); } next JOB if not $okay; } # lock db table, check job state is still queued, update to running try { my $status_updated = schema('netdisco')->txn_do(sub { my $row = schema('netdisco')->resultset('Admin')->find( {job => $job->job}, {for => 'update'} ); return 0 if $row->status ne 'queued'; $row->update({status => 'running', started => \'now()'}); return 1; }); next JOB if not $status_updated; } catch { warn "error updating job status: $_\n"; next JOB; }; # do job my ($status, $log) = $self->do_job($job); # revert to queued status if we failed to connect to device if (not $status) { try { schema('netdisco')->resultset('Admin') ->find($job->job) ->update({status => 'queued', started => undef}); } catch { warn "error updating job: $_\n" }; } else { # update job state to done/error with log try { schema('netdisco')->resultset('Admin') ->find($job->job) ->update({status => $status, log => $log, finished => \'now()'}); } catch { warn "error updating job: $_\n" }; } } $self->gd_sleep( $nd_config->{loc}->{sleep_time} ); }