]> git.proxmox.com Git - pve-common.git/blame - src/PVE/RESTEnvironment.pm
fix #4299: network : disable_ipv6: fix path checking
[pve-common.git] / src / PVE / RESTEnvironment.pm
CommitLineData
d9072797
DM
1package PVE::RESTEnvironment;
2
3# NOTE: you can/should provide your own specialice class, and
4# use this a bas class (as example see PVE::RPCEnvironment).
5
6# we use this singleton class to pass RPC related environment values
7
8use strict;
9use warnings;
2531c455 10
cc78c1eb 11use Exporter qw(import);
2531c455 12use Fcntl qw(:flock);
d9072797 13use IO::File;
2531c455 14use IO::Handle;
d9072797 15use IO::Select;
2531c455
TL
16use POSIX qw(:sys_wait_h EINTR);
17
d9072797 18use PVE::Exception qw(raise raise_perm_exc);
d9072797
DM
19use PVE::INotify;
20use PVE::ProcFSTools;
2531c455
TL
21use PVE::SafeSyslog;
22use PVE::Tools;
d9072797 23
cc78c1eb
FE
24our @EXPORT_OK = qw(log_warn);
25
d9072797
DM
26my $rest_env;
27
28# save $SIG{CHLD} handler implementation.
29# simply set $SIG{CHLD} = $worker_reaper;
30# and register forked processes with &$register_worker(pid)
31# Note: using $SIG{CHLD} = 'IGNORE' or $SIG{CHLD} = sub { wait (); } or ...
32# has serious side effects, because perls built in system() and open()
fb3a1b29 33# functions can't get the correct exit status of a child. So we can't use
d9072797
DM
34# that (also see perlipc)
35
36my $WORKER_PIDS;
37my $WORKER_FLAG = 0;
38
39my $log_task_result = sub {
40 my ($upid, $user, $status) = @_;
41
42 return if !$rest_env;
43
44 my $msg = 'successful';
45 my $pri = 'info';
46 if ($status != 0) {
47 my $ec = $status >> 8;
48 my $ic = $status & 255;
49 $msg = $ec ? "failed ($ec)" : "interrupted ($ic)";
50 $pri = 'err';
51 }
52
53 my $tlist = $rest_env->active_workers($upid);
9a42d8a2
TL
54 eval { $rest_env->broadcast_tasklist($tlist); };
55 syslog('err', $@) if $@;
d9072797
DM
56
57 my $task;
58 foreach my $t (@$tlist) {
59 if ($t->{upid} eq $upid) {
60 $task = $t;
61 last;
62 }
63 }
64 if ($task && $task->{status}) {
65 $msg = $task->{status};
66 }
67
ad7962b4 68 $rest_env->log_cluster_msg($pri, $user, "end task $upid $msg");
d9072797
DM
69};
70
71my $worker_reaper = sub {
72 local $!; local $?;
73 foreach my $pid (keys %$WORKER_PIDS) {
74 my $waitpid = waitpid ($pid, WNOHANG);
75 if (defined($waitpid) && ($waitpid == $pid)) {
76 my $info = $WORKER_PIDS->{$pid};
77 if ($info && $info->{upid} && $info->{user}) {
78 &$log_task_result($info->{upid}, $info->{user}, $?);
79 }
80 delete ($WORKER_PIDS->{$pid});
81 }
82 }
83};
84
85my $register_worker = sub {
86 my ($pid, $user, $upid) = @_;
87
88 return if !$pid;
89
90 # do not register if already finished
91 my $waitpid = waitpid ($pid, WNOHANG);
92 if (defined($waitpid) && ($waitpid == $pid)) {
93 delete ($WORKER_PIDS->{$pid});
94 return;
95 }
96
97 $WORKER_PIDS->{$pid} = {
98 user => $user,
99 upid => $upid,
100 };
101};
102
103# initialize environment - must be called once at program startup
104sub init {
105 my ($class, $type, %params) = @_;
106
107 $class = ref($class) || $class;
108
109 die "already initialized" if $rest_env;
110
111 die "unknown environment type"
112 if !$type || $type !~ m/^(cli|pub|priv|ha)$/;
113
114 $SIG{CHLD} = $worker_reaper;
115
116 # environment types
117 # cli ... command started fron command line
096b5f5c 118 # pub ... access from public server (pveproxy)
d9072797 119 # priv ... access from private server (pvedaemon)
096b5f5c 120 # ha ... access from HA resource manager agent (pve-ha-manager)
d9072797 121
ff79ee65
FE
122 my $self = {
123 type => $type,
124 warning_count => 0,
125 };
d9072797
DM
126
127 bless $self, $class;
128
129 foreach my $p (keys %params) {
130 if ($p eq 'atfork') {
131 $self->{$p} = $params{$p};
132 } else {
133 die "unknown option '$p'";
134 }
135 }
136
137 $rest_env = $self;
138
139 my ($sysname, $nodename) = POSIX::uname();
140
141 $nodename =~ s/\..*$//; # strip domain part, if any
142
143 $self->{nodename} = $nodename;
144
145 return $self;
146};
147
148# convenience function for command line tools
149sub setup_default_cli_env {
150 my ($class, $username) = @_;
151
152 $class = ref($class) || $class;
153
154 $username //= 'root@pam';
155
156 PVE::INotify::inotify_init();
157
158 my $rpcenv = $class->init('cli');
159 $rpcenv->init_request();
160 $rpcenv->set_language($ENV{LANG});
161 $rpcenv->set_user($username);
162
163 die "please run as root\n"
164 if ($username eq 'root@pam') && ($> != 0);
165}
166
167# get the singleton
168sub get {
169
170 die "REST environment not initialized" if !$rest_env;
171
172 return $rest_env;
173}
174
175sub set_client_ip {
176 my ($self, $ip) = @_;
177
178 $self->{client_ip} = $ip;
179}
180
181sub get_client_ip {
182 my ($self) = @_;
183
184 return $self->{client_ip};
185}
186
187sub set_result_attrib {
188 my ($self, $key, $value) = @_;
189
190 $self->{result_attributes}->{$key} = $value;
191}
192
193sub get_result_attrib {
194 my ($self, $key) = @_;
195
196 return $self->{result_attributes}->{$key};
197}
198
199sub set_language {
200 my ($self, $lang) = @_;
201
202 # fixme: initialize I18N
203
204 $self->{language} = $lang;
205}
206
207sub get_language {
208 my ($self) = @_;
209
210 return $self->{language};
211}
212
213sub set_user {
214 my ($self, $user) = @_;
215
216 $self->{user} = $user;
217}
218
219sub get_user {
8e6019b1 220 my ($self, $noerr) = @_;
d9072797 221
8e6019b1 222 return $self->{user} if defined($self->{user}) || $noerr;
d9072797 223
8e6019b1 224 die "user name not set\n";
d9072797
DM
225}
226
61aca93a
WB
227sub set_u2f_challenge {
228 my ($self, $challenge) = @_;
229
230 $self->{u2f_challenge} = $challenge;
231}
232
233sub get_u2f_challenge {
234 my ($self, $noerr) = @_;
235
236 return $self->{u2f_challenge} if defined($self->{u2f_challenge}) || $noerr;
237
238 die "no active u2f challenge\n";
239}
240
c7a7aa4d
WB
241sub set_request_host {
242 my ($self, $host) = @_;
243
244 $self->{request_host} = $host;
245}
246
247sub get_request_host {
248 my ($self, $noerr) = @_;
249
250 return $self->{request_host} if defined($self->{request_host}) || $noerr;
251
252 die "no hostname available in current environment\n";
253}
254
d9072797
DM
255sub is_worker {
256 my ($class) = @_;
257
258 return $WORKER_FLAG;
259}
260
f74da40e
TL
261# read/update list of active workers.
262#
263# we move all finished tasks to the archive index, but keep active, and most recent tasks in the
264# active file.
265# $nocheck ... consider $new_upid still running (avoid that we try to read the result to early).
266sub active_workers {
d9072797
DM
267 my ($self, $new_upid, $nocheck) = @_;
268
d9072797
DM
269 my $timeout = 10;
270
f74da40e 271 my $res = PVE::Tools::lock_file("/var/log/pve/tasks/.active.lock", $timeout, sub {
d9072797
DM
272 my $tasklist = PVE::INotify::read_file('active');
273
274 my @ta;
275 my $tlist = [];
276 my $thash = {}; # only list task once
277
278 my $check_task = sub {
279 my ($task, $running) = @_;
280
281 if ($running || PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart})) {
282 push @$tlist, $task;
283 } else {
284 delete $task->{pid};
285 push @ta, $task;
286 }
287 delete $task->{pstart};
288 };
289
290 foreach my $task (@$tasklist) {
291 my $upid = $task->{upid};
292 next if $thash->{$upid};
293 $thash->{$upid} = $task;
294 &$check_task($task);
295 }
296
f74da40e
TL
297 if ($new_upid && !$thash->{$new_upid}) {
298 my $task = PVE::Tools::upid_decode($new_upid);
d9072797
DM
299 $task->{upid} = $new_upid;
300 $thash->{$new_upid} = $task;
301 &$check_task($task, $nocheck);
302 }
303
304
8733bb80 305 @ta = sort { $b->{starttime} <=> $a->{starttime} } @ta;
d9072797
DM
306
307 my $save = defined($new_upid);
308
309 foreach my $task (@ta) {
310 next if $task->{endtime};
311 $task->{endtime} = time();
312 $task->{status} = PVE::Tools::upid_read_status($task->{upid});
313 $save = 1;
314 }
315
316 my $archive = '';
317 my @arlist = ();
318 foreach my $task (@ta) {
319 if (!$task->{saved}) {
320 $archive .= sprintf("%s %08X %s\n", $task->{upid}, $task->{endtime}, $task->{status});
321 $save = 1;
322 push @arlist, $task;
323 $task->{saved} = 1;
324 }
325 }
326
327 if ($archive) {
328 my $size = 0;
329 my $filename = "/var/log/pve/tasks/index";
330 eval {
331 my $fh = IO::File->new($filename, '>>', 0644) ||
332 die "unable to open file '$filename' - $!\n";
333 PVE::Tools::safe_print($filename, $fh, $archive);
334 $size = -s $fh;
335 close($fh) ||
336 die "unable to close file '$filename' - $!\n";
337 };
338 my $err = $@;
339 if ($err) {
340 syslog('err', $err);
341 foreach my $task (@arlist) { # mark as not saved
342 $task->{saved} = 0;
343 }
344 }
345 my $maxsize = 50000; # about 1000 entries
346 if ($size > $maxsize) {
347 rename($filename, "$filename.1");
348 }
349 }
350
f74da40e
TL
351 # we try to reduce the amount of data list all running tasks and task and a few others
352 my $MAX_FINISHED = 25;
353 my $max = $MAX_FINISHED - scalar(@$tlist);
d9072797
DM
354 foreach my $task (@ta) {
355 last if $max <= 0;
356 push @$tlist, $task;
357 $max--;
358 }
359
360 PVE::INotify::write_file('active', $tlist) if $save;
361
362 return $tlist;
f74da40e 363 });
d9072797
DM
364 die $@ if $@;
365
366 return $res;
367}
368
369my $kill_process_group = sub {
370 my ($pid, $pstart) = @_;
371
372 # send kill to process group (negative pid)
373 my $kpid = -$pid;
374
375 # always send signal to all pgrp members
376 kill(15, $kpid); # send TERM signal
377
378 # give max 5 seconds to shut down
379 for (my $i = 0; $i < 5; $i++) {
380 return if !PVE::ProcFSTools::check_process_running($pid, $pstart);
381 sleep (1);
382 }
383
384 # to be sure
385 kill(9, $kpid);
386};
387
388sub check_worker {
a313fe73 389 my ($self, $upid, $killit) = @_;
d9072797
DM
390
391 my $task = PVE::Tools::upid_decode($upid);
392
393 my $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
394
395 return 0 if !$running;
396
397 if ($killit) {
398 &$kill_process_group($task->{pid});
399 return 0;
400 }
401
402 return 1;
403}
404
3e2da216
TL
405# acts almost as tee: writes an output both to STDOUT and a task log,
406# we differ as we're worker aware and look also at the childs control pipe,
407# so we know if the function could be executed successfully or not.
408my $tee_worker = sub {
409 my ($childfd, $ctrlfd, $taskfh, $cpid) = @_;
410
411 eval {
412 my $int_count = 0;
413 local $SIG{INT} = local $SIG{QUIT} = local $SIG{TERM} = sub {
414 # always send signal to all pgrp members
415 my $kpid = -$cpid;
416 if ($int_count < 3) {
417 kill(15, $kpid); # send TERM signal
418 } else {
419 kill(9, $kpid); # send KILL signal
420 }
421 $int_count++;
422 };
423 local $SIG{PIPE} = sub { die "broken pipe\n"; };
424
425 my $select = new IO::Select;
426 my $fh = IO::Handle->new_from_fd($childfd, 'r');
427 $select->add($fh);
428
429 my $readbuf = '';
430 my $count;
431 while ($select->count) {
432 my @handles = $select->can_read(1);
433 if (scalar(@handles)) {
434 my $count = sysread ($handles[0], $readbuf, 4096);
435 if (!defined ($count)) {
436 my $err = $!;
437 die "sync pipe read error: $err\n";
438 }
439 last if $count == 0; # eof
440
441 print $readbuf;
442 select->flush();
443
444 print $taskfh $readbuf;
445 $taskfh->flush();
446 } else {
447 # some commands daemonize without closing stdout
448 last if !PVE::ProcFSTools::check_process_running($cpid);
449 }
450 }
451
3e2da216
TL
452 POSIX::read($ctrlfd, $readbuf, 4096);
453 if ($readbuf =~ m/^TASK OK\n?$/) {
454 # skip printing to stdout
455 print $taskfh $readbuf;
456 } elsif ($readbuf =~ m/^TASK ERROR: (.*)\n?$/) {
457 print STDERR "$1\n";
458 print $taskfh "\n$readbuf"; # ensure start on new line for webUI
ff79ee65
FE
459 } elsif ($readbuf =~ m/^TASK WARNINGS: (\d+)\n?$/) {
460 print STDERR "Task finished with $1 warning(s)!\n";
461 print $taskfh "\n$readbuf"; # ensure start on new line for webUI
3e2da216
TL
462 } else {
463 die "got unexpected control message: $readbuf\n";
464 }
465 $taskfh->flush();
466 };
467 my $err = $@;
468
469 POSIX::close($childfd);
470 POSIX::close($ctrlfd);
471
472 if ($err) {
473 $err =~ s/\n/ /mg;
474 print STDERR "$err\n";
475 print $taskfh "TASK ERROR: $err\n";
476 }
477};
478
d9072797
DM
479# start long running workers
480# STDIN is redirected to /dev/null
481# STDOUT,STDERR are redirected to the filename returned by upid_decode
482# NOTE: we simulate running in foreground if ($self->{type} eq 'cli')
483sub fork_worker {
484 my ($self, $dtype, $id, $user, $function, $background) = @_;
485
486 $dtype = 'unknown' if !defined ($dtype);
487 $id = '' if !defined ($id);
488
f44838ff
OB
489 # note: below is only used for the task log entry
490 $user = $self->get_user(1) // 'root@pam' if !defined($user);
d9072797
DM
491
492 my $sync = ($self->{type} eq 'cli' && !$background) ? 1 : 0;
493
494 local $SIG{INT} =
495 local $SIG{QUIT} =
496 local $SIG{PIPE} =
497 local $SIG{TERM} = 'IGNORE';
498
499 my $starttime = time ();
500
501 my @psync = POSIX::pipe();
502 my @csync = POSIX::pipe();
ed52a843 503 my @ctrlfd = POSIX::pipe() if $sync;
d9072797
DM
504
505 my $node = $self->{nodename};
506
507 my $cpid = fork();
508 die "unable to fork worker - $!" if !defined($cpid);
509
510 my $workerpuid = $cpid ? $cpid : $$;
511
512 my $pstart = PVE::ProcFSTools::read_proc_starttime($workerpuid) ||
513 die "unable to read process start time";
514
515 my $upid = PVE::Tools::upid_encode ({
516 node => $node, pid => $workerpuid, pstart => $pstart,
517 starttime => $starttime, type => $dtype, id => $id, user => $user });
518
519 my $outfh;
520
521 if (!$cpid) { # child
522
523 $0 = "task $upid";
524 $WORKER_FLAG = 1;
525
526 $SIG{INT} = $SIG{QUIT} = $SIG{TERM} = sub { die "received interrupt\n"; };
527
528 $SIG{CHLD} = $SIG{PIPE} = 'DEFAULT';
e97f807c 529 $SIG{TTOU} = 'IGNORE';
d9072797 530
6a09f096 531 my $ppgid;
aea06195 532 # set session/process group allows to kill the process group
e97f807c 533 if ($sync && -t STDIN) {
aea06195
TL
534 # some sync'ed workers operate on the tty but setsid sessions lose
535 # the tty, so just create a new pgroup and give it the tty
6a09f096 536 $ppgid = POSIX::getpgrp() or die "failed to get old pgid: $!\n";
4c99c559 537 POSIX::setpgid(0, 0) or die "failed to setpgid: $!\n";
e97f807c
SI
538 POSIX::tcsetpgrp(fileno(STDIN), $$) or die "failed to tcsetpgrp: $!\n";
539 } else {
540 POSIX::setsid();
541 }
d9072797
DM
542
543 POSIX::close ($psync[0]);
ed52a843 544 POSIX::close ($ctrlfd[0]) if $sync;
d9072797
DM
545 POSIX::close ($csync[1]);
546
547 $outfh = $sync ? $psync[1] : undef;
ed52a843 548 my $resfh = $sync ? $ctrlfd[1] : undef;
d9072797
DM
549
550 eval {
551 PVE::INotify::inotify_close();
552
553 if (my $atfork = $self->{atfork}) {
554 &$atfork();
555 }
556
fb3a1b29 557 # same algorithm as used inside SA
d9072797
DM
558 # STDIN = /dev/null
559 my $fd = fileno (STDIN);
560
561 if (!$sync) {
562 close STDIN;
563 POSIX::close(0) if $fd != 0;
564
565 die "unable to redirect STDIN - $!"
566 if !open(STDIN, "</dev/null");
567
568 $outfh = PVE::Tools::upid_open($upid);
4d9f4d62 569 $resfh = fileno($outfh);
d9072797
DM
570 }
571
572
573 # redirect STDOUT
574 $fd = fileno(STDOUT);
575 close STDOUT;
576 POSIX::close (1) if $fd != 1;
577
578 die "unable to redirect STDOUT - $!"
579 if !open(STDOUT, ">&", $outfh);
580
581 STDOUT->autoflush (1);
582
583 # redirect STDERR to STDOUT
584 $fd = fileno (STDERR);
585 close STDERR;
586 POSIX::close(2) if $fd != 2;
587
588 die "unable to redirect STDERR - $!"
589 if !open(STDERR, ">&1");
590
591 STDERR->autoflush(1);
592 };
593 if (my $err = $@) {
594 my $msg = "ERROR: $err";
595 POSIX::write($psync[1], $msg, length ($msg));
596 POSIX::close($psync[1]);
597 POSIX::_exit(1);
598 kill(-9, $$);
599 }
600
601 # sync with parent (signal that we are ready)
95109cc4
TL
602 POSIX::write($psync[1], $upid, length ($upid));
603 POSIX::close($psync[1]) if !$sync; # don't need output pipe if async
d9072797 604
2311859b
WB
605 eval {
606 my $readbuf = '';
607 # sync with parent (wait until parent is ready)
608 POSIX::read($csync[0], $readbuf, 4096);
609 die "parent setup error\n" if $readbuf ne 'OK';
d9072797 610
2311859b
WB
611 if ($self->{type} eq 'ha') {
612 print "task started by HA resource agent\n";
613 }
614 &$function($upid);
615 };
edbb302e 616 my ($msg, $exitcode);
d9072797
DM
617 my $err = $@;
618 if ($err) {
619 chomp $err;
620 $err =~ s/\n/ /mg;
621 syslog('err', $err);
edbb302e
SI
622 $msg = "TASK ERROR: $err\n";
623 $exitcode = -1;
ff79ee65
FE
624 } elsif (my $warnings = $self->{warning_count}) {
625 $msg = "TASK WARNINGS: $warnings\n";
626 $exitcode = 0;
d9072797 627 } else {
edbb302e
SI
628 $msg = "TASK OK\n";
629 $exitcode = 0;
d9072797 630 }
edbb302e 631 POSIX::write($resfh, $msg, length($msg));
6a09f096
SI
632
633 if ($sync) {
634 POSIX::close($resfh);
635 if ( -t STDIN) {
636 POSIX::tcsetpgrp(fileno(STDIN), $ppgid) or
637 die "failed to tcsetpgrp to parent: $!\n";
638 }
639 }
edbb302e 640 POSIX::_exit($exitcode);
a609b2f7 641 kill(-9, $$); # not really needed, just to be sure
d9072797
DM
642 }
643
644 # parent
645
646 POSIX::close ($psync[1]);
ed52a843 647 POSIX::close ($ctrlfd[1]) if $sync;
d9072797
DM
648 POSIX::close ($csync[0]);
649
650 my $readbuf = '';
651 # sync with child (wait until child starts)
652 POSIX::read($psync[0], $readbuf, 4096);
653
654 if (!$sync) {
655 POSIX::close($psync[0]);
656 &$register_worker($cpid, $user, $upid);
657 } else {
658 chomp $readbuf;
659 }
660
661 eval {
662 die "got no worker upid - start worker failed\n" if !$readbuf;
663
664 if ($readbuf =~ m/^ERROR:\s*(.+)$/m) {
665 die "starting worker failed: $1\n";
666 }
667
668 if ($readbuf ne $upid) {
669 die "got strange worker upid ('$readbuf' != '$upid') - start worker failed\n";
670 }
671
672 if ($sync) {
673 $outfh = PVE::Tools::upid_open($upid);
674 }
675 };
676 my $err = $@;
677
678 if (!$err) {
679 my $msg = 'OK';
680 POSIX::write($csync[1], $msg, length ($msg));
681 POSIX::close($csync[1]);
682
683 } else {
684 POSIX::close($csync[1]);
685 kill(-9, $cpid); # make sure it gets killed
686 die $err;
687 }
688
689 $self->log_cluster_msg('info', $user, "starting task $upid");
690
691 my $tlist = $self->active_workers($upid, $sync);
9a42d8a2
TL
692 eval { $self->broadcast_tasklist($tlist); };
693 syslog('err', $@) if $@;
d9072797
DM
694
695 my $res = 0;
696
697 if ($sync) {
ed52a843 698
3e2da216 699 $tee_worker->($psync[0], $ctrlfd[0], $outfh, $cpid);
d9072797
DM
700
701 &$kill_process_group($cpid, $pstart); # make sure it gets killed
702
703 close($outfh);
704
705 waitpid($cpid, 0);
706 $res = $?;
707 &$log_task_result($upid, $user, $res);
708 }
709
710 return wantarray ? ($upid, $res) : $upid;
711}
712
06885ac8
FE
713sub log_warn {
714 my ($message) = @_;
715
716 if ($rest_env) {
717 $rest_env->warn($message);
718 } else {
719 chomp($message);
720 print STDERR "WARN: $message\n";
721 }
722}
723
ff79ee65
FE
724sub warn {
725 my ($self, $message) = @_;
726
727 chomp($message);
728
729 print STDERR "WARN: $message\n";
730
731 $self->{warning_count}++;
732}
733
d9072797
DM
734# Abstract function
735
736sub log_cluster_msg {
737 my ($self, $pri, $user, $msg) = @_;
738
739 syslog($pri, "%s", $msg);
740
741 # PVE::Cluster::log_msg($pri, $user, $msg);
742}
743
744sub broadcast_tasklist {
745 my ($self, $tlist) = @_;
746
747 # PVE::Cluster::broadcast_tasklist($tlist);
748}
749
750sub check_api2_permissions {
751 my ($self, $perm, $username, $param) = @_;
752
753 return 1 if !$username && $perm->{user} eq 'world';
754
755 raise_perm_exc("user != null") if !$username;
756
757 return 1 if $username eq 'root@pam';
758
759 raise_perm_exc('user != root@pam') if !$perm;
760
761 return 1 if $perm->{user} && $perm->{user} eq 'all';
762
763 ##return $self->exec_api2_perm_check($perm->{check}, $username, $param)
764 ##if $perm->{check};
765
766 raise_perm_exc();
767}
768
769# init_request - should be called before each REST/CLI request
770sub init_request {
771 my ($self, %params) = @_;
772
dc9c3ffa
DM
773 $self->{result_attributes} = {}
774
775 # if you nedd more, implement in subclass
d9072797
DM
776}
777
7781;