]> git.proxmox.com Git - pve-cluster.git/blob - data/PVE/API2/ClusterConfig.pm
move cluster create to API
[pve-cluster.git] / data / PVE / API2 / ClusterConfig.pm
1 package PVE::API2::ClusterConfig;
2
3 use strict;
4 use warnings;
5
6 use PVE::Tools;
7 use PVE::SafeSyslog;
8 use PVE::RESTHandler;
9 use PVE::RPCEnvironment;
10 use PVE::JSONSchema qw(get_standard_option);
11 use PVE::Cluster;
12 use PVE::APIClient::LWP;
13 use PVE::Corosync;
14
15 use base qw(PVE::RESTHandler);
16
17 my $clusterconf = "/etc/pve/corosync.conf";
18 my $authfile = "/etc/corosync/authkey";
19
20 __PACKAGE__->register_method({
21 name => 'index',
22 path => '',
23 method => 'GET',
24 description => "Directory index.",
25 permissions => {
26 check => ['perm', '/', [ 'Sys.Audit' ]],
27 },
28 parameters => {
29 additionalProperties => 0,
30 properties => {},
31 },
32 returns => {
33 type => 'array',
34 items => {
35 type => "object",
36 properties => {},
37 },
38 links => [ { rel => 'child', href => "{name}" } ],
39 },
40 code => sub {
41 my ($param) = @_;
42
43 my $result = [
44 { name => 'nodes' },
45 { name => 'totem' },
46 { name => 'join' },
47 ];
48
49 return $result;
50 }});
51
52 __PACKAGE__->register_method ({
53 name => 'create',
54 path => '',
55 method => 'POST',
56 protected => 1,
57 description => "Generate new cluster configuration.",
58 parameters => {
59 additionalProperties => 0,
60 properties => {
61 clustername => {
62 description => "The name of the cluster.",
63 type => 'string', format => 'pve-node',
64 maxLength => 15,
65 },
66 nodeid => {
67 type => 'integer',
68 description => "Node id for this node.",
69 minimum => 1,
70 optional => 1,
71 },
72 votes => {
73 type => 'integer',
74 description => "Number of votes for this node.",
75 minimum => 1,
76 optional => 1,
77 },
78 bindnet0_addr => {
79 type => 'string', format => 'ip',
80 description => "This specifies the network address the corosync ring 0".
81 " executive should bind to and defaults to the local IP address of the node.",
82 optional => 1,
83 },
84 ring0_addr => {
85 type => 'string', format => 'address',
86 description => "Hostname (or IP) of the corosync ring0 address of this node.".
87 " Defaults to the hostname of the node.",
88 optional => 1,
89 },
90 bindnet1_addr => {
91 type => 'string', format => 'ip',
92 description => "This specifies the network address the corosync ring 1".
93 " executive should bind to and is optional.",
94 optional => 1,
95 },
96 ring1_addr => {
97 type => 'string', format => 'address',
98 description => "Hostname (or IP) of the corosync ring1 address, this".
99 " needs an valid bindnet1_addr.",
100 optional => 1,
101 },
102 },
103 },
104 returns => { type => 'null' },
105
106 code => sub {
107 my ($param) = @_;
108
109 -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
110
111 PVE::Cluster::setup_sshd_config(1);
112 PVE::Cluster::setup_rootsshconfig();
113 PVE::Cluster::setup_ssh_keys();
114
115 PVE::Tools::run_command(['/usr/sbin/corosync-keygen', '-lk', $authfile])
116 if !-f $authfile;
117 die "no authentication key available\n" if -f !$authfile;
118
119 my $nodename = PVE::INotify::nodename();
120
121 # get the corosync basis config for the new cluster
122 my $config = PVE::Corosync::create_conf($nodename, %$param);
123
124 print "Writing corosync config to /etc/pve/corosync.conf\n";
125 PVE::Corosync::atomic_write_conf($config);
126
127 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
128 PVE::Cluster::ssh_merge_keys();
129 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
130 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
131
132 print "Restart corosync and cluster filesystem\n";
133 PVE::Tools::run_command('systemctl restart corosync pve-cluster');
134
135 return undef;
136 }});
137
138 __PACKAGE__->register_method({
139 name => 'nodes',
140 path => 'nodes',
141 method => 'GET',
142 description => "Corosync node list.",
143 permissions => {
144 check => ['perm', '/', [ 'Sys.Audit' ]],
145 },
146 parameters => {
147 additionalProperties => 0,
148 properties => {},
149 },
150 returns => {
151 type => 'array',
152 items => {
153 type => "object",
154 properties => {
155 node => { type => 'string' },
156 },
157 },
158 links => [ { rel => 'child', href => "{node}" } ],
159 },
160 code => sub {
161 my ($param) = @_;
162
163
164 my $conf = PVE::Cluster::cfs_read_file('corosync.conf');
165 my $nodelist = PVE::Corosync::nodelist($conf);
166
167 return PVE::RESTHandler::hash_to_array($nodelist, 'node');
168 }});
169
170 # lock method to ensure local and cluster wide atomicity
171 # if we're a single node cluster just lock locally, we have no other cluster
172 # node which we could contend with, else also acquire a cluster wide lock
173 my $config_change_lock = sub {
174 my ($code) = @_;
175
176 my $local_lock_fn = "/var/lock/pvecm.lock";
177 PVE::Tools::lock_file($local_lock_fn, 10, sub {
178 PVE::Cluster::cfs_update(1);
179 my $members = PVE::Cluster::get_members();
180 if (scalar(keys %$members) > 1) {
181 return PVE::Cluster::cfs_lock_file('corosync.conf', 10, $code);
182 } else {
183 return $code->();
184 }
185 });
186 };
187
188 __PACKAGE__->register_method ({
189 name => 'addnode',
190 path => 'nodes/{node}',
191 method => 'POST',
192 protected => 1,
193 description => "Adds a node to the cluster configuration.",
194 parameters => {
195 additionalProperties => 0,
196 properties => {
197 node => get_standard_option('pve-node'),
198 nodeid => {
199 type => 'integer',
200 description => "Node id for this node.",
201 minimum => 1,
202 optional => 1,
203 },
204 votes => {
205 type => 'integer',
206 description => "Number of votes for this node",
207 minimum => 0,
208 optional => 1,
209 },
210 force => {
211 type => 'boolean',
212 description => "Do not throw error if node already exists.",
213 optional => 1,
214 },
215 ring0_addr => {
216 type => 'string', format => 'address',
217 description => "Hostname (or IP) of the corosync ring0 address of this node.".
218 " Defaults to nodes hostname.",
219 optional => 1,
220 },
221 ring1_addr => {
222 type => 'string', format => 'address',
223 description => "Hostname (or IP) of the corosync ring1 address, this".
224 " needs an valid bindnet1_addr.",
225 optional => 1,
226 },
227 },
228 },
229 returns => {
230 type => "object",
231 properties => {
232 corosync_authkey => {
233 type => 'string',
234 },
235 corosync_conf => {
236 type => 'string',
237 }
238 },
239 },
240 code => sub {
241 my ($param) = @_;
242
243 PVE::Cluster::check_cfs_quorum();
244
245 my $code = sub {
246 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
247 my $nodelist = PVE::Corosync::nodelist($conf);
248 my $totem_cfg = PVE::Corosync::totem_config($conf);
249
250 my $name = $param->{node};
251
252 # ensure we do not reuse an address, that can crash the whole cluster!
253 my $check_duplicate_addr = sub {
254 my $addr = shift;
255 return if !defined($addr);
256
257 while (my ($k, $v) = each %$nodelist) {
258 next if $k eq $name; # allows re-adding a node if force is set
259 if ($v->{ring0_addr} eq $addr || ($v->{ring1_addr} && $v->{ring1_addr} eq $addr)) {
260 die "corosync: address '$addr' already defined by node '$k'\n";
261 }
262 }
263 };
264
265 &$check_duplicate_addr($param->{ring0_addr});
266 &$check_duplicate_addr($param->{ring1_addr});
267
268 $param->{ring0_addr} = $name if !$param->{ring0_addr};
269
270 die "corosync: using 'ring1_addr' parameter needs a configured ring 1 interface!\n"
271 if $param->{ring1_addr} && !defined($totem_cfg->{interface}->{1});
272
273 die "corosync: ring 1 interface configured but 'ring1_addr' parameter not defined!\n"
274 if defined($totem_cfg->{interface}->{1}) && !defined($param->{ring1_addr});
275
276 if (defined(my $res = $nodelist->{$name})) {
277 $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
278 $param->{votes} = $res->{quorum_votes} if !defined($param->{votes});
279
280 if ($res->{quorum_votes} == $param->{votes} &&
281 $res->{nodeid} == $param->{nodeid} && $param->{force}) {
282 print "forcing overwrite of configured node '$name'\n";
283 } else {
284 die "can't add existing node '$name'\n";
285 }
286 } elsif (!$param->{nodeid}) {
287 my $nodeid = 1;
288
289 while(1) {
290 my $found = 0;
291 foreach my $v (values %$nodelist) {
292 if ($v->{nodeid} eq $nodeid) {
293 $found = 1;
294 $nodeid++;
295 last;
296 }
297 }
298 last if !$found;
299 };
300
301 $param->{nodeid} = $nodeid;
302 }
303
304 $param->{votes} = 1 if !defined($param->{votes});
305
306 PVE::Cluster::gen_local_dirs($name);
307
308 eval { PVE::Cluster::ssh_merge_keys(); };
309 warn $@ if $@;
310
311 $nodelist->{$name} = {
312 ring0_addr => $param->{ring0_addr},
313 nodeid => $param->{nodeid},
314 name => $name,
315 };
316 $nodelist->{$name}->{ring1_addr} = $param->{ring1_addr} if $param->{ring1_addr};
317 $nodelist->{$name}->{quorum_votes} = $param->{votes} if $param->{votes};
318
319 PVE::Cluster::log_msg('notice', 'root@pam', "adding node $name to cluster");
320
321 PVE::Corosync::update_nodelist($conf, $nodelist);
322 };
323
324 $config_change_lock->($code);
325 die $@ if $@;
326
327 my $res = {
328 corosync_authkey => PVE::Tools::file_get_contents($authfile),
329 corosync_conf => PVE::Tools::file_get_contents($clusterconf),
330 };
331
332 return $res;
333 }});
334
335
336 __PACKAGE__->register_method ({
337 name => 'delnode',
338 path => 'nodes/{node}',
339 method => 'DELETE',
340 protected => 1,
341 description => "Removes a node from the cluster configuration.",
342 parameters => {
343 additionalProperties => 0,
344 properties => {
345 node => get_standard_option('pve-node'),
346 },
347 },
348 returns => { type => 'null' },
349 code => sub {
350 my ($param) = @_;
351
352 my $local_node = PVE::INotify::nodename();
353 die "Cannot delete myself from cluster!\n" if $param->{node} eq $local_node;
354
355 PVE::Cluster::check_cfs_quorum();
356
357 my $code = sub {
358 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
359 my $nodelist = PVE::Corosync::nodelist($conf);
360
361 my $node;
362 my $nodeid;
363
364 foreach my $tmp_node (keys %$nodelist) {
365 my $d = $nodelist->{$tmp_node};
366 my $ring0_addr = $d->{ring0_addr};
367 my $ring1_addr = $d->{ring1_addr};
368 if (($tmp_node eq $param->{node}) ||
369 (defined($ring0_addr) && ($ring0_addr eq $param->{node})) ||
370 (defined($ring1_addr) && ($ring1_addr eq $param->{node}))) {
371 $node = $tmp_node;
372 $nodeid = $d->{nodeid};
373 last;
374 }
375 }
376
377 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
378 if !defined($node);
379
380 PVE::Cluster::log_msg('notice', 'root@pam', "deleting node $node from cluster");
381
382 delete $nodelist->{$node};
383
384 PVE::Corosync::update_nodelist($conf, $nodelist);
385
386 PVE::Tools::run_command(['corosync-cfgtool','-k', $nodeid]) if defined($nodeid);
387 };
388
389 $config_change_lock->($code);
390 die $@ if $@;
391
392 return undef;
393 }});
394
395 __PACKAGE__->register_method ({
396 name => 'join',
397 path => 'join',
398 method => 'POST',
399 protected => 1,
400 description => "Joins this node into an existing cluster.",
401 parameters => {
402 additionalProperties => 0,
403 properties => {
404 hostname => {
405 type => 'string',
406 description => "Hostname (or IP) of an existing cluster member."
407 },
408 nodeid => {
409 type => 'integer',
410 description => "Node id for this node.",
411 minimum => 1,
412 optional => 1,
413 },
414 votes => {
415 type => 'integer',
416 description => "Number of votes for this node",
417 minimum => 0,
418 optional => 1,
419 },
420 force => {
421 type => 'boolean',
422 description => "Do not throw error if node already exists.",
423 optional => 1,
424 },
425 ring0_addr => {
426 type => 'string', format => 'address',
427 description => "Hostname (or IP) of the corosync ring0 address of this node.".
428 " Defaults to nodes hostname.",
429 optional => 1,
430 },
431 ring1_addr => {
432 type => 'string', format => 'address',
433 description => "Hostname (or IP) of the corosync ring1 address, this".
434 " needs an valid configured ring 1 interface in the cluster.",
435 optional => 1,
436 },
437 fingerprint => get_standard_option('fingerprint-sha256'),
438 password => {
439 description => "Superuser (root) password of peer node.",
440 type => 'string',
441 maxLength => 128,
442 },
443 },
444 },
445 returns => { type => 'string' },
446 code => sub {
447 my ($param) = @_;
448
449 my $rpcenv = PVE::RPCEnvironment::get();
450 my $authuser = $rpcenv->get_user();
451
452 my $worker = sub {
453 PVE::Cluster::join($param);
454 };
455
456 return $rpcenv->fork_worker('clusterjoin', '', $authuser, $worker);
457 }});
458
459
460 __PACKAGE__->register_method({
461 name => 'totem',
462 path => 'totem',
463 method => 'GET',
464 description => "Get corosync totem protocol settings.",
465 permissions => {
466 check => ['perm', '/', [ 'Sys.Audit' ]],
467 },
468 parameters => {
469 additionalProperties => 0,
470 properties => {},
471 },
472 returns => {
473 type => "object",
474 properties => {},
475 },
476 code => sub {
477 my ($param) = @_;
478
479
480 my $conf = PVE::Cluster::cfs_read_file('corosync.conf');
481
482 my $totem_cfg = $conf->{main}->{totem};
483
484 return $totem_cfg;
485 }});
486
487 1;