]> git.proxmox.com Git - pve-cluster.git/blame - data/PVE/Cluster.pm
fixup coding style
[pve-cluster.git] / data / PVE / Cluster.pm
CommitLineData
fe000966
DM
1package PVE::Cluster;
2
3use strict;
7181f622 4use warnings;
7bac9ca5 5use POSIX qw(EEXIST ENOENT);
fe000966
DM
6use File::stat qw();
7use Socket;
8use Storable qw(dclone);
9use IO::File;
10use MIME::Base64;
440121dc 11use Digest::SHA;
fe000966 12use Digest::HMAC_SHA1;
26784563 13use Net::SSLeay;
8fdd87f9 14use PVE::Tools qw(run_command);
fe000966
DM
15use PVE::INotify;
16use PVE::IPCC;
17use PVE::SafeSyslog;
d0ad18e8 18use PVE::JSONSchema;
54d487bf 19use PVE::Network;
95c44445 20use PVE::Cluster::IPCConst;
fe000966
DM
21use JSON;
22use RRDs;
23use Encode;
ed8eb70d 24use UUID;
fe000966
DM
25use base 'Exporter';
26
27our @EXPORT_OK = qw(
28cfs_read_file
29cfs_write_file
30cfs_register_file
31cfs_lock_file);
32
33use Data::Dumper; # fixme: remove
34
35# x509 certificate utils
36
37my $basedir = "/etc/pve";
38my $authdir = "$basedir/priv";
39my $lockdir = "/etc/pve/priv/lock";
40
68491de3 41# cfs and corosync files
e02bdaae
TL
42my $dbfile = "/var/lib/pve-cluster/config.db";
43my $dbbackupdir = "/var/lib/pve-cluster/backup";
68491de3 44my $localclusterdir = "/etc/corosync";
e02bdaae 45my $localclusterconf = "$localclusterdir/corosync.conf";
68491de3
TL
46my $authfile = "$localclusterdir/authkey";
47my $clusterconf = "$basedir/corosync.conf";
48
fe000966
DM
49my $authprivkeyfn = "$authdir/authkey.key";
50my $authpubkeyfn = "$basedir/authkey.pub";
51my $pveca_key_fn = "$authdir/pve-root-ca.key";
52my $pveca_srl_fn = "$authdir/pve-root-ca.srl";
53my $pveca_cert_fn = "$basedir/pve-root-ca.pem";
54# this is just a secret accessable by the web browser
55# and is used for CSRF prevention
56my $pvewww_key_fn = "$basedir/pve-www.key";
57
58# ssh related files
59my $ssh_rsa_id_priv = "/root/.ssh/id_rsa";
60my $ssh_rsa_id = "/root/.ssh/id_rsa.pub";
61my $ssh_host_rsa_id = "/etc/ssh/ssh_host_rsa_key.pub";
62my $sshglobalknownhosts = "/etc/ssh/ssh_known_hosts";
63my $sshknownhosts = "/etc/pve/priv/known_hosts";
64my $sshauthkeys = "/etc/pve/priv/authorized_keys";
ac50b36d 65my $sshd_config_fn = "/etc/ssh/sshd_config";
fe000966 66my $rootsshauthkeys = "/root/.ssh/authorized_keys";
6056578e 67my $rootsshauthkeysbackup = "${rootsshauthkeys}.org";
f666cdde 68my $rootsshconfig = "/root/.ssh/config";
fe000966
DM
69
70my $observed = {
e1735a61 71 'vzdump.cron' => 1,
fe000966
DM
72 'storage.cfg' => 1,
73 'datacenter.cfg' => 1,
f6de131a 74 'replication.cfg' => 1,
cafc7309
DM
75 'corosync.conf' => 1,
76 'corosync.conf.new' => 1,
fe000966
DM
77 'user.cfg' => 1,
78 'domains.cfg' => 1,
79 'priv/shadow.cfg' => 1,
80 '/qemu-server/' => 1,
f71eee41 81 '/openvz/' => 1,
7f66b436 82 '/lxc/' => 1,
5a5417e6
DM
83 'ha/crm_commands' => 1,
84 'ha/manager_status' => 1,
85 'ha/resources.cfg' => 1,
86 'ha/groups.cfg' => 1,
e9af3eb7 87 'ha/fence.cfg' => 1,
9d4f69ff 88 'status.cfg' => 1,
fe000966
DM
89};
90
91# only write output if something fails
92sub run_silent_cmd {
93 my ($cmd) = @_;
94
95 my $outbuf = '';
96
97 my $record_output = sub {
98 $outbuf .= shift;
99 $outbuf .= "\n";
100 };
101
102 eval {
c53b111f 103 PVE::Tools::run_command($cmd, outfunc => $record_output,
fe000966
DM
104 errfunc => $record_output);
105 };
106
107 my $err = $@;
108
109 if ($err) {
110 print STDERR $outbuf;
111 die $err;
112 }
113}
114
115sub check_cfs_quorum {
01dddfb9
DM
116 my ($noerr) = @_;
117
fe000966
DM
118 # note: -w filename always return 1 for root, so wee need
119 # to use File::lstat here
120 my $st = File::stat::lstat("$basedir/local");
01dddfb9
DM
121 my $quorate = ($st && (($st->mode & 0200) != 0));
122
123 die "cluster not ready - no quorum?\n" if !$quorate && !$noerr;
124
125 return $quorate;
fe000966
DM
126}
127
128sub check_cfs_is_mounted {
129 my ($noerr) = @_;
130
131 my $res = -l "$basedir/local";
132
133 die "pve configuration filesystem not mounted\n"
134 if !$res && !$noerr;
135
136 return $res;
137}
138
139sub gen_local_dirs {
140 my ($nodename) = @_;
141
142 check_cfs_is_mounted();
143
144 my @required_dirs = (
145 "$basedir/priv",
c53b111f 146 "$basedir/nodes",
fe000966 147 "$basedir/nodes/$nodename",
7f66b436 148 "$basedir/nodes/$nodename/lxc",
a1c08cfa
DM
149 "$basedir/nodes/$nodename/qemu-server",
150 "$basedir/nodes/$nodename/openvz",
fe000966 151 "$basedir/nodes/$nodename/priv");
c53b111f 152
fe000966
DM
153 foreach my $dir (@required_dirs) {
154 if (! -d $dir) {
62613060 155 mkdir($dir) || $! == EEXIST || die "unable to create directory '$dir' - $!\n";
fe000966
DM
156 }
157 }
158}
159
160sub gen_auth_key {
161
162 return if -f "$authprivkeyfn";
163
164 check_cfs_is_mounted();
165
62613060 166 mkdir $authdir || $! == EEXIST || die "unable to create dir '$authdir' - $!\n";
fe000966 167
2d899b19 168 run_silent_cmd(['openssl', 'genrsa', '-out', $authprivkeyfn, '2048']);
fe000966 169
de4b4155 170 run_silent_cmd(['openssl', 'rsa', '-in', $authprivkeyfn, '-pubout', '-out', $authpubkeyfn]);
fe000966
DM
171}
172
173sub gen_pveca_key {
174
175 return if -f $pveca_key_fn;
176
177 eval {
147661a8 178 run_silent_cmd(['openssl', 'genrsa', '-out', $pveca_key_fn, '4096']);
fe000966
DM
179 };
180
181 die "unable to generate pve ca key:\n$@" if $@;
182}
183
184sub gen_pveca_cert {
185
186 if (-f $pveca_key_fn && -f $pveca_cert_fn) {
187 return 0;
188 }
189
190 gen_pveca_key();
191
192 # we try to generate an unique 'subject' to avoid browser problems
193 # (reused serial numbers, ..)
ed8eb70d
FG
194 my $uuid;
195 UUID::generate($uuid);
196 my $uuid_str;
197 UUID::unparse($uuid, $uuid_str);
fe000966
DM
198
199 eval {
f5566fc6
FG
200 # wrap openssl with faketime to prevent bug #904
201 run_silent_cmd(['faketime', 'yesterday', 'openssl', 'req', '-batch',
202 '-days', '3650', '-new', '-x509', '-nodes', '-key',
fe000966 203 $pveca_key_fn, '-out', $pveca_cert_fn, '-subj',
ed8eb70d 204 "/CN=Proxmox Virtual Environment/OU=$uuid_str/O=PVE Cluster Manager CA/"]);
fe000966
DM
205 };
206
207 die "generating pve root certificate failed:\n$@" if $@;
208
209 return 1;
210}
211
212sub gen_pve_ssl_key {
213 my ($nodename) = @_;
214
215 die "no node name specified" if !$nodename;
216
217 my $pvessl_key_fn = "$basedir/nodes/$nodename/pve-ssl.key";
218
219 return if -f $pvessl_key_fn;
220
221 eval {
222 run_silent_cmd(['openssl', 'genrsa', '-out', $pvessl_key_fn, '2048']);
223 };
224
225 die "unable to generate pve ssl key for node '$nodename':\n$@" if $@;
226}
227
228sub gen_pve_www_key {
229
230 return if -f $pvewww_key_fn;
231
232 eval {
233 run_silent_cmd(['openssl', 'genrsa', '-out', $pvewww_key_fn, '2048']);
234 };
235
236 die "unable to generate pve www key:\n$@" if $@;
237}
238
239sub update_serial {
240 my ($serial) = @_;
241
242 PVE::Tools::file_set_contents($pveca_srl_fn, $serial);
243}
244
245sub gen_pve_ssl_cert {
246 my ($force, $nodename, $ip) = @_;
247
248 die "no node name specified" if !$nodename;
249 die "no IP specified" if !$ip;
250
251 my $pvessl_cert_fn = "$basedir/nodes/$nodename/pve-ssl.pem";
252
253 return if !$force && -f $pvessl_cert_fn;
254
8acde170 255 my $names = "IP:127.0.0.1,IP:::1,DNS:localhost";
fe000966
DM
256
257 my $rc = PVE::INotify::read_file('resolvconf');
258
259 $names .= ",IP:$ip";
c53b111f 260
fe000966
DM
261 my $fqdn = $nodename;
262
263 $names .= ",DNS:$nodename";
264
265 if ($rc && $rc->{search}) {
266 $fqdn = $nodename . "." . $rc->{search};
267 $names .= ",DNS:$fqdn";
268 }
269
270 my $sslconf = <<__EOD;
271RANDFILE = /root/.rnd
272extensions = v3_req
273
274[ req ]
275default_bits = 2048
276distinguished_name = req_distinguished_name
277req_extensions = v3_req
278prompt = no
279string_mask = nombstr
280
281[ req_distinguished_name ]
282organizationalUnitName = PVE Cluster Node
283organizationName = Proxmox Virtual Environment
284commonName = $fqdn
285
286[ v3_req ]
287basicConstraints = CA:FALSE
e544d064 288extendedKeyUsage = serverAuth
fe000966
DM
289subjectAltName = $names
290__EOD
291
292 my $cfgfn = "/tmp/pvesslconf-$$.tmp";
293 my $fh = IO::File->new ($cfgfn, "w");
294 print $fh $sslconf;
295 close ($fh);
296
297 my $reqfn = "/tmp/pvecertreq-$$.tmp";
298 unlink $reqfn;
299
300 my $pvessl_key_fn = "$basedir/nodes/$nodename/pve-ssl.key";
301 eval {
302 run_silent_cmd(['openssl', 'req', '-batch', '-new', '-config', $cfgfn,
303 '-key', $pvessl_key_fn, '-out', $reqfn]);
304 };
305
306 if (my $err = $@) {
307 unlink $reqfn;
308 unlink $cfgfn;
309 die "unable to generate pve certificate request:\n$err";
310 }
311
312 update_serial("0000000000000000") if ! -f $pveca_srl_fn;
313
314 eval {
f5566fc6
FG
315 # wrap openssl with faketime to prevent bug #904
316 run_silent_cmd(['faketime', 'yesterday', 'openssl', 'x509', '-req',
317 '-in', $reqfn, '-days', '3650', '-out', $pvessl_cert_fn,
318 '-CAkey', $pveca_key_fn, '-CA', $pveca_cert_fn,
319 '-CAserial', $pveca_srl_fn, '-extfile', $cfgfn]);
fe000966
DM
320 };
321
322 if (my $err = $@) {
323 unlink $reqfn;
324 unlink $cfgfn;
325 die "unable to generate pve ssl certificate:\n$err";
326 }
327
328 unlink $cfgfn;
329 unlink $reqfn;
330}
331
332sub gen_pve_node_files {
333 my ($nodename, $ip, $opt_force) = @_;
334
335 gen_local_dirs($nodename);
336
337 gen_auth_key();
338
339 # make sure we have a (cluster wide) secret
340 # for CSRFR prevention
341 gen_pve_www_key();
342
343 # make sure we have a (per node) private key
344 gen_pve_ssl_key($nodename);
345
346 # make sure we have a CA
347 my $force = gen_pveca_cert();
348
349 $force = 1 if $opt_force;
350
351 gen_pve_ssl_cert($force, $nodename, $ip);
352}
353
bd0ae7ff
DM
354my $vzdump_cron_dummy = <<__EOD;
355# cluster wide vzdump cron schedule
356# Atomatically generated file - do not edit
357
358PATH="/usr/sbin:/usr/bin:/sbin:/bin"
359
360__EOD
361
362sub gen_pve_vzdump_symlink {
363
e1735a61 364 my $filename = "/etc/pve/vzdump.cron";
bd0ae7ff
DM
365
366 my $link_fn = "/etc/cron.d/vzdump";
367
368 if ((-f $filename) && (! -l $link_fn)) {
369 rename($link_fn, "/root/etc_cron_vzdump.org"); # make backup if file exists
370 symlink($filename, $link_fn);
371 }
372}
373
374sub gen_pve_vzdump_files {
375
e1735a61 376 my $filename = "/etc/pve/vzdump.cron";
bd0ae7ff
DM
377
378 PVE::Tools::file_set_contents($filename, $vzdump_cron_dummy)
379 if ! -f $filename;
380
381 gen_pve_vzdump_symlink();
382};
383
fe000966
DM
384my $versions = {};
385my $vmlist = {};
386my $clinfo = {};
387
388my $ipcc_send_rec = sub {
389 my ($msgid, $data) = @_;
390
391 my $res = PVE::IPCC::ipcc_send_rec($msgid, $data);
392
dd856b4d 393 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
fe000966
DM
394
395 return $res;
396};
397
398my $ipcc_send_rec_json = sub {
399 my ($msgid, $data) = @_;
400
401 my $res = PVE::IPCC::ipcc_send_rec($msgid, $data);
402
dd856b4d 403 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
fe000966
DM
404
405 return decode_json($res);
406};
407
408my $ipcc_get_config = sub {
409 my ($path) = @_;
410
411 my $bindata = pack "Z*", $path;
95c44445 412 my $res = PVE::IPCC::ipcc_send_rec(CFS_IPC_GET_CONFIG, $bindata);
2db32d95 413 if (!defined($res)) {
7bac9ca5
WB
414 if ($! != 0) {
415 return undef if $! == ENOENT;
416 die "$!\n";
417 }
2db32d95
DM
418 return '';
419 }
420
421 return $res;
fe000966
DM
422};
423
424my $ipcc_get_status = sub {
425 my ($name, $nodename) = @_;
426
427 my $bindata = pack "Z[256]Z[256]", $name, ($nodename || "");
95c44445 428 return PVE::IPCC::ipcc_send_rec(CFS_IPC_GET_STATUS, $bindata);
fe000966
DM
429};
430
431my $ipcc_update_status = sub {
432 my ($name, $data) = @_;
433
434 my $raw = ref($data) ? encode_json($data) : $data;
435 # update status
436 my $bindata = pack "Z[256]Z*", $name, $raw;
437
95c44445 438 return &$ipcc_send_rec(CFS_IPC_SET_STATUS, $bindata);
fe000966
DM
439};
440
441my $ipcc_log = sub {
442 my ($priority, $ident, $tag, $msg) = @_;
443
444 my $bindata = pack "CCCZ*Z*Z*", $priority, bytes::length($ident) + 1,
445 bytes::length($tag) + 1, $ident, $tag, $msg;
446
95c44445 447 return &$ipcc_send_rec(CFS_IPC_LOG_CLUSTER_MSG, $bindata);
fe000966
DM
448};
449
450my $ipcc_get_cluster_log = sub {
451 my ($user, $max) = @_;
452
453 $max = 0 if !defined($max);
454
455 my $bindata = pack "VVVVZ*", $max, 0, 0, 0, ($user || "");
95c44445 456 return &$ipcc_send_rec(CFS_IPC_GET_CLUSTER_LOG, $bindata);
fe000966
DM
457};
458
459my $ccache = {};
460
461sub cfs_update {
686801e4 462 my ($fail) = @_;
fe000966 463 eval {
95c44445 464 my $res = &$ipcc_send_rec_json(CFS_IPC_GET_FS_VERSION);
fe000966
DM
465 #warn "GOT1: " . Dumper($res);
466 die "no starttime\n" if !$res->{starttime};
467
468 if (!$res->{starttime} || !$versions->{starttime} ||
469 $res->{starttime} != $versions->{starttime}) {
470 #print "detected changed starttime\n";
471 $vmlist = {};
472 $clinfo = {};
473 $ccache = {};
474 }
475
476 $versions = $res;
477 };
478 my $err = $@;
479 if ($err) {
480 $versions = {};
481 $vmlist = {};
482 $clinfo = {};
483 $ccache = {};
686801e4 484 die $err if $fail;
fe000966
DM
485 warn $err;
486 }
487
488 eval {
489 if (!$clinfo->{version} || $clinfo->{version} != $versions->{clinfo}) {
490 #warn "detected new clinfo\n";
95c44445 491 $clinfo = &$ipcc_send_rec_json(CFS_IPC_GET_CLUSTER_INFO);
fe000966
DM
492 }
493 };
494 $err = $@;
495 if ($err) {
496 $clinfo = {};
686801e4 497 die $err if $fail;
fe000966
DM
498 warn $err;
499 }
500
501 eval {
502 if (!$vmlist->{version} || $vmlist->{version} != $versions->{vmlist}) {
503 #warn "detected new vmlist1\n";
95c44445 504 $vmlist = &$ipcc_send_rec_json(CFS_IPC_GET_GUEST_LIST);
fe000966
DM
505 }
506 };
507 $err = $@;
508 if ($err) {
509 $vmlist = {};
686801e4 510 die $err if $fail;
fe000966
DM
511 warn $err;
512 }
513}
514
515sub get_vmlist {
516 return $vmlist;
517}
518
519sub get_clinfo {
520 return $clinfo;
521}
522
9ddd4ae9
DM
523sub get_members {
524 return $clinfo->{nodelist};
525}
526
fe000966
DM
527sub get_nodelist {
528
529 my $nodelist = $clinfo->{nodelist};
530
531 my $result = [];
532
533 my $nodename = PVE::INotify::nodename();
534
535 if (!$nodelist || !$nodelist->{$nodename}) {
536 return [ $nodename ];
537 }
538
539 return [ keys %$nodelist ];
540}
541
1b36b6b1 542# $data must be a chronological descending ordered array of tasks
fe000966
DM
543sub broadcast_tasklist {
544 my ($data) = @_;
545
1b36b6b1
TL
546 # the serialized list may not get bigger than 32kb (CFS_MAX_STATUS_SIZE
547 # from pmxcfs) - drop older items until we satisfy this constraint
548 my $size = length(encode_json($data));
549 while ($size >= (32 * 1024)) {
550 pop @$data;
551 $size = length(encode_json($data));
552 }
553
fe000966
DM
554 eval {
555 &$ipcc_update_status("tasklist", $data);
556 };
557
558 warn $@ if $@;
559}
560
561my $tasklistcache = {};
562
563sub get_tasklist {
564 my ($nodename) = @_;
565
566 my $kvstore = $versions->{kvstore} || {};
567
568 my $nodelist = get_nodelist();
569
570 my $res = [];
571 foreach my $node (@$nodelist) {
572 next if $nodename && ($nodename ne $node);
573 eval {
574 my $ver = $kvstore->{$node}->{tasklist} if $kvstore->{$node};
575 my $cd = $tasklistcache->{$node};
c53b111f 576 if (!$cd || !$ver || !$cd->{version} ||
cebe16ec 577 ($cd->{version} != $ver)) {
fe000966
DM
578 my $raw = &$ipcc_get_status("tasklist", $node) || '[]';
579 my $data = decode_json($raw);
580 push @$res, @$data;
581 $cd = $tasklistcache->{$node} = {
582 data => $data,
583 version => $ver,
584 };
585 } elsif ($cd && $cd->{data}) {
586 push @$res, @{$cd->{data}};
587 }
588 };
589 my $err = $@;
590 syslog('err', $err) if $err;
591 }
592
593 return $res;
594}
595
596sub broadcast_rrd {
597 my ($rrdid, $data) = @_;
598
599 eval {
600 &$ipcc_update_status("rrd/$rrdid", $data);
601 };
602 my $err = $@;
603
604 warn $err if $err;
605}
606
607my $last_rrd_dump = 0;
608my $last_rrd_data = "";
609
610sub rrd_dump {
611
612 my $ctime = time();
613
614 my $diff = $ctime - $last_rrd_dump;
615 if ($diff < 2) {
616 return $last_rrd_data;
617 }
618
619 my $raw;
620 eval {
95c44445 621 $raw = &$ipcc_send_rec(CFS_IPC_GET_RRD_DUMP);
fe000966
DM
622 };
623 my $err = $@;
624
625 if ($err) {
626 warn $err;
627 return {};
628 }
629
630 my $res = {};
631
c3fabca7
DM
632 if ($raw) {
633 while ($raw =~ s/^(.*)\n//) {
634 my ($key, @ela) = split(/:/, $1);
635 next if !$key;
636 next if !(scalar(@ela) > 1);
d2aae33e 637 $res->{$key} = [ map { $_ eq 'U' ? undef : $_ } @ela ];
c3fabca7 638 }
fe000966
DM
639 }
640
641 $last_rrd_dump = $ctime;
642 $last_rrd_data = $res;
643
644 return $res;
645}
646
647sub create_rrd_data {
648 my ($rrdname, $timeframe, $cf) = @_;
649
650 my $rrddir = "/var/lib/rrdcached/db";
651
652 my $rrd = "$rrddir/$rrdname";
653
654 my $setup = {
655 hour => [ 60, 70 ],
656 day => [ 60*30, 70 ],
657 week => [ 60*180, 70 ],
658 month => [ 60*720, 70 ],
659 year => [ 60*10080, 70 ],
660 };
661
662 my ($reso, $count) = @{$setup->{$timeframe}};
663 my $ctime = $reso*int(time()/$reso);
664 my $req_start = $ctime - $reso*$count;
665
666 $cf = "AVERAGE" if !$cf;
667
668 my @args = (
669 "-s" => $req_start,
670 "-e" => $ctime - 1,
671 "-r" => $reso,
672 );
673
674 my $socket = "/var/run/rrdcached.sock";
675 push @args, "--daemon" => "unix:$socket" if -S $socket;
676
677 my ($start, $step, $names, $data) = RRDs::fetch($rrd, $cf, @args);
678
679 my $err = RRDs::error;
680 die "RRD error: $err\n" if $err;
c53b111f
DM
681
682 die "got wrong time resolution ($step != $reso)\n"
fe000966
DM
683 if $step != $reso;
684
685 my $res = [];
686 my $fields = scalar(@$names);
687 for my $line (@$data) {
688 my $entry = { 'time' => $start };
689 $start += $step;
fe000966
DM
690 for (my $i = 0; $i < $fields; $i++) {
691 my $name = $names->[$i];
692 if (defined(my $val = $line->[$i])) {
693 $entry->{$name} = $val;
694 } else {
fba7c78c
DC
695 # leave empty fields undefined
696 # maybe make this configurable?
fe000966
DM
697 }
698 }
fba7c78c 699 push @$res, $entry;
fe000966
DM
700 }
701
702 return $res;
703}
704
705sub create_rrd_graph {
706 my ($rrdname, $timeframe, $ds, $cf) = @_;
707
708 # Using RRD graph is clumsy - maybe it
709 # is better to simply fetch the data, and do all display
710 # related things with javascript (new extjs html5 graph library).
c53b111f 711
fe000966
DM
712 my $rrddir = "/var/lib/rrdcached/db";
713
714 my $rrd = "$rrddir/$rrdname";
715
31938ad4
DM
716 my @ids = PVE::Tools::split_list($ds);
717
718 my $ds_txt = join('_', @ids);
719
720 my $filename = "${rrd}_${ds_txt}.png";
fe000966
DM
721
722 my $setup = {
723 hour => [ 60, 60 ],
724 day => [ 60*30, 70 ],
725 week => [ 60*180, 70 ],
726 month => [ 60*720, 70 ],
727 year => [ 60*10080, 70 ],
728 };
729
730 my ($reso, $count) = @{$setup->{$timeframe}};
731
732 my @args = (
733 "--imgformat" => "PNG",
734 "--border" => 0,
735 "--height" => 200,
736 "--width" => 800,
737 "--start" => - $reso*$count,
738 "--end" => 'now' ,
8daa8f04 739 "--lower-limit" => 0,
fe000966
DM
740 );
741
742 my $socket = "/var/run/rrdcached.sock";
743 push @args, "--daemon" => "unix:$socket" if -S $socket;
744
fe000966
DM
745 my @coldef = ('#00ddff', '#ff0000');
746
747 $cf = "AVERAGE" if !$cf;
748
749 my $i = 0;
750 foreach my $id (@ids) {
751 my $col = $coldef[$i++] || die "fixme: no color definition";
752 push @args, "DEF:${id}=$rrd:${id}:$cf";
753 my $dataid = $id;
754 if ($id eq 'cpu' || $id eq 'iowait') {
755 push @args, "CDEF:${id}_per=${id},100,*";
756 $dataid = "${id}_per";
757 }
758 push @args, "LINE2:${dataid}${col}:${id}";
759 }
760
a665376e
DM
761 push @args, '--full-size-mode';
762
31938ad4 763 # we do not really store data into the file
f7baa598 764 my $res = RRDs::graphv('-', @args);
fe000966
DM
765
766 my $err = RRDs::error;
767 die "RRD error: $err\n" if $err;
768
31938ad4 769 return { filename => $filename, image => $res->{image} };
fe000966
DM
770}
771
772# a fast way to read files (avoid fuse overhead)
773sub get_config {
774 my ($path) = @_;
775
d3a92ba7 776 return &$ipcc_get_config($path);
fe000966
DM
777}
778
779sub get_cluster_log {
780 my ($user, $max) = @_;
781
782 return &$ipcc_get_cluster_log($user, $max);
783}
784
785my $file_info = {};
786
787sub cfs_register_file {
788 my ($filename, $parser, $writer) = @_;
789
790 $observed->{$filename} || die "unknown file '$filename'";
791
792 die "file '$filename' already registered" if $file_info->{$filename};
793
794 $file_info->{$filename} = {
795 parser => $parser,
796 writer => $writer,
797 };
798}
799
800my $ccache_read = sub {
801 my ($filename, $parser, $version) = @_;
802
803 $ccache->{$filename} = {} if !$ccache->{$filename};
804
805 my $ci = $ccache->{$filename};
806
d3a92ba7
DM
807 if (!$ci->{version} || !$version || $ci->{version} != $version) {
808 # we always call the parser, even when the file does not exists
809 # (in that case $data is undef)
fe000966 810 my $data = get_config($filename);
fe000966
DM
811 $ci->{data} = &$parser("/etc/pve/$filename", $data);
812 $ci->{version} = $version;
813 }
814
815 my $res = ref($ci->{data}) ? dclone($ci->{data}) : $ci->{data};
816
817 return $res;
818};
819
820sub cfs_file_version {
821 my ($filename) = @_;
822
823 my $version;
824 my $infotag;
6e73d5c2 825 if ($filename =~ m!^nodes/[^/]+/(openvz|lxc|qemu-server)/(\d+)\.conf$!) {
f71eee41 826 my ($type, $vmid) = ($1, $2);
fe000966
DM
827 if ($vmlist && $vmlist->{ids} && $vmlist->{ids}->{$vmid}) {
828 $version = $vmlist->{ids}->{$vmid}->{version};
829 }
f71eee41 830 $infotag = "/$type/";
fe000966
DM
831 } else {
832 $infotag = $filename;
833 $version = $versions->{$filename};
834 }
835
836 my $info = $file_info->{$infotag} ||
837 die "unknown file type '$filename'\n";
838
839 return wantarray ? ($version, $info) : $version;
840}
841
842sub cfs_read_file {
843 my ($filename) = @_;
844
c53b111f 845 my ($version, $info) = cfs_file_version($filename);
fe000966
DM
846 my $parser = $info->{parser};
847
848 return &$ccache_read($filename, $parser, $version);
849}
850
851sub cfs_write_file {
852 my ($filename, $data) = @_;
853
c53b111f 854 my ($version, $info) = cfs_file_version($filename);
fe000966
DM
855
856 my $writer = $info->{writer} || die "no writer defined";
857
858 my $fsname = "/etc/pve/$filename";
859
860 my $raw = &$writer($fsname, $data);
861
862 if (my $ci = $ccache->{$filename}) {
863 $ci->{version} = undef;
864 }
865
866 PVE::Tools::file_set_contents($fsname, $raw);
867}
868
869my $cfs_lock = sub {
870 my ($lockid, $timeout, $code, @param) = @_;
871
00ea8428
TL
872 my $prev_alarm = alarm(0); # suspend outer alarm early
873
fe000966 874 my $res;
bcdb1b3a 875 my $got_lock = 0;
fe000966
DM
876
877 # this timeout is for aquire the lock
878 $timeout = 10 if !$timeout;
879
880 my $filename = "$lockdir/$lockid";
881
fe000966
DM
882 eval {
883
884 mkdir $lockdir;
885
886 if (! -d $lockdir) {
6e13e20a 887 die "pve cluster filesystem not online.\n";
fe000966
DM
888 }
889
6e13e20a 890 my $timeout_err = sub { die "got lock request timeout\n"; };
bcdb1b3a 891 local $SIG{ALRM} = $timeout_err;
fe000966 892
bcdb1b3a
TL
893 while (1) {
894 alarm ($timeout);
895 $got_lock = mkdir($filename);
3fb23b5b 896 $timeout = alarm(0) - 1; # we'll sleep for 1s, see down below
bcdb1b3a
TL
897
898 last if $got_lock;
899
3fb23b5b 900 $timeout_err->() if $timeout <= 0;
fe000966 901
02cd5e00 902 print STDERR "trying to aquire cfs lock '$lockid' ...\n";
bcdb1b3a
TL
903 utime (0, 0, $filename); # cfs unlock request
904 sleep(1);
fe000966
DM
905 }
906
907 # fixed command timeout: cfs locks have a timeout of 120
908 # using 60 gives us another 60 seconds to abort the task
fe000966 909 local $SIG{ALRM} = sub { die "got lock timeout - aborting command\n"; };
00ea8428 910 alarm(60);
fe000966 911
9c206b2b
DM
912 cfs_update(); # make sure we read latest versions inside code()
913
fe000966
DM
914 $res = &$code(@param);
915
916 alarm(0);
917 };
918
919 my $err = $@;
920
6e13e20a 921 $err = "no quorum!\n" if !$got_lock && !check_cfs_quorum(1);
fe000966 922
96857ee4 923 rmdir $filename if $got_lock; # if we held the lock always unlock again
fe000966 924
00ea8428
TL
925 alarm($prev_alarm);
926
fe000966 927 if ($err) {
6e13e20a 928 $@ = "error with cfs lock '$lockid': $err";
fe000966
DM
929 return undef;
930 }
931
932 $@ = undef;
933
934 return $res;
935};
936
937sub cfs_lock_file {
938 my ($filename, $timeout, $code, @param) = @_;
939
940 my $info = $observed->{$filename} || die "unknown file '$filename'";
941
942 my $lockid = "file-$filename";
943 $lockid =~ s/[.\/]/_/g;
944
945 &$cfs_lock($lockid, $timeout, $code, @param);
946}
947
948sub cfs_lock_storage {
949 my ($storeid, $timeout, $code, @param) = @_;
950
951 my $lockid = "storage-$storeid";
952
953 &$cfs_lock($lockid, $timeout, $code, @param);
954}
955
78897707
TL
956sub cfs_lock_domain {
957 my ($domainname, $timeout, $code, @param) = @_;
958
959 my $lockid = "domain-$domainname";
960
961 &$cfs_lock($lockid, $timeout, $code, @param);
962}
963
4790f9f4
FG
964sub cfs_lock_acme {
965 my ($account, $timeout, $code, @param) = @_;
966
967 my $lockid = "acme-$account";
968
969 &$cfs_lock($lockid, $timeout, $code, @param);
970}
971
fe000966
DM
972my $log_levels = {
973 "emerg" => 0,
974 "alert" => 1,
975 "crit" => 2,
976 "critical" => 2,
977 "err" => 3,
978 "error" => 3,
979 "warn" => 4,
980 "warning" => 4,
981 "notice" => 5,
982 "info" => 6,
983 "debug" => 7,
984};
985
986sub log_msg {
987 my ($priority, $ident, $msg) = @_;
988
989 if (my $tmp = $log_levels->{$priority}) {
990 $priority = $tmp;
991 }
992
993 die "need numeric log priority" if $priority !~ /^\d+$/;
994
995 my $tag = PVE::SafeSyslog::tag();
996
997 $msg = "empty message" if !$msg;
998
999 $ident = "" if !$ident;
8f2d54ff 1000 $ident = encode("ascii", $ident,
fe000966
DM
1001 sub { sprintf "\\u%04x", shift });
1002
8f2d54ff 1003 my $ascii = encode("ascii", $msg, sub { sprintf "\\u%04x", shift });
fe000966
DM
1004
1005 if ($ident) {
1006 syslog($priority, "<%s> %s", $ident, $ascii);
1007 } else {
1008 syslog($priority, "%s", $ascii);
1009 }
1010
1011 eval { &$ipcc_log($priority, $ident, $tag, $ascii); };
1012
1013 syslog("err", "writing cluster log failed: $@") if $@;
1014}
1015
9d76a1bb
DM
1016sub check_vmid_unused {
1017 my ($vmid, $noerr) = @_;
c53b111f 1018
9d76a1bb
DM
1019 my $vmlist = get_vmlist();
1020
1021 my $d = $vmlist->{ids}->{$vmid};
1022 return 1 if !defined($d);
c53b111f 1023
9d76a1bb
DM
1024 return undef if $noerr;
1025
4f66b109 1026 my $vmtypestr = $d->{type} eq 'qemu' ? 'VM' : 'CT';
e75ccbee 1027 die "$vmtypestr $vmid already exists on node '$d->{node}'\n";
9d76a1bb
DM
1028}
1029
65ff467f
DM
1030sub check_node_exists {
1031 my ($nodename, $noerr) = @_;
1032
1033 my $nodelist = $clinfo->{nodelist};
1034 return 1 if $nodelist && $nodelist->{$nodename};
1035
1036 return undef if $noerr;
1037
1038 die "no such cluster node '$nodename'\n";
1039}
1040
fe000966
DM
1041# this is also used to get the IP of the local node
1042sub remote_node_ip {
1043 my ($nodename, $noerr) = @_;
1044
1045 my $nodelist = $clinfo->{nodelist};
1046 if ($nodelist && $nodelist->{$nodename}) {
1047 if (my $ip = $nodelist->{$nodename}->{ip}) {
fc31f517
WB
1048 return $ip if !wantarray;
1049 my $family = $nodelist->{$nodename}->{address_family};
1050 if (!$family) {
1051 $nodelist->{$nodename}->{address_family} =
1052 $family =
1053 PVE::Tools::get_host_address_family($ip);
1054 }
14830160 1055 return wantarray ? ($ip, $family) : $ip;
fe000966
DM
1056 }
1057 }
1058
1059 # fallback: try to get IP by other means
e064c9b0 1060 return PVE::Network::get_ip_from_hostname($nodename, $noerr);
fe000966
DM
1061}
1062
54d487bf
TL
1063sub get_local_migration_ip {
1064 my ($migration_network, $noerr) = @_;
1065
1066 my $cidr = $migration_network;
1067
1068 if (!defined($cidr)) {
1069 my $dc_conf = cfs_read_file('datacenter.cfg');
1070 $cidr = $dc_conf->{migration}->{network}
1071 if defined($dc_conf->{migration}->{network});
1072 }
1073
1074 if (defined($cidr)) {
1075 my $ips = PVE::Network::get_local_ip_from_cidr($cidr);
1076
cb8c3bc6
TL
1077 die "could not get migration ip: no IP address configured on local " .
1078 "node for network '$cidr'\n" if !$noerr && (scalar(@$ips) == 0);
54d487bf 1079
cb8c3bc6
TL
1080 die "could not get migration ip: multiple IP address configured for " .
1081 "network '$cidr'\n" if !$noerr && (scalar(@$ips) > 1);
54d487bf
TL
1082
1083 return @$ips[0];
1084 }
1085
1086 return undef;
1087};
1088
fe000966
DM
1089# ssh related utility functions
1090
1091sub ssh_merge_keys {
1092 # remove duplicate keys in $sshauthkeys
1093 # ssh-copy-id simply add keys, so the file can grow to large
1094
1095 my $data = '';
1096 if (-f $sshauthkeys) {
1097 $data = PVE::Tools::file_get_contents($sshauthkeys, 128*1024);
1098 chomp($data);
1099 }
1100
6056578e
DM
1101 my $found_backup;
1102 if (-f $rootsshauthkeysbackup) {
404343d7 1103 $data .= "\n";
6056578e
DM
1104 $data .= PVE::Tools::file_get_contents($rootsshauthkeysbackup, 128*1024);
1105 chomp($data);
1106 $found_backup = 1;
1107 }
1108
fe000966
DM
1109 # always add ourself
1110 if (-f $ssh_rsa_id) {
1111 my $pub = PVE::Tools::file_get_contents($ssh_rsa_id);
1112 chomp($pub);
1113 $data .= "\n$pub\n";
1114 }
1115
1116 my $newdata = "";
1117 my $vhash = {};
2055b0a9
DM
1118 my @lines = split(/\n/, $data);
1119 foreach my $line (@lines) {
7eb37d8d
SP
1120 if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
1121 next if $vhash->{$3}++;
fe000966 1122 }
2055b0a9 1123 $newdata .= "$line\n";
fe000966 1124 }
fe000966
DM
1125
1126 PVE::Tools::file_set_contents($sshauthkeys, $newdata, 0600);
6056578e
DM
1127
1128 if ($found_backup && -l $rootsshauthkeys) {
1129 # everything went well, so we can remove the backup
1130 unlink $rootsshauthkeysbackup;
1131 }
fe000966
DM
1132}
1133
ac50b36d 1134sub setup_sshd_config {
99fc0847 1135 my () = @_;
ac50b36d
DM
1136
1137 my $conf = PVE::Tools::file_get_contents($sshd_config_fn);
c53b111f 1138
ac50b36d
DM
1139 return if $conf =~ m/^PermitRootLogin\s+yes\s*$/m;
1140
1141 if ($conf !~ s/^#?PermitRootLogin.*$/PermitRootLogin yes/m) {
1142 chomp $conf;
1143 $conf .= "\nPermitRootLogin yes\n";
c53b111f 1144 }
ac50b36d
DM
1145
1146 PVE::Tools::file_set_contents($sshd_config_fn, $conf);
1147
99fc0847 1148 PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'sshd']);
ac50b36d
DM
1149}
1150
f666cdde
SP
1151sub setup_rootsshconfig {
1152
39df71df
DM
1153 # create ssh key if it does not exist
1154 if (! -f $ssh_rsa_id) {
1155 mkdir '/root/.ssh/';
1156 system ("echo|ssh-keygen -t rsa -N '' -b 2048 -f ${ssh_rsa_id_priv}");
1157 }
1158
f666cdde
SP
1159 # create ssh config if it does not exist
1160 if (! -f $rootsshconfig) {
9aabc24b
DM
1161 mkdir '/root/.ssh';
1162 if (my $fh = IO::File->new($rootsshconfig, O_CREAT|O_WRONLY|O_EXCL, 0640)) {
61fa3c34
FG
1163 # this is the default ciphers list from Debian's OpenSSH package (OpenSSH_7.4p1 Debian-10, OpenSSL 1.0.2k 26 Jan 2017)
1164 # changed order to put AES before Chacha20 (most hardware has AESNI)
4833829a 1165 print $fh "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm\@openssh.com,aes256-gcm\@openssh.com,chacha20-poly1305\@openssh.com\n";
f666cdde
SP
1166 close($fh);
1167 }
1168 }
1169}
1170
fe000966
DM
1171sub setup_ssh_keys {
1172
fe000966
DM
1173 mkdir $authdir;
1174
6056578e
DM
1175 my $import_ok;
1176
fe000966 1177 if (! -f $sshauthkeys) {
6056578e
DM
1178 my $old;
1179 if (-f $rootsshauthkeys) {
1180 $old = PVE::Tools::file_get_contents($rootsshauthkeys, 128*1024);
1181 }
fe000966 1182 if (my $fh = IO::File->new ($sshauthkeys, O_CREAT|O_WRONLY|O_EXCL, 0400)) {
6056578e 1183 PVE::Tools::safe_print($sshauthkeys, $fh, $old) if $old;
fe000966 1184 close($fh);
6056578e 1185 $import_ok = 1;
fe000966
DM
1186 }
1187 }
1188
c53b111f 1189 warn "can't create shared ssh key database '$sshauthkeys'\n"
fe000966
DM
1190 if ! -f $sshauthkeys;
1191
404343d7 1192 if (-f $rootsshauthkeys && ! -l $rootsshauthkeys) {
6056578e
DM
1193 if (!rename($rootsshauthkeys , $rootsshauthkeysbackup)) {
1194 warn "rename $rootsshauthkeys failed - $!\n";
1195 }
fe000966
DM
1196 }
1197
1198 if (! -l $rootsshauthkeys) {
1199 symlink $sshauthkeys, $rootsshauthkeys;
1200 }
fe000966 1201
6056578e
DM
1202 if (! -l $rootsshauthkeys) {
1203 warn "can't create symlink for ssh keys '$rootsshauthkeys' -> '$sshauthkeys'\n";
1204 } else {
1205 unlink $rootsshauthkeysbackup if $import_ok;
1206 }
fe000966
DM
1207}
1208
1209sub ssh_unmerge_known_hosts {
1210 return if ! -l $sshglobalknownhosts;
1211
1212 my $old = '';
1213 $old = PVE::Tools::file_get_contents($sshknownhosts, 128*1024)
1214 if -f $sshknownhosts;
1215
1216 PVE::Tools::file_set_contents($sshglobalknownhosts, $old);
1217}
1218
1219sub ssh_merge_known_hosts {
1220 my ($nodename, $ip_address, $createLink) = @_;
1221
1222 die "no node name specified" if !$nodename;
1223 die "no ip address specified" if !$ip_address;
c53b111f 1224
e4f92a20
TL
1225 # ssh lowercases hostnames (aliases) before comparision, so we need too
1226 $nodename = lc($nodename);
1227 $ip_address = lc($ip_address);
1228
fe000966
DM
1229 mkdir $authdir;
1230
1231 if (! -f $sshknownhosts) {
1232 if (my $fh = IO::File->new($sshknownhosts, O_CREAT|O_WRONLY|O_EXCL, 0600)) {
1233 close($fh);
1234 }
1235 }
1236
c53b111f
DM
1237 my $old = PVE::Tools::file_get_contents($sshknownhosts, 128*1024);
1238
fe000966 1239 my $new = '';
c53b111f 1240
fe000966
DM
1241 if ((! -l $sshglobalknownhosts) && (-f $sshglobalknownhosts)) {
1242 $new = PVE::Tools::file_get_contents($sshglobalknownhosts, 128*1024);
1243 }
1244
1245 my $hostkey = PVE::Tools::file_get_contents($ssh_host_rsa_id);
1d182ad3
DM
1246 # Note: file sometimes containe emty lines at start, so we use multiline match
1247 die "can't parse $ssh_host_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/m;
fe000966
DM
1248 $hostkey = $1;
1249
1250 my $data = '';
1251 my $vhash = {};
1252
1253 my $found_nodename;
1254 my $found_local_ip;
1255
1256 my $merge_line = sub {
1257 my ($line, $all) = @_;
1258
53922f63
TL
1259 return if $line =~ m/^\s*$/; # skip empty lines
1260 return if $line =~ m/^#/; # skip comments
1261
fe000966
DM
1262 if ($line =~ m/^(\S+)\s(ssh-rsa\s\S+)(\s.*)?$/) {
1263 my $key = $1;
1264 my $rsakey = $2;
1265 if (!$vhash->{$key}) {
1266 $vhash->{$key} = 1;
1267 if ($key =~ m/\|1\|([^\|\s]+)\|([^\|\s]+)$/) {
1268 my $salt = decode_base64($1);
1269 my $digest = $2;
1270 my $hmac = Digest::HMAC_SHA1->new($salt);
1271 $hmac->add($nodename);
1272 my $hd = $hmac->b64digest . '=';
1273 if ($digest eq $hd) {
1274 if ($rsakey eq $hostkey) {
1275 $found_nodename = 1;
1276 $data .= $line;
1277 }
1278 return;
1279 }
1280 $hmac = Digest::HMAC_SHA1->new($salt);
1281 $hmac->add($ip_address);
1282 $hd = $hmac->b64digest . '=';
1283 if ($digest eq $hd) {
1284 if ($rsakey eq $hostkey) {
1285 $found_local_ip = 1;
1286 $data .= $line;
1287 }
1288 return;
1289 }
e4f92a20
TL
1290 } else {
1291 $key = lc($key); # avoid duplicate entries, ssh compares lowercased
1292 if ($key eq $ip_address) {
fe129500 1293 $found_local_ip = 1 if $rsakey eq $hostkey;
e4f92a20 1294 } elsif ($key eq $nodename) {
fe129500 1295 $found_nodename = 1 if $rsakey eq $hostkey;
e4f92a20 1296 }
fe000966
DM
1297 }
1298 $data .= $line;
1299 }
1300 } elsif ($all) {
1301 $data .= $line;
1302 }
1303 };
1304
1305 while ($old && $old =~ s/^((.*?)(\n|$))//) {
1306 my $line = "$2\n";
fe000966
DM
1307 &$merge_line($line, 1);
1308 }
1309
1310 while ($new && $new =~ s/^((.*?)(\n|$))//) {
1311 my $line = "$2\n";
fe000966
DM
1312 &$merge_line($line);
1313 }
1314
53922f63
TL
1315 # add our own key if not already there
1316 $data .= "$nodename $hostkey\n" if !$found_nodename;
1317 $data .= "$ip_address $hostkey\n" if !$found_local_ip;
fe000966
DM
1318
1319 PVE::Tools::file_set_contents($sshknownhosts, $data);
1320
1321 return if !$createLink;
1322
1323 unlink $sshglobalknownhosts;
1324 symlink $sshknownhosts, $sshglobalknownhosts;
c53b111f
DM
1325
1326 warn "can't create symlink for ssh known hosts '$sshglobalknownhosts' -> '$sshknownhosts'\n"
fe000966
DM
1327 if ! -l $sshglobalknownhosts;
1328
1329}
1330
bba12ad7
TL
1331my $migration_format = {
1332 type => {
1333 default_key => 1,
1334 type => 'string',
1335 enum => ['secure', 'insecure'],
1336 description => "Migration traffic is encrypted using an SSH tunnel by " .
1337 "default. On secure, completely private networks this can be " .
1338 "disabled to increase performance.",
1339 default => 'secure',
bba12ad7
TL
1340 },
1341 network => {
1342 optional => 1,
1343 type => 'string', format => 'CIDR',
1344 format_description => 'CIDR',
1345 description => "CIDR of the (sub) network that is used for migration."
1346 },
1347};
1348
fe000966
DM
1349my $datacenter_schema = {
1350 type => "object",
1351 additionalProperties => 0,
1352 properties => {
1353 keyboard => {
1354 optional => 1,
1355 type => 'string',
1356 description => "Default keybord layout for vnc server.",
c59334cb 1357 enum => PVE::Tools::kvmkeymaplist(),
fe000966
DM
1358 },
1359 language => {
1360 optional => 1,
1361 type => 'string',
1362 description => "Default GUI language.",
1363 enum => [ 'en', 'de' ],
1364 },
1365 http_proxy => {
1366 optional => 1,
1367 type => 'string',
1368 description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
1369 pattern => "http://.*",
1370 },
a9323ef0
SP
1371 migration_unsecure => {
1372 optional => 1,
1373 type => 'boolean',
bba12ad7
TL
1374 description => "Migration is secure using SSH tunnel by default. " .
1375 "For secure private networks you can disable it to speed up " .
1376 "migration. Deprecated, use the 'migration' property instead!",
1377 },
1378 migration => {
1379 optional => 1,
1380 type => 'string', format => $migration_format,
1381 description => "For cluster wide migration settings.",
a9323ef0 1382 },
dce47328
DM
1383 console => {
1384 optional => 1,
1385 type => 'string',
a9d03528 1386 description => "Select the default Console viewer. You can either use the builtin java applet (VNC; deprecated and maps to html5), an external virt-viewer comtatible application (SPICE), an HTML5 based vnc viewer (noVNC), or an HTML5 based console client (xtermjs). If the selected viewer is not available (e.g. SPICE not activated for the VM), the fallback is noVNC.",
3e19544b 1387 enum => ['applet', 'vv', 'html5', 'xtermjs'],
dce47328 1388 },
8548bd87
SGE
1389 email_from => {
1390 optional => 1,
1391 type => 'string',
a05baf53 1392 format => 'email-opt',
8548bd87
SGE
1393 description => "Specify email address to send notification from (default is root@\$hostname)",
1394 },
66c2b1e9
TL
1395 max_workers => {
1396 optional => 1,
1397 type => 'integer',
1398 minimum => 1,
1399 description => "Defines how many workers (per node) are maximal started ".
1400 " on actions like 'stopall VMs' or task from the ha-manager.",
1401 },
8d762bd6
TL
1402 fencing => {
1403 optional => 1,
1404 type => 'string',
1405 default => 'watchdog',
1406 enum => [ 'watchdog', 'hardware', 'both' ],
1407 description => "Set the fencing mode of the HA cluster. Hardware mode " .
1408 "needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg." .
bd0c003a
FG
1409 " With both all two modes are used." .
1410 "\n\nWARNING: 'hardware' and 'both' are EXPERIMENTAL & WIP",
8d762bd6 1411 },
329da63d
WB
1412 mac_prefix => {
1413 optional => 1,
1414 type => 'string',
1415 pattern => qr/[a-f0-9]{2}(?::[a-f0-9]{2}){0,2}:?/i,
1416 description => 'Prefix for autogenerated MAC addresses.',
1417 },
bcbb9f1d 1418 bwlimit => PVE::JSONSchema::get_standard_option('bwlimit'),
fe000966
DM
1419 },
1420};
1421
1422# make schema accessible from outside (for documentation)
1423sub get_datacenter_schema { return $datacenter_schema };
1424
1425sub parse_datacenter_config {
1426 my ($filename, $raw) = @_;
1427
bba12ad7
TL
1428 my $res = PVE::JSONSchema::parse_config($datacenter_schema, $filename, $raw // '');
1429
1430 if (my $migration = $res->{migration}) {
1431 $res->{migration} = PVE::JSONSchema::parse_property_string($migration_format, $migration);
1432 }
1433
1434 # for backwards compatibility only, new migration property has precedence
1435 if (defined($res->{migration_unsecure})) {
1436 if (defined($res->{migration}->{type})) {
1437 warn "deprecated setting 'migration_unsecure' and new 'migration: type' " .
1438 "set at same time! Ignore 'migration_unsecure'\n";
1439 } else {
1440 $res->{migration}->{type} = ($res->{migration_unsecure}) ? 'insecure' : 'secure';
1441 }
1442 }
1443
0949afa7
DC
1444 # for backwards compatibility only, applet maps to html5
1445 if (defined($res->{console}) && $res->{console} eq 'applet') {
1446 $res->{console} = 'html5';
1447 }
1448
bba12ad7 1449 return $res;
fe000966
DM
1450}
1451
1452sub write_datacenter_config {
1453 my ($filename, $cfg) = @_;
bba12ad7
TL
1454
1455 # map deprecated setting to new one
8f706517 1456 if (defined($cfg->{migration_unsecure}) && !defined($cfg->{migration})) {
bba12ad7
TL
1457 my $migration_unsecure = delete $cfg->{migration_unsecure};
1458 $cfg->{migration}->{type} = ($migration_unsecure) ? 'insecure' : 'secure';
1459 }
1460
0949afa7
DC
1461 # map deprecated applet setting to html5
1462 if (defined($cfg->{console}) && $cfg->{console} eq 'applet') {
1463 $cfg->{console} = 'html5';
1464 }
1465
08115244
TL
1466 if (my $migration = $cfg->{migration}) {
1467 $cfg->{migration} = PVE::JSONSchema::print_property_string($migration, $migration_format);
1468 }
1469
fe000966
DM
1470 return PVE::JSONSchema::dump_config($datacenter_schema, $filename, $cfg);
1471}
1472
c53b111f
DM
1473cfs_register_file('datacenter.cfg',
1474 \&parse_datacenter_config,
fe000966 1475 \&write_datacenter_config);
ec48ec22 1476
26784563
DM
1477# X509 Certificate cache helper
1478
1479my $cert_cache_nodes = {};
1480my $cert_cache_timestamp = time();
1481my $cert_cache_fingerprints = {};
1482
1483sub update_cert_cache {
1484 my ($update_node, $clear) = @_;
1485
1486 syslog('info', "Clearing outdated entries from certificate cache")
1487 if $clear;
1488
1489 $cert_cache_timestamp = time() if !defined($update_node);
1490
1491 my $node_list = defined($update_node) ?
1492 [ $update_node ] : [ keys %$cert_cache_nodes ];
1493
1494 foreach my $node (@$node_list) {
1495 my $clear_old = sub {
1496 if (my $old_fp = $cert_cache_nodes->{$node}) {
1497 # distrust old fingerprint
1498 delete $cert_cache_fingerprints->{$old_fp};
1499 # ensure reload on next proxied request
1500 delete $cert_cache_nodes->{$node};
1501 }
1502 };
1503
1904862a
TL
1504 my $fp = eval { get_node_fingerprint($node) };
1505 if (my $err = $@) {
1506 warn "$err\n";
26784563
DM
1507 &$clear_old() if $clear;
1508 next;
1509 }
1510
1511 my $old_fp = $cert_cache_nodes->{$node};
1512 $cert_cache_fingerprints->{$fp} = 1;
1513 $cert_cache_nodes->{$node} = $fp;
1514
1515 if (defined($old_fp) && $fp ne $old_fp) {
1516 delete $cert_cache_fingerprints->{$old_fp};
1517 }
1518 }
1519}
1520
ab224148
DM
1521# load and cache cert fingerprint once
1522sub initialize_cert_cache {
1523 my ($node) = @_;
1524
1525 update_cert_cache($node)
1526 if defined($node) && !defined($cert_cache_nodes->{$node});
1527}
1528
1904862a
TL
1529sub read_ssl_cert_fingerprint {
1530 my ($cert_path) = @_;
1531
2a5f4bed
FG
1532 my $bio = Net::SSLeay::BIO_new_file($cert_path, 'r')
1533 or die "unable to read '$cert_path' - $!\n";
1534
1535 my $cert = Net::SSLeay::PEM_read_bio_X509($bio);
e3c4007b
SI
1536 Net::SSLeay::BIO_free($bio);
1537
5d3ccbb3 1538 die "unable to read certificate from '$cert_path'\n" if !$cert;
2a5f4bed
FG
1539
1540 my $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
1541 Net::SSLeay::X509_free($cert);
1904862a 1542
1904862a
TL
1543 die "unable to get fingerprint for '$cert_path' - got empty value\n"
1544 if !defined($fp) || $fp eq '';
1545
1546 return $fp;
1547}
1548
1549sub get_node_fingerprint {
1550 my ($node) = @_;
1551
1552 my $cert_path = "/etc/pve/nodes/$node/pve-ssl.pem";
1553 my $custom_cert_path = "/etc/pve/nodes/$node/pveproxy-ssl.pem";
1554
1555 $cert_path = $custom_cert_path if -f $custom_cert_path;
1556
1557 return read_ssl_cert_fingerprint($cert_path);
1558}
1559
1560
26784563
DM
1561sub check_cert_fingerprint {
1562 my ($cert) = @_;
1563
1564 # clear cache every 30 minutes at least
1565 update_cert_cache(undef, 1) if time() - $cert_cache_timestamp >= 60*30;
1566
1567 # get fingerprint of server certificate
2a5f4bed
FG
1568 my $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
1569 return 0 if !defined($fp) || $fp eq ''; # error
26784563
DM
1570
1571 my $check = sub {
1572 for my $expected (keys %$cert_cache_fingerprints) {
1573 return 1 if $fp eq $expected;
1574 }
1575 return 0;
1576 };
1577
1578 return 1 if &$check();
1579
1580 # clear cache and retry at most once every minute
1581 if (time() - $cert_cache_timestamp >= 60) {
1582 syslog ('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
1583 update_cert_cache();
1584 return &$check();
1585 }
1586
1587 return 0;
1588}
1589
15df58e6
DM
1590# bash completion helpers
1591
1592sub complete_next_vmid {
1593
1594 my $vmlist = get_vmlist() || {};
1595 my $idlist = $vmlist->{ids} || {};
1596
1597 for (my $i = 100; $i < 10000; $i++) {
1598 return [$i] if !defined($idlist->{$i});
1599 }
1600
1601 return [];
1602}
1603
87515b25
DM
1604sub complete_vmid {
1605
1606 my $vmlist = get_vmlist();
1607 my $ids = $vmlist->{ids} || {};
1608
1609 return [ keys %$ids ];
1610}
1611
15df58e6
DM
1612sub complete_local_vmid {
1613
1614 my $vmlist = get_vmlist();
1615 my $ids = $vmlist->{ids} || {};
1616
1617 my $nodename = PVE::INotify::nodename();
1618
1619 my $res = [];
1620 foreach my $vmid (keys %$ids) {
1621 my $d = $ids->{$vmid};
1622 next if !$d->{node} || $d->{node} ne $nodename;
1623 push @$res, $vmid;
1624 }
1625
1626 return $res;
1627}
1628
4dd189df
DM
1629sub complete_migration_target {
1630
1631 my $res = [];
1632
1633 my $nodename = PVE::INotify::nodename();
1634
1635 my $nodelist = get_nodelist();
1636 foreach my $node (@$nodelist) {
1637 next if $node eq $nodename;
1638 push @$res, $node;
1639 }
1640
1641 return $res;
1642}
1643
aabeedfb
WB
1644sub get_ssh_info {
1645 my ($node, $network_cidr) = @_;
1646
1647 my $ip;
1648 if (defined($network_cidr)) {
1649 # Use mtunnel via to get the remote node's ip inside $network_cidr.
1650 # This goes over the regular network (iow. uses get_ssh_info() with
1651 # $network_cidr undefined.
1652 # FIXME: Use the REST API client for this after creating an API entry
1653 # for get_migration_ip.
1654 my $default_remote = get_ssh_info($node, undef);
1655 my $default_ssh = ssh_info_to_command($default_remote);
1656 my $cmd =[@$default_ssh, 'pvecm', 'mtunnel',
1657 '-migration_network', $network_cidr,
1658 '-get_migration_ip'
1659 ];
1660 PVE::Tools::run_command($cmd, outfunc => sub {
1661 my ($line) = @_;
1662 chomp $line;
1663 die "internal error: unexpected output from mtunnel\n"
1664 if defined($ip);
1665 if ($line =~ /^ip: '(.*)'$/) {
1666 $ip = $1;
1667 } else {
1668 die "internal error: bad output from mtunnel\n"
1669 if defined($ip);
1670 }
1671 });
1672 die "failed to get ip for node '$node' in network '$network_cidr'\n"
1673 if !defined($ip);
1674 } else {
1675 $ip = remote_node_ip($node);
1676 }
1677
1678 return {
1679 ip => $ip,
d7d4c5b8
WB
1680 name => $node,
1681 network => $network_cidr,
aabeedfb
WB
1682 };
1683}
1684
1f1aef8b 1685sub ssh_info_to_command_base {
aabeedfb
WB
1686 my ($info, @extra_options) = @_;
1687 return [
1688 '/usr/bin/ssh',
8529e536 1689 '-e', 'none',
aabeedfb
WB
1690 '-o', 'BatchMode=yes',
1691 '-o', 'HostKeyAlias='.$info->{name},
1f1aef8b 1692 @extra_options
aabeedfb
WB
1693 ];
1694}
1695
1f1aef8b
WB
1696sub ssh_info_to_command {
1697 my ($info, @extra_options) = @_;
1698 my $cmd = ssh_info_to_command_base($info, @extra_options);
1699 push @$cmd, "root\@$info->{ip}";
1700 return $cmd;
1701}
1702
68491de3
TL
1703sub assert_joinable {
1704 my ($ring0_addr, $ring1_addr, $force) = @_;
1705
42505be3
TL
1706 my $errors = '';
1707 my $error = sub { $errors .= "* $_[0]\n"; };
68491de3 1708
42505be3
TL
1709 if (-f $authfile) {
1710 $error->("authentication key '$authfile' already exists");
1711 }
68491de3 1712
42505be3
TL
1713 if (-f $clusterconf) {
1714 $error->("cluster config '$clusterconf' already exists");
1715 }
68491de3 1716
42505be3
TL
1717 my $vmlist = get_vmlist();
1718 if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
1719 $error->("this host already contains virtual guests");
1720 }
68491de3 1721
8fdd87f9 1722 if (run_command(['corosync-quorumtool', '-l'], noerr => 1, quiet => 1) == 0) {
42505be3 1723 $error->("corosync is already running, is this node already in a cluster?!");
68491de3
TL
1724 }
1725
1726 # check if corosync ring IPs are configured on the current nodes interfaces
1727 my $check_ip = sub {
1728 my $ip = shift // return;
1729 if (!PVE::JSONSchema::pve_verify_ip($ip, 1)) {
1730 my $host = $ip;
1731 eval { $ip = PVE::Network::get_ip_from_hostname($host); };
1732 if ($@) {
42505be3 1733 $error->("cannot use '$host': $@\n") ;
68491de3
TL
1734 return;
1735 }
1736 }
1737
1738 my $cidr = (Net::IP::ip_is_ipv6($ip)) ? "$ip/128" : "$ip/32";
1739 my $configured_ips = PVE::Network::get_local_ip_from_cidr($cidr);
1740
1741 $error->("cannot use IP '$ip', it must be configured exactly once on local node!\n")
1742 if (scalar(@$configured_ips) != 1);
1743 };
1744
1745 $check_ip->($ring0_addr);
1746 $check_ip->($ring1_addr);
1747
42505be3
TL
1748 if ($errors) {
1749 warn "detected the following error(s):\n$errors";
1750 die "Check if node may join a cluster failed!\n" if !$force;
1751 }
68491de3
TL
1752}
1753
a9965358 1754# NOTE: filesystem must be offline here, no DB changes allowed
e02bdaae
TL
1755my $backup_cfs_database = sub {
1756 my ($dbfile) = @_;
1757
1758 mkdir $dbbackupdir;
1759
e02bdaae 1760 my $ctime = time();
a9965358 1761 my $backup_fn = "$dbbackupdir/config-$ctime.sql.gz";
e02bdaae 1762
a9965358 1763 print "backup old database to '$backup_fn'\n";
e02bdaae 1764
a9965358
TL
1765 my $cmd = [ ['sqlite3', $dbfile, '.dump'], ['gzip', '-', \ ">${backup_fn}"] ];
1766 run_command($cmd, 'errmsg' => "cannot backup old database\n");
e02bdaae 1767
a9965358
TL
1768 my $maxfiles = 10; # purge older backup
1769 my $backups = [ sort { $b cmp $a } <$dbbackupdir/config-*.sql.gz> ];
1770
1771 if ((my $count = scalar(@$backups)) > $maxfiles) {
1772 foreach my $f (@$backups[$maxfiles..$count-1]) {
ee0daa88 1773 next if $f !~ m/^(\S+)$/; # untaint
a9965358
TL
1774 print "delete old backup '$1'\n";
1775 unlink $1;
1776 }
e02bdaae
TL
1777 }
1778};
1779
6ed49eb1
TL
1780sub join {
1781 my ($param) = @_;
1782
1783 my $nodename = PVE::INotify::nodename();
1784
1785 setup_sshd_config();
1786 setup_rootsshconfig();
1787 setup_ssh_keys();
1788
1789 # check if we can join with the given parameters and current node state
1790 my ($ring0_addr, $ring1_addr) = $param->@{'ring0_addr', 'ring1_addr'};
1791 assert_joinable($ring0_addr, $ring1_addr, $param->{force});
1792
1793 # make sure known_hosts is on local filesystem
1794 ssh_unmerge_known_hosts();
1795
1796 my $host = $param->{hostname};
072ba2e0 1797 my $local_ip_address = remote_node_ip($nodename);
6ed49eb1
TL
1798
1799 my $conn_args = {
1800 username => 'root@pam',
1801 password => $param->{password},
1802 cookie_name => 'PVEAuthCookie',
1803 protocol => 'https',
1804 host => $host,
1805 port => 8006,
1806 };
1807
1808 if (my $fp = $param->{fingerprint}) {
1809 $conn_args->{cached_fingerprints} = { uc($fp) => 1 };
1810 } else {
1811 # API schema ensures that we can only get here from CLI handler
1812 $conn_args->{manual_verification} = 1;
1813 }
1814
1815 print "Etablishing API connection with host '$host'\n";
1816
1817 my $conn = PVE::APIClient::LWP->new(%$conn_args);
1818 $conn->login();
1819
1820 # login raises an exception on failure, so if we get here we're good
1821 print "Login succeeded.\n";
1822
1823 my $args = {};
1824 $args->{force} = $param->{force} if defined($param->{force});
1825 $args->{nodeid} = $param->{nodeid} if $param->{nodeid};
1826 $args->{votes} = $param->{votes} if defined($param->{votes});
072ba2e0 1827 $args->{ring0_addr} = $ring0_addr // $local_ip_address;
6ed49eb1
TL
1828 $args->{ring1_addr} = $ring1_addr if defined($ring1_addr);
1829
1830 print "Request addition of this node\n";
1831 my $res = $conn->post("/cluster/config/nodes/$nodename", $args);
1832
1833 print "Join request OK, finishing setup locally\n";
1834
1835 # added successfuly - now prepare local node
1836 finish_join($nodename, $res->{corosync_conf}, $res->{corosync_authkey});
1837}
1838
e02bdaae
TL
1839sub finish_join {
1840 my ($nodename, $corosync_conf, $corosync_authkey) = @_;
1841
1842 mkdir "$localclusterdir";
1843 PVE::Tools::file_set_contents($authfile, $corosync_authkey);
1844 PVE::Tools::file_set_contents($localclusterconf, $corosync_conf);
1845
1846 print "stopping pve-cluster service\n";
8fdd87f9
TL
1847 my $cmd = ['systemctl', 'stop', 'pve-cluster'];
1848 run_command($cmd, errmsg => "can't stop pve-cluster service");
e02bdaae
TL
1849
1850 $backup_cfs_database->($dbfile);
1851 unlink $dbfile;
1852
8fdd87f9
TL
1853 $cmd = ['systemctl', 'start', 'corosync', 'pve-cluster'];
1854 run_command($cmd, errmsg => "starting pve-cluster failed");
e02bdaae
TL
1855
1856 # wait for quorum
1857 my $printqmsg = 1;
1858 while (!check_cfs_quorum(1)) {
1859 if ($printqmsg) {
1860 print "waiting for quorum...";
1861 STDOUT->flush();
1862 $printqmsg = 0;
1863 }
1864 sleep(1);
1865 }
1866 print "OK\n" if !$printqmsg;
1867
8f64504c 1868 updatecerts_and_ssh(1);
e02bdaae 1869
8f64504c 1870 print "generated new node certificate, restart pveproxy and pvedaemon services\n";
8fdd87f9 1871 run_command(['systemctl', 'reload-or-restart', 'pvedaemon', 'pveproxy']);
e02bdaae
TL
1872
1873 print "successfully added node '$nodename' to cluster.\n";
1874}
1875
8f64504c
TL
1876sub updatecerts_and_ssh {
1877 my ($force_new_cert, $silent) = @_;
1878
1879 my $p = sub { print "$_[0]\n" if !$silent };
1880
1881 setup_rootsshconfig();
1882
1883 gen_pve_vzdump_symlink();
1884
1885 if (!check_cfs_quorum(1)) {
1886 return undef if $silent;
1887 die "no quorum - unable to update files\n";
1888 }
1889
1890 setup_ssh_keys();
1891
1892 my $nodename = PVE::INotify::nodename();
1893 my $local_ip_address = remote_node_ip($nodename);
1894
1895 $p->("(re)generate node files");
1896 $p->("generate new node certificate") if $force_new_cert;
1897 gen_pve_node_files($nodename, $local_ip_address, $force_new_cert);
1898
1899 $p->("merge authorized SSH keys and known hosts");
1900 ssh_merge_keys();
f3956026 1901 ssh_merge_known_hosts($nodename, $local_ip_address, 1);
8f64504c
TL
1902 gen_pve_vzdump_files();
1903}
e02bdaae 1904
ac68281b 19051;