1 package PMG
::API2
::Cluster
;
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);
14 use PVE
::APIClient
::LWP
;
16 use PMG
::RESTEnvironment
;
17 use PMG
::ClusterConfig
;
24 use base
qw(PVE::RESTHandler);
27 my ($cinfo, $conn_setup) = @_;
29 my $conn = PVE
::APIClient
::LWP-
>new(%$conn_setup);
31 my $info = PMG
::Cluster
::read_local_cluster_info
();
33 my $res = $conn->post("/config/cluster/nodes", $info);
35 foreach my $node (@$res) {
36 $cinfo->{ids
}->{$node->{cid
}} = $node;
40 print STDERR
"stop all services accessing the database\n";
41 # stop all services accessing the database
42 PMG
::Utils
::service_wait_stopped
(40, $PMG::Utils
::db_service_list
);
44 print STDERR
"save new cluster configuration\n";
47 PMG
::Cluster
::update_ssh_keys
($cinfo);
49 print STDERR
"cluster node successfully joined\n";
51 $cinfo = PMG
::ClusterConfig-
>new(); # reload
53 my $role = $cinfo->{'local'}->{type
} // '-';
54 die "local node '$cinfo->{local}->{name}' not part of cluster\n"
57 die "got unexpected role '$role' for local node '$cinfo->{local}->{name}'\n"
60 my $cid = $cinfo->{'local'}->{cid
};
62 PMG
::MailQueue
::create_spooldirs
($cid);
64 PMG
::Cluster
::sync_config_from_master
($cinfo->{master
}->{name
}, $cinfo->{master
}->{ip
});
66 PMG
::DBTools
::init_nodedb
($cinfo);
68 my $cfg = PMG
::Config-
>new();
69 my $ruledb = PMG
::RuleDB-
>new();
70 my $rulecache = PMG
::RuleCache-
>new($ruledb);
72 $cfg->rewrite_config($rulecache, 1);
74 print STDERR
"syncing quarantine data\n";
75 PMG
::Cluster
::sync_master_quar
($cinfo->{master
}->{ip
}, $cinfo->{master
}->{name
});
76 print STDERR
"syncing quarantine data finished\n";
80 foreach my $service (reverse @$PMG::Utils
::db_service_list
) {
81 eval { PVE
::Tools
::run_command
(['systemctl', 'start', $service]); };
88 __PACKAGE__-
>register_method({
92 description
=> "Directory index.",
93 permissions
=> { user
=> 'all' },
95 additionalProperties
=> 0,
104 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
111 { name
=> 'status' },
112 { name
=> 'create' },
114 { name
=> 'update-fingerprints' },
120 __PACKAGE__-
>register_method({
124 description
=> "Cluster node index.",
125 # alway read local file
127 additionalProperties
=> 0,
130 permissions
=> { check
=> [ 'admin', 'qmanager', 'audit' ] },
136 type
=> { type
=> 'string' },
137 cid
=> { type
=> 'integer' },
138 ip
=> { type
=> 'string' },
139 name
=> { type
=> 'string' },
140 hostrsapubkey
=> { type
=> 'string' },
141 rootrsapubkey
=> { type
=> 'string' },
142 fingerprint
=> { type
=> 'string' },
149 my $cinfo = PMG
::ClusterConfig-
>new();
151 if (scalar(keys %{$cinfo->{ids
}})) {
152 my $role = $cinfo->{local}->{type
} // '-';
154 die "local node '$cinfo->{local}->{name}' not part of cluster\n";
158 return PVE
::RESTHandler
::hash_to_array
($cinfo->{ids
}, 'cid');
161 __PACKAGE__-
>register_method({
165 description
=> "Cluster node status.",
166 # alway read local file
168 additionalProperties
=> 0,
170 list_single_node
=> {
171 description
=> "List local node if there is no cluster defined. Please note that RSA keys and fingerprint are not valid in that case.",
178 permissions
=> { check
=> [ 'admin', 'qmanager', 'audit' ] },
184 type
=> { type
=> 'string' },
185 cid
=> { type
=> 'integer' },
186 ip
=> { type
=> 'string' },
187 name
=> { type
=> 'string' },
188 hostrsapubkey
=> { type
=> 'string' },
189 rootrsapubkey
=> { type
=> 'string' },
190 fingerprint
=> { type
=> 'string' },
193 links
=> [ { rel
=> 'child', href
=> "{cid}" } ],
198 my $cinfo = PMG
::ClusterConfig-
>new();
199 my $nodename = PVE
::INotify
::nodename
();
202 if (scalar(keys %{$cinfo->{ids
}})) {
203 my $role = $cinfo->{local}->{type
} // '-';
205 die "local node '$cinfo->{local}->{name}' not part of cluster\n";
207 $res = PVE
::RESTHandler
::hash_to_array
($cinfo->{ids
}, 'cid');
209 } elsif ($param->{list_single_node
}) {
210 my $ni = { type
=> '-' };
211 foreach my $k (qw(ip name cid)) {
212 $ni->{$k} = $cinfo->{local}->{$k};
214 foreach my $k (qw(hostrsapubkey rootrsapubkey fingerprint)) {
215 $ni->{$k} = '-'; # invalid
220 my $rpcenv = PMG
::RESTEnvironment-
>get();
221 my $authuser = $rpcenv->get_user();
222 my $ticket = $rpcenv->get_ticket();
224 foreach my $ni (@$res) {
227 if ($ni->{cid
} eq $cinfo->{local}->{cid
}) {
228 $info = PMG
::API2
::NodeInfo-
>status({ node
=> $nodename });
230 my $conn = PVE
::APIClient
::LWP-
>new(
232 cookie_name
=> 'PMGAuthCookie',
234 cached_fingerprints
=> {
235 $ni->{fingerprint
} => 1,
238 $info = $conn->get("/nodes/localhost/status", {});
242 $ni->{conn_error
} = "$err"; # convert $err to string
245 foreach my $k (keys %$info) {
246 $ni->{$k} = $info->{$k} if !defined($ni->{$k});
253 my $add_node_schema = PMG
::ClusterConfig
::Node-
>createSchema(1);
254 delete $add_node_schema->{properties
}->{cid
};
256 __PACKAGE__-
>register_method({
260 description
=> "Add an node to the cluster config.",
263 parameters
=> $add_node_schema,
265 description
=> "Returns the resulting node list.",
270 cid
=> { type
=> 'integer' },
278 my $cinfo = PMG
::ClusterConfig-
>new();
280 die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids
}});
282 my $master = $cinfo->{master
} || die "unable to lookup master node\n";
285 foreach my $cid (keys %{$cinfo->{ids
}}) {
286 my $d = $cinfo->{ids
}->{$cid};
288 if ($d->{type
} eq 'node' && $d->{ip
} eq $param->{ip
} && $d->{name
} eq $param->{name
}) {
289 $next_cid = $cid; # allow overwrite existing node data
293 if ($d->{ip
} eq $param->{ip
}) {
294 die "ip address '$param->{ip}' is already used by existing node $d->{name}\n";
297 if ($d->{name
} eq $param->{name
}) {
298 die "node with name '$param->{name}' already exists\n";
302 if (!defined($next_cid)) {
303 $next_cid = ++$master->{maxcid
};
306 # create spooldir for new node to prevent problems if it gets
307 # delete from the cluster before being synced initially
308 PMG
::MailQueue
::create_spooldirs
($master->{maxcid
});
312 cid
=> $master->{maxcid
},
315 foreach my $k (qw(ip name hostrsapubkey rootrsapubkey fingerprint)) {
316 $node->{$k} = $param->{$k};
319 $cinfo->{ids
}->{$node->{cid
}} = $node;
323 PMG
::DBTools
::update_master_clusterinfo
($node->{cid
});
325 PMG
::Cluster
::update_ssh_keys
($cinfo);
327 return PVE
::RESTHandler
::hash_to_array
($cinfo->{ids
}, 'cid');
330 return PMG
::ClusterConfig
::lock_config
($code, "add node failed");
333 __PACKAGE__-
>register_method({
337 description
=> "Create initial cluster config with current node as master.",
338 # alway read local file
340 additionalProperties
=> 0,
344 returns
=> { type
=> 'string' },
348 my $rpcenv = PMG
::RESTEnvironment-
>get();
349 my $authuser = $rpcenv->get_user();
352 my $cinfo = PMG
::ClusterConfig-
>new();
354 die "cluster already defined\n" if scalar(keys %{$cinfo->{ids
}});
356 my $info = PMG
::Cluster
::read_local_cluster_info
();
360 $info->{type
} = 'master';
362 $info->{maxcid
} = $cid,
364 $cinfo->{ids
}->{$cid} = $info;
367 print STDERR
"stop all services accessing the database\n";
368 # stop all services accessing the database
369 PMG
::Utils
::service_wait_stopped
(40, $PMG::Utils
::db_service_list
);
371 print STDERR
"save new cluster configuration\n";
374 PMG
::DBTools
::init_masterdb
($cid);
376 PMG
::MailQueue
::create_spooldirs
($cid);
378 print STDERR
"cluster master successfully created\n";
382 foreach my $service (reverse @$PMG::Utils
::db_service_list
) {
383 eval { PVE
::Tools
::run_command
(['systemctl', 'start', $service]); };
391 return $rpcenv->fork_worker('clustercreate', undef, $authuser, $realcmd);
394 return PMG
::ClusterConfig
::lock_config
($code, "create cluster failed");
397 __PACKAGE__-
>register_method({
401 description
=> "Join local node to an existing cluster.",
402 # alway read local file
405 additionalProperties
=> 0,
408 description
=> "IP address.",
409 type
=> 'string', format
=> 'ip',
412 description
=> "SSL certificate fingerprint.",
414 pattern
=> '^(:?[A-Z0-9][A-Z0-9]:){31}[A-Z0-9][A-Z0-9]$',
417 description
=> "Superuser password.",
423 returns
=> { type
=> 'string' },
427 my $rpcenv = PMG
::RESTEnvironment-
>get();
428 my $authuser = $rpcenv->get_user();
431 my $cinfo = PMG
::ClusterConfig-
>new();
433 die "cluster already defined\n" if scalar(keys %{$cinfo->{ids
}});
436 username
=> 'root@pam',
437 password
=> $param->{password
},
438 cookie_name
=> 'PMGAuthCookie',
439 host
=> $param->{master_ip
},
440 cached_fingerprints
=> {
441 $param->{fingerprint
} => 1,
445 cluster_join
($cinfo, $setup);
449 return $rpcenv->fork_worker('clusterjoin', undef, $authuser, $realcmd);
452 return PMG
::ClusterConfig
::lock_config
($code, "cluster join failed");
455 __PACKAGE__-
>register_method({
456 name
=> 'update_fingerprints',
457 path
=> 'update-fingerprints',
459 description
=> "Update API certificate fingerprints (by fetching it via ssh).",
463 additionalProperties
=> 0,
465 returns
=> { type
=> 'null' },
470 my $cinfo = PMG
::ClusterConfig-
>new();
472 die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids
}});
474 my $localcid = $cinfo->{local}->{cid
};
476 foreach my $cid (sort keys %{$cinfo->{ids
}}) {
478 if ($cid == $localcid) {
479 $fp = PMG
::Cluster
::read_local_ssl_cert_fingerprint
();
481 $fp = PMG
::Cluster
::get_remote_cert_fingerprint
($cinfo->{ids
}->{$cid});
483 $cinfo->{ids
}->{$cid}->{fingerprint
} = $fp;
491 PMG
::ClusterConfig
::lock_config
($code, "update fingerprints failed");