]>
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
;
14 use PVE
::Cluster
::Setup
;
18 use base
qw(PVE::RESTHandler);
20 my $clusterconf = "/etc/pve/corosync.conf";
21 my $authfile = "/etc/corosync/authkey";
22 my $local_cluster_lock = "/var/lock/pvecm.lock";
26 description
=> "Node id for this node.",
30 PVE
::JSONSchema
::register_standard_option
("corosync-nodeid", $nodeid_desc);
32 __PACKAGE__-
>register_method({
36 description
=> "Directory index.",
38 check
=> ['perm', '/', [ 'Sys.Audit' ]],
41 additionalProperties
=> 0,
50 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
59 { name
=> 'qdevice' },
65 __PACKAGE__-
>register_method ({
70 description
=> "Generate new cluster configuration.",
72 additionalProperties
=> 0,
75 description
=> "The name of the cluster.",
76 type
=> 'string', format
=> 'pve-node',
79 nodeid
=> get_standard_option
('corosync-nodeid'),
82 description
=> "Number of votes for this node.",
86 link0
=> get_standard_option
('corosync-link'),
87 link1
=> get_standard_option
('corosync-link'),
90 returns
=> { type
=> 'string' },
94 -f
$clusterconf && die "cluster config '$clusterconf' already exists\n";
96 my $rpcenv = PVE
::RPCEnvironment
::get
();
97 my $authuser = $rpcenv->get_user();
101 PVE
::Cluster
::Setup
::setup_sshd_config
(1);
102 PVE
::Cluster
::Setup
::setup_rootsshconfig
();
103 PVE
::Cluster
::Setup
::setup_ssh_keys
();
105 PVE
::Tools
::run_command
(['/usr/sbin/corosync-keygen', '-lk', $authfile])
107 die "no authentication key available\n" if -f
!$authfile;
109 my $nodename = PVE
::INotify
::nodename
();
111 # get the corosync basis config for the new cluster
112 my $config = PVE
::Corosync
::create_conf
($nodename, %$param);
114 print "Writing corosync config to /etc/pve/corosync.conf\n";
115 PVE
::Corosync
::atomic_write_conf
($config);
117 my $local_ip_address = PVE
::Cluster
::remote_node_ip
($nodename);
118 PVE
::Cluster
::Setup
::ssh_merge_keys
();
119 PVE
::Cluster
::Setup
::gen_pve_node_files
($nodename, $local_ip_address);
120 PVE
::Cluster
::Setup
::ssh_merge_known_hosts
($nodename, $local_ip_address, 1);
122 print "Restart corosync and cluster filesystem\n";
123 PVE
::Tools
::run_command
('systemctl restart corosync pve-cluster');
127 PVE
::Tools
::lock_file
($local_cluster_lock, 10, $code);
131 return $rpcenv->fork_worker('clustercreate', $param->{clustername
}, $authuser, $worker);
134 __PACKAGE__-
>register_method({
138 description
=> "Corosync node list.",
140 check
=> ['perm', '/', [ 'Sys.Audit' ]],
143 additionalProperties
=> 0,
151 node
=> { type
=> 'string' },
154 links
=> [ { rel
=> 'child', href
=> "{node}" } ],
160 my $conf = PVE
::Cluster
::cfs_read_file
('corosync.conf');
161 my $nodelist = PVE
::Corosync
::nodelist
($conf);
163 return PVE
::RESTHandler
::hash_to_array
($nodelist, 'node');
166 # lock method to ensure local and cluster wide atomicity
167 # if we're a single node cluster just lock locally, we have no other cluster
168 # node which we could contend with, else also acquire a cluster wide lock
169 my $config_change_lock = sub {
172 PVE
::Tools
::lock_file
($local_cluster_lock, 10, sub {
173 PVE
::Cluster
::cfs_update
(1);
174 my $members = PVE
::Cluster
::get_members
();
175 if (scalar(keys %$members) > 1) {
176 my $res = PVE
::Cluster
::cfs_lock_file
('corosync.conf', 10, $code);
178 # cfs_lock_file only sets $@ but lock_file doesn't propagates $@ unless we die here
179 die $@ if defined($@);
188 __PACKAGE__-
>register_method ({
190 path
=> 'nodes/{node}',
193 description
=> "Adds a node to the cluster configuration. This call is for internal use.",
195 additionalProperties
=> 0,
197 node
=> get_standard_option
('pve-node'),
198 nodeid
=> get_standard_option
('corosync-nodeid'),
201 description
=> "Number of votes for this node",
207 description
=> "Do not throw error if node already exists.",
210 link0
=> get_standard_option
('corosync-link'),
211 link1
=> get_standard_option
('corosync-link'),
217 corosync_authkey
=> {
228 PVE
::Cluster
::check_cfs_quorum
();
231 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
232 my $nodelist = PVE
::Corosync
::nodelist
($conf);
233 my $totem_cfg = PVE
::Corosync
::totem_config
($conf);
235 my $name = $param->{node
};
237 # ensure we do not reuse an address, that can crash the whole cluster!
238 my $check_duplicate_addr = sub {
240 return if !defined($link) || !defined($link->{address
});
241 my $addr = $link->{address
};
243 while (my ($k, $v) = each %$nodelist) {
244 next if $k eq $name; # allows re-adding a node if force is set
246 for my $linknumber (0..1) {
247 my $id = "ring${linknumber}_addr";
248 next if !defined($v->{$id});
250 die "corosync: address '$addr' already used on link $id by node '$k'\n"
251 if $v->{$id} eq $addr;
256 my $link0 = PVE
::Cluster
::parse_corosync_link
($param->{link0
});
257 my $link1 = PVE
::Cluster
::parse_corosync_link
($param->{link1
});
259 $check_duplicate_addr->($link0);
260 $check_duplicate_addr->($link1);
262 # FIXME: handle all links (0-7), they're all independent now
263 $link0->{address
} //= $name if exists($totem_cfg->{interface
}->{0});
265 die "corosync: using 'link1' parameter needs a interface with linknumber '1' configured!\n"
266 if $link1 && !defined($totem_cfg->{interface
}->{1});
268 die "corosync: totem interface with linknumber 1 configured but 'link1' parameter not defined!\n"
269 if defined($totem_cfg->{interface
}->{1}) && !defined($link1);
271 if (defined(my $res = $nodelist->{$name})) {
272 $param->{nodeid
} = $res->{nodeid
} if !$param->{nodeid
};
273 $param->{votes
} = $res->{quorum_votes
} if !defined($param->{votes
});
275 if ($res->{quorum_votes
} == $param->{votes
} &&
276 $res->{nodeid
} == $param->{nodeid
} && $param->{force
}) {
277 print "forcing overwrite of configured node '$name'\n";
279 die "can't add existing node '$name'\n";
281 } elsif (!$param->{nodeid
}) {
286 foreach my $v (values %$nodelist) {
287 if ($v->{nodeid
} eq $nodeid) {
296 $param->{nodeid
} = $nodeid;
299 $param->{votes
} = 1 if !defined($param->{votes
});
301 PVE
::Cluster
::Setup
::gen_local_dirs
($name);
303 eval { PVE
::Cluster
::Setup
::ssh_merge_keys
(); };
306 $nodelist->{$name} = {
307 ring0_addr
=> $link0->{address
},
308 nodeid
=> $param->{nodeid
},
311 $nodelist->{$name}->{ring1_addr
} = $link1->{address
} if defined($link1);
312 $nodelist->{$name}->{quorum_votes
} = $param->{votes
} if $param->{votes
};
314 PVE
::Cluster
::log_msg
('notice', 'root@pam', "adding node $name to cluster");
316 PVE
::Corosync
::update_nodelist
($conf, $nodelist);
319 $config_change_lock->($code);
323 corosync_authkey
=> PVE
::Tools
::file_get_contents
($authfile),
324 corosync_conf
=> PVE
::Tools
::file_get_contents
($clusterconf),
331 __PACKAGE__-
>register_method ({
333 path
=> 'nodes/{node}',
336 description
=> "Removes a node from the cluster configuration.",
338 additionalProperties
=> 0,
340 node
=> get_standard_option
('pve-node'),
343 returns
=> { type
=> 'null' },
347 my $local_node = PVE
::INotify
::nodename
();
348 die "Cannot delete myself from cluster!\n" if $param->{node
} eq $local_node;
350 PVE
::Cluster
::check_cfs_quorum
();
353 my $conf = PVE
::Cluster
::cfs_read_file
("corosync.conf");
354 my $nodelist = PVE
::Corosync
::nodelist
($conf);
359 foreach my $tmp_node (keys %$nodelist) {
360 my $d = $nodelist->{$tmp_node};
361 my $ring0_addr = $d->{ring0_addr
};
362 my $ring1_addr = $d->{ring1_addr
};
363 if (($tmp_node eq $param->{node
}) ||
364 (defined($ring0_addr) && ($ring0_addr eq $param->{node
})) ||
365 (defined($ring1_addr) && ($ring1_addr eq $param->{node
}))) {
367 $nodeid = $d->{nodeid
};
372 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
375 PVE
::Cluster
::log_msg
('notice', 'root@pam', "deleting node $node from cluster");
377 delete $nodelist->{$node};
379 PVE
::Corosync
::update_nodelist
($conf, $nodelist);
381 PVE
::Tools
::run_command
(['corosync-cfgtool','-k', $nodeid]) if defined($nodeid);
384 $config_change_lock->($code);
390 __PACKAGE__-
>register_method ({
394 check
=> ['perm', '/', [ 'Sys.Audit' ]],
397 description
=> "Get information needed to join this cluster over the connected node.",
399 additionalProperties
=> 0,
401 node
=> get_standard_option
('pve-node', {
402 description
=> "The node for which the joinee gets the nodeinfo. ",
403 default => "current connected node",
410 additionalProperties
=> 0,
416 additionalProperties
=> 1,
418 name
=> get_standard_option
('pve-node'),
419 nodeid
=> get_standard_option
('corosync-nodeid'),
420 ring0_addr
=> get_standard_option
('corosync-link'),
421 quorum_votes
=> { type
=> 'integer', minimum
=> 0 },
422 pve_addr
=> { type
=> 'string', format
=> 'ip' },
423 pve_fp
=> get_standard_option
('fingerprint-sha256'),
427 preferred_node
=> get_standard_option
('pve-node'),
428 totem
=> { type
=> 'object' },
429 config_digest
=> { type
=> 'string' },
435 my $nodename = $param->{node
} // PVE
::INotify
::nodename
();
437 PVE
::Cluster
::cfs_update
(1);
438 my $conf = PVE
::Cluster
::cfs_read_file
('corosync.conf');
440 die "node is not in a cluster, no join info available!\n"
441 if !($conf && $conf->{main
});
443 my $totem_cfg = $conf->{main
}->{totem
} // {};
444 my $nodelist = $conf->{main
}->{nodelist
}->{node
} // {};
445 my $corosync_config_digest = $conf->{digest
};
447 die "unknown node '$nodename'\n" if ! $nodelist->{$nodename};
449 foreach my $name (keys %$nodelist) {
450 my $node = $nodelist->{$name};
451 $node->{pve_fp
} = PVE
::Cluster
::get_node_fingerprint
($name);
452 $node->{pve_addr
} = scalar(PVE
::Cluster
::remote_node_ip
($name));
456 nodelist
=> [ values %$nodelist ],
457 preferred_node
=> $nodename,
459 config_digest
=> $corosync_config_digest,
465 __PACKAGE__-
>register_method ({
470 description
=> "Joins this node into an existing cluster.",
472 additionalProperties
=> 0,
476 description
=> "Hostname (or IP) of an existing cluster member."
478 nodeid
=> get_standard_option
('corosync-nodeid'),
481 description
=> "Number of votes for this node",
487 description
=> "Do not throw error if node already exists.",
490 link0
=> get_standard_option
('corosync-link', {
491 default => "IP resolved by node's hostname",
493 link1
=> get_standard_option
('corosync-link'),
494 fingerprint
=> get_standard_option
('fingerprint-sha256'),
496 description
=> "Superuser (root) password of peer node.",
502 returns
=> { type
=> 'string' },
506 my $rpcenv = PVE
::RPCEnvironment
::get
();
507 my $authuser = $rpcenv->get_user();
511 PVE
::Tools
::lock_file
($local_cluster_lock, 10, \
&PVE
::Cluster
::Setup
::join, $param);
515 return $rpcenv->fork_worker('clusterjoin', undef, $authuser, $worker);
519 __PACKAGE__-
>register_method({
523 description
=> "Get corosync totem protocol settings.",
525 check
=> ['perm', '/', [ 'Sys.Audit' ]],
528 additionalProperties
=> 0,
539 my $conf = PVE
::Cluster
::cfs_read_file
('corosync.conf');
541 my $totem_cfg = $conf->{main
}->{totem
};
546 __PACKAGE__-
>register_method ({
550 description
=> 'Get QDevice status',
552 check
=> ['perm', '/', [ 'Sys.Audit' ]],
555 additionalProperties
=> 0,
565 my $socket_path = "/var/run/corosync-qdevice/corosync-qdevice.sock";
566 return $result if !-S
$socket_path;
568 my $qdevice_socket = IO
::Socket
::UNIX-
>new(
570 Peer
=> $socket_path,
573 print $qdevice_socket "status verbose\n";
577 "Last poll call" => 1,
583 while (my $line = <$qdevice_socket>) {
585 next if $line =~ /^\s/;
586 if ($line =~ /^(.*?)\s*:\s*(.*)$/) {
587 $result->{$1} = $2 if $qdevice_keys->{$1};
593 #TODO: possibly add setup and remove methods