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 => '',
+ name => 'node_tasks',
+ path => '',
method => 'GET',
- permissions => { user => 'all' },
+ 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 => {
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.',
},
},
},
items => {
type => "object",
properties => {
- upid => { type => 'string' },
+ 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}" } ],
my $filename = "/var/log/pve/tasks/index";
my $node = $param->{node};
- my $start = $param->{start} || 0;
- my $limit = $param->{limit} || 50;
+ my $start = $param->{start} // 0;
+ my $limit = $param->{limit} // 50;
my $userfilter = $param->{userfilter};
- my $errors = $param->{errors};
+ 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' ]);
+ 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 $endtime = $3;
my $status = $5;
if ((my $task = PVE::Tools::upid_decode($upid, 1))) {
- return if $userfilter && $task->{user} !~ m/\Q$userfilter\E/i;
- next if !($auditor || $user eq $task->{user});
-
- return if $errors && $status && $status eq 'OK';
-
- return if $count++ < $start;
- return if $limit <= 0;
$task->{upid} = $upid;
$task->{endtime} = hex($endtime) if $endtime;
$task->{status} = $status if $status;
- push @$res, $task;
- $limit--;
+
+ $convert_token_task->($task);
+ if (!$filter_task->($task)) {
+ push @$res, $task;
+ $limit--;
+ }
}
}
};
- if (my $bw = File::ReadBackwards->new($filename)) {
- while (defined ($line = $bw->readline)) {
- &$parse_line();
+ 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--;
+ }
}
- $bw->close();
}
- if (my $bw = File::ReadBackwards->new("$filename.1")) {
- while (defined ($line = $bw->readline)) {
- &$parse_line();
+
+ 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();
}
- $bw->close();
}
- $rpcenv->set_result_count($count);
+ $rpcenv->set_result_attrib('total', $count);
return $res;
}});
__PACKAGE__->register_method({
- name => 'upid_index',
- path => '{upid}',
+ name => 'upid_index',
+ path => '{upid}',
method => 'GET',
description => '', # index helper
permissions => { user => 'all' },
}});
__PACKAGE__->register_method({
- name => 'stop_task',
- path => '{upid}',
+ name => 'stop_task',
+ path => '{upid}',
method => 'DELETE',
description => 'Stop a task.',
- permissions => { user => 'all' },
+ 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 => {
my $user = $rpcenv->get_user();
my $node = $param->{node};
- my $sysadmin = $rpcenv->check($user, "/nodes/$node", [ 'Sys.Console' ]);
- die "Permission check failed\n"
- if !($sysadmin || $user eq $task->{user});
+ $convert_token_task->($task);
+
+ if (!$check_task_user->($task, $user)) {
+ $rpcenv->check($user, "/nodes/$node", [ 'Sys.Modify' ]);
+ }
- PVE::RPCEnvironment::check_worker($param->{upid}, 1);
+ PVE::RPCEnvironment->check_worker($param->{upid}, 1);
return undef;
}});
__PACKAGE__->register_method({
- name => 'read_task_log',
- path => '{upid}/log',
+ name => 'read_task_log',
+ path => '{upid}/log',
method => 'GET',
- permissions => { user => 'all' },
+ permissions => {
+ description => "The user needs 'Sys.Audit' permissions on '/nodes/<node>' if they aren't the owner of the task.",
+ user => 'all',
+ },
protected => 1,
description => "Read task log.",
proxyto => 'node',
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
- upid => { type => 'string' },
+ 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 => {
+ items => {
type => "object",
properties => {
n => {
my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
- my $lines = [];
-
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $node = $param->{node};
- my $auditor = $rpcenv->check($user, "/nodes/$node", [ 'Sys.Audit' ]);
- die "Permission check failed\n"
- if !($auditor || $user eq $task->{user});
-
- my $fh = IO::File->new($filename, "r");
- raise_param_exc({ upid => "no such task - unable to open file - $!" }) if !$fh;
+ $convert_token_task->($task);
- my $start = $param->{start} || 0;
- my $limit = $param->{limit} || 50;
- my $count = 0;
- my $line;
- while (defined ($line = <$fh>)) {
- next if $count++ < $start;
- next if $limit <= 0;
- chomp $line;
- push @$lines, { n => $count, t => $line};
- $limit--;
+ if (!$check_task_user->($task, $user)) {
+ $rpcenv->check($user, "/nodes/$node", [ 'Sys.Audit' ]);
}
- close($fh);
+ 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';
- # HACK: ExtJS store.guaranteeRange() does not like empty array
- # so we add a line
- if (!$count) {
- $count++;
- push @$lines, { n => $count, t => "no content"};
- }
+ 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);
- $rpcenv->set_result_count($count);
-
- return $lines;
+ return $lines;
+ }
}});
+
+my $exit_status_cache = {};
+
__PACKAGE__->register_method({
- name => 'read_task_status',
- path => '{upid}/status',
+ name => 'read_task_status',
+ path => '{upid}/status',
method => 'GET',
- permissions => { user => 'all' },
+ 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',
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
- upid => { type => 'string' },
+ upid => {
+ type => 'string',
+ description => "The task's unique ID.",
+ },
},
},
returns => {
type => "object",
properties => {
- pid => {
+ pid => {
type => 'integer'
},
- status => {
- type => 'string', enum => ['running', 'stopped'],
+ 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',
},
},
},
my $user = $rpcenv->get_user();
my $node = $param->{node};
- my $auditor = $rpcenv->check($user, "/nodes/$node", [ 'Sys.Audit' ]);
- die "Permission check failed\n"
- if !($auditor || $user eq $task->{user});
+ $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})) ?
$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;