]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/CLI/pmgcm.pm
c41c24d5d012905f10d7fd9e2d24213058f76f5b
[pmg-api.git] / src / PMG / CLI / pmgcm.pm
1 package PMG::CLI::pmgcm;
2
3 use strict;
4 use warnings;
5 use Data::Dumper;
6 use Term::ReadLine;
7 use POSIX qw(strftime);
8 use JSON;
9
10 use PVE::SafeSyslog;
11 use PVE::Tools qw(extract_param);
12 use PVE::INotify;
13 use PVE::CLIHandler;
14
15 use PMG::Utils;
16 use PMG::Ticket;
17 use PMG::RESTEnvironment;
18 use PMG::DBTools;
19 use PMG::RuleDB;
20 use PMG::RuleCache;
21 use PMG::Cluster;
22 use PMG::ClusterConfig;
23 use PMG::API2::Cluster;
24
25 use base qw(PVE::CLIHandler);
26
27 sub setup_environment {
28 PMG::RESTEnvironment->setup_default_cli_env();
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);
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
42 my $format_nodelist = sub {
43 my $res = shift;
44
45 if (!scalar(@$res)) {
46 print "no cluster defined\n";
47 return;
48 }
49
50 print "NAME(CID)--------------IPADDRESS----ROLE-STATE---------UPTIME---LOAD----MEM---DISK\n";
51 foreach my $ni (@$res) {
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
60 my $uptime = $ni->{uptime} ? PMG::Utils::format_uptime($ni->{uptime}) : '-';
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}) {
73 $disk = int(0.5 + ($d->{used}*100/$d->{total}));
74 }
75
76 printf "%-20s %-15s %-6s %1s %15s %6s %5s%% %5s%%\n",
77 "$ni->{name}($ni->{cid})", $ni->{ip}, $ni->{type},
78 $state, $uptime, $loadavg1, $mem, $disk;
79 }
80 };
81
82 __PACKAGE__->register_method({
83 name => 'join_cmd',
84 path => 'join_cmd',
85 method => 'GET',
86 description => "Prints the command for joining an new node to the cluster. You need to execute the command on the new node.",
87 parameters => {
88 additionalProperties => 0,
89 properties => {},
90 },
91 returns => { type => 'null' },
92 code => sub {
93 my ($param) = @_;
94
95 my $cinfo = PMG::ClusterConfig->new();
96
97 if (scalar(keys %{$cinfo->{ids}})) {
98
99 my $master = $cinfo->{master} ||
100 die "no master found\n";
101
102 print "pmgcm join $master->{ip} --fingerprint $master->{fingerprint}\n";
103
104 } else {
105 die "no cluster defined\n";
106 }
107
108 return undef;
109 }});
110
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
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 {
182 my $cinfo = PMG::ClusterConfig->new();
183
184 die "cluster already defined\n" if scalar(keys %{$cinfo->{ids}});
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
206 PMG::API2::Cluster::cluster_join($cinfo, $setup);
207 };
208
209 PMG::ClusterConfig::lock_config($code, "cluster join failed");
210
211 return undef;
212 }});
213
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
233 my $cinfo = PMG::ClusterConfig->new();
234
235 my $master_name = undef;
236 my $master_ip = $param->{master_ip};
237
238 if (!$master_ip && $cinfo->{master}) {
239 $master_ip = $cinfo->{master}->{ip};
240 $master_name = $cinfo->{master}->{name};
241 }
242
243 die "no master IP specified (use option --master_ip)\n" if !$master_ip;
244
245 if ($cinfo->{local}->{ip} eq $master_ip) {
246 print STDERR "local node is master - nothing to do\n";
247 return undef;
248 }
249
250 print STDERR "syncing master configuration from '${master_ip}'\n";
251
252 PMG::Cluster::sync_config_from_master($master_name, $master_ip);
253
254 my $cfg = PMG::Config->new();
255
256 $cfg->rewrite_config(undef, 1);
257
258 return undef;
259 }});
260
261 __PACKAGE__->register_method({
262 name => 'promote',
263 path => 'promote',
264 method => 'POST',
265 description => "Promote current node to become the new master.",
266 parameters => {
267 additionalProperties => 0,
268 properties => {},
269 },
270 returns => { type => 'null' },
271 code => sub {
272 my ($param) = @_;
273
274 my $code = sub {
275 my $cinfo = PMG::ClusterConfig->new();
276
277 die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids}});
278
279 my $master = $cinfo->{master} || die "unable to lookup master node\n";
280
281 die "this node is already master\n"
282 if $cinfo->{local}->{cid} == $master->{cid};
283
284 my $maxcid = $master->{maxcid};
285 $master->{type} = 'node';
286
287 my $newmaster = $cinfo->{local};
288
289 $newmaster->{maxcid} = $maxcid;
290 $newmaster->{type} = 'master';
291
292 $cinfo->{master} = $newmaster;
293
294 $cinfo->write();
295 };
296
297 PMG::ClusterConfig::lock_config($code, "promote new master failed");
298
299 return undef;
300 }});
301
302 our $cmddef = {
303 status => [ 'PMG::API2::Cluster', 'status', [], {}, $format_nodelist],
304 create => [ 'PMG::API2::Cluster', 'create', [], {}, $upid_exit],
305 delete => [ __PACKAGE__, 'delete', ['cid']],
306 join => [ __PACKAGE__, 'join', ['master_ip']],
307 join_cmd => [ __PACKAGE__, 'join_cmd', []],
308 sync => [ __PACKAGE__, 'sync', []],
309 promote => [ __PACKAGE__, 'promote', []],
310 };
311
312 1;