The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl

use strict;
use warnings;

our $home;

BEGIN {
  use FindBin;
  FindBin::again();

  $home = ($ENV{NETDISCO_HOME} || $ENV{HOME});

  # try to find a localenv if one isn't already in place.
  if (!exists $ENV{PERL_LOCAL_LIB_ROOT}) {
      use File::Spec;
      my $localenv = File::Spec->catfile($FindBin::RealBin, 'localenv');
      exec($localenv, $0, @ARGV) if -f $localenv;
      $localenv = File::Spec->catfile($home, 'perl5', 'bin', 'localenv');
      exec($localenv, $0, @ARGV) if -f $localenv;

      die "Sorry, can't find libs required for App::Netdisco.\n"
        if !exists $ENV{PERLBREW_PERL};
  }
}

BEGIN {
  use Path::Class;

  # stuff useful locations into @INC and $PATH
  unshift @INC,
    dir($FindBin::RealBin)->parent->subdir('lib')->stringify,
    dir($FindBin::RealBin, 'lib')->stringify;
}

# for netdisco app config
use App::Netdisco;
use App::Netdisco::Daemon::Job;
use Dancer qw/:moose :script/;

info "App::Netdisco version $App::Netdisco::VERSION loaded.";

use NetAddr::IP qw/:rfc3021 :lower/;
use App::Netdisco::Util::Device 'get_device';

use Try::Tiny;
use Pod::Usage;
use Scalar::Util 'blessed';
use Getopt::Long;
Getopt::Long::Configure ("bundling");

my ($device, $port, $extra, $debug);
my ($infotrace, $snmptrace, $sqltrace) = (0, 0, 0);

my $result = GetOptions(
  'device|d=s' => \$device,
  'port|p=s'   => \$port,
  'extra|e=s'  => \$extra,
  'debug|D'    => \$debug,
  'infotrace|I+' => \$infotrace,
  'snmptrace|S+' => \$snmptrace,
  'sqltrace|Q+'  => \$sqltrace,
) or pod2usage(
  -msg => 'error: bad options',
  -verbose => 0,
  -exitval => 1,
);

my $CONFIG = config();
$CONFIG->{logger} = 'console';
$CONFIG->{log} = ($debug ? 'debug' : 'info');

$ENV{INFO_TRACE} ||= $infotrace;
$ENV{SNMP_TRACE} ||= $snmptrace;
$ENV{DBIC_TRACE} ||= $sqltrace;

# reconfigure logging to force console output
Dancer::Logger->init('console', $CONFIG);

# get requested action
(my $action = shift @ARGV) =~ s/^set_//
  if scalar @ARGV;

unless ($action) {
    pod2usage(
      -msg => 'error: missing action!',
      -verbose => 2,
      -exitval => 2,
    );
}

# create worker (placeholder object for the role methods)
{
  package MyWorker;

  use Moo;
  use Module::Load ();
  use Data::Printer ();
  use Scalar::Util 'blessed';
  use NetAddr::IP qw/:rfc3021 :lower/;
  use Dancer ':script';

  use App::Netdisco::Util::SNMP ();
  use App::Netdisco::Util::Device
    qw/get_device delete_device renumber_device/;

  with 'App::Netdisco::Daemon::Worker::Poller::Device';
  with 'App::Netdisco::Daemon::Worker::Poller::Arpnip';
  with 'App::Netdisco::Daemon::Worker::Poller::Macsuck';
  with 'App::Netdisco::Daemon::Worker::Poller::Nbtstat';
  with 'App::Netdisco::Daemon::Worker::Poller::Expiry';
  with 'App::Netdisco::Daemon::Worker::Interactive::DeviceActions';
  with 'App::Netdisco::Daemon::Worker::Interactive::PortActions';

  eval { Module::Load::load 'App::Netdisco::Util::Graph' };
  sub graph {
    App::Netdisco::Util::Graph::graph();
    return ('done', 'Generated graph data.');
  }

  use App::Netdisco::Util::NodeMonitor ();
  sub monitor {
    App::Netdisco::Util::NodeMonitor::monitor();
    return ('done', 'Generated monitor data.');
  }

  sub show {
    my ($self, $job) = @_;
    my ($device, $port, $extra) = map {$job->$_} qw/device port extra/;
    return ('error', 'Missing device (-d).') if !defined $device;

    $extra ||= 'interfaces'; my $class = undef;
    ($class, $extra) = split(/::([^:]+)$/, $extra);
    if ($class and $extra) {
        $class = 'SNMP::Info::'.$class;
    }
    else {
        $extra = $class;
        undef $class;
    }
    my $i = App::Netdisco::Util::SNMP::snmp_connect($device, $class);
    Data::Printer::p($i->$extra);
    return ('done', sprintf "Showed %s response from %s.", $extra, $device->ip);
  }

  sub delete {
    my ($self, $job) = @_;
    my ($device, $port, $extra) = map {$job->$_} qw/device port extra/;
    return ('error', 'Missing device (-d).') if !defined $device;

    $port = ($port ? 1 : 0);
    delete_device($device, $port, $extra);
    return ('done', sprintf "Deleted device %s.", $device->ip);
  }

  sub renumber {
    my ($self, $job) = @_;
    my ($device, $port, $extra) = map {$job->$_} qw/device port extra/;
    return ('error', 'Missing device (-d).') if !defined $device;
    my $old_ip = $device->ip;

    my $new_ip = NetAddr::IP->new($extra);
    unless ($new_ip and $new_ip->addr ne '0.0.0.0') {
        return ('error', "Bad host or IP: ".($extra || '0.0.0.0'));
    }

    my $new_dev = get_device($new_ip->addr);
    if ($new_dev and $new_dev->in_storage and ($new_dev->ip ne $device->ip)) {
        return ('error', sprintf "Already know new device as: %s.", $new_dev->ip);
    }

    renumber_device($device, $new_ip);
    return ('done', sprintf 'Renumbered device %s to %s (%s).',
      $device->ip, $new_ip, ($device->dns || ''));
  }

  sub psql {
    my ($self, $job) = @_;
    my ($device, $port, $extra) = map {$job->$_} qw/device port extra/;

    my $name = (setting('database')->{name} || 'netdisco');
    my $host = setting('database')->{host};
    my $user = setting('database')->{user};
    my $pass = setting('database')->{pass};

    my $portnum = undef;
    if ($host and $host =~ m/([^;]+);port=(\d+)/) {
        $host = $1;
        $portnum = $2;
    }

    $ENV{PGHOST} = $host if $host;
    $ENV{PGPORT} = $portnum if defined $portnum;
    $ENV{PGDATABASE} = $name;
    $ENV{PGUSER} = $user;
    $ENV{PGPASSWORD} = $pass;
    $ENV{PGCLIENTENCODING} = 'UTF8';

    if ($extra) {
        system('psql', '-c', $extra);
    }
    else {
        system('psql');
    }
    return ('done', "psql session closed.");
  }
}
my $worker = MyWorker->new();

