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