package PVE::Cluster;
use strict;
-use POSIX;
+use warnings;
+use POSIX qw(EEXIST);
use File::stat qw();
use Socket;
use Storable qw(dclone);
use IO::File;
use MIME::Base64;
use XML::Parser;
-use Digest::SHA1;
+use Digest::SHA;
use Digest::HMAC_SHA1;
use PVE::Tools;
use PVE::INotify;
use PVE::IPCC;
use PVE::SafeSyslog;
+use PVE::JSONSchema;
use JSON;
use RRDs;
use Encode;
my $sshglobalknownhosts = "/etc/ssh/ssh_known_hosts";
my $sshknownhosts = "/etc/pve/priv/known_hosts";
my $sshauthkeys = "/etc/pve/priv/authorized_keys";
+my $sshd_config_fn = "/etc/ssh/sshd_config";
my $rootsshauthkeys = "/root/.ssh/authorized_keys";
+my $rootsshauthkeysbackup = "${rootsshauthkeys}.org";
+my $rootsshconfig = "/root/.ssh/config";
my $observed = {
'vzdump.cron' => 1,
'storage.cfg' => 1,
'datacenter.cfg' => 1,
- 'cluster.conf' => 1,
- 'cluster.conf.new' => 1,
+ 'corosync.conf' => 1,
+ 'corosync.conf.new' => 1,
'user.cfg' => 1,
'domains.cfg' => 1,
'priv/shadow.cfg' => 1,
'/qemu-server/' => 1,
'/openvz/' => 1,
+ '/lxc/' => 1,
+ 'ha/crm_commands' => 1,
+ 'ha/manager_status' => 1,
+ 'ha/resources.cfg' => 1,
+ 'ha/groups.cfg' => 1,
+ 'ha/fence.cfg' => 1,
+ 'status.cfg' => 1,
};
# only write output if something fails
"$basedir/priv",
"$basedir/nodes",
"$basedir/nodes/$nodename",
+ "$basedir/nodes/$nodename/lxc",
"$basedir/nodes/$nodename/qemu-server",
"$basedir/nodes/$nodename/openvz",
"$basedir/nodes/$nodename/priv");
foreach my $dir (@required_dirs) {
if (! -d $dir) {
- mkdir($dir) || die "unable to create directory '$dir' - $!\n";
+ mkdir($dir) || $! == EEXIST || die "unable to create directory '$dir' - $!\n";
}
}
}
check_cfs_is_mounted();
- mkdir $authdir || die "unable to create dir '$authdir' - $!\n";
+ mkdir $authdir || $! == EEXIST || die "unable to create dir '$authdir' - $!\n";
my $cmd = "openssl genrsa -out '$authprivkeyfn' 2048";
run_silent_cmd($cmd);
return if -f $pveca_key_fn;
eval {
- run_silent_cmd(['openssl', 'genrsa', '-out', $pveca_key_fn, '2048']);
+ run_silent_cmd(['openssl', 'genrsa', '-out', $pveca_key_fn, '4096']);
};
die "unable to generate pve ca key:\n$@" if $@;
my $nid = (split (/\s/, `md5sum '$pveca_key_fn'`))[0] || time();
eval {
- run_silent_cmd(['openssl', 'req', '-batch', '-days', '3650', '-new',
- '-x509', '-nodes', '-key',
+ # wrap openssl with faketime to prevent bug #904
+ run_silent_cmd(['faketime', 'yesterday', 'openssl', 'req', '-batch',
+ '-days', '3650', '-new', '-x509', '-nodes', '-key',
$pveca_key_fn, '-out', $pveca_cert_fn, '-subj',
"/CN=Proxmox Virtual Environment/OU=$nid/O=PVE Cluster Manager CA/"]);
};
return if !$force && -f $pvessl_cert_fn;
- my $names = "IP:127.0.0.1,DNS:localhost";
+ my $names = "IP:127.0.0.1,IP:::1,DNS:localhost";
my $rc = PVE::INotify::read_file('resolvconf');
[ v3_req ]
basicConstraints = CA:FALSE
-nsCertType = server
-keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+extendedKeyUsage = serverAuth
subjectAltName = $names
__EOD
update_serial("0000000000000000") if ! -f $pveca_srl_fn;
eval {
- run_silent_cmd(['openssl', 'x509', '-req', '-in', $reqfn, '-days', '3650',
- '-out', $pvessl_cert_fn, '-CAkey', $pveca_key_fn,
- '-CA', $pveca_cert_fn, '-CAserial', $pveca_srl_fn,
- '-extfile', $cfgfn]);
+ # wrap openssl with faketime to prevent bug #904
+ run_silent_cmd(['faketime', 'yesterday', 'openssl', 'x509', '-req',
+ '-in', $reqfn, '-days', '3650', '-out', $pvessl_cert_fn,
+ '-CAkey', $pveca_key_fn, '-CA', $pveca_cert_fn,
+ '-CAserial', $pveca_srl_fn, '-extfile', $cfgfn]);
};
if (my $err = $@) {
eval {
my $ver = $kvstore->{$node}->{tasklist} if $kvstore->{$node};
my $cd = $tasklistcache->{$node};
- if (!$cd || !$ver || ($cd->{version} != $ver)) {
+ if (!$cd || !$ver || !$cd->{version} ||
+ ($cd->{version} != $ver)) {
my $raw = &$ipcc_get_status("tasklist", $node) || '[]';
my $data = decode_json($raw);
push @$res, @$data;
my $res = {};
- while ($raw =~ s/^(.*)\n//) {
- my ($key, @ela) = split(/:/, $1);
- next if !$key;
- next if !(scalar(@ela) > 1);
- $res->{$key} = \@ela;
+ if ($raw) {
+ while ($raw =~ s/^(.*)\n//) {
+ my ($key, @ela) = split(/:/, $1);
+ next if !$key;
+ next if !(scalar(@ela) > 1);
+ $res->{$key} = \@ela;
+ }
}
$last_rrd_dump = $ctime;
for my $line (@$data) {
my $entry = { 'time' => $start };
$start += $step;
- my $found_undefs;
for (my $i = 0; $i < $fields; $i++) {
my $name = $names->[$i];
if (defined(my $val = $line->[$i])) {
$entry->{$name} = $val;
} else {
- # we only add entryies with all data defined
- # extjs chart has problems with undefined values
- $found_undefs = 1;
+ # leave empty fields undefined
+ # maybe make this configurable?
}
}
- push @$res, $entry if !$found_undefs;
+ push @$res, $entry;
}
return $res;
my $rrd = "$rrddir/$rrdname";
- my $filename = "$rrd.png";
+ my @ids = PVE::Tools::split_list($ds);
+
+ my $ds_txt = join('_', @ids);
+
+ my $filename = "${rrd}_${ds_txt}.png";
my $setup = {
hour => [ 60, 60 ],
"--width" => 800,
"--start" => - $reso*$count,
"--end" => 'now' ,
+ "--lower-limit" => 0,
);
my $socket = "/var/run/rrdcached.sock";
push @args, "--daemon" => "unix:$socket" if -S $socket;
- my @ids = PVE::Tools::split_list($ds);
-
my @coldef = ('#00ddff', '#ff0000');
$cf = "AVERAGE" if !$cf;
push @args, '--full-size-mode';
- RRDs::graph($filename, @args);
+ # we do not really store data into the file
+ my $res = RRDs::graphv('', @args);
my $err = RRDs::error;
die "RRD error: $err\n" if $err;
- return { filename => $filename };
+ return { filename => $filename, image => $res->{image} };
}
# a fast way to read files (avoid fuse overhead)
my $version;
my $infotag;
- if ($filename =~ m!^nodes/[^/]+/(openvz|qemu-server)/(\d+)\.conf$!) {
+ if ($filename =~ m!^nodes/[^/]+/(openvz|lxc|qemu-server)/(\d+)\.conf$!) {
my ($type, $vmid) = ($1, $2);
if ($vmlist && $vmlist->{ids} && $vmlist->{ids}->{$vmid}) {
$version = $vmlist->{ids}->{$vmid}->{version};
sub cfs_write_file {
my ($filename, $data) = @_;
- my $info = $file_info->{$filename} || die "unknown file '$filename'";
+ my ($version, $info) = cfs_file_version($filename);
my $writer = $info->{writer} || die "no writer defined";
alarm(60);
local $SIG{ALRM} = sub { die "got lock timeout - aborting command\n"; };
+ cfs_update(); # make sure we read latest versions inside code()
+
$res = &$code(@param);
alarm(0);
&$cfs_lock($lockid, $timeout, $code, @param);
}
+sub cfs_lock_domain {
+ my ($domainname, $timeout, $code, @param) = @_;
+
+ my $lockid = "domain-$domainname";
+
+ &$cfs_lock($lockid, $timeout, $code, @param);
+}
+
my $log_levels = {
"emerg" => 0,
"alert" => 1,
$msg = "empty message" if !$msg;
$ident = "" if !$ident;
- $ident = encode("ascii", decode_utf8($ident),
+ $ident = encode("ascii", $ident,
sub { sprintf "\\u%04x", shift });
- my $utf8 = decode_utf8($msg);
-
- my $ascii = encode("ascii", $utf8, sub { sprintf "\\u%04x", shift });
+ my $ascii = encode("ascii", $msg, sub { sprintf "\\u%04x", shift });
if ($ident) {
syslog($priority, "<%s> %s", $ident, $ascii);
syslog("err", "writing cluster log failed: $@") if $@;
}
+sub check_vmid_unused {
+ my ($vmid, $noerr) = @_;
+
+ my $vmlist = get_vmlist();
+
+ my $d = $vmlist->{ids}->{$vmid};
+ return 1 if !defined($d);
+
+ return undef if $noerr;
+
+ my $vmtypestr = $d->{type} eq 'qemu' ? 'VM' : 'CT';
+ die "$vmtypestr $vmid already exists on node '$d->{node}'\n";
+}
+
sub check_node_exists {
my ($nodename, $noerr) = @_;
my $nodelist = $clinfo->{nodelist};
if ($nodelist && $nodelist->{$nodename}) {
if (my $ip = $nodelist->{$nodename}->{ip}) {
- return $ip;
+ return $ip if !wantarray;
+ my $family = $nodelist->{$nodename}->{address_family};
+ if (!$family) {
+ $nodelist->{$nodename}->{address_family} =
+ $family =
+ PVE::Tools::get_host_address_family($ip);
+ }
+ return ($ip, $family);
}
}
# fallback: try to get IP by other means
- my $packed_ip = gethostbyname($nodename);
- if (defined $packed_ip) {
- my $ip = inet_ntoa($packed_ip);
+ my ($family, $packed_ip);
- if ($ip =~ m/^127\./) {
- die "hostname lookup failed - got local IP address ($nodename = $ip)\n" if !$noerr;
- return undef;
- }
+ eval {
+ my @res = PVE::Tools::getaddrinfo_all($nodename);
+ $family = $res[0]->{family};
+ $packed_ip = (PVE::Tools::unpack_sockaddr_in46($res[0]->{addr}))[2];
+ };
- return $ip;
+ if ($@) {
+ die "hostname lookup failed:\n$@" if !$noerr;
+ return undef;
}
- die "unable to get IP for node '$nodename' - node offline?\n" if !$noerr;
+ 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 undef;
+ return wantarray ? ($ip, $family) : $ip;
}
# ssh related utility functions
chomp($data);
}
+ my $found_backup;
+ if (-f $rootsshauthkeysbackup) {
+ $data .= "\n";
+ $data .= PVE::Tools::file_get_contents($rootsshauthkeysbackup, 128*1024);
+ chomp($data);
+ $found_backup = 1;
+ }
+
# always add ourself
if (-f $ssh_rsa_id) {
my $pub = PVE::Tools::file_get_contents($ssh_rsa_id);
my $newdata = "";
my $vhash = {};
- while ($data && $data =~ s/^((.*?)(\n|$))//) {
- my $line = "$2\n";
- if ($line =~ m/^ssh-rsa\s+\S+\s+(\S+)$/) {
- $vhash->{$1} = $line;
- } else {
- $newdata .= $line;
+ my @lines = split(/\n/, $data);
+ foreach my $line (@lines) {
+ if ($line !~ /^#/ && $line =~ m/(^|\s)ssh-(rsa|dsa)\s+(\S+)\s+\S+$/) {
+ next if $vhash->{$3}++;
}
+ $newdata .= "$line\n";
}
-
- $newdata .= join("", values(%$vhash));
PVE::Tools::file_set_contents($sshauthkeys, $newdata, 0600);
+
+ if ($found_backup && -l $rootsshauthkeys) {
+ # everything went well, so we can remove the backup
+ unlink $rootsshauthkeysbackup;
+ }
}
-sub setup_ssh_keys {
+sub setup_sshd_config {
+
+ my $conf = PVE::Tools::file_get_contents($sshd_config_fn);
+
+ return if $conf =~ m/^PermitRootLogin\s+yes\s*$/m;
+
+ if ($conf !~ s/^#?PermitRootLogin.*$/PermitRootLogin yes/m) {
+ chomp $conf;
+ $conf .= "\nPermitRootLogin yes\n";
+ }
+
+ PVE::Tools::file_set_contents($sshd_config_fn, $conf);
+
+ PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'sshd']);
+}
+
+sub setup_rootsshconfig {
# create ssh key if it does not exist
if (! -f $ssh_rsa_id) {
system ("echo|ssh-keygen -t rsa -N '' -b 2048 -f ${ssh_rsa_id_priv}");
}
+ # create ssh config if it does not exist
+ 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";
+ close($fh);
+ }
+ }
+}
+
+sub setup_ssh_keys {
+
mkdir $authdir;
+ my $import_ok;
+
if (! -f $sshauthkeys) {
+ my $old;
+ if (-f $rootsshauthkeys) {
+ $old = PVE::Tools::file_get_contents($rootsshauthkeys, 128*1024);
+ }
if (my $fh = IO::File->new ($sshauthkeys, O_CREAT|O_WRONLY|O_EXCL, 0400)) {
+ PVE::Tools::safe_print($sshauthkeys, $fh, $old) if $old;
close($fh);
+ $import_ok = 1;
}
}
warn "can't create shared ssh key database '$sshauthkeys'\n"
if ! -f $sshauthkeys;
- if (-f $rootsshauthkeys) {
- system("mv '$rootsshauthkeys' '$rootsshauthkeys.org'");
+ if (-f $rootsshauthkeys && ! -l $rootsshauthkeys) {
+ if (!rename($rootsshauthkeys , $rootsshauthkeysbackup)) {
+ warn "rename $rootsshauthkeys failed - $!\n";
+ }
}
if (! -l $rootsshauthkeys) {
symlink $sshauthkeys, $rootsshauthkeys;
}
- warn "can't create symlink for ssh keys '$rootsshauthkeys' -> '$sshauthkeys'\n"
- if ! -l $rootsshauthkeys;
+ if (! -l $rootsshauthkeys) {
+ warn "can't create symlink for ssh keys '$rootsshauthkeys' -> '$sshauthkeys'\n";
+ } else {
+ unlink $rootsshauthkeysbackup if $import_ok;
+ }
}
sub ssh_unmerge_known_hosts {
}
my $hostkey = PVE::Tools::file_get_contents($ssh_host_rsa_id);
- die "can't parse $ssh_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/;
+ # Note: file sometimes containe emty lines at start, so we use multiline match
+ die "can't parse $ssh_host_rsa_id" if $hostkey !~ m/^(ssh-rsa\s\S+)(\s.*)?$/m;
$hostkey = $1;
my $data = '';
description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
pattern => "http://.*",
},
+ migration_unsecure => {
+ optional => 1,
+ type => 'boolean',
+ description => "Migration is secure using SSH tunnel by default. For secure private networks you can disable it to speed up migration.",
+ },
+ console => {
+ optional => 1,
+ type => 'string',
+ description => "Select the default Console viewer. You can either use the builtin java applet (VNC), an external virt-viewer comtatible application (SPICE), or an HTML5 based viewer (noVNC).",
+ enum => ['applet', 'vv', 'html5'],
+ },
+ email_from => {
+ optional => 1,
+ type => 'string',
+ format => 'email-opt',
+ description => "Specify email address to send notification from (default is root@\$hostname)",
+ },
+ max_workers => {
+ optional => 1,
+ type => 'integer',
+ minimum => 1,
+ description => "Defines how many workers (per node) are maximal started ".
+ " on actions like 'stopall VMs' or task from the ha-manager.",
+ },
+ fencing => {
+ optional => 1,
+ type => 'string',
+ default => 'watchdog',
+ enum => [ 'watchdog', 'hardware', 'both' ],
+ description => "Set the fencing mode of the HA cluster. Hardware mode " .
+ "needs a valid configuration of fence devices in /etc/pve/ha/fence.cfg." .
+ " With both all two modes are used. " .
+ " NOTE: 'hardware' and 'both' are EXPERIMENTAL & WIP",
+ },
},
};
sub parse_datacenter_config {
my ($filename, $raw) = @_;
- return PVE::JSONSchema::parse_config($datacenter_schema, $filename, $raw);
+ return PVE::JSONSchema::parse_config($datacenter_schema, $filename, $raw // '');
}
sub write_datacenter_config {
\&parse_datacenter_config,
\&write_datacenter_config);
-sub parse_cluster_conf {
+# a very simply parser ...
+sub parse_corosync_conf {
my ($filename, $raw) = @_;
- my $conf = {};
+ return {} if !$raw;
- my $digest = Digest::SHA1::sha1_hex(defined($raw) ? $raw : '');
+ my $digest = Digest::SHA::sha1_hex(defined($raw) ? $raw : '');
- my $createNode = sub {
- my ($expat, $tag, %attrib) = @_;
- $expat->{NodeCount}++;
- return { text => $tag, id => $expat->{NodeCount}, %attrib };
- };
+ $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 $handlers = {
- Init => sub {
- my $expat = shift;
- $expat->{NodeCount} = 0;
- $expat->{NodeStack} = [];
- $expat->{CurNode} = $expat->{Tree} = &$createNode($expat, 'root');
- },
- Final => sub {
- my $expat = shift;
- delete $expat->{CurNode};
- delete $expat->{NodeStack};
- $expat->{Tree};
- },
- Start => sub {
- my $expat = shift;
- my $tag = shift;
- my $parent = $expat->{CurNode};
- push @{ $expat->{NodeStack} }, $parent;
- my $node = &$createNode($expat, $tag, @_);
- push @{$expat->{CurNode}->{children}}, $node;
- $expat->{CurNode} = $node;
- },
- End => sub {
- my $expat = shift;
- my $tag = shift;
- my $node = pop @{ $expat->{NodeStack} };
- $expat->{CurNode} = $node;
- },
- };
-
- if ($raw) {
- my $parser = new XML::Parser(Handlers => $handlers);
- $conf = $parser->parse($raw);
+ 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;
}
-sub cluster_conf_version {
- my ($conf, $noerr) = @_;
+my $dump_corosync_section;
+$dump_corosync_section = sub {
+ my ($section, $prefix) = @_;
- if ($conf && $conf->{children} && $conf->{children}->[0]) {
- my $cluster = $conf->{children}->[0];
- if ($cluster && ($cluster->{text} eq 'cluster') &&
- $cluster->{config_version}) {
- if (my $version = int($cluster->{config_version})) {
- return wantarray ? ($version, $cluster) : $version;
- }
- }
+ 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 ");
}
- return undef if $noerr;
+ $raw .= $prefix . "}\n\n";
+
+ return $raw;
+
+};
- die "no cluster config - unable to read version\n";
-}
+sub write_corosync_conf {
+ my ($filename, $conf) = @_;
-sub cluster_conf_lookup_cluster_section {
- my ($conf, $noerr) = @_;
+ my $raw = '';
- my ($version, $cluster) = cluster_conf_version($conf, $noerr);
+ my $prefix = '';
+
+ die "no main section" if $conf->{section} ne 'main';
- return $cluster;
-}
+ my @list = grep { defined($_->{key}) } @{$conf->{children}};
+ foreach my $child (sort {$a->{key} cmp $b->{key}} @list) {
+ $raw .= "$child->{key}: $child->{value}\n";
+ }
-sub cluster_conf_lookup_rm_section {
- my ($conf, $create, $noerr) = @_;
+ @list = grep { defined($_->{section}) } @{$conf->{children}};
+ foreach my $child (sort {$a->{section} cmp $b->{section}} @list) {
+ $raw .= &$dump_corosync_section($child, $prefix);
+ }
- my $cluster = cluster_conf_lookup_cluster_section($conf, $noerr);
- return undef if !$cluster;
+ return $raw;
+}
- my $rmsec;
- foreach my $child (@{$cluster->{children}}) {
- if ($child->{text} eq 'rm') {
- $rmsec = $child;
- }
- }
- if (!$rmsec) {
- if (!$create) {
- return undef if $noerr;
- die "no resource manager section\n";
+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;
+ }
+ }
}
- $rmsec = { text => 'rm' };
- push @{$cluster->{children}}, $rmsec;
}
+
+ return undef if $noerr;
- return $rmsec;
+ die "invalid corosync config - unable to read version\n";
}
-sub cluster_conf_lookup_pvevm {
- my ($conf, $create, $vmid, $noerr) = @_;
+# 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);
- my $rmsec = cluster_conf_lookup_rm_section($conf, $create, $noerr);
- return undef if !$rmsec;
+sub check_corosync_conf_exists {
+ my ($silent) = @_;
- my $vmref;
- foreach my $child (@{$rmsec->{children}}) {
- if ($child->{text} eq 'pvevm' && $child->{vmid} eq $vmid) {
- $vmref = $child;
- }
- }
+ $silent = $silent // 0;
- if (!$vmref) {
- if (!$create) {
- return undef if $noerr;
- die "unable to find service 'pvevm:$vmid'\n";
- }
- $vmref = { text => 'pvevm', vmid => $vmid };
- push @{$rmsec->{children}}, $vmref;
- } elsif ($create) {
- return undef if $noerr;
- die "unable to create service 'pvevm:$vmid' - already exists\n";
- }
+ 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 $vmref;
+ return $exists;
}
-sub xml_escape_attrib {
- my ($data) = @_;
+# bash completion helpers
+
+sub complete_next_vmid {
- return '' if !defined($data);
+ my $vmlist = get_vmlist() || {};
+ my $idlist = $vmlist->{ids} || {};
- $data =~ s/&/&/sg;
- $data =~ s/</</sg;
- $data =~ s/>/>/sg;
- $data =~ s/"/"/sg;
+ for (my $i = 100; $i < 10000; $i++) {
+ return [$i] if !defined($idlist->{$i});
+ }
- return $data;
+ return [];
}
-sub __cluster_conf_dump_node {
- my ($node, $indend) = @_;
+sub complete_vmid {
+
+ my $vmlist = get_vmlist();
+ my $ids = $vmlist->{ids} || {};
- my $xml = '';
+ return [ keys %$ids ];
+}
- $indend = '' if !defined($indend);
+sub complete_local_vmid {
- my $attribs = '';
+ my $vmlist = get_vmlist();
+ my $ids = $vmlist->{ids} || {};
- foreach my $key (sort keys %$node) {
- my $value = $node->{$key};
- next if $key eq 'id' || $key eq 'text' || $key eq 'children';
- $attribs .= " $key=\"" . xml_escape_attrib($value) . "\"";
- }
-
- my $children = $node->{children};
+ my $nodename = PVE::INotify::nodename();
- if ($children && scalar(@$children)) {
- $xml .= "$indend<$node->{text}$attribs>\n";
- my $childindend = "$indend ";
- foreach my $child (@$children) {
- $xml .= __cluster_conf_dump_node($child, $childindend);
- }
- $xml .= "$indend</$node->{text}>\n";
- } else {
- $xml .= "$indend<$node->{text}$attribs/>\n";
+ my $res = [];
+ foreach my $vmid (keys %$ids) {
+ my $d = $ids->{$vmid};
+ next if !$d->{node} || $d->{node} ne $nodename;
+ push @$res, $vmid;
}
- return $xml;
+ return $res;
}
-sub write_cluster_conf {
- my ($filename, $cfg) = @_;
+sub complete_migration_target {
- my $version = cluster_conf_version($cfg);
-
- my $res = "<?xml version=\"1.0\"?>\n";
+ my $res = [];
+
+ my $nodename = PVE::INotify::nodename();
- $res .= __cluster_conf_dump_node($cfg->{children}->[0]);
+ my $nodelist = get_nodelist();
+ foreach my $node (@$nodelist) {
+ next if $node eq $nodename;
+ push @$res, $node;
+ }
return $res;
}
-# read only - use "rename cluster.conf.new cluster.conf" to write
-PVE::Cluster::cfs_register_file('cluster.conf', \&parse_cluster_conf);
-# this is read/write
-PVE::Cluster::cfs_register_file('cluster.conf.new', \&parse_cluster_conf,
- \&write_cluster_conf);
+1;