]> git.proxmox.com Git - pve-cluster.git/blob - src/PVE/Cluster.pm
cluster fs: allow to force UTF-8 encoding for cfs_write_file
[pve-cluster.git] / src / PVE / Cluster.pm
1 package PVE::Cluster;
2
3 use strict;
4 use warnings;
5
6 use Encode;
7 use File::stat qw();
8 use File::Path qw(make_path);
9 use JSON;
10 use Net::SSLeay;
11 use POSIX qw(ENOENT);
12 use Socket;
13 use Storable qw(dclone);
14
15 use PVE::Certificate;
16 use PVE::INotify;
17 use PVE::IPCC;
18 use PVE::JSONSchema;
19 use PVE::Network;
20 use PVE::SafeSyslog;
21 use PVE::Tools qw(run_command);
22
23 use PVE::Cluster::IPCConst;
24
25 use base 'Exporter';
26
27 our @EXPORT_OK = qw(
28 cfs_read_file
29 cfs_write_file
30 cfs_register_file
31 cfs_lock_file);
32
33 # x509 certificate utils
34
35 my $basedir = "/etc/pve";
36 my $authdir = "$basedir/priv";
37 my $lockdir = "/etc/pve/priv/lock";
38
39 # cfs and corosync files
40 my $dbfile = "/var/lib/pve-cluster/config.db";
41 my $dbbackupdir = "/var/lib/pve-cluster/backup";
42
43 # this is just a readonly copy, the relevant one is in status.c from pmxcfs
44 # observed files are the one we can get directly through IPCC, they are cached
45 # using a computed version and only those can be used by the cfs_*_file methods
46 my $observed = {
47 'vzdump.cron' => 1,
48 'vzdump.conf' => 1,
49 'jobs.cfg' => 1,
50 'storage.cfg' => 1,
51 'datacenter.cfg' => 1,
52 'replication.cfg' => 1,
53 'corosync.conf' => 1,
54 'corosync.conf.new' => 1,
55 'firewall/cluster.fw' => 1,
56 'user.cfg' => 1,
57 'domains.cfg' => 1,
58 'notifications.cfg' => 1,
59 'priv/notifications.cfg' => 1,
60 'priv/shadow.cfg' => 1,
61 'priv/tfa.cfg' => 1,
62 'priv/token.cfg' => 1,
63 'priv/acme/plugins.cfg' => 1,
64 'priv/ipam.db' => 1,
65 '/qemu-server/' => 1,
66 '/openvz/' => 1,
67 '/lxc/' => 1,
68 'ha/crm_commands' => 1,
69 'ha/manager_status' => 1,
70 'ha/resources.cfg' => 1,
71 'ha/groups.cfg' => 1,
72 'ha/fence.cfg' => 1,
73 'status.cfg' => 1,
74 'ceph.conf' => 1,
75 'sdn/vnets.cfg' => 1,
76 'sdn/zones.cfg' => 1,
77 'sdn/controllers.cfg' => 1,
78 'sdn/subnets.cfg' => 1,
79 'sdn/ipams.cfg' => 1,
80 'sdn/dns.cfg' => 1,
81 'sdn/.running-config' => 1,
82 'virtual-guest/cpu-models.conf' => 1,
83 'mapping/pci.cfg' => 1,
84 'mapping/usb.cfg' => 1,
85 };
86
87 sub prepare_observed_file_basedirs {
88
89 if (!check_cfs_is_mounted(1)) {
90 warn "pmxcfs isn't mounted (/etc/pve), chickening out..\n";
91 return;
92 }
93
94 for my $f (sort keys %$observed) {
95 next if $f !~ m!^(.*)/[^/]+$!;
96 my $dir = "$basedir/$1";
97 next if -e $dir; # can also be a link, so just use -e xist check
98 print "creating directory '$dir' for observed files\n";
99 make_path($dir);
100 }
101 }
102
103 sub base_dir {
104 return $basedir;
105 }
106
107 sub auth_dir {
108 return $authdir;
109 }
110
111 sub check_cfs_quorum {
112 my ($noerr) = @_;
113
114 # note: -w filename always return 1 for root, so wee need
115 # to use File::lstat here
116 my $st = File::stat::lstat("$basedir/local");
117 my $quorate = ($st && (($st->mode & 0200) != 0));
118
119 die "cluster not ready - no quorum?\n" if !$quorate && !$noerr;
120
121 return $quorate;
122 }
123
124 sub check_cfs_is_mounted {
125 my ($noerr) = @_;
126
127 my $res = -l "$basedir/local";
128
129 die "pve configuration filesystem (pmxcfs) not mounted\n" if !$res && !$noerr;
130
131 return $res;
132 }
133
134 my $versions = {};
135 my $vmlist = {};
136 my $clinfo = {};
137
138 my $ipcc_send_rec = sub {
139 my ($msgid, $data) = @_;
140
141 my $res = PVE::IPCC::ipcc_send_rec($msgid, $data);
142
143 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
144
145 return $res;
146 };
147
148 my $ipcc_send_rec_json = sub {
149 my ($msgid, $data) = @_;
150
151 my $res = PVE::IPCC::ipcc_send_rec($msgid, $data);
152
153 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
154
155 return decode_json($res);
156 };
157
158 my $ipcc_get_config = sub {
159 my ($path) = @_;
160
161 my $bindata = pack "Z*", $path;
162 my $res = PVE::IPCC::ipcc_send_rec(CFS_IPC_GET_CONFIG, $bindata);
163 if (!defined($res)) {
164 if ($! != 0) {
165 return undef if $! == ENOENT;
166 die "$!\n";
167 }
168 return '';
169 }
170
171 return $res;
172 };
173
174 my $ipcc_get_status = sub {
175 my ($name, $nodename) = @_;
176
177 my $bindata = pack "Z[256]Z[256]", $name, ($nodename || "");
178 return PVE::IPCC::ipcc_send_rec(CFS_IPC_GET_STATUS, $bindata);
179 };
180
181 my $ipcc_remove_status = sub {
182 my ($name) = @_;
183 # we just omit the data payload, pmxcfs takes this as hint and removes this
184 # key from the status hashtable
185 my $bindata = pack "Z[256]", $name;
186 return &$ipcc_send_rec(CFS_IPC_SET_STATUS, $bindata);
187 };
188
189 my $ipcc_update_status = sub {
190 my ($name, $data) = @_;
191
192 my $raw = ref($data) ? encode_json($data) : $data;
193 # update status
194 my $bindata = pack "Z[256]Z*", $name, $raw;
195
196 return &$ipcc_send_rec(CFS_IPC_SET_STATUS, $bindata);
197 };
198
199 my $ipcc_log = sub {
200 my ($priority, $ident, $tag, $msg) = @_;
201
202 my $bindata = pack "CCCZ*Z*Z*", $priority, bytes::length($ident) + 1,
203 bytes::length($tag) + 1, $ident, $tag, $msg;
204
205 return &$ipcc_send_rec(CFS_IPC_LOG_CLUSTER_MSG, $bindata);
206 };
207
208 my $ipcc_get_cluster_log = sub {
209 my ($user, $max) = @_;
210
211 $max = 0 if !defined($max);
212
213 my $bindata = pack "VVVVZ*", $max, 0, 0, 0, ($user || "");
214 return &$ipcc_send_rec(CFS_IPC_GET_CLUSTER_LOG, $bindata);
215 };
216
217 my $ipcc_verify_token = sub {
218 my ($full_token) = @_;
219
220 my $bindata = pack "Z*", $full_token;
221 my $res = PVE::IPCC::ipcc_send_rec(CFS_IPC_VERIFY_TOKEN, $bindata);
222
223 return 1 if $! == 0;
224 return 0 if $! == ENOENT;
225
226 die "$!\n";
227 };
228
229 my $ccache = {};
230
231 sub cfs_update {
232 my ($fail) = @_;
233 eval {
234 my $res = &$ipcc_send_rec_json(CFS_IPC_GET_FS_VERSION);
235 die "no starttime\n" if !$res->{starttime};
236
237 if (!$res->{starttime} || !$versions->{starttime} ||
238 $res->{starttime} != $versions->{starttime}) {
239 #print "detected changed starttime\n";
240 $vmlist = {};
241 $clinfo = {};
242 $ccache = {};
243 }
244
245 $versions = $res;
246 };
247 my $err = $@;
248 if ($err) {
249 $versions = {};
250 $vmlist = {};
251 $clinfo = {};
252 $ccache = {};
253 die $err if $fail;
254 warn $err;
255 }
256
257 eval {
258 if (!$clinfo->{version} || $clinfo->{version} != $versions->{clinfo}) {
259 #warn "detected new clinfo\n";
260 $clinfo = &$ipcc_send_rec_json(CFS_IPC_GET_CLUSTER_INFO);
261 }
262 };
263 $err = $@;
264 if ($err) {
265 $clinfo = {};
266 die $err if $fail;
267 warn $err;
268 }
269
270 eval {
271 if (!$vmlist->{version} || $vmlist->{version} != $versions->{vmlist}) {
272 #warn "detected new vmlist1\n";
273 $vmlist = &$ipcc_send_rec_json(CFS_IPC_GET_GUEST_LIST);
274 }
275 };
276 $err = $@;
277 if ($err) {
278 $vmlist = {};
279 die $err if $fail;
280 warn $err;
281 }
282 }
283
284 sub get_vmlist {
285 return $vmlist;
286 }
287
288 sub get_clinfo {
289 return $clinfo;
290 }
291
292 sub get_members {
293 return $clinfo->{nodelist};
294 }
295
296 sub get_nodelist {
297 my $nodelist = $clinfo->{nodelist};
298
299 my $nodename = PVE::INotify::nodename();
300
301 if (!$nodelist || !$nodelist->{$nodename}) {
302 return [ $nodename ];
303 }
304
305 return [ keys %$nodelist ];
306 }
307
308 # only stored in a in-memory hashtable inside pmxcfs, local data is gone after
309 # a restart (of pmxcfs or the node), peer data is still available then
310 # best used for status data, like running (ceph) services, package versions, ...
311 sub broadcast_node_kv {
312 my ($key, $data) = @_;
313
314 if (!defined($data)) {
315 eval { $ipcc_remove_status->("kv/$key") };
316 } else {
317 die "cannot send a reference\n" if ref($data);
318 my $size = length($data);
319 die "data for '$key' too big\n" if $size >= (32 * 1024); # limit from pmxfs
320
321 eval { $ipcc_update_status->("kv/$key", $data) };
322 }
323 warn $@ if $@;
324 }
325
326 # nodename is optional
327 sub get_node_kv {
328 my ($key, $nodename) = @_;
329
330 my $res = {};
331 my $get_node_data = sub {
332 my ($node) = @_;
333 my $raw = $ipcc_get_status->("kv/$key", $node);
334 $res->{$node} = unpack("Z*", $raw) if $raw;
335 };
336
337 if ($nodename) {
338 $get_node_data->($nodename);
339 } else {
340 for my $node (get_nodelist()->@*) {
341 $get_node_data->($node);
342 }
343 }
344
345 return $res;
346 }
347
348 # properties: an array-ref of config properties you want to get, e.g., this
349 # is perfect to get multiple properties of a guest _fast_
350 # (>100 faster than manual parsing here)
351 # vmid: optional, if a valid is passed we only check that one, else return all
352 # NOTE: does *not* searches snapshot and PENDING entries sections!
353 # NOTE: returns the guest config lines (excluding trailing whitespace) as is,
354 # so for non-trivial properties, checking the validity must be done
355 # NOTE: no permission check is done, that is the responsibilty of the caller
356 sub get_guest_config_properties {
357 my ($properties, $vmid) = @_;
358
359 die "properties required" if !defined($properties);
360
361 my $num_props = scalar(@$properties);
362 die "only up to 255 properties supported" if $num_props > 255;
363 my $bindata = pack "VC", $vmid // 0, $num_props;
364 for my $property (@$properties) {
365 $bindata .= pack "Z*", $property;
366 }
367 my $res = $ipcc_send_rec_json->(CFS_IPC_GET_GUEST_CONFIG_PROPERTIES, $bindata);
368
369 return $res;
370 }
371
372 # property: a config property you want to get, e.g., this is perfect to get
373 # the 'lock' entry of a guest _fast_ (>100 faster than manual parsing here)
374 # vmid: optional, if a valid is passed we only check that one, else return all
375 # NOTE: does *not* searches snapshot and PENDING entries sections!
376 # NOTE: returns the guest config lines (excluding trailing whitespace) as is,
377 # so for non-trivial properties, checking the validity must be done
378 # NOTE: no permission check is done, that is the responsibilty of the caller
379 sub get_guest_config_property {
380 my ($property, $vmid) = @_;
381
382 die "property is required" if !defined($property);
383
384 my $bindata = pack "VZ*", $vmid // 0, $property;
385 my $res = $ipcc_send_rec_json->(CFS_IPC_GET_GUEST_CONFIG_PROPERTY, $bindata);
386
387 return $res;
388 }
389
390 # $data must be a chronological descending ordered array of tasks
391 sub broadcast_tasklist {
392 my ($data) = @_;
393
394 # the serialized list may not get bigger than 128 KiB (CFS_MAX_STATUS_SIZE from pmxcfs)
395 # drop older items until we satisfy this constraint
396 my $size = length(encode_json($data));
397 while ($size >= (32 * 1024)) { # TODO: update to 128 KiB in PVE 8.x
398 pop @$data;
399 $size = length(encode_json($data));
400 }
401
402 eval { $ipcc_update_status->("tasklist", $data) };
403 warn $@ if $@;
404 }
405
406 my $tasklistcache = {};
407
408 sub get_tasklist {
409 my ($nodename) = @_;
410
411 my $kvstore = $versions->{kvstore} || {};
412
413 my $nodelist = get_nodelist();
414
415 my $res = [];
416 foreach my $node (@$nodelist) {
417 next if $nodename && ($nodename ne $node);
418 eval {
419 my $ver = exists $kvstore->{$node} ? $kvstore->{$node}->{tasklist} : undef;
420 my $cache = $tasklistcache->{$node};
421 if (!$cache || !$ver || !$cache->{version} || ($cache->{version} != $ver)) {
422 my $tasks = [];
423 if (my $raw = $ipcc_get_status->("tasklist", $node)) {
424 my $json_str = unpack("Z*", $raw);
425 $tasks = decode_json($json_str);
426 }
427 push @$res, @$tasks;
428 $tasklistcache->{$node} = {
429 data => $tasks,
430 version => $ver,
431 };
432 } elsif ($cache && $cache->{data}) {
433 push @$res, $cache->{data}->@*;
434 }
435 };
436 my $err = $@;
437 syslog('err', $err) if $err;
438 }
439
440 return $res;
441 }
442
443 sub broadcast_rrd {
444 my ($rrdid, $data) = @_;
445
446 eval {
447 &$ipcc_update_status("rrd/$rrdid", $data);
448 };
449 my $err = $@;
450
451 warn $err if $err;
452 }
453
454 my $last_rrd_dump = 0;
455 my $last_rrd_data = "";
456
457 sub rrd_dump {
458
459 my $ctime = time();
460
461 my $diff = $ctime - $last_rrd_dump;
462 if ($diff < 2) {
463 return $last_rrd_data;
464 }
465
466 my $raw;
467 eval {
468 $raw = &$ipcc_send_rec(CFS_IPC_GET_RRD_DUMP);
469 };
470 my $err = $@;
471
472 if ($err) {
473 warn $err;
474 return {};
475 }
476
477 my $res = {};
478
479 if ($raw) {
480 while ($raw =~ s/^(.*)\n//) {
481 my ($key, @ela) = split(/:/, $1);
482 next if !$key;
483 next if !(scalar(@ela) > 1);
484 $res->{$key} = [ map { $_ eq 'U' ? undef : $_ } @ela ];
485 }
486 }
487
488 $last_rrd_dump = $ctime;
489 $last_rrd_data = $res;
490
491 return $res;
492 }
493
494
495 # a fast way to read files (avoid fuse overhead)
496 sub get_config {
497 my ($path) = @_;
498
499 return &$ipcc_get_config($path);
500 }
501
502 sub get_cluster_log {
503 my ($user, $max) = @_;
504
505 return &$ipcc_get_cluster_log($user, $max);
506 }
507
508 sub verify_token {
509 my ($userid, $token) = @_;
510
511 return &$ipcc_verify_token("$userid $token");
512 }
513
514 my $file_info = {};
515
516 sub cfs_register_file {
517 my ($filename, $parser, $writer) = @_;
518
519 $observed->{$filename} || die "unknown file '$filename'";
520
521 die "file '$filename' already registered" if $file_info->{$filename};
522
523 $file_info->{$filename} = {
524 parser => $parser,
525 writer => $writer,
526 };
527 }
528
529 my $ccache_read = sub {
530 my ($filename, $parser, $version) = @_;
531
532 $ccache->{$filename} = {} if !$ccache->{$filename};
533
534 my $ci = $ccache->{$filename};
535
536 if (!$ci->{version} || !$version || $ci->{version} != $version) {
537 # we always call the parser, even when the file does not exist
538 # (in that case $data is undef)
539 my $data = get_config($filename);
540 $ci->{data} = &$parser("/etc/pve/$filename", $data);
541 $ci->{version} = $version;
542 }
543
544 my $res = ref($ci->{data}) ? dclone($ci->{data}) : $ci->{data};
545
546 return $res;
547 };
548
549 sub cfs_file_version {
550 my ($filename) = @_;
551
552 my $version;
553 my $infotag;
554 if ($filename =~ m!^nodes/[^/]+/(openvz|lxc|qemu-server)/(\d+)\.conf$!) {
555 my ($type, $vmid) = ($1, $2);
556 if ($vmlist && $vmlist->{ids} && $vmlist->{ids}->{$vmid}) {
557 $version = $vmlist->{ids}->{$vmid}->{version};
558 }
559 $infotag = "/$type/";
560 } else {
561 $infotag = $filename;
562 $version = $versions->{$filename};
563 }
564
565 my $info = $file_info->{$infotag} ||
566 die "unknown file type '$filename'\n";
567
568 return wantarray ? ($version, $info) : $version;
569 }
570
571 sub cfs_read_file {
572 my ($filename) = @_;
573
574 my ($version, $info) = cfs_file_version($filename);
575 my $parser = $info->{parser};
576
577 return &$ccache_read($filename, $parser, $version);
578 }
579
580 sub cfs_write_file {
581 my ($filename, $data, $force_utf8) = @_;
582
583 my ($version, $info) = cfs_file_version($filename);
584
585 my $writer = $info->{writer} || die "no writer defined";
586
587 my $fsname = "/etc/pve/$filename";
588
589 my $raw = &$writer($fsname, $data);
590
591 if (my $ci = $ccache->{$filename}) {
592 $ci->{version} = undef;
593 }
594
595 PVE::Tools::file_set_contents($fsname, $raw, undef, 1);
596 }
597
598 my $cfs_lock = sub {
599 my ($lockid, $timeout, $code, @param) = @_;
600
601 my $prev_alarm = alarm(0); # suspend outer alarm early
602
603 my $res;
604 my $got_lock = 0;
605
606 # this timeout is for acquire the lock
607 $timeout = 10 if !$timeout;
608
609 my $filename = "$lockdir/$lockid";
610
611 my $is_code_err = 0;
612 eval {
613
614 mkdir $lockdir;
615
616 if (! -d $lockdir) {
617 die "pve cluster filesystem not online.\n";
618 }
619
620 my $timeout_err = sub { die "got lock request timeout\n"; };
621 local $SIG{ALRM} = $timeout_err;
622
623 while (1) {
624 alarm ($timeout);
625 $got_lock = mkdir($filename);
626 $timeout = alarm(0) - 1; # we'll sleep for 1s, see down below
627
628 last if $got_lock;
629
630 $timeout_err->() if $timeout <= 0;
631
632 print STDERR "trying to acquire cfs lock '$lockid' ...\n";
633 utime (0, 0, $filename); # cfs unlock request
634 sleep(1);
635 }
636
637 # fixed command timeout: cfs locks have a timeout of 120
638 # using 60 gives us another 60 seconds to abort the task
639 local $SIG{ALRM} = sub { die "'$lockid'-locked command timed out - aborting\n"; };
640 alarm(60);
641
642 cfs_update(); # make sure we read latest versions inside code()
643
644 $is_code_err = 1; # allows to differ between locking and actual-work errors
645
646 $res = &$code(@param);
647
648 alarm(0);
649 };
650
651 my $err = $@;
652
653 $err = "no quorum!\n" if !$got_lock && !check_cfs_quorum(1);
654
655 rmdir $filename if $got_lock; # if we held the lock always unlock again
656
657 alarm($prev_alarm);
658
659 if ($err) {
660 if (ref($err) eq 'PVE::Exception' || $is_code_err) {
661 # re-raise defined exceptions
662 $@ = $err;
663 } else {
664 # add lock info for plain errors comming from the locking itself
665 $@ = "cfs-lock '$lockid' error: $err";
666 }
667 return undef;
668 }
669
670 $@ = undef;
671
672 return $res;
673 };
674
675 sub cfs_lock_file {
676 my ($filename, $timeout, $code, @param) = @_;
677
678 my $info = $observed->{$filename} || die "unknown file '$filename'";
679
680 my $lockid = "file-$filename";
681 $lockid =~ s/[.\/]/_/g;
682
683 &$cfs_lock($lockid, $timeout, $code, @param);
684 }
685
686 sub cfs_lock_storage {
687 my ($storeid, $timeout, $code, @param) = @_;
688
689 my $lockid = "storage-$storeid";
690
691 &$cfs_lock($lockid, $timeout, $code, @param);
692 }
693
694 sub cfs_lock_domain {
695 my ($domainname, $timeout, $code, @param) = @_;
696
697 my $lockid = "domain-$domainname";
698
699 &$cfs_lock($lockid, $timeout, $code, @param);
700 }
701
702 sub cfs_lock_acme {
703 my ($account, $timeout, $code, @param) = @_;
704
705 my $lockid = "acme-$account";
706
707 &$cfs_lock($lockid, $timeout, $code, @param);
708 }
709
710 sub cfs_lock_authkey {
711 my ($timeout, $code, @param) = @_;
712
713 $cfs_lock->('authkey', $timeout, $code, @param);
714 }
715
716 sub cfs_lock_firewall {
717 my ($scope, $timeout, $code, @param) = @_;
718
719 my $lockid = "firewall-$scope";
720
721 $cfs_lock->($lockid, $timeout, $code, @param);
722 }
723
724 my $log_levels = {
725 "emerg" => 0,
726 "alert" => 1,
727 "crit" => 2,
728 "critical" => 2,
729 "err" => 3,
730 "error" => 3,
731 "warn" => 4,
732 "warning" => 4,
733 "notice" => 5,
734 "info" => 6,
735 "debug" => 7,
736 };
737
738 sub log_msg {
739 my ($priority, $ident, $msg) = @_;
740
741 if (my $tmp = $log_levels->{$priority}) {
742 $priority = $tmp;
743 }
744
745 die "need numeric log priority" if $priority !~ /^\d+$/;
746
747 my $tag = PVE::SafeSyslog::tag();
748
749 $msg = "empty message" if !$msg;
750
751 $ident = "" if !$ident;
752 $ident = encode("ascii", $ident,
753 sub { sprintf "\\u%04x", shift });
754
755 my $ascii = encode("ascii", $msg, sub { sprintf "\\u%04x", shift });
756
757 if ($ident) {
758 syslog($priority, "<%s> %s", $ident, $ascii);
759 } else {
760 syslog($priority, "%s", $ascii);
761 }
762
763 eval { &$ipcc_log($priority, $ident, $tag, $ascii); };
764
765 syslog("err", "writing cluster log failed: $@") if $@;
766 }
767
768 sub check_vmid_unused {
769 my ($vmid, $noerr) = @_;
770
771 my $vmlist = get_vmlist();
772
773 my $d = $vmlist->{ids}->{$vmid};
774 return 1 if !defined($d);
775
776 return undef if $noerr;
777
778 my $vmtypestr = $d->{type} eq 'qemu' ? 'VM' : 'CT';
779 die "$vmtypestr $vmid already exists on node '$d->{node}'\n";
780 }
781
782 sub check_node_exists {
783 my ($nodename, $noerr) = @_;
784
785 my $nodelist = $clinfo->{nodelist};
786 return 1 if $nodelist && $nodelist->{$nodename};
787
788 return undef if $noerr;
789
790 die "no such cluster node '$nodename'\n";
791 }
792
793 # this is also used to get the IP of the local node
794 sub remote_node_ip {
795 my ($nodename, $noerr) = @_;
796
797 my $nodelist = $clinfo->{nodelist};
798 if ($nodelist && $nodelist->{$nodename}) {
799 if (my $ip = $nodelist->{$nodename}->{ip}) {
800 return $ip if !wantarray;
801 my $family = $nodelist->{$nodename}->{address_family};
802 if (!$family) {
803 $nodelist->{$nodename}->{address_family} =
804 $family =
805 PVE::Tools::get_host_address_family($ip);
806 }
807 return wantarray ? ($ip, $family) : $ip;
808 }
809 }
810
811 # fallback: try to get IP by other means
812 return PVE::Network::get_ip_from_hostname($nodename, $noerr);
813 }
814
815 sub get_node_fingerprint {
816 my ($node) = @_;
817
818 my $cert_path = "/etc/pve/nodes/$node/pve-ssl.pem";
819 my $custom_cert_path = "/etc/pve/nodes/$node/pveproxy-ssl.pem";
820
821 $cert_path = $custom_cert_path if -f $custom_cert_path;
822
823 return PVE::Certificate::get_certificate_fingerprint($cert_path);
824 }
825
826 # bash completion helpers
827
828 sub complete_next_vmid {
829
830 my $vmlist = get_vmlist() || {};
831 my $idlist = $vmlist->{ids} || {};
832
833 for (my $i = 100; $i < 10000; $i++) {
834 return [$i] if !defined($idlist->{$i});
835 }
836
837 return [];
838 }
839
840 sub complete_vmid {
841
842 my $vmlist = get_vmlist();
843 my $ids = $vmlist->{ids} || {};
844
845 return [ keys %$ids ];
846 }
847
848 sub complete_local_vmid {
849
850 my $vmlist = get_vmlist();
851 my $ids = $vmlist->{ids} || {};
852
853 my $nodename = PVE::INotify::nodename();
854
855 my $res = [];
856 foreach my $vmid (keys %$ids) {
857 my $d = $ids->{$vmid};
858 next if !$d->{node} || $d->{node} ne $nodename;
859 push @$res, $vmid;
860 }
861
862 return $res;
863 }
864
865 sub complete_migration_target {
866
867 my $res = [];
868
869 my $nodename = PVE::INotify::nodename();
870
871 my $nodelist = get_nodelist();
872 foreach my $node (@$nodelist) {
873 next if $node eq $nodename;
874 push @$res, $node;
875 }
876
877 return $res;
878 }
879
880
881 # NOTE: filesystem must be offline here, no DB changes allowed
882 sub cfs_backup_database {
883 mkdir $dbbackupdir;
884
885 my $ctime = time();
886 my $backup_fn = "$dbbackupdir/config-$ctime.sql.gz";
887
888 print "backup old database to '$backup_fn'\n";
889
890 my $cmd = [ ['sqlite3', $dbfile, '.dump'], ['gzip', '-', \ ">${backup_fn}"] ];
891 run_command($cmd, 'errmsg' => "cannot backup old database\n");
892
893 my $maxfiles = 10; # purge older backup
894 my $backups = [ sort { $b cmp $a } <$dbbackupdir/config-*.sql.gz> ];
895
896 if ((my $count = scalar(@$backups)) > $maxfiles) {
897 foreach my $f (@$backups[$maxfiles..$count-1]) {
898 next if $f !~ m/^(\S+)$/; # untaint
899 print "delete old backup '$1'\n";
900 unlink $1;
901 }
902 }
903
904 return $dbfile;
905 }
906
907 1;