# belt and braces check before we go ahead
if (not $worker->can( $action )) {
  pod2usage(
    -msg => (sprintf 'error: %s is not a valid action', $action),
    -verbose => 2,
    -exitval => 3,
  );
}

my $net = NetAddr::IP->new($device);
if ($device and (!$net or $net->num == 0 or $net->addr eq '0.0.0.0')) {
    info sprintf '%s: error - Bad host, IP or prefix: %s', $action, $device;
    exit 1;
}

my @hostlist = defined $device ? ($net->hostenum) : (undef);
my $exitstatus = 0;

foreach my $host (@hostlist) {
    my $dev = $host ? get_device($host->addr) : undef;
    if ($dev and not (blessed $dev and $dev->in_storage) and $action ne 'discover') {
        info sprintf "%s: error - Don't know device: %s", $action, $host->addr;
        next;
    }

    # what job are we asked to do?
    my $job = App::Netdisco::Daemon::Job->new({
      job => 0,
      action => $action,
      device => $dev,
      port   => $port,
      subaction => $extra,
    });

    # do job
    my ($status, $log);
    try {
        info sprintf '%s: started at %s', $action, scalar localtime;
        ($status, $log) = $worker->$action($job);
    }
    catch {
        $status = 'error';
        $log = "error running job: $_";
    };

    info sprintf '%s: finished at %s', $action, scalar localtime;
    info sprintf '%s: status %s: %s', $action, $status, $log;
    $exitstatus = 1 if !defined $status or $status eq 'error';
}

exit $exitstatus;

=head1 NAME

netdisco-do - Run any Netdisco job from the command-line.

=head1 SYNOPSIS

 ~/bin/netdisco-do <action> [-DISQ] [-d <device> [-p <port>] [-e <extra>]]

=head1 DESCRIPTION

This program allows you to run any Netdisco poller job from the command-line.

The C<-d> option will accept a hostname (that can be resolved to an IP with
DNS), an IP address, or IP prefix (subnets in CIDR format). It can be any
interface on the device known to Netdisco.

Note that some jobs (C<discoverall>, C<macwalk>, C<arpwalk>, C<nbtwalk>)
simply add entries to the Netdisco job queue for other jobs, so won't seem
to do much when you trigger them.

=head1 ACTIONS

=head2 discover

Run a discover on the device (specified with C<-d>).

 ~netdisco/bin/netdisco-do discover -d 192.0.2.1

=head2 discoverall

Run a discover for all known devices.

=head2 macsuck

Run a macsuck on the device (specified with C<-d>).

 ~netdisco/bin/netdisco-do macsuck -d 192.0.2.1

=head2 macwalk

Run a macsuck for all known devices.

=head2 arpnip

Run an arpnip on the device (specified with C<-d>).

 ~netdisco/bin/netdisco-do arpnip -d 192.0.2.1

=head2 arpwalk

Run an arpnip for all known devices.

=head2 delete

Delete a device (specified with C<-d>). Pass a log message for the action in
the C<-e> parameter. Optionally request for associated nodes to be archived
(rather than deleted) by setting the C<-p> parameter to "C<yes>" (mnemonic:
B<p>reserve).

 ~netdisco/bin/netdisco-do delete -d 192.0.2.1
 ~netdisco/bin/netdisco-do delete -d 192.0.2.1 -e 'older than the sun'
 ~netdisco/bin/netdisco-do delete -d 192.0.2.1 -e 'older than the sun' -p yes

