use strict;
use warnings;
-use POSIX qw(EEXIST);
+use POSIX qw(EEXIST ENOENT);
use File::stat qw();
use Socket;
use Storable qw(dclone);
'vzdump.cron' => 1,
'storage.cfg' => 1,
'datacenter.cfg' => 1,
+ 'replication.cfg' => 1,
'corosync.conf' => 1,
'corosync.conf.new' => 1,
'user.cfg' => 1,
my $res = PVE::IPCC::ipcc_send_rec($msgid, $data);
- die "ipcc_send_rec failed: $!\n" if !defined($res) && ($! != 0);
+ die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
return $res;
};
my $res = PVE::IPCC::ipcc_send_rec($msgid, $data);
- die "ipcc_send_rec failed: $!\n" if !defined($res) && ($! != 0);
+ die "ipcc_send_rec[$msgid] failed: $!\n" if !defined($res) && ($! != 0);
return decode_json($res);
};
my $bindata = pack "Z*", $path;
my $res = PVE::IPCC::ipcc_send_rec(6, $bindata);
if (!defined($res)) {
- return undef if ($! != 0);
+ if ($! != 0) {
+ return undef if $! == ENOENT;
+ die "$!\n";
+ }
return '';
}
my $ccache = {};
sub cfs_update {
+ my ($fail) = @_;
eval {
my $res = &$ipcc_send_rec_json(1);
#warn "GOT1: " . Dumper($res);
$vmlist = {};
$clinfo = {};
$ccache = {};
+ die $err if $fail;
warn $err;
}
$err = $@;
if ($err) {
$clinfo = {};
+ die $err if $fail;
warn $err;
}
$err = $@;
if ($err) {
$vmlist = {};
+ die $err if $fail;
warn $err;
}
}
return [ keys %$nodelist ];
}
+# $data must be a chronological descending ordered array of tasks
sub broadcast_tasklist {
my ($data) = @_;
+ # the serialized list may not get bigger than 32kb (CFS_MAX_STATUS_SIZE
+ # from pmxcfs) - drop older items until we satisfy this constraint
+ my $size = length(encode_json($data));
+ while ($size >= (32 * 1024)) {
+ pop @$data;
+ $size = length(encode_json($data));
+ }
+
eval {
&$ipcc_update_status("tasklist", $data);
};
push @args, '--full-size-mode';
# we do not really store data into the file
- my $res = RRDs::graphv('', @args);
+ my $res = RRDs::graphv('-', @args);
my $err = RRDs::error;
die "RRD error: $err\n" if $err;
$family =
PVE::Tools::get_host_address_family($ip);
}
- return ($ip, $family);
+ return wantarray ? ($ip, $family) : $ip;
}
}
# fallback: try to get IP by other means
- my ($family, $packed_ip);
-
- eval {
- my @res = PVE::Tools::getaddrinfo_all($nodename);
- $family = $res[0]->{family};
- $packed_ip = (PVE::Tools::unpack_sockaddr_in46($res[0]->{addr}))[2];
- };
-
- if ($@) {
- die "hostname lookup failed:\n$@" if !$noerr;
- return undef;
- }
-
- my $ip = Socket::inet_ntop($family, $packed_ip);
- if ($ip =~ m/^127\.|^::1$/) {
- die "hostname lookup failed - got local IP address ($nodename = $ip)\n" if !$noerr;
- return undef;
- }
-
- return wantarray ? ($ip, $family) : $ip;
+ return PVE::Network::get_ip_from_hostname($nodename, $noerr);
}
sub get_local_migration_ip {
if (! -f $rootsshconfig) {
mkdir '/root/.ssh';
if (my $fh = IO::File->new($rootsshconfig, O_CREAT|O_WRONLY|O_EXCL, 0640)) {
- # this is the default ciphers list from debian openssl0.9.8 except blowfish is added as prefered
- print $fh "Ciphers blowfish-cbc,aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc\n";
+ # this is the default ciphers list from Debian's OpenSSH package (OpenSSH_7.4p1 Debian-10, OpenSSL 1.0.2k 26 Jan 2017)
+ # changed order to put AES before Chacha20 (most hardware has AESNI)
+ print $fh "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm\@openssh.com,aes256-gcm\@openssh.com,chacha20-poly1305\@openssh.com\n";
close($fh);
}
}
die "no node name specified" if !$nodename;
die "no ip address specified" if !$ip_address;
+ # ssh lowercases hostnames (aliases) before comparision, so we need too
+ $nodename = lc($nodename);
+ $ip_address = lc($ip_address);
+
mkdir $authdir;
if (! -f $sshknownhosts) {
my $merge_line = sub {
my ($line, $all) = @_;
+ return if $line =~ m/^\s*$/; # skip empty lines
+ return if $line =~ m/^#/; # skip comments
+
if ($line =~ m/^(\S+)\s(ssh-rsa\s\S+)(\s.*)?$/) {
my $key = $1;
my $rsakey = $2;
}
return;
}
+ } else {
+ $key = lc($key); # avoid duplicate entries, ssh compares lowercased
+ if ($key eq $ip_address) {
+ $found_local_ip = 1 if $rsakey eq $hostkey;
+ } elsif ($key eq $nodename) {
+ $found_nodename = 1 if $rsakey eq $hostkey;
+ }
}
$data .= $line;
}
while ($old && $old =~ s/^((.*?)(\n|$))//) {
my $line = "$2\n";
- next if $line =~ m/^\s*$/; # skip empty lines
- next if $line =~ m/^#/; # skip comments
&$merge_line($line, 1);
}
while ($new && $new =~ s/^((.*?)(\n|$))//) {
my $line = "$2\n";
- next if $line =~ m/^\s*$/; # skip empty lines
- next if $line =~ m/^#/; # skip comments
&$merge_line($line);
}
- my $addIndex = $$;
- my $add_known_hosts_entry = sub {
- my ($name, $hostkey) = @_;
- $addIndex++;
- my $hmac = Digest::HMAC_SHA1->new("$addIndex" . time());
- my $b64salt = $hmac->b64digest . '=';
- $hmac = Digest::HMAC_SHA1->new(decode_base64($b64salt));
- $hmac->add($name);
- my $digest = $hmac->b64digest . '=';
- $data .= "|1|$b64salt|$digest $hostkey\n";
- };
-
- if (!$found_nodename || !$found_local_ip) {
- &$add_known_hosts_entry($nodename, $hostkey) if !$found_nodename;
- &$add_known_hosts_entry($ip_address, $hostkey) if !$found_local_ip;
- }
+ # add our own key if not already there
+ $data .= "$nodename $hostkey\n" if !$found_nodename;
+ $data .= "$ip_address $hostkey\n" if !$found_local_ip;
PVE::Tools::file_set_contents($sshknownhosts, $data);
\&parse_datacenter_config,
\&write_datacenter_config);
-# a very simply parser ...
-sub parse_corosync_conf {
- my ($filename, $raw) = @_;
-
- return {} if !$raw;
-
- my $digest = Digest::SHA::sha1_hex(defined($raw) ? $raw : '');
-
- $raw =~ s/#.*$//mg;
- $raw =~ s/\r?\n/ /g;
- $raw =~ s/\s+/ /g;
- $raw =~ s/^\s+//;
- $raw =~ s/\s*$//;
-
- my @tokens = split(/\s/, $raw);
-
- my $conf = { section => 'main', children => [] };
-
- my $stack = [];
- my $section = $conf;
-
- while (defined(my $token = shift @tokens)) {
- my $nexttok = $tokens[0];
-
- if ($nexttok && ($nexttok eq '{')) {
- shift @tokens; # skip '{'
- my $new_section = {
- section => $token,
- children => [],
- };
- push @{$section->{children}}, $new_section;
- push @$stack, $section;
- $section = $new_section;
- next;
- }
-
- if ($token eq '}') {
- $section = pop @$stack;
- die "parse error - uncexpected '}'\n" if !$section;
- next;
- }
-
- my $key = $token;
- die "missing ':' after key '$key'\n" if ! ($key =~ s/:$//);
-
- die "parse error - no value for '$key'\n" if !defined($nexttok);
- my $value = shift @tokens;
-
- push @{$section->{children}}, { key => $key, value => $value };
- }
-
- $conf->{digest} = $digest;
-
- return $conf;
-}
-
-my $dump_corosync_section;
-$dump_corosync_section = sub {
- my ($section, $prefix) = @_;
-
- my $raw = $prefix . $section->{section} . " {\n";
-
- my @list = grep { defined($_->{key}) } @{$section->{children}};
- foreach my $child (sort {$a->{key} cmp $b->{key}} @list) {
- $raw .= $prefix . " $child->{key}: $child->{value}\n";
- }
-
- @list = grep { defined($_->{section}) } @{$section->{children}};
- foreach my $child (sort {$a->{section} cmp $b->{section}} @list) {
- $raw .= &$dump_corosync_section($child, "$prefix ");
- }
-
- $raw .= $prefix . "}\n\n";
-
- return $raw;
-
-};
-
-sub write_corosync_conf {
- my ($filename, $conf) = @_;
-
- my $raw = '';
-
- my $prefix = '';
-
- die "no main section" if $conf->{section} ne 'main';
-
- my @list = grep { defined($_->{key}) } @{$conf->{children}};
- foreach my $child (sort {$a->{key} cmp $b->{key}} @list) {
- $raw .= "$child->{key}: $child->{value}\n";
- }
-
- @list = grep { defined($_->{section}) } @{$conf->{children}};
- foreach my $child (sort {$a->{section} cmp $b->{section}} @list) {
- $raw .= &$dump_corosync_section($child, $prefix);
- }
-
- return $raw;
-}
-
-sub corosync_conf_version {
- my ($conf, $noerr, $new_value) = @_;
-
- foreach my $child (@{$conf->{children}}) {
- next if !defined($child->{section});
- if ($child->{section} eq 'totem') {
- foreach my $e (@{$child->{children}}) {
- next if !defined($e->{key});
- if ($e->{key} eq 'config_version') {
- if ($new_value) {
- $e->{value} = $new_value;
- return $new_value;
- } elsif (my $version = int($e->{value})) {
- return $version;
- }
- last;
- }
- }
- }
- }
-
- return undef if $noerr;
-
- die "invalid corosync config - unable to read version\n";
-}
-
-# read only - use "rename corosync.conf.new corosync.conf" to write
-PVE::Cluster::cfs_register_file('corosync.conf', \&parse_corosync_conf);
-# this is read/write
-PVE::Cluster::cfs_register_file('corosync.conf.new', \&parse_corosync_conf,
- \&write_corosync_conf);
-
-sub check_corosync_conf_exists {
- my ($silent) = @_;
-
- $silent = $silent // 0;
-
- my $exists = -f "$basedir/corosync.conf";
-
- warn "Corosync config '$basedir/corosync.conf' does not exist - is this node part of a cluster?\n"
- if !$silent && !$exists;
-
- return $exists;
-}
-
-sub corosync_update_nodelist {
- my ($conf, $nodelist) = @_;
-
- delete $conf->{digest};
-
- my $version = corosync_conf_version($conf);
- corosync_conf_version($conf, undef, $version + 1);
-
- my $children = [];
- foreach my $v (values %$nodelist) {
- next if !($v->{ring0_addr} || $v->{name});
- my $kv = [];
- foreach my $k (keys %$v) {
- push @$kv, { key => $k, value => $v->{$k} };
- }
- my $ns = { section => 'node', children => $kv };
- push @$children, $ns;
- }
-
- foreach my $main (@{$conf->{children}}) {
- next if !defined($main->{section});
- if ($main->{section} eq 'nodelist') {
- $main->{children} = $children;
- last;
- }
- }
-
-
- cfs_write_file("corosync.conf.new", $conf);
-
- rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
- || die "activate corosync.conf.new failed - $!\n";
-}
-
-sub corosync_nodelist {
- my ($conf) = @_;
-
- my $nodelist = {};
-
- foreach my $main (@{$conf->{children}}) {
- next if !defined($main->{section});
- if ($main->{section} eq 'nodelist') {
- foreach my $ne (@{$main->{children}}) {
- next if !defined($ne->{section}) || ($ne->{section} ne 'node');
- my $node = { quorum_votes => 1 };
- my $name;
- foreach my $child (@{$ne->{children}}) {
- next if !defined($child->{key});
- $node->{$child->{key}} = $child->{value};
- # use 'name' over 'ring0_addr' if set
- if ($child->{key} eq 'name') {
- delete $nodelist->{$name} if $name;
- $name = $child->{value};
- $nodelist->{$name} = $node;
- } elsif(!$name && $child->{key} eq 'ring0_addr') {
- $name = $child->{value};
- $nodelist->{$name} = $node;
- }
- }
- }
- }
- }
-
- return $nodelist;
-}
-
-# get a hash representation of the corosync config totem section
-sub corosync_totem_config {
- my ($conf) = @_;
-
- my $res = {};
-
- foreach my $main (@{$conf->{children}}) {
- next if !defined($main->{section}) ||
- $main->{section} ne 'totem';
-
- foreach my $e (@{$main->{children}}) {
-
- if ($e->{section} && $e->{section} eq 'interface') {
- my $entry = {};
-
- $res->{interface} = {};
-
- foreach my $child (@{$e->{children}}) {
- next if !defined($child->{key});
- $entry->{$child->{key}} = $child->{value};
- if($child->{key} eq 'ringnumber') {
- $res->{interface}->{$child->{value}} = $entry;
- }
- }
-
- } elsif ($e->{key}) {
- $res->{$e->{key}} = $e->{value};
- }
- }
- }
-
- return $res;
-}
-
# X509 Certificate cache helper
my $cert_cache_nodes = {};
return $res;
}
+sub get_ssh_info {
+ my ($node, $network_cidr) = @_;
+
+ my $ip;
+ if (defined($network_cidr)) {
+ # Use mtunnel via to get the remote node's ip inside $network_cidr.
+ # This goes over the regular network (iow. uses get_ssh_info() with
+ # $network_cidr undefined.
+ # FIXME: Use the REST API client for this after creating an API entry
+ # for get_migration_ip.
+ my $default_remote = get_ssh_info($node, undef);
+ my $default_ssh = ssh_info_to_command($default_remote);
+ my $cmd =[@$default_ssh, 'pvecm', 'mtunnel',
+ '-migration_network', $network_cidr,
+ '-get_migration_ip'
+ ];
+ PVE::Tools::run_command($cmd, outfunc => sub {
+ my ($line) = @_;
+ chomp $line;
+ die "internal error: unexpected output from mtunnel\n"
+ if defined($ip);
+ if ($line =~ /^ip: '(.*)'$/) {
+ $ip = $1;
+ } else {
+ die "internal error: bad output from mtunnel\n"
+ if defined($ip);
+ }
+ });
+ die "failed to get ip for node '$node' in network '$network_cidr'\n"
+ if !defined($ip);
+ } else {
+ $ip = remote_node_ip($node);
+ }
+
+ return {
+ ip => $ip,
+ name => $node,
+ network => $network_cidr,
+ };
+}
+
+sub ssh_info_to_command_base {
+ my ($info, @extra_options) = @_;
+ return [
+ '/usr/bin/ssh',
+ '-o', 'BatchMode=yes',
+ '-o', 'HostKeyAlias='.$info->{name},
+ @extra_options
+ ];
+}
+
+sub ssh_info_to_command {
+ my ($info, @extra_options) = @_;
+ my $cmd = ssh_info_to_command_base($info, @extra_options);
+ push @$cmd, "root\@$info->{ip}";
+ return $cmd;
+}
+
1;