]>
Commit | Line | Data |
---|---|---|
cba17aeb DM |
1 | package PMG::CLI::pmgcm; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use Data::Dumper; | |
e16a9efc | 6 | use Term::ReadLine; |
ba11e2d3 | 7 | use POSIX qw(strftime); |
e16a9efc | 8 | use JSON; |
cba17aeb DM |
9 | |
10 | use PVE::SafeSyslog; | |
11 | use PVE::Tools qw(extract_param); | |
12 | use PVE::INotify; | |
13 | use PVE::CLIHandler; | |
14 | ||
8162c745 | 15 | use PMG::Utils; |
ba11e2d3 | 16 | use PMG::Ticket; |
cfdf6608 | 17 | use PMG::RESTEnvironment; |
cba17aeb | 18 | use PMG::DBTools; |
9d369141 DM |
19 | use PMG::RuleDB; |
20 | use PMG::RuleCache; | |
cba17aeb DM |
21 | use PMG::Cluster; |
22 | use PMG::ClusterConfig; | |
fb5f2d1e | 23 | use PMG::API2::Cluster; |
cba17aeb DM |
24 | |
25 | use base qw(PVE::CLIHandler); | |
26 | ||
cfdf6608 DM |
27 | sub setup_environment { |
28 | PMG::RESTEnvironment->setup_default_cli_env(); | |
ba11e2d3 DM |
29 | |
30 | my $rpcenv = PMG::RESTEnvironment->get(); | |
31 | # API /config/cluster/nodes need a ticket to connect to other nodes | |
32 | my $ticket = PMG::Ticket::assemble_ticket('root@pam'); | |
33 | $rpcenv->set_ticket($ticket); | |
cfdf6608 DM |
34 | } |
35 | ||
36 | my $upid_exit = sub { | |
37 | my $upid = shift; | |
38 | my $status = PVE::Tools::upid_read_status($upid); | |
39 | exit($status eq 'OK' ? 0 : -1); | |
40 | }; | |
41 | ||
cba17aeb DM |
42 | my $format_nodelist = sub { |
43 | my $res = shift; | |
44 | ||
3862c23d DM |
45 | if (!scalar(@$res)) { |
46 | print "no cluster defined\n"; | |
47 | return; | |
48 | } | |
49 | ||
cba17aeb DM |
50 | print "NAME(CID)--------------IPADDRESS----ROLE-STATE---------UPTIME---LOAD----MEM---DISK\n"; |
51 | foreach my $ni (@$res) { | |
ba11e2d3 DM |
52 | my $state = 'A'; |
53 | $state = 'S' if !$ni->{insync}; | |
54 | ||
55 | if (my $err = $ni->{conn_error}) { | |
56 | $err =~ s/\n/ /g; | |
57 | $state = "ERROR: $err"; | |
58 | } | |
59 | ||
8162c745 | 60 | my $uptime = $ni->{uptime} ? PMG::Utils::format_uptime($ni->{uptime}) : '-'; |
ba11e2d3 DM |
61 | |
62 | my $loadavg1 = '-'; | |
63 | if (my $d = $ni->{loadavg}) { | |
64 | $loadavg1 = $d->[0]; | |
65 | } | |
66 | ||
67 | my $mem = '-'; | |
68 | if (my $d = $ni->{memory}) { | |
69 | $mem = int(0.5 + ($d->{used}*100/$d->{total})); | |
70 | } | |
71 | my $disk = '-'; | |
72 | if (my $d = $ni->{rootfs}) { | |
d6de75ed | 73 | $disk = int(0.5 + ($d->{used}*100/$d->{total})); |
ba11e2d3 | 74 | } |
3879a8aa DM |
75 | |
76 | printf "%-20s %-15s %-6s %1s %15s %6s %5s%% %5s%%\n", | |
77 | "$ni->{name}($ni->{cid})", $ni->{ip}, $ni->{type}, | |
ba11e2d3 | 78 | $state, $uptime, $loadavg1, $mem, $disk; |
cba17aeb DM |
79 | } |
80 | }; | |
81 | ||
3879a8aa DM |
82 | __PACKAGE__->register_method({ |
83 | name => 'join_cmd', | |
84 | path => 'join_cmd', | |
85 | method => 'GET', | |
cf9a8dea | 86 | description => "Prints the command for joining an new node to the cluster. You need to execute the command on the new node.", |
3879a8aa DM |
87 | parameters => { |
88 | additionalProperties => 0, | |
89 | properties => {}, | |
90 | }, | |
91 | returns => { type => 'null' }, | |
92 | code => sub { | |
93 | my ($param) = @_; | |
94 | ||
9595814c | 95 | my $cinfo = PMG::ClusterConfig->new(); |
3879a8aa | 96 | |
9595814c | 97 | if (scalar(keys %{$cinfo->{ids}})) { |
3879a8aa | 98 | |
9595814c | 99 | my $master = $cinfo->{master} || |
3879a8aa DM |
100 | die "no master found\n"; |
101 | ||
e16a9efc | 102 | print "pmgcm join $master->{ip} --fingerprint $master->{fingerprint}\n"; |
3879a8aa DM |
103 | |
104 | } else { | |
105 | die "no cluster defined\n"; | |
106 | } | |
107 | ||
108 | return undef; | |
109 | }}); | |
110 | ||
e411340d DM |
111 | __PACKAGE__->register_method({ |
112 | name => 'delete', | |
113 | path => 'delete', | |
114 | method => 'GET', | |
115 | description => "Remove a node from the cluster.", | |
116 | parameters => { | |
117 | additionalProperties => 0, | |
118 | properties => { | |
119 | cid => { | |
120 | description => "Cluster Node ID.", | |
121 | type => 'integer', | |
122 | minimum => 1, | |
123 | }, | |
124 | }, | |
125 | }, | |
126 | returns => { type => 'null' }, | |
127 | code => sub { | |
128 | my ($param) = @_; | |
129 | ||
130 | my $code = sub { | |
131 | my $cinfo = PMG::ClusterConfig->new(); | |
132 | ||
133 | die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids}}); | |
134 | ||
135 | my $master = $cinfo->{master} || die "unable to lookup master node\n"; | |
136 | ||
137 | die "operation not permitted (not master)\n" | |
138 | if $cinfo->{local}->{cid} != $master->{cid}; | |
139 | ||
140 | my $cid = $param->{cid}; | |
141 | ||
142 | die "unable to delete master node\n" | |
143 | if $cinfo->{local}->{cid} == $cid; | |
144 | ||
145 | die "no such node (cid == $cid does not exists)\n" if !$cinfo->{ids}->{$cid}; | |
146 | ||
147 | delete $cinfo->{ids}->{$cid}; | |
148 | ||
149 | $cinfo->write(); | |
150 | }; | |
151 | ||
152 | PMG::ClusterConfig::lock_config($code, "delete cluster node failed"); | |
153 | ||
154 | return undef; | |
155 | }}); | |
156 | ||
e16a9efc DM |
157 | __PACKAGE__->register_method({ |
158 | name => 'join', | |
159 | path => 'join', | |
160 | method => 'GET', | |
161 | description => "Join a new node to an existing cluster.", | |
162 | parameters => { | |
163 | additionalProperties => 0, | |
164 | properties => { | |
165 | master_ip => { | |
166 | description => "IP address.", | |
167 | type => 'string', format => 'ip', | |
168 | }, | |
169 | fingerprint => { | |
170 | description => "SSL certificate fingerprint.", | |
171 | type => 'string', | |
172 | pattern => '^(:?[A-Z0-9][A-Z0-9]:){31}[A-Z0-9][A-Z0-9]$', | |
173 | optional => 1, | |
174 | }, | |
175 | }, | |
176 | }, | |
177 | returns => { type => 'null' }, | |
178 | code => sub { | |
179 | my ($param) = @_; | |
180 | ||
181 | my $code = sub { | |
9595814c | 182 | my $cinfo = PMG::ClusterConfig->new(); |
e16a9efc | 183 | |
56001f4f | 184 | die "cluster already defined\n" if scalar(keys %{$cinfo->{ids}}); |
e16a9efc DM |
185 | |
186 | my $term = new Term::ReadLine ('pmgcm'); | |
187 | my $attribs = $term->Attribs; | |
188 | $attribs->{redisplay_function} = $attribs->{shadow_redisplay}; | |
189 | my $password = $term->readline('Enter password: '); | |
190 | ||
191 | my $setup = { | |
192 | username => 'root@pam', | |
193 | password => $password, | |
194 | cookie_name => 'PMGAuthCookie', | |
195 | host => $param->{master_ip}, | |
196 | }; | |
197 | if ($param->{fingerprint}) { | |
198 | $setup->{cached_fingerprints} = { | |
199 | $param->{fingerprint} => 1, | |
200 | }; | |
201 | } else { | |
202 | # allow manual fingerprint verification | |
203 | $setup->{manual_verification} = 1; | |
204 | } | |
205 | ||
9595814c | 206 | PMG::API2::Cluster::cluster_join($cinfo, $setup); |
e16a9efc DM |
207 | }; |
208 | ||
209 | PMG::ClusterConfig::lock_config($code, "cluster join failed"); | |
210 | ||
211 | return undef; | |
212 | }}); | |
213 | ||
d0d3d29a DM |
214 | __PACKAGE__->register_method({ |
215 | name => 'sync', | |
216 | path => 'sync', | |
217 | method => 'GET', | |
218 | description => "Synchronize cluster configuration.", | |
219 | parameters => { | |
220 | additionalProperties => 0, | |
221 | properties => { | |
222 | master_ip => { | |
223 | description => 'Optional IP address for master node.', | |
224 | type => 'string', format => 'ip', | |
225 | optional => 1, | |
226 | } | |
227 | }, | |
228 | }, | |
229 | returns => { type => 'null' }, | |
230 | code => sub { | |
231 | my ($param) = @_; | |
232 | ||
29ecd597 | 233 | my $cinfo = PMG::ClusterConfig->new(); |
d0d3d29a | 234 | |
8871a5f0 DM |
235 | my $master_name = undef; |
236 | my $master_ip = $param->{master_ip}; | |
d0d3d29a | 237 | |
9d369141 DM |
238 | if (!$master_ip && $cinfo->{master}) { |
239 | $master_ip = $cinfo->{master}->{ip}; | |
240 | $master_name = $cinfo->{master}->{name}; | |
8871a5f0 | 241 | } |
d0d3d29a | 242 | |
8871a5f0 | 243 | die "no master IP specified (use option --master_ip)\n" if !$master_ip; |
d0d3d29a | 244 | |
9d369141 | 245 | if ($cinfo->{local}->{ip} eq $master_ip) { |
809ae8f4 DM |
246 | print STDERR "local node is master - nothing to do\n"; |
247 | return undef; | |
248 | } | |
249 | ||
8871a5f0 | 250 | print STDERR "syncing master configuration from '${master_ip}'\n"; |
d0d3d29a | 251 | |
5314a3a9 | 252 | my $restart = PMG::Cluster::sync_config_from_master($master_name, $master_ip); |
809ae8f4 | 253 | |
9d369141 | 254 | my $cfg = PMG::Config->new(); |
9d369141 | 255 | |
58217372 | 256 | $cfg->rewrite_config(undef, 1); |
9d369141 | 257 | |
5314a3a9 DC |
258 | if (scalar(keys %$restart)) { |
259 | print "please restart the following daemons:\n"; | |
260 | for my $service (sort keys %$restart) { | |
261 | print "$service\n" | |
262 | } | |
263 | } | |
264 | ||
809ae8f4 | 265 | return undef; |
d0d3d29a DM |
266 | }}); |
267 | ||
64244a54 DM |
268 | __PACKAGE__->register_method({ |
269 | name => 'promote', | |
270 | path => 'promote', | |
271 | method => 'POST', | |
272 | description => "Promote current node to become the new master.", | |
273 | parameters => { | |
274 | additionalProperties => 0, | |
275 | properties => {}, | |
276 | }, | |
277 | returns => { type => 'null' }, | |
278 | code => sub { | |
279 | my ($param) = @_; | |
280 | ||
281 | my $code = sub { | |
282 | my $cinfo = PMG::ClusterConfig->new(); | |
283 | ||
284 | die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids}}); | |
285 | ||
286 | my $master = $cinfo->{master} || die "unable to lookup master node\n"; | |
287 | ||
288 | die "this node is already master\n" | |
289 | if $cinfo->{local}->{cid} == $master->{cid}; | |
290 | ||
291 | my $maxcid = $master->{maxcid}; | |
292 | $master->{type} = 'node'; | |
293 | ||
294 | my $newmaster = $cinfo->{local}; | |
295 | ||
296 | $newmaster->{maxcid} = $maxcid; | |
297 | $newmaster->{type} = 'master'; | |
298 | ||
299 | $cinfo->{master} = $newmaster; | |
300 | ||
301 | $cinfo->write(); | |
302 | }; | |
303 | ||
304 | PMG::ClusterConfig::lock_config($code, "promote new master failed"); | |
305 | ||
306 | return undef; | |
307 | }}); | |
308 | ||
7d463861 | 309 | __PACKAGE__->register_method({ |
40cf389c TL |
310 | name => 'update_fingerprints', |
311 | path => 'update-fingerprints', | |
7d463861 SI |
312 | method => 'POST', |
313 | description => "Notify master to refresh all certificate fingerprints", | |
314 | parameters => { | |
315 | additionalProperties => 0, | |
316 | properties => {}, | |
317 | }, | |
318 | returns => { type => 'null' }, | |
319 | code => sub { | |
320 | my ($param) = @_; | |
321 | ||
322 | my $cinfo = PMG::ClusterConfig->new(); | |
72eb40e2 TL |
323 | if (!scalar(keys %{$cinfo->{ids}})) { |
324 | warn "no cluster defined, nothing to do...\n"; | |
325 | return undef; | |
326 | } | |
7d463861 SI |
327 | |
328 | PMG::Cluster::trigger_update_fingerprints($cinfo); | |
329 | }}); | |
330 | ||
cba17aeb | 331 | our $cmddef = { |
150e8421 | 332 | status => [ 'PMG::API2::Cluster', 'status', [], {}, $format_nodelist], |
cfdf6608 | 333 | create => [ 'PMG::API2::Cluster', 'create', [], {}, $upid_exit], |
e411340d | 334 | delete => [ __PACKAGE__, 'delete', ['cid']], |
e16a9efc | 335 | join => [ __PACKAGE__, 'join', ['master_ip']], |
5b548bb6 TL |
336 | 'join-cmd' => [ __PACKAGE__, 'join_cmd', []], |
337 | join_cmd => { alias => 'join-cmd' }, | |
d0d3d29a | 338 | sync => [ __PACKAGE__, 'sync', []], |
64244a54 | 339 | promote => [ __PACKAGE__, 'promote', []], |
40cf389c | 340 | 'update-fingerprints' => [ __PACKAGE__, 'update_fingerprints'], |
cba17aeb DM |
341 | }; |
342 | ||
343 | 1; |