diff --git a/lib/App/Netdisco/JobQueue/PostgreSQL.pm b/lib/App/Netdisco/JobQueue/PostgreSQL.pm index dd88b820..3da2c626 100644 --- a/lib/App/Netdisco/JobQueue/PostgreSQL.pm +++ b/lib/App/Netdisco/JobQueue/PostgreSQL.pm @@ -315,7 +315,7 @@ sub jq_userlog { return schema(vars->{'tenant'})->resultset('Admin')->search({ username => $user, log => { '-not_like' => 'duplicate of %' }, - finished => { '>' => \"(LOCALTIMESTAMP - interval '5 seconds')" }, + finished => { '>' => \"(CURRENT_TIMESTAMP - interval '5 seconds')" }, })->with_times->all; } diff --git a/lib/App/Netdisco/Web.pm b/lib/App/Netdisco/Web.pm index 75158bfc..e67d6084 100644 --- a/lib/App/Netdisco/Web.pm +++ b/lib/App/Netdisco/Web.pm @@ -106,6 +106,7 @@ use App::Netdisco::Web::Search; use App::Netdisco::Web::Device; use App::Netdisco::Web::Report; use App::Netdisco::Web::API::Objects; +use App::Netdisco::Web::API::Queue; use App::Netdisco::Web::AdminTask; use App::Netdisco::Web::TypeAhead; use App::Netdisco::Web::PortControl; @@ -405,6 +406,8 @@ $swagger_doc->{tags} = [ description => 'Device, Port, and associated Node Data'}, {name => 'Reports', description => 'Canned and Custom Reports'}, + {name => 'Queue', + description => 'Operations on the Job Queue'}, ]; $swagger_doc->{securityDefinitions} = { APIKeyHeader => diff --git a/lib/App/Netdisco/Web/API/Objects.pm b/lib/App/Netdisco/Web/API/Objects.pm index 29452543..1acbe56a 100644 --- a/lib/App/Netdisco/Web/API/Objects.pm +++ b/lib/App/Netdisco/Web/API/Objects.pm @@ -46,6 +46,59 @@ foreach my $rel (qw/device_ips vlans ports modules port_vlans wireless_ports ssi }; } +swagger_path { + tags => ['Objects'], + path => (setting('api_base') || '').'/object/device/{ip}/jobs', + description => 'Delete jobs and clear skiplist for a device, optionally filtered by fields', + parameters => [ + ip => { + description => 'Canonical IP of the Device. Use Search methods to find this.', + required => 1, + in => 'path', + }, + port => { + description => 'Port field of the Job', + }, + action => { + description => 'Action field of the Job', + }, + status => { + description => 'Status field of the Job', + }, + username => { + description => 'Username of the Job submitter', + }, + userip => { + description => 'IP address of the Job submitter', + }, + backend => { + description => 'Backend instance assigned the Job', + }, + ], + responses => { default => {} }, +}, del '/api/v1/object/device/:ip/jobs' => require_role api_admin => sub { + my $device = try { schema(vars->{'tenant'})->resultset('Device') + ->find( params->{ip} ) } or send_error('Bad Device', 404); + + my $gone = schema(vars->{'tenant'})->resultset('Admin')->search({ + device => param('ip'), + ( param('port') ? ( port => param('port') ) : () ), + ( param('action') ? ( action => param('action') ) : () ), + ( param('status') ? ( status => param('status') ) : () ), + ( param('username') ? ( username => param('username') ) : () ), + ( param('userip') ? ( userip => param('userip') ) : () ), + ( param('backend') ? ( backend => param('backend') ) : () ), + })->delete; + + schema(vars->{'tenant'})->resultset('DeviceSkip')->search({ + device => param('ip'), + ( param('action') ? ( actionset => { '&&' => \[ 'ARRAY[?]', param('action') ] } ) : () ), + ( param('backend') ? ( backend => param('backend') ) : () ), + })->delete; + + return to_json { deleted => ($gone || 0)}; +}; + foreach my $rel (qw/nodes active_nodes nodes_with_age active_nodes_with_age vlans logs/) { swagger_path { tags => ['Objects'], diff --git a/lib/App/Netdisco/Web/API/Queue.pm b/lib/App/Netdisco/Web/API/Queue.pm new file mode 100644 index 00000000..3aebd1d8 --- /dev/null +++ b/lib/App/Netdisco/Web/API/Queue.pm @@ -0,0 +1,179 @@ +package App::Netdisco::Web::API::Queue; + +use Dancer ':syntax'; +use Dancer::Plugin::DBIC; +use Dancer::Plugin::Swagger; +use Dancer::Plugin::Auth::Extensible; + +use App::Netdisco::JobQueue 'jq_insert'; +use Try::Tiny; + +swagger_path { + tags => ['Queue'], + path => (setting('api_base') || '').'/queue/backends', + description => 'Return list of currently active backend names (usually FQDN)', + responses => { default => {} }, +}, get '/api/v1/queue/backends' => require_role api_admin => sub { + # from 1d988bbf7 this always returns an entry + my @names = schema(vars->{'tenant'})->resultset('DeviceSkip') + ->get_distinct_col('backend'); + + return to_json \@names; +}; + +swagger_path { + tags => ['Queue'], + path => (setting('api_base') || '').'/queue/jobs', + description => 'Return jobs in the queue, optionally filtered by fields', + parameters => [ + limit => { + description => 'Maximum number of Jobs to return', + type => 'integer', + default => (setting('jobs_qdepth') || 50), + }, + device => { + description => 'IP address field of the Job', + }, + port => { + description => 'Port field of the Job', + }, + action => { + description => 'Action field of the Job', + }, + status => { + description => 'Status field of the Job', + }, + username => { + description => 'Username of the Job submitter', + }, + userip => { + description => 'IP address of the Job submitter', + }, + backend => { + description => 'Backend instance assigned the Job', + }, + ], + responses => { default => {} }, +}, get '/api/v1/queue/jobs' => require_role api_admin => sub { + my @set = schema(vars->{'tenant'})->resultset('Admin')->search({ + ( param('device') ? ( device => param('device') ) : () ), + ( param('port') ? ( port => param('port') ) : () ), + ( param('action') ? ( action => param('action') ) : () ), + ( param('status') ? ( status => param('status') ) : () ), + ( param('username') ? ( username => param('username') ) : () ), + ( param('userip') ? ( userip => param('userip') ) : () ), + ( param('backend') ? ( backend => param('backend') ) : () ), + -or => [ + { 'log' => undef }, + { 'log' => { '-not_like' => 'duplicate of %' } }, + ], + }, { + order_by => { -desc => [qw/entered device action/] }, + rows => (param('limit') || setting('jobs_qdepth') || 50), + })->with_times->hri->all; + + return to_json \@set; +}; + +swagger_path { + tags => ['Queue'], + path => (setting('api_base') || '').'/queue/jobs', + description => 'Delete jobs and skiplist entries, optionally filtered by fields', + parameters => [ + device => { + description => 'IP address field of the Job', + }, + port => { + description => 'Port field of the Job', + }, + action => { + description => 'Action field of the Job', + }, + status => { + description => 'Status field of the Job', + }, + username => { + description => 'Username of the Job submitter', + }, + userip => { + description => 'IP address of the Job submitter', + }, + backend => { + description => 'Backend instance assigned the Job', + }, + ], + responses => { default => {} }, +}, del '/api/v1/queue/jobs' => require_role api_admin => sub { + my $gone = schema(vars->{'tenant'})->resultset('Admin')->search({ + ( param('device') ? ( device => param('device') ) : () ), + ( param('port') ? ( port => param('port') ) : () ), + ( param('action') ? ( action => param('action') ) : () ), + ( param('status') ? ( status => param('status') ) : () ), + ( param('username') ? ( username => param('username') ) : () ), + ( param('userip') ? ( userip => param('userip') ) : () ), + ( param('backend') ? ( backend => param('backend') ) : () ), + })->delete; + + schema(vars->{'tenant'})->resultset('DeviceSkip')->search({ + ( param('device') ? ( device => param('device') ) : () ), + ( param('action') ? ( actionset => { '&&' => \[ 'ARRAY[?]', param('action') ] } ) : () ), + ( param('backend') ? ( backend => param('backend') ) : () ), + })->delete; + + return to_json { deleted => ($gone || 0)}; +}; + +swagger_path { + tags => ['Queue'], + path => (setting('api_base') || '').'/queue/jobs', + description => 'Submit jobs to the queue', + parameters => [ + jobs => { + description => 'List of job specifications (action, device?, port?, extra?).', + default => '[]', + schema => { + type => 'array', + items => { + type => 'object', + properties => { + action => { + type => 'string', + required => 1, + }, + device => { + type => 'string', + required => 0, + }, + port => { + type => 'string', + required => 0, + }, + extra => { + type => 'string', + required => 0, + } + } + } + }, + in => 'body', + }, + ], + responses => { default => {} }, +}, post '/api/v1/queue/jobs' => require_role api_admin => sub { + my $data = request->body || ''; + my $jobs = (length $data ? try { from_json($data) } : []); + + (ref [] eq ref $jobs) or send_error('Malformed body', 400); + + foreach my $job (@$jobs) { + ref {} eq ref $job or send_error('Malformed job', 400); + $job->{username} = session('logged_in_user'); + $job->{userip} = request->remote_address; + } + + my $happy = jq_insert($jobs); + + return to_json { success => $happy }; +}; + +true; diff --git a/lib/App/Netdisco/Web/AuthN.pm b/lib/App/Netdisco/Web/AuthN.pm index 29ff6ddd..9f3f74b4 100644 --- a/lib/App/Netdisco/Web/AuthN.pm +++ b/lib/App/Netdisco/Web/AuthN.pm @@ -202,8 +202,8 @@ get '/logout' => sub { redirect uri_for(setting('web_home'))->path; }; -# user redirected here (POST -> GET) when login fails -get qr{^/(?:login(?:/denied)?)?} => sub { +# user redirected here when require_role does not succeed +any qr{^/(?:login(?:/denied)?)?} => sub { my $api = ((request->accept and request->accept =~ m/(?:json|javascript)/) ? true : false); if ($api) {