]>
git.proxmox.com Git - pve-cluster.git/blob - data/PVE/CLI/pvecm.pm
1 package PVE
::CLI
::pvecm
;
9 use PVE
::Tools
qw(run_command);
16 use base
qw(PVE::CLIHandler);
18 $ENV{HOME
} = '/root'; # for ssh-copy-id
20 my $basedir = "/etc/pve";
21 my $clusterconf = "$basedir/corosync.conf";
22 my $libdir = "/var/lib/pve-cluster";
23 my $backupdir = "/var/lib/pve-cluster/backup";
24 my $dbfile = "$libdir/config.db";
25 my $authfile = "/etc/corosync/authkey";
29 print "backup old database\n";
37 ['gzip', '-', \
">${backupdir}/config-${ctime}.sql.gz"],
40 run_command
($cmd, 'errmsg' => "cannot backup old database\n");
46 foreach my $fn (<$backupdir/config-*.sql
.gz
>) {
47 if ($fn =~ m!/config-(\d+)\.sql.gz$!) {
48 push @bklist, [$fn, $1];
52 @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
54 while (scalar (@bklist) >= $maxfiles) {
56 print "delete old backup '$d->[0]'\n";
61 # lock method to ensure local and cluster wide atomicity
62 # if we're a single node cluster just lock locally, we have no other cluster
63 # node which we could contend with, else also acquire a cluster wide lock
64 my $config_change_lock = sub {
67 my $local_lock_fn = "/var/lock/pvecm.lock";
68 PVE
::Tools
::lock_file
($local_lock_fn, 10, sub {
69 PVE
::Cluster
::cfs_update
(1);
70 my $members = PVE
::Cluster
::get_members
();
71 if (scalar(keys %$members) > 1) {
72 return PVE
::Cluster
::cfs_lock_file
('corosync.conf', 10, $code);
79 __PACKAGE__-
>register_method ({
83 description
=> "Generate new cryptographic key for corosync.",
85 additionalProperties
=> 0,
89 description
=> "Output file name"
93 returns
=> { type
=> 'null' },
98 my $filename = $param->{filename
};
101 $> == 0 || die "Error: Authorization key must be generated as root user.\n";
102 my $dirname = dirname
($filename);
104 die "key file '$filename' already exists\n" if -e
$filename;
106 File
::Path
::make_path
($dirname) if $dirname;
108 run_command
(['corosync-keygen', '-l', '-k', $filename]);
113 __PACKAGE__-
>register_method ({
117 description
=> "Generate new cluster configuration.",
119 additionalProperties
=> 0,
122 description
=> "The name of the cluster.",
123 type
=> 'string', format
=> 'pve-node',
128 description
=> "Node id for this node.",
134 description
=> "Number of votes for this node.",
139 type
=> 'string', format
=> 'ip',
140 description
=> "This specifies the network address the corosync ring 0".
141 " executive should bind to and defaults to the local IP address of the node.",
145 type
=> 'string', format
=> 'address',
146 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
147 " Defaults to the hostname of the node.",
151 type
=> 'string', format
=> 'ip',
152 description
=> "This specifies the network address the corosync ring 1".
153 " executive should bind to and is optional.",
157 type
=> 'string', format
=> 'address',
158 description
=> "Hostname (or IP) of the corosync ring1 address, this".
159 " needs an valid bindnet1_addr.",
164 returns
=> { type
=> 'null' },
169 -f
$clusterconf && die "cluster config '$clusterconf' already exists\n";
171 PVE
::Cluster
::setup_sshd_config
(1);
172 PVE
::Cluster
::setup_rootsshconfig
();
173 PVE
::Cluster
::setup_ssh_keys
();
175 -f
$authfile || __PACKAGE__-
>keygen({filename
=> $authfile});
177 -f
$authfile || die "no authentication key available\n";
179 my $clustername = $param->{clustername
};
181 $param->{nodeid
} = 1 if !$param->{nodeid
};
183 $param->{votes
} = 1 if !defined($param->{votes
});
185 my $nodename = PVE
::INotify
::nodename
();
187 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
189 $param->{bindnet0_addr
} = $local_ip_address
190 if !defined($param->{bindnet0_addr
});
192 $param->{ring0_addr
} = $nodename if !defined($param->{ring0_addr
});
194 die "Param bindnet1_addr and ring1_addr are dependend, use both or none!\n"
195 if (defined($param->{bindnet1_addr
}) != defined($param->{ring1_addr
}));
197 my $bind_is_ipv6 = Net
::IP
::ip_is_ipv6
($param->{bindnet0_addr
});
199 # use string as here-doc format distracts more
200 my $interfaces = "interface {\n ringnumber: 0\n" .
201 " bindnetaddr: $param->{bindnet0_addr}\n }";
203 my $ring_addresses = "ring0_addr: $param->{ring0_addr}" ;
205 # allow use of multiple rings (rrp) at cluster creation time
206 if ($param->{bindnet1_addr
}) {
207 die "IPv6 and IPv4 cannot be mixed, use one or the other!\n"
208 if Net
::IP
::ip_is_ipv6
($param->{bindnet1_addr
}) != $bind_is_ipv6;
210 $interfaces .= "\n interface {\n ringnumber: 1\n" .
211 " bindnetaddr: $param->{bindnet1_addr}\n }\n";
213 $interfaces .= "rrp_mode: passive\n"; # only passive is stable and tested
215 $ring_addresses .= "\n ring1_addr: $param->{ring1_addr}";
217 } elsif($param->{rrp_mode
} && $param->{rrp_mode
} ne 'none') {
219 warn "rrp_mode '$param->{rrp_mode}' useless when using only one".
220 " ring, using 'none' instead";
221 # corosync defaults to none if only one interface is configured
222 $param->{rrp_mode
} = undef;
226 # No, corosync cannot deduce this on its own
227 my $ipversion = $bind_is_ipv6 ?
'ipv6' : 'ipv4';
233 cluster_name: $clustername
235 ip_version: $ipversion
243 nodeid: $param->{nodeid}
244 quorum_votes: $param->{votes}
249 provider: corosync_votequorum
258 PVE
::Tools
::file_set_contents
($clusterconf, $config);
260 PVE
::Cluster
::ssh_merge_keys
();
262 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address);
264 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
266 run_command
('systemctl restart pve-cluster'); # restart
268 run_command
('systemctl restart corosync'); # restart
273 __PACKAGE__-
>register_method ({
277 description
=> "Adds a node to the cluster configuration.",
279 additionalProperties
=> 0,
281 node
=> PVE
::JSONSchema
::get_standard_option
('pve-node'),
284 description
=> "Node id for this node.",
290 description
=> "Number of votes for this node",
296 description
=> "Do not throw error if node already exists.",
300 type
=> 'string', format
=> 'address',
301 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
302 " Defaults to nodes hostname.",
306 type
=> 'string', format
=> 'address',
307 description
=> "Hostname (or IP) of the corosync ring1 address, this".
308 " needs an valid bindnet1_addr.",
313 returns
=> { type
=> 'null' },
318 if (!$param->{force
} && (-t STDIN
|| -t STDOUT
)) {
319 die "error: `addnode` should not get called interactively!\nUse ".
320 "`pvecm add <cluster-node>` to add a node to a cluster!\n";
323 PVE
::Cluster
::check_cfs_quorum
();
326 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
327 my $nodelist = PVE
::Corosync
::nodelist
($conf);
328 my $totem_cfg = PVE
::Corosync
::totem_config
($conf);
330 my $name = $param->{node
};
332 # ensure we do not reuse an address, that can crash the whole cluster!
333 my $check_duplicate_addr = sub {
335 return if !defined($addr);
337 while (my ($k, $v) = each %$nodelist) {
338 next if $k eq $name; # allows re-adding a node if force is set
339 if ($v->{ring0_addr
} eq $addr || ($v->{ring1_addr
} && $v->{ring1_addr
} eq $addr)) {
340 die "corosync: address '$addr' already defined by node '$k'\n";
345 &$check_duplicate_addr($param->{ring0_addr
});
346 &$check_duplicate_addr($param->{ring1_addr
});
348 $param->{ring0_addr
} = $name if !$param->{ring0_addr
};
350 die "corosync: using 'ring1_addr' parameter needs a configured ring 1 interface!\n"
351 if $param->{ring1_addr
} && !defined($totem_cfg->{interface
}->{1});
353 die "corosync: ring 1 interface configured but 'ring1_addr' parameter not defined!\n"
354 if defined($totem_cfg->{interface
}->{1}) && !defined($param->{ring1_addr
});
356 if (defined(my $res = $nodelist->{$name})) {
357 $param->{nodeid
} = $res->{nodeid
} if !$param->{nodeid
};
358 $param->{votes
} = $res->{quorum_votes
} if !defined($param->{votes
});
360 if ($res->{quorum_votes
} == $param->{votes
} &&
361 $res->{nodeid
} == $param->{nodeid
}) {
362 print "node $name already defined\n";
363 if ($param->{force
}) {
369 die "can't add existing node\n";
371 } elsif (!$param->{nodeid
}) {
376 foreach my $v (values %$nodelist) {
377 if ($v->{nodeid
} eq $nodeid) {
386 $param->{nodeid
} = $nodeid;
389 $param->{votes
} = 1 if !defined($param->{votes
});
391 PVE
::Cluster
::gen_local_dirs
($name);
393 eval { PVE
::Cluster
::ssh_merge_keys
(); };
396 $nodelist->{$name} = {
397 ring0_addr
=> $param->{ring0_addr
},
398 nodeid
=> $param->{nodeid
},
401 $nodelist->{$name}->{ring1_addr
} = $param->{ring1_addr
} if $param->{ring1_addr
};
402 $nodelist->{$name}->{quorum_votes
} = $param->{votes
} if $param->{votes
};
404 PVE
::Corosync
::update_nodelist
($conf, $nodelist);
407 $config_change_lock->($code);
414 __PACKAGE__-
>register_method ({
418 description
=> "Removes a node to the cluster configuration.",
420 additionalProperties
=> 0,
424 description
=> "Hostname or IP of the corosync ring0 address of this node.",
428 returns
=> { type
=> 'null' },
433 my $local_node = PVE
::INotify
::nodename
();
434 die "Cannot delete myself from cluster!\n" if $param->{node
} eq $local_node;
436 PVE
::Cluster
::check_cfs_quorum
();
439 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
440 my $nodelist = PVE
::Corosync
::nodelist
($conf);
445 foreach my $tmp_node (keys %$nodelist) {
446 my $d = $nodelist->{$tmp_node};
447 my $ring0_addr = $d->{ring0_addr
};
448 my $ring1_addr = $d->{ring1_addr
};
449 if (($tmp_node eq $param->{node
}) ||
450 (defined($ring0_addr) && ($ring0_addr eq $param->{node
})) ||
451 (defined($ring1_addr) && ($ring1_addr eq $param->{node
}))) {
453 $nodeid = $d->{nodeid
};
458 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
461 delete $nodelist->{$node};
463 PVE
::Corosync
::update_nodelist
($conf, $nodelist);
465 run_command
(['corosync-cfgtool','-k', $nodeid]) if defined($nodeid);
468 $config_change_lock->($code);
474 __PACKAGE__-
>register_method ({
478 description
=> "Adds the current node to an existing cluster.",
480 additionalProperties
=> 0,
484 description
=> "Hostname (or IP) of an existing cluster member."
488 description
=> "Node id for this node.",
494 description
=> "Number of votes for this node",
500 description
=> "Do not throw error if node already exists.",
504 type
=> 'string', format
=> 'address',
505 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
506 " Defaults to nodes hostname.",
510 type
=> 'string', format
=> 'address',
511 description
=> "Hostname (or IP) of the corosync ring1 address, this".
512 " needs an valid configured ring 1 interface in the cluster.",
517 returns
=> { type
=> 'null' },
522 my $nodename = PVE
::INotify
::nodename
();
524 PVE
::Cluster
::setup_sshd_config
();
525 PVE
::Cluster
::setup_rootsshconfig
();
526 PVE
::Cluster
::setup_ssh_keys
();
528 my $host = $param->{hostname
};
530 my ($errors, $warnings) = ('', '');
533 my ($msg, $suppress) = @_;
536 $warnings .= "* $msg\n";
538 $errors .= "* $msg\n";
542 if (!$param->{force
}) {
545 &$error("authentication key '$authfile' already exists", $param->{force
});
548 if (-f
$clusterconf) {
549 &$error("cluster config '$clusterconf' already exists", $param->{force
});
552 my $vmlist = PVE
::Cluster
::get_vmlist
();
553 if ($vmlist && $vmlist->{ids
} && scalar(keys %{$vmlist->{ids
}})) {
554 &$error("this host already contains virtual guests", $param->{force
});
557 if (system("corosync-quorumtool -l >/dev/null 2>&1") == 0) {
558 &$error("corosync is already running, is this node already in a cluster?!", $param->{force
});
562 # check if corosync ring IPs are configured on the current nodes interfaces
566 if (!PVE
::JSONSchema
::pve_verify_ip
($ip, 1)) {
568 eval { $ip = PVE
::Network
::get_ip_from_hostname
($host); };
570 &$error("cannot use '$host': $@\n") ;
575 my $cidr = (Net
::IP
::ip_is_ipv6
($ip)) ?
"$ip/128" : "$ip/32";
576 my $configured_ips = PVE
::Network
::get_local_ip_from_cidr
($cidr);
578 &$error("cannot use IP '$ip', it must be configured exactly once on local node!\n")
579 if (scalar(@$configured_ips) != 1);
583 &$check_ip($param->{ring0_addr
});
584 &$check_ip($param->{ring1_addr
});
586 warn "warning, ignore the following errors:\n$warnings" if $warnings;
587 die "detected the following error(s):\n$errors" if $errors;
589 # make sure known_hosts is on local filesystem
590 PVE
::Cluster
::ssh_unmerge_known_hosts
();
592 my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"];
593 run_command
($cmd, 'outfunc' => sub {}, 'errfunc' => sub {},
594 'errmsg' => "unable to copy ssh ID");
596 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
597 'pvecm', 'addnode', $nodename, '--force', 1];
599 push @$cmd, '--nodeid', $param->{nodeid
} if $param->{nodeid
};
601 push @$cmd, '--votes', $param->{votes
} if defined($param->{votes
});
603 push @$cmd, '--ring0_addr', $param->{ring0_addr
} if defined($param->{ring0_addr
});
605 push @$cmd, '--ring1_addr', $param->{ring1_addr
} if defined($param->{ring1_addr
});
607 if (system (@$cmd) != 0) {
608 my $cmdtxt = join (' ', @$cmd);
609 die "unable to add node: command failed ($cmdtxt)\n";
612 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
616 print "copy corosync auth key\n";
617 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
618 "[$host]:$authfile $clusterconf", $tmpdir];
620 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
622 mkdir "/etc/corosync";
623 my $confbase = basename
($clusterconf);
625 $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
626 system($cmd) == 0 || die "can't copy cluster configuration\n";
628 my $keybase = basename
($authfile);
629 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
630 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
632 print "stopping pve-cluster service\n";
634 system("umount $basedir -f >/dev/null 2>&1");
635 system("systemctl stop pve-cluster") == 0 ||
636 die "can't stop pve-cluster service\n";
642 system("systemctl start pve-cluster") == 0 ||
643 die "starting pve-cluster failed\n";
645 system("systemctl start corosync");
649 while (!PVE
::Cluster
::check_cfs_quorum
(1)) {
651 print "waiting for quorum...";
657 print "OK\n" if !$printqmsg;
659 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
661 print "generating node certificates\n";
662 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address);
664 print "merge known_hosts file\n";
665 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
667 print "restart services\n";
668 # restart pvedaemon (changed certs)
669 system("systemctl restart pvedaemon");
670 # restart pveproxy (changed certs)
671 system("systemctl restart pveproxy");
673 print "successfully added node '$nodename' to cluster.\n";
684 __PACKAGE__-
>register_method ({
688 description
=> "Displays the local view of the cluster status.",
690 additionalProperties
=> 0,
693 returns
=> { type
=> 'null' },
698 PVE
::Corosync
::check_conf_exists
();
700 my $cmd = ['corosync-quorumtool', '-siH'];
704 exit (-1); # should not be reached
707 __PACKAGE__-
>register_method ({
711 description
=> "Displays the local view of the cluster nodes.",
713 additionalProperties
=> 0,
716 returns
=> { type
=> 'null' },
721 PVE
::Corosync
::check_conf_exists
();
723 my $cmd = ['corosync-quorumtool', '-l'];
727 exit (-1); # should not be reached
730 __PACKAGE__-
>register_method ({
734 description
=> "Tells corosync a new value of expected votes.",
736 additionalProperties
=> 0,
740 description
=> "Expected votes",
745 returns
=> { type
=> 'null' },
750 PVE
::Corosync
::check_conf_exists
();
752 my $cmd = ['corosync-quorumtool', '-e', $param->{expected
}];
756 exit (-1); # should not be reached
760 __PACKAGE__-
>register_method ({
761 name
=> 'updatecerts',
762 path
=> 'updatecerts',
764 description
=> "Update node certificates (and generate all needed files/directories).",
766 additionalProperties
=> 0,
769 description
=> "Force generation of new SSL certifate.",
774 description
=> "Ignore errors (i.e. when cluster has no quorum).",
780 returns
=> { type
=> 'null' },
784 PVE
::Cluster
::setup_rootsshconfig
();
786 PVE
::Cluster
::gen_pve_vzdump_symlink
();
788 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
789 return undef if $param->{silent
};
790 die "no quorum - unable to update files\n";
793 PVE
::Cluster
::setup_ssh_keys
();
795 my $nodename = PVE
::INotify
::nodename
();
797 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
799 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address, $param->{force
});
800 PVE
::Cluster
::ssh_merge_keys
();
801 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address);
802 PVE
::Cluster
::gen_pve_vzdump_files
();
807 __PACKAGE__-
>register_method ({
811 description
=> "Used by VM/CT migration - do not use manually.",
813 additionalProperties
=> 0,
815 get_migration_ip
=> {
818 description
=> 'return the migration IP, if configured',
821 migration_network
=> {
824 description
=> 'the migration network used to detect the local migration IP',
829 description
=> 'Run a command with a tcp socket as standard input.'
830 .' The IP address and port are printed via this'
831 ." command's stdandard output first, each on a separate line.",
834 'extra-args' => PVE
::JSONSchema
::get_standard_option
('extra-args'),
837 returns
=> { type
=> 'null'},
841 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
846 my $network = $param->{migration_network
};
847 if ($param->{get_migration_ip
}) {
848 die "cannot use --run-command with --get_migration_ip\n"
849 if $param->{'run-command'};
850 if (my $ip = PVE
::Cluster
::get_local_migration_ip
($network)) {
855 # do not keep tunnel open when asked for migration ip
859 if ($param->{'run-command'}) {
860 my $cmd = $param->{'extra-args'};
861 die "missing command\n"
862 if !$cmd || !scalar(@$cmd);
864 # Get an ip address to listen on, and find a free migration port
866 if (defined($network)) {
867 $ip = PVE
::Cluster
::get_local_migration_ip
($network)
868 or die "failed to get migration IP address to listen on\n";
869 $family = PVE
::Tools
::get_host_address_family
($ip);
871 my $nodename = PVE
::INotify
::nodename
();
872 ($ip, $family) = PVE
::Network
::get_ip_from_hostname
($nodename, 0);
874 my $port = PVE
::Tools
::next_migrate_port
($family, $ip);
876 PVE
::Tools
::pipe_socket_to_command
($cmd, $ip, $port);
880 print "tunnel online\n";
883 while (my $line = <>) {
885 last if $line =~ m/^quit$/;
893 keygen
=> [ __PACKAGE__
, 'keygen', ['filename']],
894 create
=> [ __PACKAGE__
, 'create', ['clustername']],
895 add
=> [ __PACKAGE__
, 'add', ['hostname']],
896 addnode
=> [ __PACKAGE__
, 'addnode', ['node']],
897 delnode
=> [ __PACKAGE__
, 'delnode', ['node']],
898 status
=> [ __PACKAGE__
, 'status' ],
899 nodes
=> [ __PACKAGE__
, 'nodes' ],
900 expected
=> [ __PACKAGE__
, 'expected', ['expected']],
901 updatecerts
=> [ __PACKAGE__
, 'updatecerts', []],
902 mtunnel
=> [ __PACKAGE__
, 'mtunnel', ['extra-args']],