=head2 renumber

Change the canonical IP address of a device (specified with C<-d>). Pass the
new IP address in the C<-e> parameter. All related records such as topology,
log and node information will also be updated to refer to the new device.

Note that I<no> check is made as to whether the new IP is reachable for future
polling.

 ~netdisco/bin/netdisco-do renumber -d 192.0.2.1 -e 192.0.2.254

=head2 nbtstat

Run an nbtstat on the node (specified with C<-d>).

 ~netdisco/bin/netdisco-do nbtstat -d 192.0.2.2

=head2 nbtwalk

Run an nbtstat for all known nodes.

=head2 expire

Run Device and Node expiry actions according to configuration.

=head2 expirenodes

Archive nodes on the specified device. If you want to delete nodes, set the
C<-e> parameter to "C<no>" (mnemonic: B<e>xpire). If you want to perform the
action on a specific port, set the C<-p> parameter.

 ~netdisco/bin/netdisco-do expirenodes -d 192.0.2.1
 ~netdisco/bin/netdisco-do expirenodes -d 192.0.2.1 -p FastEthernet0/1 -e no

=head2 graph

Generate GraphViz graphs for the largest cluster of devices.

You'll need to install the L<Graph::Undirected> and L<GraphViz> Perl modules,
and possibly also the C<graphviz> utility for your operating system. Also
create a directory for the output files.

 mkdir ~netdisco/graph
 ~netdisco/bin/localenv cpanm Graph::Undirected
 ~netdisco/bin/localenv cpanm GraphViz

=head2 show

Dump the content of an SNMP MIB leaf, which is useful for diagnostics and
troubleshooting. You should provide the "C<-e>" option which is the name of
the leaf (such as C<interfaces> or C<uptime>).

If you wish to test with a device class other than that discovered, prefix the
leaf with the class short name, for example "C<Layer3::C3550::interfaces>" or
"C<Layer2::HP::uptime>".

 ~netdisco/bin/netdisco-do show -d 192.0.2.1 -e interfaces
 ~netdisco/bin/netdisco-do show -d 192.0.2.1 -e Layer2::HP::interfaces

=head2 psql

Start an interactive terminal with the Netdisco PostgreSQL database. If you
pass an SQL statement in the C<-e> option then it will be executed.

 ~netdisco/bin/netdisco-do psql
 ~netdisco/bin/netdisco-do psql -e 'SELECT ip, dns FROM device'
 ~netdisco/bin/netdisco-do psql -e 'COPY (SELECT ip, dns FROM device) TO STDOUT WITH CSV HEADER'

=head2 location

Set the SNMP location field on the device (specified with C<-d>). Pass the
location string in the C<-e> extra parameter.

 ~netdisco/bin/netdisco-do location -d 192.0.2.1 -e 'wiring closet'

=head2 contact

Set the SNMP contact field on the device (specified with C<-d>). Pass the
contact name in the C<-e> extra parameter.

 ~netdisco/bin/netdisco-do contact -d 192.0.2.1 -e 'tel: 555-2453'

=head2 portname

Set the description on a device port. Requires the C<-d> parameter (device),
C<-p> parameter (port), and C<-e> parameter (description).

 ~netdisco/bin/netdisco-do portname -d 192.0.2.1 -p FastEthernet0/1 -e 'Web Server'

=head2 portcontrol

Set the up/down status on a device port. Requires the C<-d> parameter
(device), C<-p> parameter (port), and C<-e> parameter ("up" or "down").

 ~netdisco/bin/netdisco-do portcontrol -d 192.0.2.1 -p FastEthernet0/1 -e up
 ~netdisco/bin/netdisco-do portcontrol -d 192.0.2.1 -p FastEthernet0/1 -e down

=head2 vlan

Set the native VLAN on a device port. Requires the C<-d> parameter (device),
C<-p> parameter (port), and C<-e> parameter (VLAN number).

 ~netdisco/bin/netdisco-do vlan -d 192.0.2.1 -p FastEthernet0/1 -e 102

=head2 power

Set the PoE on/off status on a device port. Requires the C<-d> parameter
(device), C<-p> parameter (port), and C<-e> parameter ("on" or "off").

 ~netdisco/bin/netdisco-do power -d 192.0.2.1 -p FastEthernet0/1 -e on
 ~netdisco/bin/netdisco-do power -d 192.0.2.1 -p FastEthernet0/1 -e off

=head1 DEBUG LEVELS

The flags "C<-DISQ>" can be specified, multiple times, and enable the
following items in order:

=over 4

=item C<-D>

Netdisco debug log level

=item C<-I> or C<-II>

L<SNMP::Info> trace level (1 or 2).

=item C<-S> or C<-SS> or C<-SSS>

L<SNMP> (net-snmp) trace level (1, 2 or 3).

=item C<-Q>

L<DBIx::Class> trace enabled

=back

=cut