8 use File
::Path
qw(make_path);
13 use Storable
qw(dclone);
21 use PVE
::Tools
qw(run_command);
23 use PVE
::Cluster
::IPCConst
;
33 # x509 certificate utils
35 my $basedir = "/etc/pve";
36 my $authdir = "$basedir/priv";
37 my $lockdir = "/etc/pve/priv/lock";
39 # cfs and corosync files
40 my $dbfile = "/var/lib/pve-cluster/config.db";
41 my $dbbackupdir = "/var/lib/pve-cluster/backup";
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
51 'datacenter.cfg' => 1,
52 'replication.cfg' => 1,
54 'corosync.conf.new' => 1,
55 'firewall/cluster.fw' => 1,
58 'priv/shadow.cfg' => 1,
60 'priv/token.cfg' => 1,
61 'priv/acme/plugins.cfg' => 1,
66 'ha/crm_commands' => 1,
67 'ha/manager_status' => 1,
68 'ha/resources.cfg' => 1,
75 'sdn/controllers.cfg' => 1,
76 'sdn/subnets.cfg' => 1,
79 'sdn/.running-config' => 1,
80 'virtual-guest/cpu-models.conf' => 1,
83 sub prepare_observed_file_basedirs
{
85 if (!check_cfs_is_mounted
(1)) {
86 warn "pmxcfs isn't mounted (/etc/pve), chickening out..\n";
90 for my $f (sort keys %$observed) {
91 next if $f !~ m!^(.*)/[^/]+$!;
92 my $dir = "$basedir/$1";
93 next if -e
$dir; # can also be a link, so just use -e xist check
94 print "creating directory '$dir' for observerd files\n";
107 sub check_cfs_quorum
{
110 # note: -w filename always return 1 for root, so wee need
111 # to use File::lstat here
112 my $st = File
::stat::lstat("$basedir/local");
113 my $quorate = ($st && (($st->mode & 0200) != 0));
115 die "cluster not ready - no quorum?\n" if !$quorate && !$noerr;
120 sub check_cfs_is_mounted
{
123 my $res = -l
"$basedir/local";
125 die "pve configuration filesystem (pmxcfs) not mounted\n" if !$res && !$noerr;
134 my $ipcc_send_rec = sub {
135 my ($msgid, $data) = @_;
137 my $res = PVE
::IPCC
::ipcc_send_rec
($msgid, $data);
139 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
144 my $ipcc_send_rec_json = sub {
145 my ($msgid, $data) = @_;
147 my $res = PVE
::IPCC
::ipcc_send_rec
($msgid, $data);
149 die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
151 return decode_json
($res);
154 my $ipcc_get_config = sub {
157 my $bindata = pack "Z*", $path;
158 my $res = PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_GET_CONFIG
, $bindata);
159 if (!defined($res)) {
161 return undef if $! == ENOENT
;
170 my $ipcc_get_status = sub {
171 my ($name, $nodename) = @_;
173 my $bindata = pack "Z[256]Z[256]", $name, ($nodename || "");
174 return PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_GET_STATUS
, $bindata);
177 my $ipcc_remove_status = sub {
179 # we just omit the data payload, pmxcfs takes this as hint and removes this
180 # key from the status hashtable
181 my $bindata = pack "Z[256]", $name;
182 return &$ipcc_send_rec(CFS_IPC_SET_STATUS
, $bindata);
185 my $ipcc_update_status = sub {
186 my ($name, $data) = @_;
188 my $raw = ref($data) ? encode_json
($data) : $data;
190 my $bindata = pack "Z[256]Z*", $name, $raw;
192 return &$ipcc_send_rec(CFS_IPC_SET_STATUS
, $bindata);
196 my ($priority, $ident, $tag, $msg) = @_;
198 my $bindata = pack "CCCZ*Z*Z*", $priority, bytes
::length($ident) + 1,
199 bytes
::length($tag) + 1, $ident, $tag, $msg;
201 return &$ipcc_send_rec(CFS_IPC_LOG_CLUSTER_MSG
, $bindata);
204 my $ipcc_get_cluster_log = sub {
205 my ($user, $max) = @_;
207 $max = 0 if !defined($max);
209 my $bindata = pack "VVVVZ*", $max, 0, 0, 0, ($user || "");
210 return &$ipcc_send_rec(CFS_IPC_GET_CLUSTER_LOG
, $bindata);
213 my $ipcc_verify_token = sub {
214 my ($full_token) = @_;
216 my $bindata = pack "Z*", $full_token;
217 my $res = PVE
::IPCC
::ipcc_send_rec
(CFS_IPC_VERIFY_TOKEN
, $bindata);
220 return 0 if $! == ENOENT
;
230 my $res = &$ipcc_send_rec_json(CFS_IPC_GET_FS_VERSION
);
231 die "no starttime\n" if !$res->{starttime
};
233 if (!$res->{starttime
} || !$versions->{starttime
} ||
234 $res->{starttime
} != $versions->{starttime
}) {
235 #print "detected changed starttime\n";
254 if (!$clinfo->{version
} || $clinfo->{version
} != $versions->{clinfo
}) {
255 #warn "detected new clinfo\n";
256 $clinfo = &$ipcc_send_rec_json(CFS_IPC_GET_CLUSTER_INFO
);
267 if (!$vmlist->{version
} || $vmlist->{version
} != $versions->{vmlist
}) {
268 #warn "detected new vmlist1\n";
269 $vmlist = &$ipcc_send_rec_json(CFS_IPC_GET_GUEST_LIST
);
289 return $clinfo->{nodelist
};
293 my $nodelist = $clinfo->{nodelist
};
295 my $nodename = PVE
::INotify
::nodename
();
297 if (!$nodelist || !$nodelist->{$nodename}) {
298 return [ $nodename ];
301 return [ keys %$nodelist ];
304 # only stored in a in-memory hashtable inside pmxcfs, local data is gone after
305 # a restart (of pmxcfs or the node), peer data is still available then
306 # best used for status data, like running (ceph) services, package versions, ...
307 sub broadcast_node_kv
{
308 my ($key, $data) = @_;
310 if (!defined($data)) {
311 eval { $ipcc_remove_status->("kv/$key") };
313 die "cannot send a reference\n" if ref($data);
314 my $size = length($data);
315 die "data for '$key' too big\n" if $size >= (32 * 1024); # limit from pmxfs
317 eval { $ipcc_update_status->("kv/$key", $data) };
322 # nodename is optional
324 my ($key, $nodename) = @_;
327 my $get_node_data = sub {
329 my $raw = $ipcc_get_status->("kv/$key", $node);
330 $res->{$node} = unpack("Z*", $raw) if $raw;
334 $get_node_data->($nodename);
336 for my $node (get_nodelist
()->@*) {
337 $get_node_data->($node);
344 # properties: an array-ref of config properties you want to get, e.g., this
345 # is perfect to get multiple properties of a guest _fast_
346 # (>100 faster than manual parsing here)
347 # vmid: optional, if a valid is passed we only check that one, else return all
348 # NOTE: does *not* searches snapshot and PENDING entries sections!
349 # NOTE: returns the guest config lines (excluding trailing whitespace) as is,
350 # so for non-trivial properties, checking the validity must be done
351 # NOTE: no permission check is done, that is the responsibilty of the caller
352 sub get_guest_config_properties
{
353 my ($properties, $vmid) = @_;
355 die "properties required" if !defined($properties);
357 my $num_props = scalar(@$properties);
358 die "only up to 255 properties supported" if $num_props > 255;
359 my $bindata = pack "VC", $vmid // 0, $num_props;
360 for my $property (@$properties) {
361 $bindata .= pack "Z*", $property;
363 my $res = $ipcc_send_rec_json->(CFS_IPC_GET_GUEST_CONFIG_PROPERTIES
, $bindata);
368 # property: a config property you want to get, e.g., this is perfect to get
369 # the 'lock' entry of a guest _fast_ (>100 faster than manual parsing here)
370 # vmid: optional, if a valid is passed we only check that one, else return all
371 # NOTE: does *not* searches snapshot and PENDING entries sections!
372 # NOTE: returns the guest config lines (excluding trailing whitespace) as is,
373 # so for non-trivial properties, checking the validity must be done
374 # NOTE: no permission check is done, that is the responsibilty of the caller
375 sub get_guest_config_property
{
376 my ($property, $vmid) = @_;
378 die "property is required" if !defined($property);
380 my $bindata = pack "VZ*", $vmid // 0, $property;
381 my $res = $ipcc_send_rec_json->(CFS_IPC_GET_GUEST_CONFIG_PROPERTY
, $bindata);
386 # $data must be a chronological descending ordered array of tasks
387 sub broadcast_tasklist
{
390 # the serialized list may not get bigger than 128 KiB (CFS_MAX_STATUS_SIZE from pmxcfs)
391 # drop older items until we satisfy this constraint
392 my $size = length(encode_json
($data));
393 while ($size >= (32 * 1024)) { # TODO: update to 128 KiB in PVE 8.x
395 $size = length(encode_json
($data));
398 eval { $ipcc_update_status->("tasklist", $data) };
402 my $tasklistcache = {};
407 my $kvstore = $versions->{kvstore
} || {};
409 my $nodelist = get_nodelist
();
412 foreach my $node (@$nodelist) {
413 next if $nodename && ($nodename ne $node);
415 my $ver = exists $kvstore->{$node} ?
$kvstore->{$node}->{tasklist
} : undef;
416 my $cache = $tasklistcache->{$node};
417 if (!$cache || !$ver || !$cache->{version
} || ($cache->{version
} != $ver)) {
419 if (my $raw = $ipcc_get_status->("tasklist", $node)) {
420 my $json_str = unpack("Z*", $raw);
421 $tasks = decode_json
($json_str);
424 $tasklistcache->{$node} = {
428 } elsif ($cache && $cache->{data
}) {
429 push @$res, $cache->{data
}->@*;
433 syslog
('err', $err) if $err;
440 my ($rrdid, $data) = @_;
443 &$ipcc_update_status("rrd/$rrdid", $data);
450 my $last_rrd_dump = 0;
451 my $last_rrd_data = "";
457 my $diff = $ctime - $last_rrd_dump;
459 return $last_rrd_data;
464 $raw = &$ipcc_send_rec(CFS_IPC_GET_RRD_DUMP
);
476 while ($raw =~ s/^(.*)\n//) {
477 my ($key, @ela) = split(/:/, $1);
479 next if !(scalar(@ela) > 1);
480 $res->{$key} = [ map { $_ eq 'U' ?
undef : $_ } @ela ];
484 $last_rrd_dump = $ctime;
485 $last_rrd_data = $res;
491 # a fast way to read files (avoid fuse overhead)
495 return &$ipcc_get_config($path);
498 sub get_cluster_log
{
499 my ($user, $max) = @_;
501 return &$ipcc_get_cluster_log($user, $max);
505 my ($userid, $token) = @_;
507 return &$ipcc_verify_token("$userid $token");
512 sub cfs_register_file
{
513 my ($filename, $parser, $writer) = @_;
515 $observed->{$filename} || die "unknown file '$filename'";
517 die "file '$filename' already registered" if $file_info->{$filename};
519 $file_info->{$filename} = {
525 my $ccache_read = sub {
526 my ($filename, $parser, $version) = @_;
528 $ccache->{$filename} = {} if !$ccache->{$filename};
530 my $ci = $ccache->{$filename};
532 if (!$ci->{version
} || !$version || $ci->{version
} != $version) {
533 # we always call the parser, even when the file does not exist
534 # (in that case $data is undef)
535 my $data = get_config
($filename);
536 $ci->{data
} = &$parser("/etc/pve/$filename", $data);
537 $ci->{version
} = $version;
540 my $res = ref($ci->{data
}) ? dclone
($ci->{data
}) : $ci->{data
};
545 sub cfs_file_version
{
550 if ($filename =~ m!^nodes/[^/]+/(openvz|lxc|qemu-server)/(\d+)\.conf$!) {
551 my ($type, $vmid) = ($1, $2);
552 if ($vmlist && $vmlist->{ids
} && $vmlist->{ids
}->{$vmid}) {
553 $version = $vmlist->{ids
}->{$vmid}->{version
};
555 $infotag = "/$type/";
557 $infotag = $filename;
558 $version = $versions->{$filename};
561 my $info = $file_info->{$infotag} ||
562 die "unknown file type '$filename'\n";
564 return wantarray ?
($version, $info) : $version;
570 my ($version, $info) = cfs_file_version
($filename);
571 my $parser = $info->{parser
};
573 return &$ccache_read($filename, $parser, $version);
577 my ($filename, $data) = @_;
579 my ($version, $info) = cfs_file_version
($filename);
581 my $writer = $info->{writer
} || die "no writer defined";
583 my $fsname = "/etc/pve/$filename";
585 my $raw = &$writer($fsname, $data);
587 if (my $ci = $ccache->{$filename}) {
588 $ci->{version
} = undef;
591 PVE
::Tools
::file_set_contents
($fsname, $raw);
595 my ($lockid, $timeout, $code, @param) = @_;
597 my $prev_alarm = alarm(0); # suspend outer alarm early
602 # this timeout is for acquire the lock
603 $timeout = 10 if !$timeout;
605 my $filename = "$lockdir/$lockid";
613 die "pve cluster filesystem not online.\n";
616 my $timeout_err = sub { die "got lock request timeout\n"; };
617 local $SIG{ALRM
} = $timeout_err;
621 $got_lock = mkdir($filename);
622 $timeout = alarm(0) - 1; # we'll sleep for 1s, see down below
626 $timeout_err->() if $timeout <= 0;
628 print STDERR
"trying to acquire cfs lock '$lockid' ...\n";
629 utime (0, 0, $filename); # cfs unlock request
633 # fixed command timeout: cfs locks have a timeout of 120
634 # using 60 gives us another 60 seconds to abort the task
635 local $SIG{ALRM
} = sub { die "'$lockid'-locked command timed out - aborting\n"; };
638 cfs_update
(); # make sure we read latest versions inside code()
640 $is_code_err = 1; # allows to differ between locking and actual-work errors
642 $res = &$code(@param);
649 $err = "no quorum!\n" if !$got_lock && !check_cfs_quorum
(1);
651 rmdir $filename if $got_lock; # if we held the lock always unlock again
656 if (ref($err) eq 'PVE::Exception' || $is_code_err) {
657 # re-raise defined exceptions
660 # add lock info for plain errors comming from the locking itself
661 $@ = "cfs-lock '$lockid' error: $err";
672 my ($filename, $timeout, $code, @param) = @_;
674 my $info = $observed->{$filename} || die "unknown file '$filename'";
676 my $lockid = "file-$filename";
677 $lockid =~ s/[.\/]/_
/g
;
679 &$cfs_lock($lockid, $timeout, $code, @param);
682 sub cfs_lock_storage
{
683 my ($storeid, $timeout, $code, @param) = @_;
685 my $lockid = "storage-$storeid";
687 &$cfs_lock($lockid, $timeout, $code, @param);
690 sub cfs_lock_domain
{
691 my ($domainname, $timeout, $code, @param) = @_;
693 my $lockid = "domain-$domainname";
695 &$cfs_lock($lockid, $timeout, $code, @param);
699 my ($account, $timeout, $code, @param) = @_;
701 my $lockid = "acme-$account";
703 &$cfs_lock($lockid, $timeout, $code, @param);
706 sub cfs_lock_authkey
{
707 my ($timeout, $code, @param) = @_;
709 $cfs_lock->('authkey', $timeout, $code, @param);
712 sub cfs_lock_firewall
{
713 my ($scope, $timeout, $code, @param) = @_;
715 my $lockid = "firewall-$scope";
717 $cfs_lock->($lockid, $timeout, $code, @param);
735 my ($priority, $ident, $msg) = @_;
737 if (my $tmp = $log_levels->{$priority}) {
741 die "need numeric log priority" if $priority !~ /^\d+$/;
743 my $tag = PVE
::SafeSyslog
::tag
();
745 $msg = "empty message" if !$msg;
747 $ident = "" if !$ident;
748 $ident = encode
("ascii", $ident,
749 sub { sprintf "\\u%04x", shift });
751 my $ascii = encode
("ascii", $msg, sub { sprintf "\\u%04x", shift });
754 syslog
($priority, "<%s> %s", $ident, $ascii);
756 syslog
($priority, "%s", $ascii);
759 eval { &$ipcc_log($priority, $ident, $tag, $ascii); };
761 syslog
("err", "writing cluster log failed: $@") if $@;
764 sub check_vmid_unused
{
765 my ($vmid, $noerr) = @_;
767 my $vmlist = get_vmlist
();
769 my $d = $vmlist->{ids
}->{$vmid};
770 return 1 if !defined($d);
772 return undef if $noerr;
774 my $vmtypestr = $d->{type
} eq 'qemu' ?
'VM' : 'CT';
775 die "$vmtypestr $vmid already exists on node '$d->{node}'\n";
778 sub check_node_exists
{
779 my ($nodename, $noerr) = @_;
781 my $nodelist = $clinfo->{nodelist
};
782 return 1 if $nodelist && $nodelist->{$nodename};
784 return undef if $noerr;
786 die "no such cluster node '$nodename'\n";
789 # this is also used to get the IP of the local node
791 my ($nodename, $noerr) = @_;
793 my $nodelist = $clinfo->{nodelist
};
794 if ($nodelist && $nodelist->{$nodename}) {
795 if (my $ip = $nodelist->{$nodename}->{ip
}) {
796 return $ip if !wantarray;
797 my $family = $nodelist->{$nodename}->{address_family
};
799 $nodelist->{$nodename}->{address_family
} =
801 PVE
::Tools
::get_host_address_family
($ip);
803 return wantarray ?
($ip, $family) : $ip;
807 # fallback: try to get IP by other means
808 return PVE
::Network
::get_ip_from_hostname
($nodename, $noerr);
811 sub get_node_fingerprint
{
814 my $cert_path = "/etc/pve/nodes/$node/pve-ssl.pem";
815 my $custom_cert_path = "/etc/pve/nodes/$node/pveproxy-ssl.pem";
817 $cert_path = $custom_cert_path if -f
$custom_cert_path;
819 return PVE
::Certificate
::get_certificate_fingerprint
($cert_path);
822 # bash completion helpers
824 sub complete_next_vmid
{
826 my $vmlist = get_vmlist
() || {};
827 my $idlist = $vmlist->{ids
} || {};
829 for (my $i = 100; $i < 10000; $i++) {
830 return [$i] if !defined($idlist->{$i});
838 my $vmlist = get_vmlist
();
839 my $ids = $vmlist->{ids
} || {};
841 return [ keys %$ids ];
844 sub complete_local_vmid
{
846 my $vmlist = get_vmlist
();
847 my $ids = $vmlist->{ids
} || {};
849 my $nodename = PVE
::INotify
::nodename
();
852 foreach my $vmid (keys %$ids) {
853 my $d = $ids->{$vmid};
854 next if !$d->{node
} || $d->{node
} ne $nodename;
861 sub complete_migration_target
{
865 my $nodename = PVE
::INotify
::nodename
();
867 my $nodelist = get_nodelist
();
868 foreach my $node (@$nodelist) {
869 next if $node eq $nodename;
877 # NOTE: filesystem must be offline here, no DB changes allowed
878 sub cfs_backup_database
{
882 my $backup_fn = "$dbbackupdir/config-$ctime.sql.gz";
884 print "backup old database to '$backup_fn'\n";
886 my $cmd = [ ['sqlite3', $dbfile, '.dump'], ['gzip', '-', \
">${backup_fn}"] ];
887 run_command
($cmd, 'errmsg' => "cannot backup old database\n");
889 my $maxfiles = 10; # purge older backup
890 my $backups = [ sort { $b cmp $a } <$dbbackupdir/config-*.sql
.gz
> ];
892 if ((my $count = scalar(@$backups)) > $maxfiles) {
893 foreach my $f (@$backups[$maxfiles..$count-1]) {
894 next if $f !~ m/^(\S+)$/; # untaint
895 print "delete old backup '$1'\n";