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