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