]> git.proxmox.com Git - pve-manager.git/blob - bin/pvesh
pvesh usage: new option --returns to print result schema
[pve-manager.git] / bin / pvesh
1 #!/usr/bin/perl
2
3 package pvesh;
4
5 use strict;
6 use warnings;
7 use HTTP::Status qw(:constants :is status_message);
8 use String::ShellQuote;
9 use PVE::JSONSchema qw(get_standard_option);
10 use PVE::SafeSyslog;
11 use PVE::Cluster;
12 use PVE::INotify;
13 use PVE::RPCEnvironment;
14 use PVE::RESTHandler;
15 use PVE::CLIFormatter;
16 use PVE::CLIHandler;
17 use PVE::API2Tools;
18 use PVE::API2;
19 use JSON;
20
21 use base qw(PVE::CLIHandler);
22
23 my $disable_proxy = 0;
24 my $opt_nooutput = 0;
25
26 # compatibility code
27 my $optmatch;
28 do {
29 $optmatch = 0;
30 if ($ARGV[0]) {
31 if ($ARGV[0] eq '--noproxy') {
32 shift @ARGV;
33 $disable_proxy = 1;
34 $optmatch = 1;
35 } elsif ($ARGV[0] eq '--nooutput') {
36 # we use this when starting task in CLI (suppress printing upid)
37 # for example 'pvesh --nooutput create /nodes/localhost/stopall'
38 shift @ARGV;
39 $opt_nooutput = 1;
40 $optmatch = 1;
41 }
42 }
43 } while ($optmatch);
44
45 sub setup_environment {
46 PVE::RPCEnvironment->setup_default_cli_env();
47 }
48
49 sub complete_api_path {
50 my($text) = @_;
51
52 my ($dir, undef, $rest) = $text =~ m|^(.*/)?(([^/]*))?$|;
53
54 my $path = $dir // ''; # copy
55
56 $path =~ s|/+|/|g;
57 $path =~ s|^\/||;
58 $path =~ s|\/$||;
59
60 my $res = [];
61
62 my $di = dir_info($path);
63 if (my $children = $di->{children}) {
64 foreach my $c (@$children) {
65 if ($c =~ /^\Q$rest/) {
66 my $new = $dir ? "$dir$c" : $c;
67 push @$res, $new;
68 }
69 }
70 }
71
72 if (scalar(@$res) == 1) {
73 return [$res->[0], "$res->[0]/"];
74 }
75
76 return $res;
77 }
78
79 my $method_map = {
80 create => 'POST',
81 set => 'PUT',
82 get => 'GET',
83 delete => 'DELETE',
84 };
85
86 sub check_proxyto {
87 my ($info, $uri_param) = @_;
88
89 my $rpcenv = PVE::RPCEnvironment->get();
90
91 if ($info->{proxyto} || $info->{proxyto_callback}) {
92 my $node = PVE::API2Tools::resolve_proxyto(
93 $rpcenv, $info->{proxyto_callback}, $info->{proxyto}, $uri_param);
94
95 if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) {
96 die "proxy loop detected - aborting\n" if $disable_proxy;
97 my $remip = PVE::Cluster::remote_node_ip($node);
98 return ($node, $remip);
99 }
100 }
101
102 return undef;
103 }
104
105 sub proxy_handler {
106 my ($node, $remip, $path, $cmd, $param, $noout) = @_;
107
108 my $args = [];
109 foreach my $key (keys %$param) {
110 push @$args, "--$key", $param->{$key};
111 }
112
113 push @$args, '--quiet' if $noout;
114
115 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
116 'pvesh', '--noproxy', $cmd, $path,
117 '--format', 'json'];
118
119 if (scalar(@$args)) {
120 my $cmdargs = [String::ShellQuote::shell_quote(@$args)];
121 push @$remcmd, @$cmdargs;
122 }
123
124 my $json = '';
125 PVE::Tools::run_command($remcmd, errmsg => "proxy handler failed",
126 outfunc => sub { $json .= shift });
127
128 return decode_json($json);
129 }
130
131 sub extract_children {
132 my ($lnk, $data) = @_;
133
134 my $res = [];
135
136 return $res if !($lnk && $data);
137
138 my $href = $lnk->{href};
139 if ($href =~ m/^\{(\S+)\}$/) {
140 my $prop = $1;
141
142 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
143 next if !ref($elem);
144 my $value = $elem->{$prop};
145 push @$res, $value;
146 }
147 }
148
149 return $res;
150 }
151
152 sub dir_info {
153 my ($path) = @_;
154
155 my $res = { path => $path };
156 my $uri_param = {};
157 my ($handler, $info, $pm) = PVE::API2->find_handler('GET', $path, $uri_param);
158 if ($handler && $info) {
159 eval {
160 my $data = $handler->handle($info, $uri_param);
161 my $lnk = PVE::JSONSchema::method_get_child_link($info);
162 $res->{children} = extract_children($lnk, $data);
163 }; # ignore errors ?
164 }
165 return $res;
166 }
167
168
169 # dynamically update schema definition
170 # like: pvesh <get|set|create|delete|help> <path>
171
172 sub extract_path_info {
173 my ($uri_param) = @_;
174
175 my $info;
176
177 my $test_path_properties = sub {
178 my ($method, $path) = @_;
179 (undef, $info) = PVE::API2->find_handler($method, $path, $uri_param);
180 };
181
182 if (defined(my $cmd = $ARGV[0])) {
183 if (my $method = $method_map->{$cmd}) {
184 if (my $path = $ARGV[1]) {
185 $test_path_properties->($method, $path);
186 }
187 } elsif ($cmd eq 'bashcomplete') {
188 my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT});
189 my $args = PVE::Tools::split_args($cmdline);
190 if (defined(my $cmd = $args->[1])) {
191 if (my $method = $method_map->{$cmd}) {
192 if (my $path = $args->[2]) {
193 $test_path_properties->($method, $path);
194 }
195 }
196 }
197 }
198 }
199
200 return $info;
201 }
202
203
204 my $path_properties = {};
205 my $path_returns = { type => 'null' };
206
207 my $api_path_property = {
208 description => "API path.",
209 type => 'string',
210 completion => sub {
211 my ($cmd, $pname, $cur, $args) = @_;
212 return complete_api_path($cur);
213 },
214 };
215
216 my $uri_param = {};
217 if (my $info = extract_path_info($uri_param)) {
218 foreach my $key (keys %{$info->{parameters}->{properties}}) {
219 next if defined($uri_param->{$key});
220 $path_properties->{$key} = $info->{parameters}->{properties}->{$key};
221 }
222 $path_returns = $info->{returns};
223 }
224
225 $path_properties->{format} = get_standard_option('pve-output-format');
226 $path_properties->{api_path} = $api_path_property;
227 $path_properties->{noproxy} = {
228 description => "Disable automatic proxying.",
229 type => 'boolean',
230 optional => 1,
231 };
232 $path_properties->{quiet} = {
233 description => "Suppress printing results.",
234 type => 'boolean',
235 optional => 1,
236 };
237
238 my $format_result = sub {
239 my ($data, $result_schema, $options) = @_;
240
241 return if $opt_nooutput || ($options->{format}//'') eq 'none';
242
243 PVE::CLIFormatter::print_api_result($data, $path_returns, undef, $options);
244 };
245
246 sub call_api_method {
247 my ($cmd, $param) = @_;
248
249 my $method = $method_map->{$cmd} || die "unable to map command '$cmd'";
250
251 my $path = PVE::Tools::extract_param($param, 'api_path');
252 die "missing API path\n" if !defined($path);
253
254 my $uri_param = {};
255 my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
256 if (!$handler || !$info) {
257 die "no '$cmd' handler for '$path'\n";
258 }
259
260 my ($node, $remip) = check_proxyto($info, $uri_param);
261 return proxy_handler($node, $remip, $path, $cmd, $param, $opt_nooutput) if $node;
262
263 foreach my $p (keys %$uri_param) {
264 $param->{$p} = $uri_param->{$p};
265 }
266
267 my $data = $handler->handle($info, $param);
268
269 return $data;
270 }
271
272 __PACKAGE__->register_method ({
273 name => 'get',
274 path => 'get',
275 method => 'GET',
276 description => "Call API GET on <api_path>.",
277 parameters => {
278 additionalProperties => 0,
279 properties => $path_properties,
280 },
281 returns => $path_returns,
282 code => sub {
283 my ($param) = @_;
284
285 return call_api_method('get', $param);
286 }});
287
288 __PACKAGE__->register_method ({
289 name => 'set',
290 path => 'set',
291 method => 'PUT',
292 description => "Call API PUT on <api_path>.",
293 parameters => {
294 additionalProperties => 0,
295 properties => $path_properties,
296 },
297 returns => $path_returns,
298 code => sub {
299 my ($param) = @_;
300
301 return call_api_method('set', $param);
302 }});
303
304 __PACKAGE__->register_method ({
305 name => 'create',
306 path => 'create',
307 method => 'POST',
308 description => "Call API POST on <api_path>.",
309 parameters => {
310 additionalProperties => 0,
311 properties => $path_properties,
312 },
313 returns => $path_returns,
314 code => sub {
315 my ($param) = @_;
316
317 return call_api_method('create', $param);
318 }});
319
320 __PACKAGE__->register_method ({
321 name => 'delete',
322 path => 'delete',
323 method => 'DELETE',
324 description => "Call API DELETE on <api_path>.",
325 parameters => {
326 additionalProperties => 0,
327 properties => $path_properties,
328 },
329 returns => $path_returns,
330 code => sub {
331 my ($param) = @_;
332
333 return call_api_method('delete', $param);
334 }});
335
336 __PACKAGE__->register_method ({
337 name => 'usage',
338 path => 'usage',
339 method => 'GET',
340 description => "print API usage information for <api_path>.",
341 parameters => {
342 additionalProperties => 0,
343 properties => {
344 api_path => $api_path_property,
345 verbose => {
346 description => "Verbose output format.",
347 type => 'boolean',
348 optional => 1,
349 },
350 returns => {
351 description => "Including schema for returned data.",
352 type => 'boolean',
353 optional => 1,
354 },
355 command => {
356 description => "API command.",
357 type => 'string',
358 enum => [ keys %$method_map ],
359 optional => 1,
360 },
361 },
362 },
363 returns => { type => 'null' },
364 code => sub {
365 my ($param) = @_;
366
367 $opt_nooutput = 1; # we print directly
368
369 my $path = $param->{api_path};
370
371 my $found = 0;
372 foreach my $cmd (qw(get set create delete)) {
373 next if $param->{command} && $cmd ne $param->{command};
374 my $method = $method_map->{$cmd};
375 my ($handler, $info) = PVE::API2->find_handler($method, $path);
376 next if !$handler;
377 $found = 1;
378
379 if ($param->{verbose}) {
380 print $handler->usage_str(
381 $info->{name}, "pvesh $cmd $path", undef, {}, 'full');
382
383 } else {
384 print "USAGE: " . $handler->usage_str(
385 $info->{name}, "pvesh $cmd $path", undef, {}, 'short');
386 }
387 if ($param-> {returns}) {
388 my $schema = to_json($info->{returns}, {utf8 => 1, canonical => 1, pretty => 1 });
389 print "RETURNS: $schema\n";
390 }
391 }
392
393 if (!$found) {
394 if ($param->{command}) {
395 die "no '$param->{command}' handler for '$path'\n";
396 } else {
397 die "no such resource '$path'\n"
398 }
399 }
400
401 return undef;
402 }});
403
404 our $cmddef = {
405 usage => [ __PACKAGE__, 'usage', ['api_path'], {}, $format_result ],
406 get => [ __PACKAGE__, 'get', ['api_path'], {}, $format_result ],
407 set => [ __PACKAGE__, 'set', ['api_path'], {}, $format_result ],
408 create => [ __PACKAGE__, 'create', ['api_path'], {}, $format_result ],
409 delete => [ __PACKAGE__, 'delete', ['api_path'], {}, $format_result ],
410 };
411
412 my $cmd = $ARGV[0];
413
414 __PACKAGE__->run_cli_handler();
415
416
417 __END__
418
419 =head1 NAME
420
421 pvesh - shell interface to the Promox VE API
422
423 =head1 SYNOPSIS
424
425 pvesh [get|set|create|delete|usage] [REST API path] [--verbose]
426
427 =head1 DESCRIPTION
428
429 pvesh provides a command line interface to the Proxmox VE REST API.
430
431 =head1 EXAMPLES
432
433 get the list of nodes in my cluster
434
435 pvesh get /nodes
436
437 get a list of available options for the datacenter
438
439 pvesh usage cluster/options -v
440
441 set the HTMl5 NoVNC console as the default console for the datacenter
442
443 pvesh set cluster/options -console html5
444
445 =head1 SEE ALSO
446
447 qm(1), pct(1)