From 993edd0c6a25f1292dcfaf3debcbbe45e3344419 Mon Sep 17 00:00:00 2001 From: Oliver Gorwits Date: Wed, 27 Sep 2023 12:03:49 +0100 Subject: [PATCH] ACL support for scheduled jobs (#1106) implements #580 --- lib/App/Netdisco/Backend/Role/Scheduler.pm | 46 +++++++++++----- lib/App/Netdisco/Util/Device.pm | 31 ++++++++++- lib/App/Netdisco/Worker/Plugin/Scheduler.pm | 59 +++++++++++++++++++++ share/config.yml | 10 ++-- 4 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 lib/App/Netdisco/Worker/Plugin/Scheduler.pm diff --git a/lib/App/Netdisco/Backend/Role/Scheduler.pm b/lib/App/Netdisco/Backend/Role/Scheduler.pm index fb8154ae..fc4a0500 100644 --- a/lib/App/Netdisco/Backend/Role/Scheduler.pm +++ b/lib/App/Netdisco/Backend/Role/Scheduler.pm @@ -3,7 +3,9 @@ package App::Netdisco::Backend::Role::Scheduler; use Dancer qw/:moose :syntax :script/; use NetAddr::IP; +use JSON::PP (); use Algorithm::Cron; + use App::Netdisco::Util::MCE; use App::Netdisco::JobQueue qw/jq_insert/; @@ -23,6 +25,12 @@ sub worker_begin { my $config = setting('schedule')->{$action} or next; + if (not $config->{when}) { + error sprintf 'sch (%s): schedule %s is missing time spec', + $wid, $action; + next; + } + # accept either single crontab format, or individual time fields $config->{when} = Algorithm::Cron->new( base => 'local', @@ -44,6 +52,8 @@ sub worker_body { return debug "sch ($wid): no need for scheduler... quitting" } + my $coder = JSON::PP->new->utf8(0)->allow_nonref(1)->allow_unknown(1); + while (1) { # sleep until some point in the next minute my $naptime = 60 - (time % 60) + int(rand(45)); @@ -68,21 +78,31 @@ sub worker_body { $win_start, $win_end, $sched->{when}->next_time($win_start); next unless $sched->{when}->next_time($win_start) <= $win_end; - my $net = NetAddr::IP->new($sched->{device}); - next if ($sched->{device} - and (!$net or $net->num == 0 or $net->addr eq '0.0.0.0')); - - my @hostlist = map { (ref $_) ? $_->addr : undef } - (defined $sched->{device} ? ($net->hostenum) : (undef)); my @job_specs = (); - foreach my $host (@hostlist) { - push @job_specs, { - action => $real_action, - device => $host, - port => $sched->{port}, - subaction => $sched->{extra}, - }; + if ($sched->{only} or $sched->{no}) { + $sched->{label} = $action; + push @job_specs, { + action => 'scheduler', + subaction => $coder->encode($sched), + }; + } + else { + my $net = NetAddr::IP->new($sched->{device}); + next if ($sched->{device} + and (!$net or $net->num == 0 or $net->addr eq '0.0.0.0')); + + my @hostlist = map { (ref $_) ? $_->addr : undef } + (defined $sched->{device} ? ($net->hostenum) : (undef)); + + foreach my $host (@hostlist) { + push @job_specs, { + action => $real_action, + device => $host, + port => $sched->{port}, + subaction => $sched->{extra}, + }; + } } info sprintf 'sched (%s): queueing %s %s jobs', diff --git a/lib/App/Netdisco/Util/Device.pm b/lib/App/Netdisco/Util/Device.pm index 16843203..b6b66ea2 100644 --- a/lib/App/Netdisco/Util/Device.pm +++ b/lib/App/Netdisco/Util/Device.pm @@ -4,8 +4,10 @@ use Dancer qw/:syntax :script/; use Dancer::Plugin::DBIC 'schema'; use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/; +use List::MoreUtils (); use File::Spec::Functions qw(catdir catfile); use File::Path 'make_path'; +use NetAddr::IP; use base 'Exporter'; our @EXPORT = (); @@ -362,7 +364,34 @@ sub get_denied_actions { push @badactions, 'arpnip' if not is_arpnipable($device); - return @badactions; + # add pseudo-actions for schedule entries with ACLs + my $schedule = setting('schedule') || {}; + foreach my $label (keys %$schedule) { + my $sched = $schedule->{$label} || next; + next unless $sched->{only} or $sched->{no}; + + my $action = $sched->{action} || $label; + my $pseudo_action = "scheduled-$label"; + + # if this action is denied in global config then schedule should not run + if (scalar grep {$_ eq $action} @badactions) { + push @badactions, $pseudo_action; + next; + } + + my $net = NetAddr::IP->new($sched->{device}); + next if ($sched->{device} + and (!$net or $net->num == 0 or $net->addr eq '0.0.0.0')); + + push @badactions, $pseudo_action + if $sched->{device} and not acl_matches_only($device, $net->cidr); + push @badactions, $pseudo_action + if $sched->{no} and acl_matches($device, $sched->{no}); + push @badactions, $pseudo_action + if $sched->{only} and not acl_matches_only($device, $sched->{only}); + } + + return List::MoreUtils::uniq @badactions; } 1; diff --git a/lib/App/Netdisco/Worker/Plugin/Scheduler.pm b/lib/App/Netdisco/Worker/Plugin/Scheduler.pm new file mode 100644 index 00000000..a4f825f9 --- /dev/null +++ b/lib/App/Netdisco/Worker/Plugin/Scheduler.pm @@ -0,0 +1,59 @@ +package App::Netdisco::Worker::Plugin::Scheduler; + +use Dancer ':syntax'; +use App::Netdisco::Worker::Plugin; +use aliased 'App::Netdisco::Worker::Status'; + +use App::Netdisco::JobQueue 'jq_insert'; +use Dancer::Plugin::DBIC 'schema'; + +use JSON::PP (); + +register_worker({ phase => 'check' }, sub { + my ($job, $workerconf) = @_; + + return Status->error("Missing data of Scheduler entry") + unless $job->extra; + + return Status->defer("scheduler skipped: have not yet primed skiplist") + unless schema(vars->{'tenant'})->resultset('DeviceSkip') + ->search({ + backend => setting('workers')->{'BACKEND'}, + device => '255.255.255.255', + })->count(); + + return Status->done('Scheduler is able to run'); +}); + +register_worker({ phase => 'main' }, sub { + my ($job, $workerconf) = @_; + + my $coder = JSON::PP->new->utf8(0)->allow_nonref(1)->allow_unknown(1); + my $sched = $coder->decode( $job->extra || {} ); + my $action = $sched->{action} || $sched->{label}; + + return Status->error("Missing label of Scheduler entry") + unless $action; + + my @walk = schema(vars->{'tenant'})->resultset('Virtual::WalkJobs') + ->search(undef,{ bind => [ + $action, ('scheduled-'. $sched->{label}), + setting('workers')->{'max_deferrals'}, + setting('workers')->{'retry_after'}, + ]})->get_column('ip')->all; + + jq_insert([ + map {{ + device => $_, + action => $action, + port => $sched->{port}, + subaction => $sched->{subaction}, + username => $job->username, + userip => $job->userip, + }} (@walk) + ]); + + return Status->done(sprintf 'Queued %s job for all devices', $action); +}); + +true; diff --git a/share/config.yml b/share/config.yml index 7d32289c..3078a0ff 100644 --- a/share/config.yml +++ b/share/config.yml @@ -509,6 +509,7 @@ job_prio: - 'macwalk' - 'nbtstat' - 'nbtwalk' + - 'scheduler' - 'stats' worker_plugins: @@ -567,6 +568,7 @@ worker_plugins: - 'PrimeSkiplist' - 'Psql' - 'Renumber' + - 'Scheduler' - 'Show' - 'Snapshot' - 'Stats' @@ -584,11 +586,13 @@ driver_priority: snmp: 100 deferrable_actions: - - 'snapshot' - - 'nbtwalk' - - 'macwalk' - 'arpwalk' - 'discoverall' + - 'macwalk' + - 'nbtwalk' + - 'primeskiplist' + - 'scheduler' + - 'snapshot' # --------------- # GraphViz Export