make an App::Netdisco dist using Module::Install

This commit is contained in:
Oliver Gorwits
2012-12-17 18:31:16 +00:00
parent 6a0aa7864e
commit 05086e8b78
125 changed files with 428 additions and 127 deletions

View File

@@ -0,0 +1,23 @@
use utf8;
package App::Netdisco::Daemon::DB;
use strict;
use warnings;
use base 'DBIx::Class::Schema';
__PACKAGE__->load_namespaces;
our $VERSION = 1; # schema version used for upgrades, keep as integer
use Path::Class;
use File::Basename;
my (undef, $libpath, undef) = fileparse( $INC{ 'App/Netdisco/Daemon/DB.pm' } );
our $schema_versions_dir = Path::Class::Dir->new($libpath)
->subdir("DB", "schema_versions")->stringify;
__PACKAGE__->load_components(qw/Schema::Versioned/);
__PACKAGE__->upgrade_directory($schema_versions_dir);
1;

View File

@@ -0,0 +1,37 @@
use utf8;
package App::Netdisco::Daemon::DB::Result::Admin;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("admin");
__PACKAGE__->add_columns(
"job",
{
data_type => "integer",
is_nullable => 0,
},
"started",
{ data_type => "timestamp", is_nullable => 1 },
"finished",
{ data_type => "timestamp", is_nullable => 1 },
"device",
{ data_type => "inet", is_nullable => 1 },
"port",
{ data_type => "text", is_nullable => 1 },
"action",
{ data_type => "text", is_nullable => 1 },
"subaction",
{ data_type => "text", is_nullable => 1 },
"status",
{ data_type => "text", is_nullable => 1 },
"log",
{ data_type => "text", is_nullable => 1 },
"debug",
{ data_type => "boolean", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("job");
1;

View File

@@ -0,0 +1,111 @@
package App::Netdisco::Daemon::Worker::Interactive;
use Dancer qw/:moose :syntax :script/;
use Dancer::Plugin::DBIC 'schema';
use Try::Tiny;
use Role::Tiny;
use namespace::clean;
# add dispatch methods for interactive actions
with 'App::Netdisco::Daemon::Worker::Interactive::DeviceActions',
'App::Netdisco::Daemon::Worker::Interactive::PortActions';
sub worker_body {
my $self = shift;
# get all pending jobs
my $rs = schema('daemon')->resultset('Admin')->search({
action => [qw/location contact portcontrol portname vlan power/],
status => 'queued',
});
while (1) {
while (my $job = $rs->next) {
my $target = 'set_'. $job->action;
next unless $self->can($target);
# mark job as running
next unless $self->lock_job($job);
# do job
my ($status, $log);
try {
($status, $log) = $self->$target($job);
}
catch { warn "error running job: $_\n" };
# revert to queued status if we failed to action the job
if (not $status) {
$self->revert_job($job->job);
}
else {
# update job state to done/error with log
$self->close_job($job->job, $status, $log);
}
}
# reset iterator so ->next() triggers another DB query
$rs->reset;
$self->gd_sleep( setting('daemon_sleep_time') || 5 );
}
}
sub lock_job {
my ($self, $job) = @_;
my $happy = 1;
# lock db table, check job state is still queued, update to running
try {
my $status_updated = schema('daemon')->txn_do(sub {
my $row = schema('daemon')->resultset('Admin')->find(
{job => $job->job},
{for => 'update'}
);
$happy = 0 if $row->status ne 'queued';
$row->update({status => "running-$$", started => \"datetime('now')" });
});
$happy = 0 if not $status_updated;
}
catch {
warn "error locking job: $_\n";
$happy = 0;
};
return $happy;
}
sub revert_job {
my ($self, $id) = @_;
try {
schema('daemon')->resultset('Admin')
->find($id)
->update({status => 'queued', started => undef});
}
catch { warn "error reverting job: $_\n" };
}
sub close_job {
my ($self, $id, $status, $log) = @_;
try {
my $local = schema('daemon')->resultset('Admin')->find($id);
schema('netdisco')->resultset('Admin')
->find($id)
->update({
status => $status,
log => $log,
started => $local->started,
finished => \'now()',
});
$local->delete;
}
catch { warn "error closing job: $_\n" };
}
1;

View File

@@ -0,0 +1,49 @@
package App::Netdisco::Daemon::Worker::Interactive::DeviceActions;
use App::Netdisco::Util::Connect qw/snmp_connect get_device/;
use App::Netdisco::Daemon::Worker::Interactive::Util ':all';
use Role::Tiny;
use namespace::clean;
sub set_location {
my ($self, $job) = @_;
return _set_device_generic($job->device, 'location', $job->subaction);
}
sub set_contact {
my ($self, $job) = @_;
return _set_device_generic($job->device, 'contact', $job->subaction);
}
sub _set_device_generic {
my ($ip, $slot, $data) = @_;
$data ||= '';
# snmp connect using rw community
my $info = snmp_connect($ip)
or return error("Failed to connect to device [$ip] to update $slot");
my $method = 'set_'. $slot;
my $rv = $info->$method($data);
if (!defined $rv) {
return error(sprintf 'Failed to set %s on [%s]: %s',
$slot, $ip, ($info->error || ''));
}
# confirm the set happened
$info->clear_cache;
my $new_data = ($info->$slot || '');
if ($new_data ne $data) {
return error("Verify of $slot update failed on [$ip]: $new_data");
}
# update netdisco DB
my $device = get_device($ip);
$device->update({$slot => $data});
return done("Updated $slot on [$ip] to [$data]");
}
1;

View File

@@ -0,0 +1,136 @@
package App::Netdisco::Daemon::Worker::Interactive::PortActions;
use App::Netdisco::Util::Connect ':all';
use App::Netdisco::Util::Permissions ':all';
use App::Netdisco::Daemon::Worker::Interactive::Util ':all';
use Role::Tiny;
use namespace::clean;
sub set_portname {
my ($self, $job) = @_;
return _set_port_generic($job, 'alias', 'name');
}
sub set_portcontrol {
my ($self, $job) = @_;
my $port = get_port($job->device, $job->port)
or return error(sprintf "Unknown port name [%s] on device [%s]",
$job->port, $job->device);
my $reconfig_check = port_reconfig_check($port);
return error("Cannot alter port: $reconfig_check")
if length $reconfig_check;
return _set_port_generic($job, 'up_admin');
}
sub set_vlan {
my ($self, $job) = @_;
my $port = get_port($job->device, $job->port)
or return error(sprintf "Unknown port name [%s] on device [%s]",
$job->port, $job->device);
my $port_reconfig_check = port_reconfig_check($port);
return error("Cannot alter port: $port_reconfig_check")
if length $port_reconfig_check;
my $vlan_reconfig_check = vlan_reconfig_check($port);
return error("Cannot alter vlan: $vlan_reconfig_check")
if length $vlan_reconfig_check;
return _set_port_generic($job, 'vlan');
}
sub _set_port_generic {
my ($job, $slot, $column) = @_;
$column ||= $slot;
my $ip = $job->device;
my $pn = $job->port;
(my $data = $job->subaction) =~ s/-\w+//;
my $port = get_port($ip, $pn)
or return error("Unknown port name [$pn] on device [$ip]");
# snmp connect using rw community
my $info = snmp_connect($ip)
or return error("Failed to connect to device [$ip] to control port");
my $iid = get_iid($info, $port)
or return error("Failed to get port ID for [$pn] from [$ip]");
my $method = 'set_i_'. $slot;
my $rv = $info->$method($data, $iid);
if (!defined $rv) {
return error(sprintf 'Failed to set [%s] %s to [%s] on [%s]: %s',
$pn, $slot, $data, $ip, ($info->error || ''));
}
# confirm the set happened
$info->clear_cache;
my $check_method = 'i_'. $slot;
my $state = ($info->$check_method($iid) || '');
if (ref {} ne ref $state or $state->{$iid} ne $data) {
return error("Verify of [$pn] $slot failed on [$ip]");
}
# update netdisco DB
$port->update({$column => $data});
return done("Updated [$pn] $slot status on [$ip] to [$data]");
}
sub set_power {
my ($self, $job) = @_;
my $port = get_port($job->device, $job->port)
or return error(sprintf "Unknown port name [%s] on device [%s]",
$job->port, $job->device);
return error("No PoE service on port [%s] on device [%s]")
unless $port->power;
my $reconfig_check = port_reconfig_check($port);
return error("Cannot alter port: $reconfig_check")
if length $reconfig_check;
my $ip = $job->device;
my $pn = $job->port;
(my $data = $job->subaction) =~ s/-\w+//;
# snmp connect using rw community
my $info = snmp_connect($ip)
or return error("Failed to connect to device [$ip] to control port");
my $powerid = get_powerid($info, $port)
or return error("Failed to get power ID for [$pn] from [$ip]");
my $rv = $info->set_peth_port_admin($data, $powerid);
if (!defined $rv) {
return error(sprintf 'Failed to set [%s] power to [%s] on [%s]: %s',
$pn, $data, $ip, ($info->error || ''));
}
# confirm the set happened
$info->clear_cache;
my $state = ($info->peth_port_admin($powerid) || '');
if (ref {} ne ref $state or $state->{$powerid} ne $data) {
return error("Verify of [$pn] power failed on [$ip]");
}
# update netdisco DB
$port->power->update({
admin => $data,
status => ($data eq 'false' ? 'disabled' : 'searching'),
});
return done("Updated [$pn] power status on [$ip] to [$data]");
}
1;

View File

@@ -0,0 +1,15 @@
package App::Netdisco::Daemon::Worker::Interactive::Util;
# support utilities for Daemon Actions
use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = qw/ done error /;
our %EXPORT_TAGS = (
all => [qw/ done error /],
);
sub done { return ('done', shift) }
sub error { return ('error', shift) }
1;

View File

@@ -0,0 +1,123 @@
package App::Netdisco::Daemon::Worker::Manager;
use Dancer qw/:moose :syntax :script/;
use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::DeviceProperties 'is_discoverable';
use Try::Tiny;
use Role::Tiny;
use namespace::clean;
sub worker_begin {
my $self = shift;
my $daemon = schema('daemon');
# deploy local db if not already done
try {
$daemon->storage->dbh_do(sub {
my ($storage, $dbh) = @_;
$dbh->selectrow_arrayref("SELECT * FROM admin WHERE 0 = 1");
});
}
catch { $daemon->txn_do( $daemon->deploy ) };
$daemon->storage->disconnect;
if ($daemon->get_db_version < $daemon->schema_version) {
$daemon->txn_do( $daemon->upgrade );
}
# on start, any jobs previously grabbed by a daemon on this host
# will be reset to "queued", which is the simplest way to restart them.
my $rs = schema('netdisco')->resultset('Admin')->search({
status => "running-$self->{nd_host}"
});
if ($rs->count > 0) {
$daemon->resultset('Admin')->delete;
$rs->update({status => 'queued', started => undef});
}
}
sub worker_body {
my $self = shift;
# get all pending jobs
my $rs = schema('netdisco')->resultset('Admin')
->search({status => 'queued'});
while (1) {
while (my $job = $rs->next) {
# filter for discover_*
next unless is_discoverable($job->device);
# mark job as running
next unless $self->lock_job($job);
# copy job to local queue
$self->copy_job($job)
or $self->revert_job($job->job);
}
# reset iterator so ->next() triggers another DB query
$rs->reset;
$self->gd_sleep( setting('daemon_sleep_time') || 5 );
}
}
sub lock_job {
my ($self, $job) = @_;
my $happy = 1;
# 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'}
);
$happy = 0 if $row->status ne 'queued';
$row->update({
status => "running-$self->{nd_host}",
started => \'now()'
});
});
$happy = 0 if not $status_updated;
}
catch {
warn "error locking job: $_\n";
$happy = 0;
};
return $happy;
}
sub copy_job {
my ($self, $job) = @_;
try {
my %data = $job->get_columns;
delete $data{$_} for qw/entered username userip/;
schema('daemon')->resultset('Admin')->update_or_create({
%data, status => 'queued', started => undef,
});
}
catch { warn "error copying job: $_\n" };
}
sub revert_job {
my ($self, $id) = @_;
try {
schema('netdisco')->resultset('Admin')
->find($id)
->update({status => 'queued', started => undef});
}
catch { warn "error reverting job: $_\n" };
}
1;