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