]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/Tasks.pm
fix #4521: api/tasks: replace upid as filename for task log downloads
[pmg-api.git] / src / PMG / API2 / Tasks.pm
1 package PMG::API2::Tasks;
2
3 use strict;
4 use warnings;
5 use POSIX;
6 use IO::File;
7 use File::ReadBackwards;
8 use PVE::Tools;
9 use PVE::SafeSyslog;
10 use PVE::RESTHandler;
11 use PVE::ProcFSTools;
12 use PMG::RESTEnvironment;
13 use PVE::JSONSchema qw(get_standard_option);
14
15 use base qw(PVE::RESTHandler);
16
17 __PACKAGE__->register_method({
18 name => 'node_tasks',
19 path => '',
20 method => 'GET',
21 description => "Read task list for one node (finished tasks).",
22 proxyto => 'node',
23 permissions => { check => [ 'admin', 'audit' ] },
24 parameters => {
25 additionalProperties => 0,
26 properties => {
27 node => get_standard_option('pve-node'),
28 start => {
29 type => 'integer',
30 minimum => 0,
31 optional => 1,
32 },
33 limit => {
34 type => 'integer',
35 minimum => 0,
36 optional => 1,
37 },
38 userfilter => {
39 type => 'string',
40 optional => 1,
41 },
42 errors => {
43 type => 'boolean',
44 optional => 1,
45 },
46 typefilter => {
47 type => 'string',
48 optional => 1,
49 description => 'Only list tasks of this type (e.g., aptupdate, saupdate).',
50 },
51 since => {
52 type => 'integer',
53 description => "Only list tasks since this UNIX epoch.",
54 optional => 1,
55 },
56 until => {
57 type => 'integer',
58 description => "Only list tasks until this UNIX epoch.",
59 optional => 1,
60 },
61 statusfilter => {
62 type => 'string',
63 format => 'pve-task-status-type-list',
64 optional => 1,
65 description => 'List of Task States that should be returned.',
66 },
67 },
68 },
69 returns => {
70 type => 'array',
71 items => {
72 type => "object",
73 properties => {
74 upid => { type => 'string' },
75 },
76 },
77 links => [ { rel => 'child', href => "{upid}" } ],
78 },
79 code => sub {
80 my ($param) = @_;
81
82 my $restenv = PMG::RESTEnvironment->get();
83
84 my $res = [];
85
86 my $filename = "/var/log/pve/tasks/index";
87
88 my $node = $param->{node};
89 my $start = $param->{start} || 0;
90 my $limit = $param->{limit} || 50;
91 my $userfilter = $param->{userfilter};
92 my $typefilter = $param->{typefilter};
93 my $since = $param->{since};
94 my $until = $param->{until};
95 my $errors = $param->{errors};
96
97 my $statusfilter = {
98 ok => 1,
99 warning => 1,
100 error => 1,
101 unknown => 1,
102 };
103
104 if (defined($param->{statusfilter}) && !$errors) {
105 $statusfilter = {
106 ok => 0,
107 warning => 0,
108 error => 0,
109 unknown => 0,
110 };
111 for my $filter (PVE::Tools::split_list($param->{statusfilter})) {
112 $statusfilter->{lc($filter)} = 1 ;
113 }
114 } elsif ($errors) {
115 $statusfilter->{ok} = 0;
116 }
117
118 my $count = 0;
119 my $line;
120
121 my $parse_line = sub {
122 if ($line =~ m/^(\S+)(\s([0-9A-Za-z]{8})(\s(\S.*))?)?$/) {
123 my $upid = $1;
124 my $endtime = $3;
125 my $status = $5;
126 if ((my $task = PVE::Tools::upid_decode($upid, 1))) {
127 return if $userfilter && $task->{user} !~ m/\Q$userfilter\E/i;
128 return if defined($since) && $task->{starttime} < $since;
129 return if defined($until) && $task->{starttime} > $until;
130 return if $typefilter && $task->{type} ne $typefilter;
131
132 my $statustype = PVE::Tools::upid_normalize_status_type($status);
133 return if !$statusfilter->{$statustype};
134
135 return if $count++ < $start;
136 return if $limit <= 0;
137
138 $task->{upid} = $upid;
139 $task->{endtime} = hex($endtime) if $endtime;
140 $task->{status} = $status if $status;
141 push @$res, $task;
142 $limit--;
143 }
144 }
145 };
146
147 if (my $bw = File::ReadBackwards->new($filename)) {
148 while (defined ($line = $bw->readline)) {
149 &$parse_line();
150 }
151 $bw->close();
152 }
153 if (my $bw = File::ReadBackwards->new("$filename.1")) {
154 while (defined ($line = $bw->readline)) {
155 &$parse_line();
156 }
157 $bw->close();
158 }
159
160 $restenv->set_result_attrib('total', $count);
161
162 return $res;
163 }});
164
165 __PACKAGE__->register_method({
166 name => 'upid_index',
167 path => '{upid}',
168 method => 'GET',
169 description => '', # index helper
170 permissions => { check => [ 'admin', 'audit' ] },
171 parameters => {
172 additionalProperties => 0,
173 properties => {
174 node => get_standard_option('pve-node'),
175 upid => { type => 'string' },
176 }
177 },
178 returns => {
179 type => 'array',
180 items => {
181 type => "object",
182 properties => {},
183 },
184 links => [ { rel => 'child', href => "{name}" } ],
185 },
186 code => sub {
187 my ($param) = @_;
188
189 return [
190 { name => 'log' },
191 { name => 'status' }
192 ];
193 }});
194
195 __PACKAGE__->register_method({
196 name => 'stop_task',
197 path => '{upid}',
198 method => 'DELETE',
199 description => 'Stop a task.',
200 protected => 1,
201 proxyto => 'node',
202 permissions => { check => [ 'admin' ] },
203 parameters => {
204 additionalProperties => 0,
205 properties => {
206 node => get_standard_option('pve-node'),
207 upid => { type => 'string' },
208 }
209 },
210 returns => { type => 'null' },
211 code => sub {
212 my ($param) = @_;
213
214 my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
215 raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
216 raise_param_exc({ upid => "no such task" }) if ! -f $filename;
217
218 my $restenv = PMG::RESTEnvironment->get();
219 PMG::RESTEnvironment->check_worker($param->{upid}, 1);
220
221 return undef;
222 }});
223
224 __PACKAGE__->register_method({
225 name => 'read_task_log',
226 path => '{upid}/log',
227 method => 'GET',
228 protected => 1,
229 description => "Read task log.",
230 proxyto => 'node',
231 permissions => { check => [ 'admin', 'audit' ] },
232 parameters => {
233 additionalProperties => 0,
234 properties => {
235 node => get_standard_option('pve-node'),
236 upid => { type => 'string' },
237 start => {
238 type => 'integer',
239 minimum => 0,
240 optional => 1,
241 description => "Start at this line when reading the tasklog",
242 },
243 limit => {
244 type => 'integer',
245 minimum => 0,
246 optional => 1,
247 description => "The amount of lines to read from the tasklog.",
248 },
249 download => {
250 type => 'boolean',
251 optional => 1,
252 description => "Whether the tasklog file should be downloaded. This parameter can't be used in conjunction with other parameters",
253 }
254 },
255 },
256 returns => {
257 type => 'array',
258 items => {
259 type => "object",
260 properties => {
261 n => {
262 description=> "Line number",
263 type=> 'integer',
264 },
265 t => {
266 description=> "Line text",
267 type => 'string',
268 }
269 }
270 }
271 },
272 code => sub {
273 my ($param) = @_;
274
275 my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
276 raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
277
278 my $restenv = PMG::RESTEnvironment->get();
279
280 if ($param->{download}) {
281 die "Parameter 'download' can't be used with other parameters\n"
282 if (defined($param->{start}) || defined($param->{limit}));
283
284 my $fh;
285 my $use_compression = ( -s $filename ) > 1024;
286
287 # 1024 is a practical cutoff for the size distribution of our log files.
288 if ($use_compression) {
289 open($fh, "-|", "/usr/bin/gzip", "-c", "$filename")
290 or die "Could not create compressed file stream for file '$filename' - $!\n";
291 } else {
292 open($fh, '<', $filename) or die "Could not open file '$filename' - $!\n";
293 }
294
295 my $task_time = strftime('%FT%TZ', gmtime($task->{starttime}));
296 my $download_name = 'task-'.$task->{node}.'-'.$task->{type}.'-'.$task_time.'.log';
297
298 return {
299 download => {
300 fh => $fh,
301 stream => 1,
302 'content-encoding' => $use_compression ? 'gzip' : undef,
303 'content-type' => "text/plain",
304 'content-disposition' => "attachment; filename=\"".$download_name."\"",
305 },
306 },
307 } else {
308 my $start = $param->{start} // 0;
309 my $limit = $param->{limit} // 50;
310
311 my ($count, $lines) = PVE::Tools::dump_logfile($filename, $start, $limit);
312
313 $restenv->set_result_attrib('total', $count);
314
315 return $lines;
316 }
317 }});
318
319
320 my $exit_status_cache = {};
321
322 __PACKAGE__->register_method({
323 name => 'read_task_status',
324 path => '{upid}/status',
325 method => 'GET',
326 protected => 1,
327 description => "Read task status.",
328 proxyto => 'node',
329 permissions => { check => [ 'admin', 'audit' ] },
330 parameters => {
331 additionalProperties => 0,
332 properties => {
333 node => get_standard_option('pve-node'),
334 upid => { type => 'string' },
335 },
336 },
337 returns => {
338 type => "object",
339 properties => {
340 pid => {
341 type => 'integer'
342 },
343 status => {
344 type => 'string', enum => ['running', 'stopped'],
345 },
346 },
347 },
348 code => sub {
349 my ($param) = @_;
350
351 my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
352 raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
353 raise_param_exc({ upid => "no such task" }) if ! -f $filename;
354
355 my $lines = [];
356
357 my $pstart = PVE::ProcFSTools::read_proc_starttime($task->{pid});
358 $task->{status} = ($pstart && ($pstart == $task->{pstart})) ?
359 'running' : 'stopped';
360
361 $task->{upid} = $param->{upid}; # include upid
362
363 if ($task->{status} eq 'stopped') {
364 if (!defined($exit_status_cache->{$task->{upid}})) {
365 $exit_status_cache->{$task->{upid}} =
366 PVE::Tools::upid_read_status($task->{upid});
367 }
368 $task->{exitstatus} = $exit_status_cache->{$task->{upid}};
369 }
370
371 return $task;
372 }});
373
374 1;