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