pve-manager/PVE/API2/Tasks.pm
Fabian Grünbichler 1258eb90b3 api: annotate download endpoints
both the task and system log API endpoints support downloading the log data.

annotate the API method schema accordingly to allow passing the newly
introduced checks in the API handler that limit download functionality to
annotated endpoints.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2024-09-23 14:18:30 +02:00

527 lines
13 KiB
Perl

package PVE::API2::Tasks;
use strict;
use warnings;
use POSIX;
use IO::File;
use File::ReadBackwards;
use PVE::Tools;
use PVE::SafeSyslog;
use PVE::RESTHandler;
use PVE::ProcFSTools;
use PVE::RPCEnvironment;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Exception qw(raise_param_exc);
use PVE::AccessControl;
use base qw(PVE::RESTHandler);
my $convert_token_task = sub {
my ($task) = @_;
if (PVE::AccessControl::pve_verify_tokenid($task->{user}, 1)) {
($task->{user}, $task->{tokenid}) = PVE::AccessControl::split_tokenid($task->{user});
}
};
my $check_task_user = sub {
my ($task, $user) = @_;
if ($task->{tokenid}) {
my $fulltoken = PVE::AccessControl::join_tokenid($task->{user}, $task->{tokenid});
# token only sees token tasks, user sees user + token tasks
return $user eq $fulltoken || $user eq $task->{user};
} else {
return $user eq $task->{user};
}
};
__PACKAGE__->register_method({
name => 'node_tasks',
path => '',
method => 'GET',
permissions => {
description => "List task associated with the current user, or all task the user has 'Sys.Audit' permissions on /nodes/<node> (the <node> the task runs on).",
user => 'all'
},
description => "Read task list for one node (finished tasks).",
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
start => {
type => 'integer',
minimum => 0,
default => 0,
optional => 1,
description => "List tasks beginning from this offset.",
},
limit => {
type => 'integer',
minimum => 0,
default => 50,
optional => 1,
description => "Only list this amount of tasks.",
},
userfilter => {
type => 'string',
optional => 1,
description => "Only list tasks from this user.",
},
typefilter => {
type => 'string',
optional => 1,
description => 'Only list tasks of this type (e.g., vzstart, vzdump).',
},
vmid => get_standard_option('pve-vmid', {
description => "Only list tasks for this VM.",
optional => 1,
}),
errors => {
type => 'boolean',
default => 0,
optional => 1,
description => 'Only list tasks with a status of ERROR.',
},
source => {
type => 'string',
enum => ['archive', 'active', 'all'],
default => 'archive',
optional => 1,
description => 'List archived, active or all tasks.',
},
since => {
type => 'integer',
description => "Only list tasks since this UNIX epoch.",
optional => 1,
},
until => {
type => 'integer',
description => "Only list tasks until this UNIX epoch.",
optional => 1,
},
statusfilter => {
type => 'string',
format => 'pve-task-status-type-list',
optional => 1,
description => 'List of Task States that should be returned.',
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
upid => { type => 'string', title => 'UPID', },
node => { type => 'string', title => 'Node', },
pid => { type => 'integer', title => 'PID', },
pstart => { type => 'integer', },
starttime => { type => 'integer', title => 'Starttime', },
type => { type => 'string', title => 'Type', },
id => { type => 'string', title => 'ID', },
user => { type => 'string', title => 'User', },
endtime => { type => 'integer', optional => 1, title => 'Endtime', },
status => { type => 'string', optional => 1, title => 'Status', },
},
},
links => [ { rel => 'child', href => "{upid}" } ],
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $res = [];
my $filename = "/var/log/pve/tasks/index";
my $node = $param->{node};
my $start = $param->{start} // 0;
my $limit = $param->{limit} // 50;
my $userfilter = $param->{userfilter};
my $typefilter = $param->{typefilter};
my $errors = $param->{errors} // 0;
my $source = $param->{source} // 'archive';
my $since = $param->{since};
my $until = $param->{until};
my $statusfilter = {
ok => 1,
warning => 1,
error => 1,
unknown => 1,
};
if (defined($param->{statusfilter}) && !$errors) {
$statusfilter = {
ok => 0,
warning => 0,
error => 0,
unknown => 0,
};
for my $filter (PVE::Tools::split_list($param->{statusfilter})) {
$statusfilter->{lc($filter)} = 1 ;
}
} elsif ($errors) {
$statusfilter->{ok} = 0;
}
my $count = 0;
my $line;
my $auditor = $rpcenv->check($user, "/nodes/$node", [ 'Sys.Audit' ], 1);
my $filter_task = sub {
my $task = shift;
return 1 if $userfilter && $task->{user} !~ m/\Q$userfilter\E/i;
return 1 if !($auditor || $check_task_user->($task, $user));
return 1 if $typefilter && $task->{type} ne $typefilter;
return 1 if $param->{vmid} && (!$task->{id} || $task->{id} ne $param->{vmid});
return 1 if defined($since) && $task->{starttime} < $since;
return 1 if defined($until) && $task->{starttime} > $until;
my $type = PVE::Tools::upid_normalize_status_type($task->{status});
return 1 if !$statusfilter->{$type};
return 1 if $count++ < $start;
return 1 if $limit <= 0;
return 0;
};
my $parse_line = sub {
if ($line =~ m/^(\S+)(\s([0-9A-Za-z]{8})(\s(\S.*))?)?$/) {
my $upid = $1;
my $endtime = $3;
my $status = $5;
if ((my $task = PVE::Tools::upid_decode($upid, 1))) {
$task->{upid} = $upid;
$task->{endtime} = hex($endtime) if $endtime;
$task->{status} = $status if $status;
$convert_token_task->($task);
if (!$filter_task->($task)) {
push @$res, $task;
$limit--;
}
}
}
};
if ($source eq 'active' || $source eq 'all') {
my $recent_tasks = PVE::INotify::read_file('active');
for my $task (@$recent_tasks) {
next if $task->{saved}; # archived task, already in index(.1)
if (!$filter_task->($task)) {
$task->{status} = 'RUNNING' if !$task->{status}; # otherwise it would be archived
push @$res, $task;
$limit--;
}
}
}
if ($source ne 'active') {
if (my $bw = File::ReadBackwards->new($filename)) {
while (defined ($line = $bw->readline)) {
&$parse_line();
}
$bw->close();
}
if (my $bw = File::ReadBackwards->new("$filename.1")) {
while (defined ($line = $bw->readline)) {
&$parse_line();
}
$bw->close();
}
}
$rpcenv->set_result_attrib('total', $count);
return $res;
}});
__PACKAGE__->register_method({
name => 'upid_index',
path => '{upid}',
method => 'GET',
description => '', # index helper
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => { type => 'string' },
}
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
},
code => sub {
my ($param) = @_;
return [
{ name => 'log' },
{ name => 'status' }
];
}});
__PACKAGE__->register_method({
name => 'stop_task',
path => '{upid}',
method => 'DELETE',
description => 'Stop a task.',
permissions => {
description => "The user needs 'Sys.Modify' permissions on '/nodes/<node>' if they aren't the owner of the task.",
user => 'all',
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => { type => 'string' },
}
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
raise_param_exc({ upid => "no such task" }) if ! -f $filename;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $node = $param->{node};
$convert_token_task->($task);
if (!$check_task_user->($task, $user)) {
$rpcenv->check($user, "/nodes/$node", [ 'Sys.Modify' ]);
}
PVE::RPCEnvironment->check_worker($param->{upid}, 1);
return undef;
}});
__PACKAGE__->register_method({
name => 'read_task_log',
path => '{upid}/log',
method => 'GET',
permissions => {
description => "The user needs 'Sys.Audit' permissions on '/nodes/<node>' if they aren't the owner of the task.",
user => 'all',
},
protected => 1,
download => 1,
description => "Read task log.",
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => {
type => 'string',
description => "The task's unique ID.",
},
start => {
type => 'integer',
minimum => 0,
default => 0,
optional => 1,
description => "Start at this line when reading the tasklog",
},
limit => {
type => 'integer',
minimum => 0,
default => 50,
optional => 1,
description => "The amount of lines to read from the tasklog.",
},
download => {
type => 'boolean',
optional => 1,
description => "Whether the tasklog file should be downloaded. This parameter can't be used in conjunction with other parameters",
}
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
n => {
description=> "Line number",
type=> 'integer',
},
t => {
description=> "Line text",
type => 'string',
}
}
}
},
code => sub {
my ($param) = @_;
my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $node = $param->{node};
$convert_token_task->($task);
if (!$check_task_user->($task, $user)) {
$rpcenv->check($user, "/nodes/$node", [ 'Sys.Audit' ]);
}
if ($param->{download}) {
if (defined($param->{start}) || defined($param->{limit})) {
die "'download' cannot be used together with 'start' or 'limit' parameters\n";
}
# 1024 is a practical cutoff for the size distribution of our log files.
my $use_compression = ( -s $filename ) > 1024;
my $fh;
if ($use_compression) {
open($fh, "-|", "/usr/bin/gzip", "-c", "$filename")
or die "Could not create compressed file stream for file '$filename' - $!\n";
} else {
open($fh, '<', $filename) or die "Could not open file '$filename' - $!\n";
}
my $task_time = strftime('%FT%TZ', gmtime($task->{starttime}));
my $download_name = 'task-'.$task->{node}.'-'.$task->{type}.'-'.$task_time.'.log';
return {
download => {
fh => $fh,
stream => 1,
'content-encoding' => $use_compression ? 'gzip' : undef,
'content-type' => "text/plain",
'content-disposition' => "attachment; filename=\"".$download_name."\"",
},
},
} else {
my $start = $param->{start} // 0;
my $limit = $param->{limit} // 50;
my ($count, $lines) = PVE::Tools::dump_logfile($filename, $start, $limit);
$rpcenv->set_result_attrib('total', $count);
return $lines;
}
}});
my $exit_status_cache = {};
__PACKAGE__->register_method({
name => 'read_task_status',
path => '{upid}/status',
method => 'GET',
permissions => {
description => "The user needs 'Sys.Audit' permissions on '/nodes/<node>' if they are not the owner of the task.",
user => 'all',
},
protected => 1,
description => "Read task status.",
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => {
type => 'string',
description => "The task's unique ID.",
},
},
},
returns => {
type => "object",
properties => {
pid => {
type => 'integer'
},
status => {
type => 'string', enum => ['running', 'stopped'],
},
type => {
type => 'string',
},
id => {
type => 'string',
},
user => {
type => 'string',
},
exitstatus => {
type => 'string',
optional => 1,
},
upid => {
type => 'string',
},
starttime => {
type => 'number',
},
node => {
type => 'string',
},
},
},
code => sub {
my ($param) = @_;
my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
raise_param_exc({ upid => "no such task" }) if ! -f $filename;
my $lines = [];
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $node = $param->{node};
$convert_token_task->($task);
if (!$check_task_user->($task, $user)) {
$rpcenv->check($user, "/nodes/$node", [ 'Sys.Audit' ]);
}
my $pstart = PVE::ProcFSTools::read_proc_starttime($task->{pid});
$task->{status} = ($pstart && ($pstart == $task->{pstart})) ?
'running' : 'stopped';
$task->{upid} = $param->{upid}; # include upid
if ($task->{status} eq 'stopped') {
if (!defined($exit_status_cache->{$task->{upid}})) {
$exit_status_cache->{$task->{upid}} =
PVE::Tools::upid_read_status($task->{upid});
}
$task->{exitstatus} = $exit_status_cache->{$task->{upid}};
}
return $task;
}});
1;