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";
36 my $cmd = "echo '.dump' |";
37 $cmd .= "sqlite3 '$dbfile' |";
38 $cmd .= "gzip - >'${backupdir}/config-${ctime}.sql.gz'";
41 die "can't backup old database: $!\n";
47 foreach my $fn (<$backupdir/config-*.sql
.gz
>) {
48 if ($fn =~ m!/config-(\d+)\.sql.gz$!) {
49 push @bklist, [$fn, $1];
53 @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
55 while (scalar (@bklist) >= $maxfiles) {
57 print "delete old backup '$d->[0]'\n";
62 __PACKAGE__-
>register_method ({
66 description
=> "Generate new cryptographic key for corosync.",
68 additionalProperties
=> 0,
72 description
=> "Output file name"
76 returns
=> { type
=> 'null' },
81 my $filename = $param->{filename
};
84 $> == 0 || die "Error: Authorization key must be generated as root user.\n";
85 my $dirname = dirname
($filename);
86 my $basename = basename
($filename);
88 die "key file '$filename' already exists\n" if -e
$filename;
90 File
::Path
::make_path
($dirname) if $dirname;
92 my $cmd = ['corosync-keygen', '-l', '-k', $filename];
93 PVE
::Tools
::run_command
($cmd);
98 __PACKAGE__-
>register_method ({
102 description
=> "Generate new cluster configuration.",
104 additionalProperties
=> 0,
107 description
=> "The name of the cluster.",
108 type
=> 'string', format
=> 'pve-node',
113 description
=> "Node id for this node.",
119 description
=> "Number of votes for this node.",
124 type
=> 'string', format
=> 'ip',
125 description
=> "This specifies the network address the corosync ring 0".
126 " executive should bind to and defaults to the local IP address of the node.",
130 type
=> 'string', format
=> 'address',
131 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
132 " Defaults to the hostname of the node.",
137 enum
=> ['none', 'active', 'passive'],
138 description
=> "This specifies the mode of redundant ring, which" .
139 " may be none, active or passive. Using multiple interfaces".
140 " only allows 'active' or 'passive'.",
145 type
=> 'string', format
=> 'ip',
146 description
=> "This specifies the network address the corosync ring 1".
147 " executive should bind to and is optional.",
151 type
=> 'string', format
=> 'address',
152 description
=> "Hostname (or IP) of the corosync ring1 address, this".
153 " needs an valid bindnet1_addr.",
158 returns
=> { type
=> 'null' },
163 -f
$clusterconf && die "cluster config '$clusterconf' already exists\n";
165 PVE
::Cluster
::setup_sshd_config
();
166 PVE
::Cluster
::setup_rootsshconfig
();
167 PVE
::Cluster
::setup_ssh_keys
();
169 -f
$authfile || __PACKAGE__-
>keygen({filename
=> $authfile});
171 -f
$authfile || die "no authentication key available\n";
173 my $clustername = $param->{clustername
};
175 $param->{nodeid
} = 1 if !$param->{nodeid
};
177 $param->{votes
} = 1 if !defined($param->{votes
});
179 my $nodename = PVE
::INotify
::nodename
();
181 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
183 $param->{bindnet0_addr
} = $local_ip_address
184 if !defined($param->{bindnet0_addr
});
186 $param->{ring0_addr
} = $nodename if !defined($param->{ring0_addr
});
188 die "Param bindnet1_addr and ring1_addr are dependend, use both or none!\n"
189 if (defined($param->{bindnet1_addr
}) != defined($param->{ring1_addr
}));
191 my $bind_is_ipv6 = Net
::IP
::ip_is_ipv6
($param->{bindnet0_addr
});
193 # use string as here-doc format distracts more
194 my $interfaces = "interface {\n ringnumber: 0\n" .
195 " bindnetaddr: $param->{bindnet0_addr}\n }";
197 my $ring_addresses = "ring0_addr: $param->{ring0_addr}" ;
199 # allow use of multiple rings (rrp) at cluster creation time
200 if ($param->{bindnet1_addr
}) {
201 die "IPv6 and IPv4 cannot be mixed, use one or the other!\n"
202 if Net
::IP
::ip_is_ipv6
($param->{bindnet1_addr
}) != $bind_is_ipv6;
204 die "rrp_mode 'none' is not allowed when using multiple interfaces,".
205 " use 'active' or 'passive'!\n"
206 if !$param->{rrp_mode
} || $param->{rrp_mode
} eq 'none';
208 $interfaces .= "\n interface {\n ringnumber: 1\n" .
209 " bindnetaddr: $param->{bindnet1_addr}\n }\n";
211 $ring_addresses .= "\n ring1_addr: $param->{ring1_addr}";
213 } elsif($param->{rrp_mode
} && $param->{rrp_mode
} ne 'none') {
215 warn "rrp_mode '$param->{rrp_mode}' useless when using only one".
216 " ring, using 'none' instead";
217 # corosync defaults to none if only one interface is configured
218 $param->{rrp_mode
} = undef;
222 $interfaces = "rrp_mode: $param->{rrp_mode}\n " . $interfaces
223 if $param->{rrp_mode
};
225 # No, corosync cannot deduce this on its own
226 my $ipversion = $bind_is_ipv6 ?
'ipv6' : 'ipv4';
232 cluster_name: $clustername
234 ip_version: $ipversion
242 nodeid: $param->{nodeid}
243 quorum_votes: $param->{votes}
248 provider: corosync_votequorum
257 PVE
::Tools
::file_set_contents
($clusterconf, $config);
259 PVE
::Cluster
::ssh_merge_keys
();
261 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address);
263 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
265 PVE
::Tools
::run_command
('systemctl restart pve-cluster'); # restart
267 PVE
::Tools
::run_command
('systemctl restart corosync'); # restart
272 __PACKAGE__-
>register_method ({
276 description
=> "Adds a node to the cluster configuration.",
278 additionalProperties
=> 0,
280 node
=> PVE
::JSONSchema
::get_standard_option
('pve-node'),
283 description
=> "Node id for this node.",
289 description
=> "Number of votes for this node",
295 description
=> "Do not throw error if node already exists.",
299 type
=> 'string', format
=> 'address',
300 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
301 " Defaults to nodes hostname.",
305 type
=> 'string', format
=> 'address',
306 description
=> "Hostname (or IP) of the corosync ring1 address, this".
307 " needs an valid bindnet1_addr.",
312 returns
=> { type
=> 'null' },
317 PVE
::Cluster
::check_cfs_quorum
();
319 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
321 my $nodelist = corosync_nodelist
($conf);
323 my $totem_cfg = corosync_totem_config
($conf);
325 my $name = $param->{node
};
327 $param->{ring0_addr
} = $name if !$param->{ring0_addr
};
329 die " ring1_addr needs a configured ring 1 interface!\n"
330 if $param->{ring1_addr
} && !defined($totem_cfg->{interface
}->{1});
332 if (defined(my $res = $nodelist->{$name})) {
333 $param->{nodeid
} = $res->{nodeid
} if !$param->{nodeid
};
334 $param->{votes
} = $res->{quorum_votes
} if !defined($param->{votes
});
336 if ($res->{quorum_votes
} == $param->{votes
} &&
337 $res->{nodeid
} == $param->{nodeid
}) {
338 print "node $name already defined\n";
339 if ($param->{force
}) {
345 die "can't add existing node\n";
347 } elsif (!$param->{nodeid
}) {
352 foreach my $v (values %$nodelist) {
353 if ($v->{nodeid
} eq $nodeid) {
362 $param->{nodeid
} = $nodeid;
365 $param->{votes
} = 1 if !defined($param->{votes
});
367 PVE
::Cluster
::gen_local_dirs
($name);
369 eval { PVE
::Cluster
::ssh_merge_keys
(); };
372 $nodelist->{$name} = {
373 ring0_addr
=> $param->{ring0_addr
},
374 nodeid
=> $param->{nodeid
},
377 $nodelist->{$name}->{ring1_addr
} = $param->{ring1_addr
} if $param->{ring1_addr
};
378 $nodelist->{$name}->{quorum_votes
} = $param->{votes
} if $param->{votes
};
380 corosync_update_nodelist
($conf, $nodelist);
386 __PACKAGE__-
>register_method ({
390 description
=> "Removes a node to the cluster configuration.",
392 additionalProperties
=> 0,
394 node
=> PVE
::JSONSchema
::get_standard_option
('pve-node'),
397 returns
=> { type
=> 'null' },
402 PVE
::Cluster
::check_cfs_quorum
();
404 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
406 my $nodelist = corosync_nodelist
($conf);
408 my $nd = delete $nodelist->{$param->{node
}};
409 die "no such node '$param->{node}'\n" if !$nd;
411 corosync_update_nodelist
($conf, $nodelist);
416 __PACKAGE__-
>register_method ({
420 description
=> "Adds the current node to an existing cluster.",
422 additionalProperties
=> 0,
426 description
=> "Hostname (or IP) of an existing cluster member."
430 description
=> "Node id for this node.",
436 description
=> "Number of votes for this node",
442 description
=> "Do not throw error if node already exists.",
446 type
=> 'string', format
=> 'address',
447 description
=> "Hostname (or IP) of the corosync ring0 address of this node.".
448 " Defaults to nodes hostname.",
452 type
=> 'string', format
=> 'address',
453 description
=> "Hostname (or IP) of the corosync ring1 address, this".
454 " needs an valid configured ring 1 interface in the cluster.",
459 returns
=> { type
=> 'null' },
464 my $nodename = PVE
::INotify
::nodename
();
466 PVE
::Cluster
::setup_sshd_config
();
467 PVE
::Cluster
::setup_rootsshconfig
();
468 PVE
::Cluster
::setup_ssh_keys
();
470 my $host = $param->{hostname
};
472 if (!$param->{force
}) {
475 die "authentication key already exists\n";
478 if (-f
$clusterconf) {
479 die "cluster config '$clusterconf' already exists\n";
482 my $vmlist = PVE
::Cluster
::get_vmlist
();
483 if ($vmlist && $vmlist->{ids
} && scalar(keys %{$vmlist->{ids
}})) {
484 die "this host already contains virtual machines - please remove them first\n";
487 if (system("corosync-quorumtool >/dev/null 2>&1") == 0) {
488 die "corosync is already running\n";
492 # make sure known_hosts is on local filesystem
493 PVE
::Cluster
::ssh_unmerge_known_hosts
();
495 my $cmd = "ssh-copy-id -i /root/.ssh/id_rsa 'root\@$host' >/dev/null 2>&1";
496 system ($cmd) == 0 ||
497 die "unable to copy ssh ID\n";
499 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
500 'pvecm', 'addnode', $nodename, '--force', 1];
502 push @$cmd, '--nodeid', $param->{nodeid
} if $param->{nodeid
};
504 push @$cmd, '--votes', $param->{votes
} if defined($param->{votes
});
506 push @$cmd, '--ring0_addr', $param->{ring0_addr
} if defined($param->{ring0_addr
});
508 push @$cmd, '--ring1_addr', $param->{ring1_addr
} if defined($param->{ring1_addr
});
510 if (system (@$cmd) != 0) {
511 my $cmdtxt = join (' ', @$cmd);
512 die "unable to add node: command failed ($cmdtxt)\n";
515 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
519 print "copy corosync auth key\n";
520 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
521 "[$host]:$authfile $clusterconf", $tmpdir];
523 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
525 mkdir "/etc/corosync";
526 my $confbase = basename
($clusterconf);
528 $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
529 system($cmd) == 0 || die "can't copy cluster configuration\n";
531 my $keybase = basename
($authfile);
532 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
533 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
535 print "stopping pve-cluster service\n";
537 system("umount $basedir -f >/dev/null 2>&1");
538 system("systemctl stop pve-cluster") == 0 ||
539 die "can't stop pve-cluster service\n";
545 system("systemctl start pve-cluster") == 0 ||
546 die "starting pve-cluster failed\n";
548 system("systemctl start corosync");
552 while (!PVE
::Cluster
::check_cfs_quorum
(1)) {
554 print "waiting for quorum...";
560 print "OK\n" if !$printqmsg;
562 # system("systemctl start clvm");
564 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
566 print "generating node certificates\n";
567 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address);
569 print "merge known_hosts file\n";
570 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
572 print "restart services\n";
573 # restart pvedaemon (changed certs)
574 system("systemctl restart pvedaemon");
575 # restart pveproxy (changed certs)
576 system("systemctl restart pveproxy");
578 print "successfully added node '$nodename' to cluster.\n";
589 __PACKAGE__-
>register_method ({
593 description
=> "Displays the local view of the cluster status.",
595 additionalProperties
=> 0,
598 returns
=> { type
=> 'null' },
603 PVE
::Cluster
::check_corosync_conf_exists
();
605 my $cmd = ['corosync-quorumtool', '-siH'];
609 exit (-1); # should not be reached
612 __PACKAGE__-
>register_method ({
616 description
=> "Displays the local view of the cluster nodes.",
618 additionalProperties
=> 0,
621 returns
=> { type
=> 'null' },
626 PVE
::Cluster
::check_corosync_conf_exists
();
628 my $cmd = ['corosync-quorumtool', '-l'];
632 exit (-1); # should not be reached
635 __PACKAGE__-
>register_method ({
639 description
=> "Tells corosync a new value of expected votes.",
641 additionalProperties
=> 0,
645 description
=> "Expected votes",
650 returns
=> { type
=> 'null' },
655 PVE
::Cluster
::check_corosync_conf_exists
();
657 my $cmd = ['corosync-quorumtool', '-e', $param->{expected
}];
661 exit (-1); # should not be reached
665 sub corosync_update_nodelist
{
666 my ($conf, $nodelist) = @_;
668 delete $conf->{digest
};
670 my $version = PVE
::Cluster
::corosync_conf_version
($conf);
671 PVE
::Cluster
::corosync_conf_version
($conf, undef, $version + 1);
674 foreach my $v (values %$nodelist) {
675 next if !($v->{ring0_addr
} || $v->{name
});
677 foreach my $k (keys %$v) {
678 push @$kv, { key
=> $k, value
=> $v->{$k} };
680 my $ns = { section
=> 'node', children
=> $kv };
681 push @$children, $ns;
684 foreach my $main (@{$conf->{children
}}) {
685 next if !defined($main->{section
});
686 if ($main->{section
} eq 'nodelist') {
687 $main->{children
} = $children;
693 PVE
::Cluster
::cfs_write_file
("corosync.conf.new", $conf);
695 rename("/etc/pve/corosync.conf.new", "/etc/pve/corosync.conf")
696 || die "activate corosync.conf.new failed - $!\n";
699 sub corosync_nodelist
{
704 foreach my $main (@{$conf->{children
}}) {
705 next if !defined($main->{section
});
706 if ($main->{section
} eq 'nodelist') {
707 foreach my $ne (@{$main->{children
}}) {
708 next if !defined($ne->{section
}) || ($ne->{section
} ne 'node');
709 my $node = { quorum_votes
=> 1 };
711 foreach my $child (@{$ne->{children
}}) {
712 next if !defined($child->{key
});
713 $node->{$child->{key
}} = $child->{value
};
714 # use 'name' over 'ring0_addr' if set
715 if ($child->{key
} eq 'name') {
716 delete $nodelist->{$name} if $name;
717 $name = $child->{value
};
718 $nodelist->{$name} = $node;
719 } elsif(!$name && $child->{key
} eq 'ring0_addr') {
720 $name = $child->{value
};
721 $nodelist->{$name} = $node;
731 # get a hash representation of the corosync config totem section
732 sub corosync_totem_config
{
737 foreach my $main (@{$conf->{children
}}) {
738 next if !defined($main->{section
}) ||
739 $main->{section
} ne 'totem';
741 foreach my $e (@{$main->{children
}}) {
743 if ($e->{section
} && $e->{section
} eq 'interface') {
746 $res->{interface
} = {};
748 foreach my $child (@{$e->{children
}}) {
749 next if !defined($child->{key
});
750 $entry->{$child->{key
}} = $child->{value
};
751 if($child->{key
} eq 'ringnumber') {
752 $res->{interface
}->{$child->{value
}} = $entry;
756 } elsif ($e->{key
}) {
757 $res->{$e->{key
}} = $e->{value
};
765 __PACKAGE__-
>register_method ({
766 name
=> 'updatecerts',
767 path
=> 'updatecerts',
769 description
=> "Update node certificates (and generate all needed files/directories).",
771 additionalProperties
=> 0,
774 description
=> "Force generation of new SSL certifate.",
779 description
=> "Ignore errors (i.e. when cluster has no quorum).",
785 returns
=> { type
=> 'null' },
789 PVE
::Cluster
::setup_rootsshconfig
();
791 PVE
::Cluster
::gen_pve_vzdump_symlink
();
793 if (!PVE
::Cluster
::check_cfs_quorum
(1)) {
794 return undef if $param->{silent
};
795 die "no quorum - unable to update files\n";
798 PVE
::Cluster
::setup_ssh_keys
();
800 my $nodename = PVE
::INotify
::nodename
();
802 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
804 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address, $param->{force
});
805 PVE
::Cluster
::ssh_merge_keys
();
806 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address);
807 PVE
::Cluster
::gen_pve_vzdump_files
();
814 keygen
=> [ __PACKAGE__
, 'keygen', ['filename']],
815 create
=> [ __PACKAGE__
, 'create', ['clustername']],
816 add
=> [ __PACKAGE__
, 'add', ['hostname']],
817 addnode
=> [ __PACKAGE__
, 'addnode', ['node']],
818 delnode
=> [ __PACKAGE__
, 'delnode', ['node']],
819 status
=> [ __PACKAGE__
, 'status' ],
820 nodes
=> [ __PACKAGE__
, 'nodes' ],
821 expected
=> [ __PACKAGE__
, 'expected', ['expected']],
822 updatecerts
=> [ __PACKAGE__
, 'updatecerts', []],