]> git.proxmox.com Git - pve-manager.git/blob - PVE/CLI/pvesh.pm
61b4464d63ea209a784581656d0d5d1c0b2ee7d4
[pve-manager.git] / PVE / CLI / pvesh.pm
1 package PVE::CLI::pvesh;
2
3 use strict;
4 use warnings;
5 use HTTP::Status qw(:constants :is status_message);
6 use String::ShellQuote;
7 use PVE::JSONSchema qw(get_standard_option);
8 use PVE::SafeSyslog;
9 use PVE::Cluster;
10 use PVE::INotify;
11 use PVE::RPCEnvironment;
12 use PVE::RESTHandler;
13 use PVE::CLIFormatter;
14 use PVE::CLIHandler;
15 use PVE::API2Tools;
16 use PVE::API2;
17 use JSON;
18 use IO::Uncompress::Gunzip qw(gunzip);
19
20 use base qw(PVE::CLIHandler);
21
22 my $disable_proxy = 0;
23 my $opt_nooutput = 0;
24
25 # compatibility code
26 my $optmatch;
27 do {
28 $optmatch = 0;
29 if ($ARGV[0]) {
30 if ($ARGV[0] eq '--noproxy') {
31 shift @ARGV;
32 $disable_proxy = 1;
33 $optmatch = 1;
34 } elsif ($ARGV[0] eq '--nooutput') {
35 # we use this when starting task in CLI (suppress printing upid)
36 # for example 'pvesh --nooutput create /nodes/localhost/stopall'
37 shift @ARGV;
38 $opt_nooutput = 1;
39 $optmatch = 1;
40 }
41 }
42 } while ($optmatch);
43
44 sub setup_environment {
45 PVE::RPCEnvironment->setup_default_cli_env();
46 }
47
48 sub complete_api_path {
49 my($text) = @_;
50
51 my ($dir, undef, $rest) = $text =~ m|^(.*/)?(([^/]*))?$|;
52
53 my $path = $dir // ''; # copy
54
55 $path =~ s|/+|/|g;
56 $path =~ s|^\/||;
57 $path =~ s|\/$||;
58
59 my $res = [];
60
61 my $di = dir_info($path);
62 if (my $children = $di->{children}) {
63 foreach my $c (@$children) {
64 if ($c =~ /^\Q$rest/) {
65 my $new = $dir ? "$dir$c" : $c;
66 push @$res, $new;
67 }
68 }
69 }
70
71 if (scalar(@$res) == 1) {
72 return [$res->[0], "$res->[0]/"];
73 }
74
75 return $res;
76 }
77
78 my $method_map = {
79 create => 'POST',
80 set => 'PUT',
81 get => 'GET',
82 delete => 'DELETE',
83 };
84
85 sub check_proxyto {
86 my ($info, $uri_param, $params) = @_;
87
88 my $rpcenv = PVE::RPCEnvironment->get();
89
90 my $all_params = { %$uri_param, %$params };
91
92 if ($info->{proxyto} || $info->{proxyto_callback}) {
93 my $node = PVE::API2Tools::resolve_proxyto(
94 $rpcenv, $info->{proxyto_callback}, $info->{proxyto}, $all_params);
95
96 if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) {
97 die "proxy loop detected - aborting\n" if $disable_proxy;
98 my $remip = PVE::Cluster::remote_node_ip($node);
99 return ($node, $remip);
100 }
101 }
102
103 return undef;
104 }
105
106 sub proxy_handler {
107 my ($node, $remip, $path, $cmd, $param) = @_;
108
109 my $args = [];
110 foreach my $key (keys %$param) {
111 next if $key eq 'quiet' || $key eq 'output-format'; # just to be sure
112 push @$args, "--$key", $_ for split(/\0/, $param->{$key});
113 }
114
115 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
116 'pvesh', '--noproxy', $cmd, $path,
117 '--output-format', 'json'];
118
119 if (scalar(@$args)) {
120 my $cmdargs = [String::ShellQuote::shell_quote(@$args)];
121 push @$remcmd, @$cmdargs;
122 }
123
124 my $res = '';
125 PVE::Tools::run_command($remcmd, errmsg => "proxy handler failed",
126 outfunc => sub { $res .= shift });
127
128 my $decoded_json = eval { decode_json($res) };
129 if ($@) {
130 return $res; # do not error, '' (null) is valid too
131 }
132 return $decoded_json;
133 }
134
135 sub extract_children {
136 my ($lnk, $data) = @_;
137
138 my $res = [];
139
140 return $res if !($lnk && $data);
141
142 my $href = $lnk->{href};
143 if ($href =~ m/^\{(\S+)\}$/) {
144 my $prop = $1;
145
146 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
147 next if !ref($elem);
148 my $value = $elem->{$prop};
149 push @$res, $value;
150 }
151 }
152
153 return $res;
154 }
155
156 sub dir_info {
157 my ($path) = @_;
158
159 my $res = { path => $path };
160 my $uri_param = {};
161 my ($handler, $info, $pm) = PVE::API2->find_handler('GET', $path, $uri_param);
162 if ($handler && $info) {
163 eval {
164 my $data = $handler->handle($info, $uri_param);
165 my $lnk = PVE::JSONSchema::method_get_child_link($info);
166 $res->{children} = extract_children($lnk, $data);
167 }; # ignore errors ?
168 }
169 return $res;
170 }
171
172 sub resource_cap {
173 my ($path) = @_;
174
175 my $res = '';
176
177 my ($handler, $info) = PVE::API2->find_handler('GET', $path);
178 if (!($handler && $info)) {
179 $res .= '--';
180 } else {
181 if (PVE::JSONSchema::method_get_child_link($info)) {
182 $res .= 'Dr';
183 } else {
184 $res .= '-r';
185 }
186 }
187
188 ($handler, $info) = PVE::API2->find_handler('PUT', $path);
189 if (!($handler && $info)) {
190 $res .= '-';
191 } else {
192 $res .= 'w';
193 }
194
195 ($handler, $info) = PVE::API2->find_handler('POST', $path);
196 if (!($handler && $info)) {
197 $res .= '-';
198 } else {
199 $res .= 'c';
200 }
201
202 ($handler, $info) = PVE::API2->find_handler('DELETE', $path);
203 if (!($handler && $info)) {
204 $res .= '-';
205 } else {
206 $res .= 'd';
207 }
208
209 return $res;
210 }
211
212 # dynamically update schema definition
213 # like: pvesh <get|set|create|delete|help> <path>
214
215 sub extract_path_info {
216 my ($uri_param) = @_;
217
218 my ($handler, $info);
219
220 my $test_path_properties = sub {
221 my ($method, $path) = @_;
222 ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
223 };
224
225 if (defined(my $cmd = $ARGV[0])) {
226 if (my $method = $method_map->{$cmd}) {
227 if (my $path = $ARGV[1]) {
228 $test_path_properties->($method, $path);
229 if (!defined($handler)) {
230 print STDERR "No '$cmd' handler defined for '$path'\n";
231 exit(1);
232 }
233 }
234 } elsif ($cmd eq 'bashcomplete') {
235 my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT});
236 my $args = PVE::Tools::split_args($cmdline);
237 if (defined(my $cmd = $args->[1])) {
238 if (my $method = $method_map->{$cmd}) {
239 if (my $path = $args->[2]) {
240 $test_path_properties->($method, $path);
241 }
242 }
243 }
244 }
245 }
246
247 return $info;
248 }
249
250
251 my $path_properties = {};
252
253 my $api_path_property = {
254 description => "API path.",
255 type => 'string',
256 completion => sub {
257 my ($cmd, $pname, $cur, $args) = @_;
258 return complete_api_path($cur);
259 },
260 };
261
262 my $uri_param = {};
263 if (my $info = extract_path_info($uri_param)) {
264 foreach my $key (keys %{$info->{parameters}->{properties}}) {
265 next if defined($uri_param->{$key});
266 $path_properties->{$key} = $info->{parameters}->{properties}->{$key};
267 }
268 }
269
270 $path_properties->{api_path} = $api_path_property;
271 $path_properties->{noproxy} = {
272 description => "Disable automatic proxying.",
273 type => 'boolean',
274 optional => 1,
275 };
276
277 my $extract_std_options = 1;
278
279 my $cond_add_standard_output_properties = sub {
280 my ($props) = @_;
281
282 my $keys = [ grep { !defined($props->{$_}) } keys %$PVE::RESTHandler::standard_output_options ];
283
284 return PVE::RESTHandler::add_standard_output_properties($props, $keys);
285 };
286
287 my $handle_streamed_response = sub {
288 my ($download) = @_;
289 my ($fh, $path, $encoding, $type) =
290 $download->@{'fh', 'path', 'content-encoding', 'content-type'};
291
292 die "{download} returned but neither fh nor path given\n"
293 if !defined($fh) && !defined($path);
294
295 die "unknown 'content-encoding' $encoding\n"
296 if defined($encoding) && $encoding ne 'gzip';
297
298 die "unknown 'content-type' $type\n"
299 if defined($type) && $type !~ qw!^(text/plain)|(application/json)$!;
300
301 if (defined($path)) {
302 open($fh, '<', $path)
303 or die "open stream path '$path' for reading failed: $!\n";
304 }
305
306 local $/;
307 my $data = <$fh>;
308
309 if (defined($encoding)) {
310 my $out;
311 gunzip(\$data => \$out);
312 $data = $out;
313 }
314
315 if (defined($type) && $type eq 'application/json') {
316 $data = decode_json($data)->{data};
317 }
318
319 return $data;
320 };
321
322 sub call_api_method {
323 my ($cmd, $param) = @_;
324
325 my $method = $method_map->{$cmd} || die "unable to map command '$cmd'";
326
327 my $path = PVE::Tools::extract_param($param, 'api_path');
328 die "missing API path\n" if !defined($path);
329
330 my $stdopts = $extract_std_options ?
331 PVE::RESTHandler::extract_standard_output_properties($param) : {};
332
333 $opt_nooutput = 1 if $stdopts->{quiet};
334
335 my $uri_param = {};
336 my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
337 if (!$handler || !$info) {
338 die "no '$cmd' handler for '$path'\n";
339 }
340
341 my $data;
342 my ($node, $remip) = check_proxyto($info, $uri_param, $param);
343 if ($node) {
344 $data = proxy_handler($node, $remip, $path, $cmd, $param);
345 } else {
346 foreach my $p (keys %$uri_param) {
347 $param->{$p} = $uri_param->{$p};
348 }
349
350 $data = $handler->handle($info, $param);
351
352 $data = &$handle_streamed_response($data->{download})
353 if ref($data) eq 'HASH' && ref($data->{download}) eq 'HASH';
354 }
355
356 return if $opt_nooutput || $stdopts->{quiet};
357
358 PVE::CLIFormatter::print_api_result($data, $info->{returns}, undef, $stdopts);
359 }
360
361 __PACKAGE__->register_method ({
362 name => 'ls',
363 path => 'ls',
364 method => 'GET',
365 description => "List child objects on <api_path>.",
366 parameters => {
367 additionalProperties => 0,
368 properties => $cond_add_standard_output_properties->($path_properties),
369 },
370 returns => { type => 'null' },
371 code => sub {
372 my ($param) = @_;
373
374 my $path = PVE::Tools::extract_param($param, 'api_path');
375
376 my $stdopts = PVE::RESTHandler::extract_standard_output_properties($param);
377
378 my $uri_param = {};
379 my ($handler, $info) = PVE::API2->find_handler('GET', $path, $uri_param);
380 if (!$handler || !$info) {
381 die "no such resource '$path'\n";
382 }
383
384 my $link = PVE::JSONSchema::method_get_child_link($info);
385 die "resource '$path' does not define child links\n" if !$link;
386
387 my $res;
388
389 my ($node, $remip) = check_proxyto($info, $uri_param, $param);
390 if ($node) {
391 $res = proxy_handler($node, $remip, $path, 'ls', $param);
392 } else {
393 foreach my $p (keys %$uri_param) {
394 $param->{$p} = $uri_param->{$p};
395 }
396
397 my $data = $handler->handle($info, $param);
398
399 my $children = extract_children($link, $data);
400
401 $res = [];
402 foreach my $c (@$children) {
403 my $item = { name => $c, capabilities => resource_cap("$path/$c")};
404 push @$res, $item;
405 }
406 }
407
408 my $schema = { type => 'array', items => { type => 'object' }};
409 $stdopts->{sort_key} = 'name';
410 $stdopts->{noborder} //= 1;
411 $stdopts->{noheader} //= 1;
412 PVE::CLIFormatter::print_api_result($res, $schema, ['capabilities', 'name'], $stdopts);
413
414 return undef;
415 }});
416
417 __PACKAGE__->register_method ({
418 name => 'get',
419 path => 'get',
420 method => 'GET',
421 description => "Call API GET on <api_path>.",
422 parameters => {
423 additionalProperties => 0,
424 properties => $cond_add_standard_output_properties->($path_properties),
425 },
426 returns => { type => 'null' },
427 code => sub {
428 my ($param) = @_;
429
430 call_api_method('get', $param);
431
432 return undef;
433 }});
434
435 __PACKAGE__->register_method ({
436 name => 'set',
437 path => 'set',
438 method => 'PUT',
439 description => "Call API PUT on <api_path>.",
440 parameters => {
441 additionalProperties => 0,
442 properties => $cond_add_standard_output_properties->($path_properties),
443 },
444 returns => { type => 'null' },
445 code => sub {
446 my ($param) = @_;
447
448 call_api_method('set', $param);
449
450 return undef;
451 }});
452
453 __PACKAGE__->register_method ({
454 name => 'create',
455 path => 'create',
456 method => 'POST',
457 description => "Call API POST on <api_path>.",
458 parameters => {
459 additionalProperties => 0,
460 properties => $cond_add_standard_output_properties->($path_properties),
461 },
462 returns => { type => 'null' },
463 code => sub {
464 my ($param) = @_;
465
466 call_api_method('create', $param);
467
468 return undef;
469 }});
470
471 __PACKAGE__->register_method ({
472 name => 'delete',
473 path => 'delete',
474 method => 'DELETE',
475 description => "Call API DELETE on <api_path>.",
476 parameters => {
477 additionalProperties => 0,
478 properties => $cond_add_standard_output_properties->($path_properties),
479 },
480 returns => { type => 'null' },
481 code => sub {
482 my ($param) = @_;
483
484 call_api_method('delete', $param);
485
486 return undef;
487 }});
488
489 __PACKAGE__->register_method ({
490 name => 'usage',
491 path => 'usage',
492 method => 'GET',
493 description => "print API usage information for <api_path>.",
494 parameters => {
495 additionalProperties => 0,
496 properties => {
497 api_path => $api_path_property,
498 verbose => {
499 description => "Verbose output format.",
500 type => 'boolean',
501 optional => 1,
502 },
503 returns => {
504 description => "Including schema for returned data.",
505 type => 'boolean',
506 optional => 1,
507 },
508 command => {
509 description => "API command.",
510 type => 'string',
511 enum => [ keys %$method_map ],
512 optional => 1,
513 },
514 },
515 },
516 returns => { type => 'null' },
517 code => sub {
518 my ($param) = @_;
519
520 my $path = $param->{api_path};
521
522 my $found = 0;
523 foreach my $cmd (qw(get set create delete)) {
524 next if $param->{command} && $cmd ne $param->{command};
525 my $method = $method_map->{$cmd};
526 my $uri_param = {};
527 my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
528 next if !$handler;
529 $found = 1;
530
531 if ($param->{verbose}) {
532 print $handler->usage_str(
533 $info->{name}, "pvesh $cmd $path", undef, $uri_param, 'full');
534
535 } else {
536 print "USAGE: " . $handler->usage_str(
537 $info->{name}, "pvesh $cmd $path", undef, $uri_param, 'short');
538 }
539 if ($param-> {returns}) {
540 my $schema = to_json($info->{returns}, {utf8 => 1, canonical => 1, pretty => 1 });
541 print "RETURNS: $schema\n";
542 }
543 }
544
545 if (!$found) {
546 if ($param->{command}) {
547 die "no '$param->{command}' handler for '$path'\n";
548 } else {
549 die "no such resource '$path'\n"
550 }
551 }
552
553 return undef;
554 }});
555
556 our $cmddef = {
557 usage => [ __PACKAGE__, 'usage', ['api_path']],
558 get => [ __PACKAGE__, 'get', ['api_path']],
559 ls => [ __PACKAGE__, 'ls', ['api_path']],
560 set => [ __PACKAGE__, 'set', ['api_path']],
561 create => [ __PACKAGE__, 'create', ['api_path']],
562 delete => [ __PACKAGE__, 'delete', ['api_path']],
563 };
564
565 1;