]>
git.proxmox.com Git - pve-cluster.git/blob - data/PVE/API2/ClusterConfig.pm
1 package PVE
::API2
::ClusterConfig
;
9 use PVE
::RPCEnvironment
;
10 use PVE
::JSONSchema
qw(get_standard_option);
12 use PVE
::APIClient
::LWP
;
17 use base
qw(PVE::RESTHandler);
19 my $clusterconf = "/etc/pve/corosync.conf";
20 my $authfile = "/etc/corosync/authkey";
21 my $local_cluster_lock = "/var/lock/pvecm.lock";
25 description
=> "Node id for this node.",
29 PVE
::JSONSchema
::register_standard_option
("corosync-nodeid", $nodeid_desc);
31 __PACKAGE__-
>register_method({
35 description
=> "Directory index.",
37 check
=> ['perm', '/', [ 'Sys.Audit' ]],
40 additionalProperties
=> 0,
49 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
58 { name
=> 'qdevice' },
64 __PACKAGE__-
>register_method ({
69 description
=> "Generate new cluster configuration.",
71 additionalProperties
=> 0,
74 description
=> "The name of the cluster.",
75 type
=> 'string', format
=> 'pve-node',
78 nodeid
=> get_standard_option
('corosync-nodeid'),
81 description
=> "Number of votes for this node.",
85 link0
=> get_standard_option
('corosync-link'),
86 link1
=> get_standard_option
('corosync-link'),
89 returns
=> { type
=> 'string' },
93 -f
$clusterconf && die "cluster config '$clusterconf' already exists\n";
95 my $rpcenv = PVE
::RPCEnvironment
::get
();
96 my $authuser = $rpcenv->get_user();
100 PVE
::Cluster
::setup_sshd_config
(1);
101 PVE
::Cluster
::setup_rootsshconfig
();
102 PVE
::Cluster
::setup_ssh_keys
();
104 PVE
::Tools
::run_command
(['/usr/sbin/corosync-keygen', '-lk', $authfile])
106 die "no authentication key available\n" if -f
!$authfile;
108 my $nodename = PVE
::INotify
::nodename
();
110 # get the corosync basis config for the new cluster
111 my $config = PVE
::Corosync
::create_conf
($nodename, %$param);
113 print "Writing corosync config to /etc/pve/corosync.conf\n";
114 PVE
::Corosync
::atomic_write_conf
($config);
116 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
117 PVE
::Cluster
::ssh_merge_keys
();
118 PVE
::Cluster
::gen_pve_node_files
($nodename, $local_ip_address);
119 PVE
::Cluster
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
121 print "Restart corosync and cluster filesystem\n";
122 PVE
::Tools
::run_command
('systemctl restart corosync pve-cluster');
126 PVE
::Tools
::lock_file
($local_cluster_lock, 10, $code);
130 return $rpcenv->fork_worker('clustercreate', $param->{clustername
}, $authuser, $worker);
133 __PACKAGE__-
>register_method({
137 description
=> "Corosync node list.",
139 check
=> ['perm', '/', [ 'Sys.Audit' ]],
142 additionalProperties
=> 0,
150 node
=> { type
=> 'string' },
153 links
=> [ { rel
=> 'child', href
=> "{node}" } ],
159 my $conf = PVE
::Cluster
::cfs_read_file
('corosync.conf');
160 my $nodelist = PVE
::Corosync
::nodelist
($conf);
162 return PVE
::RESTHandler
::hash_to_array
($nodelist, 'node');
165 # lock method to ensure local and cluster wide atomicity
166 # if we're a single node cluster just lock locally, we have no other cluster
167 # node which we could contend with, else also acquire a cluster wide lock
168 my $config_change_lock = sub {
171 PVE
::Tools
::lock_file
($local_cluster_lock, 10, sub {
172 PVE
::Cluster
::cfs_update
(1);
173 my $members = PVE
::Cluster
::get_members
();
174 if (scalar(keys %$members) > 1) {
175 return PVE
::Cluster
::cfs_lock_file
('corosync.conf', 10, $code);
182 __PACKAGE__-
>register_method ({
184 path
=> 'nodes/{node}',
187 description
=> "Adds a node to the cluster configuration. This call is for internal use.",
189 additionalProperties
=> 0,
191 node
=> get_standard_option
('pve-node'),
192 nodeid
=> get_standard_option
('corosync-nodeid'),
195 description
=> "Number of votes for this node",
201 description
=> "Do not throw error if node already exists.",
204 link0
=> get_standard_option
('corosync-link'),
205 link1
=> get_standard_option
('corosync-link'),
211 corosync_authkey
=> {
222 PVE
::Cluster
::check_cfs_quorum
();
225 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
226 my $nodelist = PVE
::Corosync
::nodelist
($conf);
227 my $totem_cfg = PVE
::Corosync
::totem_config
($conf);
229 my $name = $param->{node
};
231 # ensure we do not reuse an address, that can crash the whole cluster!
232 my $check_duplicate_addr = sub {
234 return if !defined($link) || !defined($link->{address
});
235 my $addr = $link->{address
};
237 while (my ($k, $v) = each %$nodelist) {
238 next if $k eq $name; # allows re-adding a node if force is set
240 for my $linknumber (0..1) {
241 my $id = "ring${linknumber}_addr";
242 next if !defined($v->{$id});
244 die "corosync: address '$addr' already used on link $id by node '$k'\n"
245 if $v->{$id} eq $addr;
250 my $link0 = PVE
::Cluster
::parse_corosync_link
($param->{link0
});
251 my $link1 = PVE
::Cluster
::parse_corosync_link
($param->{link1
});
253 $check_duplicate_addr->($link0);
254 $check_duplicate_addr->($link1);
256 # FIXME: handle all links (0-7), they're all independent now
257 $link0->{address
} //= $name if exists($totem_cfg->{interface
}->{0});
259 die "corosync: using 'link1' parameter needs a interface with linknumber '1' configured!\n"
260 if $link1 && !defined($totem_cfg->{interface
}->{1});
262 die "corosync: totem interface with linknumber 1 configured but 'link1' parameter not defined!\n"
263 if defined($totem_cfg->{interface
}->{1}) && !defined($link1);
265 if (defined(my $res = $nodelist->{$name})) {
266 $param->{nodeid
} = $res->{nodeid
} if !$param->{nodeid
};
267 $param->{votes
} = $res->{quorum_votes
} if !defined($param->{votes
});
269 if ($res->{quorum_votes
} == $param->{votes
} &&
270 $res->{nodeid
} == $param->{nodeid
} && $param->{force
}) {
271 print "forcing overwrite of configured node '$name'\n";
273 die "can't add existing node '$name'\n";
275 } elsif (!$param->{nodeid
}) {
280 foreach my $v (values %$nodelist) {
281 if ($v->{nodeid
} eq $nodeid) {
290 $param->{nodeid
} = $nodeid;
293 $param->{votes
} = 1 if !defined($param->{votes
});
295 PVE
::Cluster
::gen_local_dirs
($name);
297 eval { PVE
::Cluster
::ssh_merge_keys
(); };
300 $nodelist->{$name} = {
301 ring0_addr
=> $link0->{address
},
302 nodeid
=> $param->{nodeid
},
305 $nodelist->{$name}->{ring1_addr
} = $link1->{address
} if defined($link1);
306 $nodelist->{$name}->{quorum_votes
} = $param->{votes
} if $param->{votes
};
308 PVE
::Cluster
::log_msg
('notice', 'root@pam', "adding node $name to cluster");
310 PVE
::Corosync
::update_nodelist
($conf, $nodelist);
313 $config_change_lock->($code);
317 corosync_authkey
=> PVE
::Tools
::file_get_contents
($authfile),
318 corosync_conf
=> PVE
::Tools
::file_get_contents
($clusterconf),
325 __PACKAGE__-
>register_method ({
327 path
=> 'nodes/{node}',
330 description
=> "Removes a node from the cluster configuration.",
332 additionalProperties
=> 0,
334 node
=> get_standard_option
('pve-node'),
337 returns
=> { type
=> 'null' },
341 my $local_node = PVE
::INotify
::nodename
();
342 die "Cannot delete myself from cluster!\n" if $param->{node
} eq $local_node;
344 PVE
::Cluster
::check_cfs_quorum
();
347 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
348 my $nodelist = PVE
::Corosync
::nodelist
($conf);
353 foreach my $tmp_node (keys %$nodelist) {
354 my $d = $nodelist->{$tmp_node};
355 my $ring0_addr = $d->{ring0_addr
};
356 my $ring1_addr = $d->{ring1_addr
};
357 if (($tmp_node eq $param->{node
}) ||
358 (defined($ring0_addr) && ($ring0_addr eq $param->{node
})) ||
359 (defined($ring1_addr) && ($ring1_addr eq $param->{node
}))) {
361 $nodeid = $d->{nodeid
};
366 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
369 PVE
::Cluster
::log_msg
('notice', 'root@pam', "deleting node $node from cluster");
371 delete $nodelist->{$node};
373 PVE
::Corosync
::update_nodelist
($conf, $nodelist);
375 PVE
::Tools
::run_command
(['corosync-cfgtool','-k', $nodeid]) if defined($nodeid);
378 $config_change_lock->($code);
384 __PACKAGE__-
>register_method ({
388 check
=> ['perm', '/', [ 'Sys.Audit' ]],
391 description
=> "Get information needed to join this cluster over the connected node.",
393 additionalProperties
=> 0,
395 node
=> get_standard_option
('pve-node', {
396 description
=> "The node for which the joinee gets the nodeinfo. ",
397 default => "current connected node",
404 additionalProperties
=> 0,
410 additionalProperties
=> 1,
412 name
=> get_standard_option
('pve-node'),
413 nodeid
=> get_standard_option
('corosync-nodeid'),
414 ring0_addr
=> get_standard_option
('corosync-link'),
415 quorum_votes
=> { type
=> 'integer', minimum
=> 0 },
416 pve_addr
=> { type
=> 'string', format
=> 'ip' },
417 pve_fp
=> get_standard_option
('fingerprint-sha256'),
421 preferred_node
=> get_standard_option
('pve-node'),
422 totem
=> { type
=> 'object' },
423 config_digest
=> { type
=> 'string' },
429 my $nodename = $param->{node
} // PVE
::INotify
::nodename
();
431 PVE
::Cluster
::cfs_update
(1);
432 my $conf = PVE
::Cluster
::cfs_read_file
('corosync.conf');
434 die "node is not in a cluster, no join info available!\n"
435 if !($conf && $conf->{main
});
437 my $totem_cfg = $conf->{main
}->{totem
} // {};
438 my $nodelist = $conf->{main
}->{nodelist
}->{node
} // {};
439 my $corosync_config_digest = $conf->{digest
};
441 die "unknown node '$nodename'\n" if ! $nodelist->{$nodename};
443 foreach my $name (keys %$nodelist) {
444 my $node = $nodelist->{$name};
445 $node->{pve_fp
} = PVE
::Cluster
::get_node_fingerprint
($name);
446 $node->{pve_addr
} = scalar(PVE
::Cluster
::remote_node_ip
($name));
450 nodelist
=> [ values %$nodelist ],
451 preferred_node
=> $nodename,
453 config_digest
=> $corosync_config_digest,
459 __PACKAGE__-
>register_method ({
464 description
=> "Joins this node into an existing cluster.",
466 additionalProperties
=> 0,
470 description
=> "Hostname (or IP) of an existing cluster member."
472 nodeid
=> get_standard_option
('corosync-nodeid'),
475 description
=> "Number of votes for this node",
481 description
=> "Do not throw error if node already exists.",
484 link0
=> get_standard_option
('corosync-link', {
485 default => "IP resolved by node's hostname",
487 link1
=> get_standard_option
('corosync-link'),
488 fingerprint
=> get_standard_option
('fingerprint-sha256'),
490 description
=> "Superuser (root) password of peer node.",
496 returns
=> { type
=> 'string' },
500 my $rpcenv = PVE
::RPCEnvironment
::get
();
501 my $authuser = $rpcenv->get_user();
505 PVE
::Tools
::lock_file
($local_cluster_lock, 10, \
&PVE
::Cluster
::join, $param);
509 return $rpcenv->fork_worker('clusterjoin', undef, $authuser, $worker);
513 __PACKAGE__-
>register_method({
517 description
=> "Get corosync totem protocol settings.",
519 check
=> ['perm', '/', [ 'Sys.Audit' ]],
522 additionalProperties
=> 0,
533 my $conf = PVE
::Cluster
::cfs_read_file
('corosync.conf');
535 my $totem_cfg = $conf->{main
}->{totem
};
540 __PACKAGE__-
>register_method ({
544 description
=> 'Get QDevice status',
546 check
=> ['perm', '/', [ 'Sys.Audit' ]],
549 additionalProperties
=> 0,
559 my $socket_path = "/var/run/corosync-qdevice/corosync-qdevice.sock";
560 return $result if !-S
$socket_path;
562 my $qdevice_socket = IO
::Socket
::UNIX-
>new(
564 Peer
=> $socket_path,
567 print $qdevice_socket "status verbose\n";
571 "Last poll call" => 1,
577 while (my $line = <$qdevice_socket>) {
579 next if $line =~ /^\s/;
580 if ($line =~ /^(.*?)\s*:\s*(.*)$/) {
581 $result->{$1} = $2 if $qdevice_keys->{$1};
587 #TODO: possibly add setup and remove methods