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