diff --git a/lib/App/Netdisco/DB/Result/Admin.pm b/lib/App/Netdisco/DB/Result/Admin.pm index 324f7e98..e320c1f6 100644 --- a/lib/App/Netdisco/DB/Result/Admin.pm +++ b/lib/App/Netdisco/DB/Result/Admin.pm @@ -161,4 +161,12 @@ between the date stamp and time stamp. That is: sub finished_stamp { return (shift)->get_column('finished_stamp') } +=head2 duration + +Formatted version of the C field, accurate to the minute. + +=cut + +sub duration { return (shift)->get_column('duration') } + 1; diff --git a/lib/App/Netdisco/DB/Result/Virtual/JobQueue.pm b/lib/App/Netdisco/DB/Result/Virtual/JobQueue.pm new file mode 100644 index 00000000..feb9fbcc --- /dev/null +++ b/lib/App/Netdisco/DB/Result/Virtual/JobQueue.pm @@ -0,0 +1,73 @@ +package App::Netdisco::DB::Result::Virtual::JobQueue; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +__PACKAGE__->table_class('DBIx::Class::ResultSource::View'); + +__PACKAGE__->table('job_queue'); +__PACKAGE__->result_source_instance->is_virtual(1); +__PACKAGE__->result_source_instance->view_definition(<add_columns( + "device", + { data_type => "inet", is_nullable => 0 }, + "action", + { data_type => "text", is_nullable => 1 }, + "subaction", + { data_type => "text", is_nullable => 1 }, + "port", + { data_type => "text", is_nullable => 1 }, + "status", + { data_type => "text", is_nullable => 1 }, + "username", + { data_type => "text", is_nullable => 1 }, + "log", + { data_type => "text", is_nullable => 1 }, + + "skips", + { data_type => "[text]", is_nullable => 1 }, + + "entered_stamp", + { data_type => "text", is_nullable => 1 }, + "duration", + { data_type => "text", is_nullable => 1 }, +); + +__PACKAGE__->belongs_to('target', 'App::Netdisco::DB::Result::Device', + { 'foreign.ip' => 'self.device' }, { join_type => 'LEFT' } ); + +1; diff --git a/lib/App/Netdisco/DB/ResultSet/Admin.pm b/lib/App/Netdisco/DB/ResultSet/Admin.pm index c1323cdc..7e2d22f5 100644 --- a/lib/App/Netdisco/DB/ResultSet/Admin.pm +++ b/lib/App/Netdisco/DB/ResultSet/Admin.pm @@ -46,6 +46,8 @@ will add the following additional synthesized columns to the result set: =item finished_stamp +=item duration + =back =cut @@ -61,6 +63,7 @@ sub with_times { entered_stamp => \"to_char(entered, 'YYYY-MM-DD HH24:MI')", started_stamp => \"to_char(started, 'YYYY-MM-DD HH24:MI')", finished_stamp => \"to_char(finished, 'YYYY-MM-DD HH24:MI')", + duration => \"replace(age(finished, started)::text, 'mon', 'month')", }, }); } diff --git a/lib/App/Netdisco/JobQueue/PostgreSQL.pm b/lib/App/Netdisco/JobQueue/PostgreSQL.pm index a16913f0..0de24761 100644 --- a/lib/App/Netdisco/JobQueue/PostgreSQL.pm +++ b/lib/App/Netdisco/JobQueue/PostgreSQL.pm @@ -270,11 +270,13 @@ sub jq_complete { } sub jq_log { - return schema('netdisco')->resultset('Admin')->search({}, { - prefetch => 'target', - order_by => { -desc => [qw/entered device action/] }, + my $filter = shift; + return schema('netdisco')->resultset('Virtual::JobQueue')->search({}, { + join => 'target', + '+columns' => ['target.dns','target.ip'], + bind => [ setting('job_prio')->{'high'}, $filter, $filter ], rows => 50, - })->with_times->hri->all; + })->hri->all; } sub jq_userlog { diff --git a/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm b/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm index 5cf97cac..b5939c1c 100644 --- a/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm +++ b/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm @@ -8,6 +8,11 @@ use Dancer::Plugin::Auth::Extensible; use App::Netdisco::Web::Plugin; use App::Netdisco::JobQueue qw/jq_log jq_delete/; +use utf8; +use Time::Piece; +use Text::CSV_XS 'csv'; +use NetAddr::IP::Lite ':lower'; + register_admin_task({ tag => 'jobqueue', label => 'Job Queue', @@ -23,9 +28,61 @@ ajax '/ajax/control/admin/jobqueue/delall' => require_role admin => sub { }; ajax '/ajax/content/admin/jobqueue' => require_role admin => sub { + my $filter = NetAddr::IP::Lite->new(param('q')); + my @data = jq_log($filter); + + foreach my $r (@data) { + $r->{qstat}->{acl} = []; + $r->{qstat}->{next} = []; + $r->{qstat}->{fails} = []; + next unless ($r->{status} eq 'queued'); + + foreach my $s (@{$r->{skips}}) { + (my $row = $s) =~ s/(^\(|\)$)//g; + next unless $row; + my %skip = @{ [@{csv(in => \$row)}]->[0] }; + next unless scalar keys %skip; + $skip{actionset} =~ s/(^{|}$)//g; + $skip{actionset} = [@{ csv(in => \$skip{actionset}) }]->[0] || []; + + if ($skip{deferrals}) { + unshift @{$r->{qstat}->{fails}}, sprintf '%s connection failure%s from %s', + $skip{deferrals}, ($skip{deferrals} > 1 ? 's' : ''), $skip{backend}; + } + else { + unshift @{$r->{qstat}->{fails}}, sprintf 'No connection failures from %s', + $skip{backend}; + } + + if (scalar @{$skip{actionset}} + and scalar grep {$_ eq $r->{action}} @{$skip{actionset}}) { + $r->{qstat}->{badacl} = true; + unshift @{$r->{qstat}->{acl}}, sprintf 'Blocked by ACL on %s', $skip{backend}; + } + else { + push @{$r->{qstat}->{acl}}, sprintf '✔ on %s', $skip{backend}; + } + + if ($skip{deferrals} >= setting('workers')->{'max_deferrals'}) { + $r->{qstat}->{last_defer} = true; + my $after = (localtime($skip{last_defer}) + setting('workers')->{'retry_after'}); + unshift @{$r->{qstat}->{next}}, sprintf 'Will retry after %s on %s', + $after->cdate, $skip{backend}; + } + elsif ($skip{deferrals} > 0) { + $r->{qstat}->{last_defer} = true; + unshift @{$r->{qstat}->{next}}, sprintf 'Connect failed at %s on %s', + localtime($skip{last_defer})->cdate, $skip{backend}; + } + else { + push @{$r->{qstat}->{next}}, sprintf '✔ on %s', $skip{backend}; + } + } + } + content_type('text/html'); template 'ajax/admintask/jobqueue.tt', { - results => [ jq_log ], + results => \@data, }, { layout => undef }; }; diff --git a/share/public/css/netdisco.css b/share/public/css/netdisco.css index 6f0f9f01..ed592d10 100644 --- a/share/public/css/netdisco.css +++ b/share/public/css/netdisco.css @@ -454,6 +454,10 @@ td > form.nd_inline-form { .tooltip-wrapper .input[disabled] { pointer-events: none; } +.tooltip-inner { + max-width: none; + white-space: nowrap; +} /* vlan entry box for netmap */ #nd_vlan-label { diff --git a/share/views/admintask.tt b/share/views/admintask.tt index 23d8faaa..2c6e6718 100644 --- a/share/views/admintask.tt +++ b/share/views/admintask.tt @@ -30,10 +30,10 @@ href="#[% task.tag %]_pane">[% task.label %] [% IF task.tag == 'jobqueue' %] - - + + - + [% ELSIF task.tag == 'userlog' %] diff --git a/share/views/ajax/admintask/jobqueue.tt b/share/views/ajax/admintask/jobqueue.tt index 10ec7df6..2a326dcc 100644 --- a/share/views/ajax/admintask/jobqueue.tt +++ b/share/views/ajax/admintask/jobqueue.tt @@ -5,14 +5,12 @@ Entered - Action - Status - Device - Port - Param User - Started - Finished + Action + Device + Parameters + Status + Duration Action @@ -25,16 +23,12 @@ data-content="
[% row.log | html_entity %]
" > [% row.entered_stamp | html_entity %] + [% row.username | html_entity %] [% FOREACH word IN row.action.split('_') %] [% word.ucfirst | html_entity %]  [% END %] - [% IF row.status.search('^queued-') %] - Running on "[% row.status.remove('^queued-') | html_entity %]" - [% ELSE %] - [% row.status.ucfirst | html_entity %] - [% END %] [% IF row.action == 'discover' AND row.status == 'error' %] [% row.device | html_entity %] @@ -42,11 +36,29 @@ [% row.target.dns || row.device | html_entity %] [% END %] - [% row.port | html_entity %] - [% row.subaction | html_entity %] - [% row.username | html_entity %] - [% row.started_stamp | html_entity %] - [% row.finished_stamp | html_entity %] + [% row.port | html_entity %][% ', ' IF row.port AND row.subaction %][% row.subaction | html_entity %] + + [% IF row.status.search('^queued-') %] + Running on "[% row.status.remove('^queued-') | html_entity %]" + [% ELSIF row.status == 'queued' %] + + +   + +   + + + [% ELSE %] + [% row.status.ucfirst | html_entity %] + [% END %] + + [% row.duration | html_entity %] diff --git a/share/views/js/admintask.js b/share/views/js/admintask.js index 7b11bc3c..cc3edf86 100644 --- a/share/views/js/admintask.js +++ b/share/views/js/admintask.js @@ -71,8 +71,10 @@ ,minLength: 0 }); - // activate modals + // activate modals, tooltips and popovers $('.nd_modal').modal({show: false}); + $("[rel=tooltip]").tooltip({live: true}); + $("[rel=popover]").popover({live: true}); } // on load, establish global delegations for now and future