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