]>
Commit | Line | Data |
---|---|---|
fb5f2d1e | 1 | package PMG::API2::Cluster; |
5ac6bd0e DM |
2 | |
3 | use strict; | |
4 | use warnings; | |
5 | use Data::Dumper; | |
6 | ||
7 | use PVE::SafeSyslog; | |
8 | use PVE::Tools qw(extract_param); | |
9 | use HTTP::Status qw(:constants); | |
10 | use Storable qw(dclone); | |
11 | use PVE::JSONSchema qw(get_standard_option); | |
12 | use PVE::RESTHandler; | |
13 | use PVE::INotify; | |
e16a9efc | 14 | use PVE::APIClient::LWP; |
5ac6bd0e | 15 | |
cfdf6608 | 16 | use PMG::RESTEnvironment; |
5ac6bd0e | 17 | use PMG::ClusterConfig; |
e16a9efc | 18 | use PMG::Cluster; |
cfdf6608 | 19 | use PMG::DBTools; |
5ac6bd0e | 20 | |
ba11e2d3 DM |
21 | use PMG::API2::Nodes; |
22 | ||
5ac6bd0e DM |
23 | use base qw(PVE::RESTHandler); |
24 | ||
e16a9efc | 25 | sub cluster_join { |
245b527c | 26 | my ($cinfo, $conn_setup) = @_; |
e16a9efc DM |
27 | |
28 | my $conn = PVE::APIClient::LWP->new(%$conn_setup); | |
29 | ||
30 | my $info = PMG::Cluster::read_local_cluster_info(); | |
31 | ||
32 | my $res = $conn->post("/config/cluster/nodes", $info); | |
275d6d6d DM |
33 | |
34 | foreach my $node (@$res) { | |
245b527c | 35 | $cinfo->{ids}->{$node->{cid}} = $node; |
275d6d6d DM |
36 | } |
37 | ||
5a312cf1 DM |
38 | eval { |
39 | print STDERR "stop all services accessing the database\n"; | |
40 | # stop all services accessing the database | |
00d8b7f7 | 41 | PMG::Utils::service_wait_stopped(40, $PMG::Utils::db_service_list); |
5a312cf1 DM |
42 | |
43 | print STDERR "save new cluster configuration\n"; | |
245b527c | 44 | $cinfo->write(); |
5a312cf1 | 45 | |
245b527c | 46 | PMG::Cluster::update_ssh_keys($cinfo); |
5a312cf1 DM |
47 | |
48 | print STDERR "cluster node successfully joined\n"; | |
49 | ||
245b527c | 50 | $cinfo = PMG::ClusterConfig->new(); # reload |
5a312cf1 | 51 | |
245b527c DM |
52 | my $role = $cinfo->{'local'}->{type} // '-'; |
53 | die "local node '$cinfo->{local}->{name}' not part of cluster\n" | |
5a312cf1 DM |
54 | if $role eq '-'; |
55 | ||
245b527c | 56 | die "got unexpected role '$role' for local node '$cinfo->{local}->{name}'\n" |
809ae8f4 DM |
57 | if $role ne 'node'; |
58 | ||
245b527c | 59 | my $cid = $cinfo->{'local'}->{cid}; |
5a312cf1 DM |
60 | |
61 | PMG::Cluster::create_needed_dirs($cid, 1); | |
62 | ||
245b527c | 63 | PMG::Cluster::sync_config_from_master($cinfo->{master}->{name}, $cinfo->{master}->{ip}); |
5a312cf1 | 64 | |
245b527c | 65 | PMG::DBTools::init_nodedb($cinfo); |
5a312cf1 | 66 | |
00d8b7f7 | 67 | my $cfg = PMG::Config->new(); |
245b527c DM |
68 | my $ruledb = PMG::RuleDB->new(); |
69 | my $rulecache = PMG::RuleCache->new($ruledb); | |
70 | ||
71 | $cfg->rewrite_config($rulecache, 1); | |
5a312cf1 DM |
72 | |
73 | print STDERR "syncing quarantine data\n"; | |
245b527c | 74 | PMG::Cluster::sync_master_quar($cinfo->{master}->{ip}, $cinfo->{master}->{name}); |
5a312cf1 DM |
75 | print STDERR "syncing quarantine data finished\n"; |
76 | }; | |
77 | my $err = $@; | |
78 | ||
00d8b7f7 | 79 | foreach my $service (reverse @$PMG::Utils::db_service_list) { |
5a312cf1 DM |
80 | eval { PVE::Tools::run_command(['systemctl', 'start', $service]); }; |
81 | warn $@ if $@; | |
82 | } | |
83 | ||
84 | die $err if $err; | |
e16a9efc DM |
85 | } |
86 | ||
5ac6bd0e DM |
87 | __PACKAGE__->register_method({ |
88 | name => 'index', | |
89 | path => '', | |
90 | method => 'GET', | |
fb5f2d1e DM |
91 | description => "Directory index.", |
92 | permissions => { user => 'all' }, | |
93 | parameters => { | |
94 | additionalProperties => 0, | |
95 | properties => {}, | |
96 | }, | |
97 | returns => { | |
98 | type => 'array', | |
99 | items => { | |
100 | type => "object", | |
101 | properties => {}, | |
102 | }, | |
103 | links => [ { rel => 'child', href => "{name}" } ], | |
104 | }, | |
105 | code => sub { | |
106 | my ($param) = @_; | |
107 | ||
108 | my $result = [ | |
109 | { name => 'nodes' }, | |
150e8421 | 110 | { name => 'status' }, |
fb5f2d1e DM |
111 | { name => 'create' }, |
112 | { name => 'join' }, | |
113 | ]; | |
114 | ||
115 | return $result; | |
116 | }}); | |
117 | ||
118 | __PACKAGE__->register_method({ | |
119 | name => 'nodes', | |
120 | path => 'nodes', | |
121 | method => 'GET', | |
5ac6bd0e DM |
122 | description => "Cluster node index.", |
123 | # alway read local file | |
124 | parameters => { | |
125 | additionalProperties => 0, | |
126 | properties => {}, | |
127 | }, | |
cba17aeb | 128 | permissions => { check => [ 'admin' ] }, |
5ac6bd0e DM |
129 | returns => { |
130 | type => 'array', | |
131 | items => { | |
132 | type => "object", | |
133 | properties => { | |
150e8421 | 134 | type => { type => 'string' }, |
5ac6bd0e | 135 | cid => { type => 'integer' }, |
150e8421 DM |
136 | ip => { type => 'string' }, |
137 | name => { type => 'string' }, | |
138 | hostrsapubkey => { type => 'string' }, | |
139 | rootrsapubkey => { type => 'string' }, | |
140 | fingerprint => { type => 'string' }, | |
141 | }, | |
142 | }, | |
143 | links => [ { rel => 'child', href => "{cid}" } ], | |
144 | }, | |
145 | code => sub { | |
146 | my ($param) = @_; | |
147 | ||
245b527c | 148 | my $cinfo = PMG::ClusterConfig->new(); |
150e8421 | 149 | |
245b527c DM |
150 | if (scalar(keys %{$cinfo->{ids}})) { |
151 | my $role = $cinfo->{local}->{type} // '-'; | |
150e8421 | 152 | if ($role eq '-') { |
245b527c | 153 | die "local node '$cinfo->{local}->{name}' not part of cluster\n"; |
150e8421 DM |
154 | } |
155 | } | |
156 | ||
245b527c | 157 | return PVE::RESTHandler::hash_to_array($cinfo->{ids}, 'cid'); |
150e8421 DM |
158 | }}); |
159 | ||
160 | __PACKAGE__->register_method({ | |
161 | name => 'status', | |
162 | path => 'status', | |
163 | method => 'GET', | |
164 | description => "Cluster node status.", | |
165 | # alway read local file | |
166 | parameters => { | |
167 | additionalProperties => 0, | |
168 | properties => {}, | |
169 | }, | |
170 | permissions => { check => [ 'admin' ] }, | |
171 | returns => { | |
172 | type => 'array', | |
173 | items => { | |
174 | type => "object", | |
175 | properties => { | |
176 | type => { type => 'string' }, | |
177 | cid => { type => 'integer' }, | |
178 | ip => { type => 'string' }, | |
179 | name => { type => 'string' }, | |
180 | hostrsapubkey => { type => 'string' }, | |
181 | rootrsapubkey => { type => 'string' }, | |
182 | fingerprint => { type => 'string' }, | |
5ac6bd0e DM |
183 | }, |
184 | }, | |
185 | links => [ { rel => 'child', href => "{cid}" } ], | |
186 | }, | |
187 | code => sub { | |
188 | my ($param) = @_; | |
189 | ||
245b527c | 190 | my $cinfo = PMG::ClusterConfig->new(); |
5ac6bd0e | 191 | |
245b527c DM |
192 | if (scalar(keys %{$cinfo->{ids}})) { |
193 | my $role = $cinfo->{local}->{type} // '-'; | |
3862c23d | 194 | if ($role eq '-') { |
245b527c | 195 | die "local node '$cinfo->{local}->{name}' not part of cluster\n"; |
3862c23d DM |
196 | } |
197 | } | |
198 | ||
245b527c | 199 | my $res = PVE::RESTHandler::hash_to_array($cinfo->{ids}, 'cid'); |
ba11e2d3 DM |
200 | |
201 | my $rpcenv = PMG::RESTEnvironment->get(); | |
202 | my $authuser = $rpcenv->get_user(); | |
203 | my $ticket = $rpcenv->get_ticket(); | |
204 | ||
205 | foreach my $ni (@$res) { | |
206 | my $info; | |
207 | eval { | |
245b527c | 208 | if ($ni->{cid} eq $cinfo->{local}->{cid}) { |
ba11e2d3 DM |
209 | $info = PMG::API2::NodeInfo->status({ node => PVE::INotify::nodename()}); |
210 | } else { | |
211 | my $conn = PVE::APIClient::LWP->new( | |
212 | ticket => $ticket, | |
213 | cookie_name => 'PMGAuthCookie', | |
214 | host => $ni->{ip}, | |
215 | cached_fingerprints => { | |
216 | $ni->{fingerprint} => 1, | |
217 | }); | |
218 | ||
219 | $info = $conn->get("/nodes/localhost/status", {}); | |
220 | } | |
221 | }; | |
222 | if (my $err = $@) { | |
15eac67d | 223 | $ni->{conn_error} = "$err"; # convert $err to string |
ba11e2d3 DM |
224 | next; |
225 | } | |
226 | foreach my $k (keys %$info) { | |
227 | $ni->{$k} = $info->{$k} if !defined($ni->{$k}); | |
228 | } | |
229 | } | |
230 | ||
231 | return $res; | |
5ac6bd0e DM |
232 | }}); |
233 | ||
e16a9efc DM |
234 | my $add_node_schema = PMG::ClusterConfig::Node->createSchema(1); |
235 | delete $add_node_schema->{properties}->{cid}; | |
236 | ||
237 | __PACKAGE__->register_method({ | |
238 | name => 'add_node', | |
239 | path => 'nodes', | |
240 | method => 'POST', | |
241 | description => "Add an node to the cluster config.", | |
242 | proxyto => 'master', | |
243 | protected => 1, | |
244 | parameters => $add_node_schema, | |
275d6d6d DM |
245 | returns => { |
246 | description => "Returns the resulting node list.", | |
247 | type => 'array', | |
248 | items => { | |
249 | type => "object", | |
250 | properties => { | |
251 | cid => { type => 'integer' }, | |
252 | }, | |
253 | }, | |
254 | }, | |
e16a9efc DM |
255 | code => sub { |
256 | my ($param) = @_; | |
257 | ||
258 | my $code = sub { | |
245b527c | 259 | my $cinfo = PMG::ClusterConfig->new(); |
e16a9efc | 260 | |
245b527c | 261 | die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids}}); |
e16a9efc | 262 | |
245b527c | 263 | my $master = $cinfo->{master} || die "unable to lookup master node\n"; |
e16a9efc | 264 | |
92c560f7 | 265 | my $next_cid; |
245b527c DM |
266 | foreach my $cid (keys %{$cinfo->{ids}}) { |
267 | my $d = $cinfo->{ids}->{$cid}; | |
92c560f7 DM |
268 | |
269 | if ($d->{type} eq 'node' && $d->{ip} eq $param->{ip} && $d->{name} eq $param->{name}) { | |
275d6d6d | 270 | $next_cid = $cid; # allow overwrite existing node data |
92c560f7 DM |
271 | last; |
272 | } | |
273 | ||
274 | if ($d->{ip} eq $param->{ip}) { | |
275 | die "ip address '$param->{ip}' is already used by existing node $d->{name}\n"; | |
276 | } | |
277 | ||
278 | if ($d->{name} eq $param->{name}) { | |
279 | die "node with name '$param->{name}' already exists\n"; | |
280 | } | |
281 | } | |
282 | ||
283 | if (!defined($next_cid)) { | |
284 | $next_cid = ++$master->{maxcid}; | |
285 | } | |
e16a9efc DM |
286 | |
287 | my $node = { | |
288 | type => 'node', | |
289 | cid => $master->{maxcid}, | |
290 | }; | |
291 | ||
292 | foreach my $k (qw(ip name hostrsapubkey rootrsapubkey fingerprint)) { | |
293 | $node->{$k} = $param->{$k}; | |
294 | } | |
295 | ||
245b527c | 296 | $cinfo->{ids}->{$node->{cid}} = $node; |
e16a9efc | 297 | |
245b527c | 298 | $cinfo->write(); |
e16a9efc | 299 | |
5a312cf1 DM |
300 | PMG::DBTools::update_master_clusterinfo($node->{cid}); |
301 | ||
245b527c | 302 | PMG::Cluster::update_ssh_keys($cinfo); |
5a312cf1 | 303 | |
245b527c | 304 | return PVE::RESTHandler::hash_to_array($cinfo->{ids}, 'cid'); |
275d6d6d | 305 | }; |
e16a9efc | 306 | |
275d6d6d | 307 | return PMG::ClusterConfig::lock_config($code, "add node failed"); |
e16a9efc DM |
308 | }}); |
309 | ||
cba17aeb | 310 | __PACKAGE__->register_method({ |
fb5f2d1e DM |
311 | name => 'create', |
312 | path => 'create', | |
cba17aeb DM |
313 | method => 'POST', |
314 | description => "Create initial cluster config with current node as master.", | |
315 | # alway read local file | |
316 | parameters => { | |
317 | additionalProperties => 0, | |
318 | properties => {}, | |
319 | }, | |
cfdf6608 DM |
320 | protected => 1, |
321 | returns => { type => 'string' }, | |
cba17aeb DM |
322 | code => sub { |
323 | my ($param) = @_; | |
324 | ||
cfdf6608 DM |
325 | my $rpcenv = PMG::RESTEnvironment->get(); |
326 | my $authuser = $rpcenv->get_user(); | |
327 | ||
328 | my $realcmd = sub { | |
245b527c | 329 | my $cinfo = PMG::ClusterConfig->new(); |
cba17aeb | 330 | |
245b527c | 331 | die "cluster alreayd defined\n" if scalar(keys %{$cinfo->{ids}}); |
cba17aeb DM |
332 | |
333 | my $info = PMG::Cluster::read_local_cluster_info(); | |
334 | ||
cfdf6608 DM |
335 | my $cid = 1; |
336 | ||
cba17aeb | 337 | $info->{type} = 'master'; |
cba17aeb | 338 | |
cfdf6608 | 339 | $info->{maxcid} = $cid, |
cba17aeb | 340 | |
245b527c | 341 | $cinfo->{ids}->{$cid} = $info; |
cfdf6608 | 342 | |
cfdf6608 DM |
343 | eval { |
344 | print STDERR "stop all services accessing the database\n"; | |
345 | # stop all services accessing the database | |
00d8b7f7 | 346 | PMG::Utils::service_wait_stopped(40, $PMG::Utils::db_service_list); |
cfdf6608 DM |
347 | |
348 | print STDERR "save new cluster configuration\n"; | |
245b527c | 349 | $cinfo->write(); |
cfdf6608 DM |
350 | |
351 | PMG::DBTools::init_masterdb($cid); | |
352 | ||
353 | PMG::Cluster::create_needed_dirs($cid, 1); | |
354 | ||
355 | print STDERR "cluster master successfully created\n"; | |
356 | }; | |
357 | my $err = $@; | |
358 | ||
00d8b7f7 | 359 | foreach my $service (reverse @$PMG::Utils::db_service_list) { |
cfdf6608 DM |
360 | eval { PVE::Tools::run_command(['systemctl', 'start', $service]); }; |
361 | warn $@ if $@; | |
362 | } | |
363 | ||
364 | die $err if $err; | |
cba17aeb DM |
365 | }; |
366 | ||
cfdf6608 DM |
367 | my $code = sub { |
368 | return $rpcenv->fork_worker('clustercreate', undef, $authuser, $realcmd); | |
369 | }; | |
cba17aeb | 370 | |
cfdf6608 | 371 | return PMG::ClusterConfig::lock_config($code, "create cluster failed"); |
cba17aeb DM |
372 | }}); |
373 | ||
fb5f2d1e DM |
374 | __PACKAGE__->register_method({ |
375 | name => 'join', | |
376 | path => 'join', | |
377 | method => 'POST', | |
378 | description => "Join local node to an existing cluster.", | |
379 | # alway read local file | |
4884557f | 380 | protected => 1, |
fb5f2d1e DM |
381 | parameters => { |
382 | additionalProperties => 0, | |
383 | properties => { | |
384 | master_ip => { | |
385 | description => "IP address.", | |
386 | type => 'string', format => 'ip', | |
387 | }, | |
388 | fingerprint => { | |
389 | description => "SSL certificate fingerprint.", | |
390 | type => 'string', | |
391 | pattern => '^(:?[A-Z0-9][A-Z0-9]:){31}[A-Z0-9][A-Z0-9]$', | |
392 | }, | |
e16a9efc DM |
393 | password => { |
394 | description => "Superuser password.", | |
395 | type => 'string', | |
396 | maxLength => 128, | |
397 | }, | |
fb5f2d1e DM |
398 | }, |
399 | }, | |
5a312cf1 | 400 | returns => { type => 'string' }, |
fb5f2d1e DM |
401 | code => sub { |
402 | my ($param) = @_; | |
403 | ||
5a312cf1 DM |
404 | my $rpcenv = PMG::RESTEnvironment->get(); |
405 | my $authuser = $rpcenv->get_user(); | |
406 | ||
407 | my $realcmd = sub { | |
245b527c | 408 | my $cinfo = PMG::ClusterConfig->new(); |
fb5f2d1e | 409 | |
245b527c | 410 | die "cluster alreayd defined\n" if scalar(keys %{$cinfo->{ids}}); |
fb5f2d1e | 411 | |
e16a9efc DM |
412 | my $setup = { |
413 | username => 'root@pam', | |
414 | password => $param->{password}, | |
415 | cookie_name => 'PMGAuthCookie', | |
416 | host => $param->{master_ip}, | |
417 | cached_fingerprints => { | |
418 | $param->{fingerprint} => 1, | |
419 | } | |
420 | }; | |
421 | ||
245b527c | 422 | cluster_join($cinfo, $setup); |
fb5f2d1e DM |
423 | }; |
424 | ||
5a312cf1 DM |
425 | my $code = sub { |
426 | return $rpcenv->fork_worker('clusterjoin', undef, $authuser, $realcmd); | |
427 | }; | |
fb5f2d1e | 428 | |
5a312cf1 | 429 | return PMG::ClusterConfig::lock_config($code, "cluster join failed"); |
fb5f2d1e DM |
430 | }}); |
431 | ||
cba17aeb | 432 | |
5ac6bd0e | 433 | 1; |