1 package PVE
::CLI
::pvecm
;
11 use Data
::Dumper
; # fixme: remove
18 use base
qw(PVE::CLIHandler);
20 $ENV{HOME
} = '/root'; # for ssh-copy-id
22 my $basedir = "/etc/pve";
23 my $clusterconf = "$basedir/corosync.conf";
24 my $libdir = "/var/lib/pve-cluster";
25 my $backupdir = "/var/lib/pve-cluster/backup";
26 my $dbfile = "$libdir/config.db";
27 my $authfile = "/etc/corosync/authkey";
31 print "backup old database\n";
39 ['gzip', '-', \
">${backupdir}/config-${ctime}.sql.gz"],
42 PVE
::Tools
::run_command
($cmd, 'errmsg' => "cannot backup old database\n");
48 foreach my $fn (<$backupdir/config-*.sql
.gz
>) {
49 if ($fn =~ m!/config-(\d+)\.sql.gz$!) {
50 push @bklist, [$fn, $1];
54 @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
56 while (scalar (@bklist) >= $maxfiles) {
58 print "delete old backup '$d->[0]'\n";
63 __PACKAGE__-
>register_method ({
67 description
=> "Generate new cryptographic key for corosync.",
69 additionalProperties
=> 0,
73 description
=> "Output file name"
77 returns
=> { type
=> 'null' },
82 my $filename = $param->{filename
};
85 $> == 0 || die "Error: Authorization key must be generated as root user.\n";
86 my $dirname = dirname
($filename);
87 my $basename = basename
($filename);
89 die "key file '$filename' already exists\n" if -e
$filename;
91 File
::Path
::make_path
($dirname) if $dirname;
93 my $cmd = ['corosync-keygen', '-l', '-k', $filename];
94 PVE
::Tools
::run_command
($cmd);
99 __PACKAGE__-
>register_method ({
103 description
=> "Generate new cluster configuration.",
105 additionalProperties
=> 0,
108 description
=> "The name of the cluster.",
109 type
=> 'string', format
=> 'pve-node',
114 description
=> "Node id for this node.",
120 description
=> "Number of votes for this node.",
125 type
=> 'string', format
=> 'ip',
126 description
=> "This specifies the network address the corosync ring 0".
127 " executive should bind to and defaults to the local IP address of the node.",
131 type
=> 'string', format
=> 'address',
132 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
133 " Defaults to the hostname of the node.",
137 type
=> 'string', format
=> 'ip',
138 description
=> "This specifies the network address the corosync ring 1".
139 " executive should bind to and is optional.",
143 type
=> 'string', format
=> 'address',
144 description
=> "Hostname (or IP) of the corosync ring1 address, this".
145 " needs an valid bindnet1_addr.",
150 returns
=> { type
=> 'null' },
155 -f
$clusterconf && die "cluster config '$clusterconf' already exists\n";
157 PVE
::Cluster
::setup_sshd_config
(1);
158 PVE
::Cluster
::setup_rootsshconfig
();
159 PVE
::Cluster
::setup_ssh_keys
();
161 -f
$authfile || __PACKAGE__-
>keygen({filename
=> $authfile});
163 -f
$authfile || die "no authentication key available\n";
165 my $clustername = $param->{clustername
};
167 $param->{nodeid
} = 1 if !$param->{nodeid
};
169 $param->{votes
} = 1 if !defined($param->{votes
});
171 my $nodename = PVE
::INotify
::nodename
();
173 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
175 $param->{bindnet0_addr
} = $local_ip_address
176 if !defined($param->{bindnet0_addr
});
178 $param->{ring0_addr
} = $nodename if !defined($param->{ring0_addr
});
180 die "Param bindnet1_addr and ring1_addr are dependend, use both or none!\n"
181 if (defined($param->{bindnet1_addr
}) != defined($param->{ring1_addr
}));
183 my $bind_is_ipv6 = Net
::IP
::ip_is_ipv6
($param->{bindnet0_addr
});
185 # use string as here-doc format distracts more
186 my $interfaces = "interface {\n ringnumber: 0\n" .
187 " bindnetaddr: $param->{bindnet0_addr}\n }";
189 my $ring_addresses = "ring0_addr: $param->{ring0_addr}" ;
191 # allow use of multiple rings (rrp) at cluster creation time
192 if ($param->{bindnet1_addr
}) {
193 die "IPv6 and IPv4 cannot be mixed, use one or the other!\n"
194 if Net
::IP
::ip_is_ipv6
($param->{bindnet1_addr
}) != $bind_is_ipv6;
196 $interfaces .= "\n interface {\n ringnumber: 1\n" .
197 " bindnetaddr: $param->{bindnet1_addr}\n }\n";
199 $interfaces .= "rrp_mode: passive\n"; # only passive is stable and tested
201 $ring_addresses .= "\n ring1_addr: $param->{ring1_addr}";
203 } elsif($param->{rrp_mode
} && $param->{rrp_mode
} ne 'none') {
205 warn "rrp_mode '$param->{rrp_mode}' useless when using only one".
206 " ring, using 'none' instead";
207 # corosync defaults to none if only one interface is configured
208 $param->{rrp_mode
} = undef;
212 # No, corosync cannot deduce this on its own
213 my $ipversion = $bind_is_ipv6 ?
'ipv6' : 'ipv4';
219 cluster_name: $clustername
221 ip_version: $ipversion
229 nodeid: $param->{nodeid}
230 quorum_votes: $param->{votes}
235 provider: corosync_votequorum
244 PVE
::Tools
::file_set_contents
($clusterconf, $config);
246 PVE
::Cluster
::ssh_merge_keys
();
248 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address);
250 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
252 PVE
::Tools
::run_command
('systemctl restart pve-cluster'); # restart
254 PVE
::Tools
::run_command
('systemctl restart corosync'); # restart
259 __PACKAGE__-
>register_method ({
263 description
=> "Adds a node to the cluster configuration.",
265 additionalProperties
=> 0,
267 node
=> PVE
::JSONSchema
::get_standard_option
('pve-node'),
270 description
=> "Node id for this node.",
276 description
=> "Number of votes for this node",
282 description
=> "Do not throw error if node already exists.",
286 type
=> 'string', format
=> 'address',
287 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
288 " Defaults to nodes hostname.",
292 type
=> 'string', format
=> 'address',
293 description
=> "Hostname (or IP) of the corosync ring1 address, this".
294 " needs an valid bindnet1_addr.",
299 returns
=> { type
=> 'null' },
304 if (!$param->{force
} && (-t STDIN
|| -t STDOUT
)) {
305 die "error: `addnode` should not get called interactively!\nUse ".
306 "`pvecm add <cluster-node>` to add a node to a cluster!\n";
309 PVE
::Cluster
::check_cfs_quorum
();
311 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
313 my $nodelist = PVE
::Cluster
::corosync_nodelist
($conf);
315 my $totem_cfg = PVE
::Cluster
::corosync_totem_config
($conf);
317 my $name = $param->{node
};
319 # ensure we do not reuse an address, that can crash the whole cluster!
320 my $check_duplicate_addr = sub {
322 return if !defined($addr);
324 while (my ($k, $v) = each %$nodelist) {
325 next if $k eq $name; # allows re-adding a node if force is set
326 if ($v->{ring0_addr
} eq $addr || ($v->{ring1_addr
} && $v->{ring1_addr
} eq $addr)) {
327 die "corosync: address '$addr' already defined by node '$k'\n";
332 &$check_duplicate_addr($param->{ring0_addr
});
333 &$check_duplicate_addr($param->{ring1_addr
});
335 $param->{ring0_addr
} = $name if !$param->{ring0_addr
};
337 die "corosync: using 'ring1_addr' parameter needs a configured ring 1 interface!\n"
338 if $param->{ring1_addr
} && !defined($totem_cfg->{interface
}->{1});
340 die "corosync: ring 1 interface configured but 'ring1_addr' parameter not defined!\n"
341 if defined($totem_cfg->{interface
}->{1}) && !defined($param->{ring1_addr
});
343 if (defined(my $res = $nodelist->{$name})) {
344 $param->{nodeid
} = $res->{nodeid
} if !$param->{nodeid
};
345 $param->{votes
} = $res->{quorum_votes
} if !defined($param->{votes
});
347 if ($res->{quorum_votes
} == $param->{votes
} &&
348 $res->{nodeid
} == $param->{nodeid
}) {
349 print "node $name already defined\n";
350 if ($param->{force
}) {
356 die "can't add existing node\n";
358 } elsif (!$param->{nodeid
}) {
363 foreach my $v (values %$nodelist) {
364 if ($v->{nodeid
} eq $nodeid) {
373 $param->{nodeid
} = $nodeid;
376 $param->{votes
} = 1 if !defined($param->{votes
});
378 PVE
::Cluster
::gen_local_dirs
($name);
380 eval { PVE
::Cluster
::ssh_merge_keys
(); };
383 $nodelist->{$name} = {
384 ring0_addr
=> $param->{ring0_addr
},
385 nodeid
=> $param->{nodeid
},
388 $nodelist->{$name}->{ring1_addr
} = $param->{ring1_addr
} if $param->{ring1_addr
};
389 $nodelist->{$name}->{quorum_votes
} = $param->{votes
} if $param->{votes
};
391 PVE
::Cluster
::corosync_update_nodelist
($conf, $nodelist);
397 __PACKAGE__-
>register_method ({
401 description
=> "Removes a node to the cluster configuration.",
403 additionalProperties
=> 0,
407 description
=> "Hostname or IP of the corosync ring0 address of this node.",
411 returns
=> { type
=> 'null' },
416 PVE
::Cluster
::check_cfs_quorum
();
418 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
420 my $nodelist = PVE
::Cluster
::corosync_nodelist
($conf);
425 foreach my $tmp_node (keys %$nodelist) {
426 my $d = $nodelist->{$tmp_node};
427 my $ring0_addr = $d->{ring0_addr
};
428 my $ring1_addr = $d->{ring1_addr
};
429 if (($tmp_node eq $param->{node
}) ||
430 (defined($ring0_addr) && ($ring0_addr eq $param->{node
})) ||
431 (defined($ring1_addr) && ($ring1_addr eq $param->{node
}))) {
433 $nodeid = $d->{nodeid
};
438 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
441 delete $nodelist->{$node};
443 PVE
::Cluster
::corosync_update_nodelist
($conf, $nodelist);
445 PVE
::Tools
::run_command
(['corosync-cfgtool','-k', $nodeid])
451 __PACKAGE__-
>register_method ({
455 description
=> "Adds the current node to an existing cluster.",
457 additionalProperties
=> 0,
461 description
=> "Hostname (or IP) of an existing cluster member."
465 description
=> "Node id for this node.",
471 description
=> "Number of votes for this node",
477 description
=> "Do not throw error if node already exists.",
481 type
=> 'string', format
=> 'address',
482 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
483 " Defaults to nodes hostname.",
487 type
=> 'string', format
=> 'address',
488 description
=> "Hostname (or IP) of the corosync ring1 address, this".
489 " needs an valid configured ring 1 interface in the cluster.",
494 returns
=> { type
=> 'null' },
499 my $nodename = PVE
::INotify
::nodename
();
501 PVE
::Cluster
::setup_sshd_config
(1);
502 PVE
::Cluster
::setup_rootsshconfig
();
503 PVE
::Cluster
::setup_ssh_keys
();
505 my $host = $param->{hostname
};
507 my ($errors, $warnings) = ('', '');
510 my ($msg, $suppress) = @_;
513 $warnings .= "* $msg\n";
515 $errors .= "* $msg\n";
519 if (!$param->{force
}) {
522 &$error("authentication key '$authfile' already exists", $param->{force
});
525 if (-f
$clusterconf) {
526 &$error("cluster config '$clusterconf' already exists", $param->{force
});
529 my $vmlist = PVE
::Cluster
::get_vmlist
();
530 if ($vmlist && $vmlist->{ids
} && scalar(keys %{$vmlist->{ids
}})) {
531 &$error("this host already contains virtual guests", $param->{force
});
534 if (system("corosync-quorumtool -l >/dev/null 2>&1") == 0) {
535 &$error("corosync is already running, is this node already in a cluster?!", $param->{force
});
539 warn "warning, ignore the following errors:\n$warnings" if $warnings;
540 die "detected the following error(s):\n$errors" if $errors;
542 # make sure known_hosts is on local filesystem
543 PVE
::Cluster
::ssh_unmerge_known_hosts
();
545 my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"];
546 PVE
::Tools
::run_command
($cmd, 'outfunc' => sub {}, 'errfunc' => sub {},
547 'errmsg' => "unable to copy ssh ID");
549 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
550 'pvecm', 'addnode', $nodename, '--force', 1];
552 push @$cmd, '--nodeid', $param->{nodeid
} if $param->{nodeid
};
554 push @$cmd, '--votes', $param->{votes
} if defined($param->{votes
});
556 push @$cmd, '--ring0_addr', $param->{ring0_addr
} if defined($param->{ring0_addr
});
558 push @$cmd, '--ring1_addr', $param->{ring1_addr
} if defined($param->{ring1_addr
});
560 if (system (@$cmd) != 0) {
561 my $cmdtxt = join (' ', @$cmd);
562 die "unable to add node: command failed ($cmdtxt)\n";
565 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
569 print "copy corosync auth key\n";
570 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
571 "[$host]:$authfile $clusterconf", $tmpdir];
573 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
575 mkdir "/etc/corosync";
576 my $confbase = basename
($clusterconf);
578 $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
579 system($cmd) == 0 || die "can't copy cluster configuration\n";
581 my $keybase = basename
($authfile);
582 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
583 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
585 print "stopping pve-cluster service\n";
587 system("umount $basedir -f >/dev/null 2>&1");
588 system("systemctl stop pve-cluster") == 0 ||
589 die "can't stop pve-cluster service\n";
595 system("systemctl start pve-cluster") == 0 ||
596 die "starting pve-cluster failed\n";
598 system("systemctl start corosync");
602 while (!PVE
::Cluster
::check_cfs_quorum
(1)) {
604 print "waiting for quorum...";
610 print "OK\n" if !$printqmsg;
612 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
614 print "generating node certificates\n";
615 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address);
617 print "merge known_hosts file\n";
618 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
620 print "restart services\n";
621 # restart pvedaemon (changed certs)
622 system("systemctl restart pvedaemon");
623 # restart pveproxy (changed certs)
624 system("systemctl restart pveproxy");
626 print "successfully added node '$nodename' to cluster.\n";
637 __PACKAGE__-
>register_method ({
641 description
=> "Displays the local view of the cluster status.",
643 additionalProperties
=> 0,
646 returns
=> { type
=> 'null' },
651 PVE
::Cluster
::check_corosync_conf_exists
();
653 my $cmd = ['corosync-quorumtool', '-siH'];
657 exit (-1); # should not be reached
660 __PACKAGE__-
>register_method ({
664 description
=> "Displays the local view of the cluster nodes.",
666 additionalProperties
=> 0,
669 returns
=> { type
=> 'null' },
674 PVE
::Cluster
::check_corosync_conf_exists
();
676 my $cmd = ['corosync-quorumtool', '-l'];
680 exit (-1); # should not be reached
683 __PACKAGE__-
>register_method ({
687 description
=> "Tells corosync a new value of expected votes.",
689 additionalProperties
=> 0,
693 description
=> "Expected votes",
698 returns
=> { type
=> 'null' },
703 PVE
::Cluster
::check_corosync_conf_exists
();
705 my $cmd = ['corosync-quorumtool', '-e', $param->{expected
}];
709 exit (-1); # should not be reached
713 __PACKAGE__-
>register_method ({
714 name
=> 'updatecerts',
715 path
=> 'updatecerts',
717 description
=> "Update node certificates (and generate all needed files/directories).",
719 additionalProperties
=> 0,
722 description
=> "Force generation of new SSL certifate.",
727 description
=> "Ignore errors (i.e. when cluster has no quorum).",
733 returns
=> { type
=> 'null' },
737 PVE
::Cluster
::setup_sshd_config
(0);
738 PVE
::Cluster
::setup_rootsshconfig
();
740 PVE
::Cluster
::gen_pve_vzdump_symlink
();
742 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
743 return undef if $param->{silent
};
744 die "no quorum - unable to update files\n";
747 PVE
::Cluster
::setup_ssh_keys
();
749 my $nodename = PVE
::INotify
::nodename
();
751 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
753 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address, $param->{force
});
754 PVE
::Cluster
::ssh_merge_keys
();
755 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address);
756 PVE
::Cluster
::gen_pve_vzdump_files
();
761 __PACKAGE__-
>register_method ({
765 description
=> "Used by VM/CT migration - do not use manually.",
767 additionalProperties
=> 0,
769 get_migration_ip
=> {
772 description
=> 'return the migration IP, if configured',
775 migration_network
=> {
778 description
=> 'the migration network used to detect the local migration IP',
783 returns
=> { type
=> 'null'},
787 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
792 if ($param->{get_migration_ip
}) {
793 my $network = $param->{migration_network
};
794 if (my $ip = PVE
::Cluster
::get_local_migration_ip
($network)) {
799 # do not keep tunnel open when asked for migration ip
803 print "tunnel online\n";
806 while (my $line = <>) {
808 last if $line =~ m/^quit$/;
816 keygen
=> [ __PACKAGE__
, 'keygen', ['filename']],
817 create
=> [ __PACKAGE__
, 'create', ['clustername']],
818 add
=> [ __PACKAGE__
, 'add', ['hostname']],
819 addnode
=> [ __PACKAGE__
, 'addnode', ['node']],
820 delnode
=> [ __PACKAGE__
, 'delnode', ['node']],
821 status
=> [ __PACKAGE__
, 'status' ],
822 nodes
=> [ __PACKAGE__
, 'nodes' ],
823 expected
=> [ __PACKAGE__
, 'expected', ['expected']],
824 updatecerts
=> [ __PACKAGE__
, 'updatecerts', []],
825 mtunnel
=> [ __PACKAGE__
, 'mtunnel', []],