]> git.proxmox.com Git - pve-manager.git/blob - PVE/CLI/pvesh.pm
appliance index: fix precedence in size check for log rotation
[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 if (ref($param->{$key}) eq 'ARRAY') {
113 push @$args, "--$key", $_ for $param->{$key}->@*;
114 } else {
115 push @$args, "--$key", $_ for split(/\0/, $param->{$key});
116 }
117 }
118
119 my @ssh_tunnel_cmd = ('ssh', '-o', 'BatchMode=yes', "root\@$remip");
120
121 my @pvesh_cmd = ('pvesh', '--noproxy', $cmd, $path, '--output-format', 'json');
122 if (scalar(@$args)) {
123 my $cmdargs = [ String::ShellQuote::shell_quote(@$args) ];
124 push @pvesh_cmd, @$cmdargs;
125 }
126
127 my $res = '';
128 PVE::Tools::run_command(
129 [ @ssh_tunnel_cmd, '--', @pvesh_cmd ],
130 errmsg => "proxy handler failed",
131 outfunc => sub { $res .= shift },
132 );
133
134 my $decoded_json = eval { decode_json($res) };
135 if ($@) {
136 return $res; # do not error, '' (null) is valid too
137 }
138 return $decoded_json;
139 }
140
141 sub extract_children {
142 my ($lnk, $data) = @_;
143
144 my $res = [];
145
146 return $res if !($lnk && $data);
147
148 my $href = $lnk->{href};
149 if ($href =~ m/^\{(\S+)\}$/) {
150 my $prop = $1;
151
152 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
153 next if !ref($elem);
154 my $value = $elem->{$prop};
155 push @$res, $value;
156 }
157 }
158
159 return $res;
160 }
161
162 sub dir_info {
163 my ($path) = @_;
164
165 my $res = { path => $path };
166 my $uri_param = {};
167 my ($handler, $info, $pm) = PVE::API2->find_handler('GET', $path, $uri_param);
168 if ($handler && $info) {
169 eval {
170 my $data = $handler->handle($info, $uri_param);
171 my $lnk = PVE::JSONSchema::method_get_child_link($info);
172 $res->{children} = extract_children($lnk, $data);
173 }; # ignore errors ?
174 }
175 return $res;
176 }
177
178 sub resource_cap {
179 my ($path) = @_;
180
181 my $res = '';
182
183 my ($handler, $info) = PVE::API2->find_handler('GET', $path);
184 if (!($handler && $info)) {
185 $res .= '--';
186 } else {
187 if (PVE::JSONSchema::method_get_child_link($info)) {
188 $res .= 'Dr';
189 } else {
190 $res .= '-r';
191 }
192 }
193
194 ($handler, $info) = PVE::API2->find_handler('PUT', $path);
195 if (!($handler && $info)) {
196 $res .= '-';
197 } else {
198 $res .= 'w';
199 }
200
201 ($handler, $info) = PVE::API2->find_handler('POST', $path);
202 if (!($handler && $info)) {
203 $res .= '-';
204 } else {
205 $res .= 'c';
206 }
207
208 ($handler, $info) = PVE::API2->find_handler('DELETE', $path);
209 if (!($handler && $info)) {
210 $res .= '-';
211 } else {
212 $res .= 'd';
213 }
214
215 return $res;
216 }
217
218 # dynamically update schema definition
219 # like: pvesh <get|set|create|delete|help> <path>
220
221 sub extract_path_info {
222 my ($uri_param) = @_;
223
224 my ($handler, $info);
225
226 my $test_path_properties = sub {
227 my ($method, $path) = @_;
228 ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
229 };
230
231 if (defined(my $cmd = $ARGV[0])) {
232 if (my $method = $method_map->{$cmd}) {
233 if (my $path = $ARGV[1]) {
234 $test_path_properties->($method, $path);
235 if (!defined($handler)) {
236 print STDERR "No '$cmd' handler defined for '$path'\n";
237 exit(1);
238 }
239 }
240 } elsif ($cmd eq 'bashcomplete') {
241 my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT});
242 my $args = PVE::Tools::split_args($cmdline);
243 if (defined(my $cmd = $args->[1])) {
244 if (my $method = $method_map->{$cmd}) {
245 if (my $path = $args->[2]) {
246 $test_path_properties->($method, $path);
247 }
248 }
249 }
250 }
251 }
252
253 return $info;
254 }
255
256
257 my $path_properties = {};
258
259 my $api_path_property = {
260 description => "API path.",
261 type => 'string',
262 completion => sub {
263 my ($cmd, $pname, $cur, $args) = @_;
264 return complete_api_path($cur);
265 },
266 };
267
268 my $uri_param = {};
269 if (my $info = extract_path_info($uri_param)) {
270 foreach my $key (keys %{$info->{parameters}->{properties}}) {
271 next if defined($uri_param->{$key});
272 $path_properties->{$key} = $info->{parameters}->{properties}->{$key};
273 }
274 }
275
276 $path_properties->{api_path} = $api_path_property;
277 $path_properties->{noproxy} = {
278 description => "Disable automatic proxying.",
279 type => 'boolean',
280 optional => 1,
281 };
282
283 my $extract_std_options = 1;
284
285 my $cond_add_standard_output_properties = sub {
286 my ($props) = @_;
287
288 my $keys = [ grep { !defined($props->{$_}) } keys %$PVE::RESTHandler::standard_output_options ];
289
290 return PVE::RESTHandler::add_standard_output_properties($props, $keys);
291 };
292
293 my $handle_streamed_response = sub {
294 my ($download) = @_;
295 my ($fh, $path, $encoding, $type) =
296 $download->@{'fh', 'path', 'content-encoding', 'content-type'};
297
298 die "{download} returned but neither fh nor path given\n" if !defined($fh) && !defined($path);
299
300 die "unknown 'content-encoding' $encoding\n" if defined($encoding) && $encoding ne 'gzip';
301 die "unknown 'content-type' $type\n" if defined($type) && $type !~ qw!^(?:text/plain|application/json)$!;
302
303 if (defined($path)) {
304 open($fh, '<', $path) or die "open stream path '$path' for reading failed - $!\n";
305 }
306
307 local $/;
308 my $data = <$fh>;
309
310 if (defined($encoding)) {
311 my $out;
312 gunzip(\$data => \$out);
313 $data = $out;
314 }
315
316 if (defined($type) && $type eq 'application/json') {
317 $data = decode_json($data)->{data};
318 }
319
320 return $data;
321 };
322
323 sub call_api_method {
324 my ($cmd, $param) = @_;
325
326 my $method = $method_map->{$cmd} || die "unable to map command '$cmd'";
327
328 my $path = PVE::Tools::extract_param($param, 'api_path');
329 die "missing API path\n" if !defined($path);
330
331 my $stdopts = $extract_std_options
332 ? PVE::RESTHandler::extract_standard_output_properties($param)
333 : {};
334
335 $opt_nooutput = 1 if $stdopts->{quiet};
336
337 my $uri_param = {};
338 my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
339 if (!$handler || !$info) {
340 die "no '$cmd' handler for '$path'\n";
341 }
342
343 my $data;
344 my ($node, $remip) = check_proxyto($info, $uri_param, $param);
345 if ($node) {
346 $data = proxy_handler($node, $remip, $path, $cmd, $param);
347 } else {
348 foreach my $p (keys %$uri_param) {
349 $param->{$p} = $uri_param->{$p};
350 }
351
352 $data = $handler->handle($info, $param);
353
354 if (ref($data) eq 'HASH' && ref($data->{download}) eq 'HASH') {
355 $data = $handle_streamed_response->($data->{download})
356 }
357 }
358
359 return if $opt_nooutput || $stdopts->{quiet};
360
361 PVE::CLIFormatter::print_api_result($data, $info->{returns}, undef, $stdopts);
362 }
363
364 __PACKAGE__->register_method ({
365 name => 'ls',
366 path => 'ls',
367 method => 'GET',
368 description => "List child objects on <api_path>.",
369 parameters => {
370 additionalProperties => 0,
371 properties => $cond_add_standard_output_properties->($path_properties),
372 },
373 returns => { type => 'null' },
374 code => sub {
375 my ($param) = @_;
376
377 my $path = PVE::Tools::extract_param($param, 'api_path');
378
379 my $stdopts = PVE::RESTHandler::extract_standard_output_properties($param);
380
381 my $uri_param = {};
382 my ($handler, $info) = PVE::API2->find_handler('GET', $path, $uri_param);
383 if (!$handler || !$info) {
384 die "no such resource '$path'\n";
385 }
386
387 my $link = PVE::JSONSchema::method_get_child_link($info);
388 die "resource '$path' does not define child links\n" if !$link;
389
390 my $res;
391
392 my ($node, $remip) = check_proxyto($info, $uri_param, $param);
393 if ($node) {
394 $res = proxy_handler($node, $remip, $path, 'ls', $param);
395 } else {
396 foreach my $p (keys %$uri_param) {
397 $param->{$p} = $uri_param->{$p};
398 }
399
400 my $data = $handler->handle($info, $param);
401
402 my $children = extract_children($link, $data);
403
404 $res = [];
405 foreach my $c (@$children) {
406 my $item = { name => $c, capabilities => resource_cap("$path/$c")};
407 push @$res, $item;
408 }
409 }
410
411 my $schema = { type => 'array', items => { type => 'object' }};
412 $stdopts->{sort_key} = 'name';
413 $stdopts->{noborder} //= 1;
414 $stdopts->{noheader} //= 1;
415 PVE::CLIFormatter::print_api_result($res, $schema, ['capabilities', 'name'], $stdopts);
416
417 return undef;
418 }});
419
420 __PACKAGE__->register_method ({
421 name => 'get',
422 path => 'get',
423 method => 'GET',
424 description => "Call API GET on <api_path>.",
425 parameters => {
426 additionalProperties => 0,
427 properties => $cond_add_standard_output_properties->($path_properties),
428 },
429 returns => { type => 'null' },
430 code => sub {
431 my ($param) = @_;
432
433 call_api_method('get', $param);
434
435 return undef;
436 }});
437
438 __PACKAGE__->register_method ({
439 name => 'set',
440 path => 'set',
441 method => 'PUT',
442 description => "Call API PUT on <api_path>.",
443 parameters => {
444 additionalProperties => 0,
445 properties => $cond_add_standard_output_properties->($path_properties),
446 },
447 returns => { type => 'null' },
448 code => sub {
449 my ($param) = @_;
450
451 call_api_method('set', $param);
452
453 return undef;
454 }});
455
456 __PACKAGE__->register_method ({
457 name => 'create',
458 path => 'create',
459 method => 'POST',
460 description => "Call API POST on <api_path>.",
461 parameters => {
462 additionalProperties => 0,
463 properties => $cond_add_standard_output_properties->($path_properties),
464 },
465 returns => { type => 'null' },
466 code => sub {
467 my ($param) = @_;
468
469 call_api_method('create', $param);
470
471 return undef;
472 }});
473
474 __PACKAGE__->register_method ({
475 name => 'delete',
476 path => 'delete',
477 method => 'DELETE',
478 description => "Call API DELETE on <api_path>.",
479 parameters => {
480 additionalProperties => 0,
481 properties => $cond_add_standard_output_properties->($path_properties),
482 },
483 returns => { type => 'null' },
484 code => sub {
485 my ($param) = @_;
486
487 call_api_method('delete', $param);
488
489 return undef;
490 }});
491
492 __PACKAGE__->register_method ({
493 name => 'usage',
494 path => 'usage',
495 method => 'GET',
496 description => "print API usage information for <api_path>.",
497 parameters => {
498 additionalProperties => 0,
499 properties => {
500 api_path => $api_path_property,
501 verbose => {
502 description => "Verbose output format.",
503 type => 'boolean',
504 optional => 1,
505 },
506 returns => {
507 description => "Including schema for returned data.",
508 type => 'boolean',
509 optional => 1,
510 },
511 command => {
512 description => "API command.",
513 type => 'string',
514 enum => [ keys %$method_map ],
515 optional => 1,
516 },
517 },
518 },
519 returns => { type => 'null' },
520 code => sub {
521 my ($param) = @_;
522
523 my $path = $param->{api_path};
524
525 my $found = 0;
526 foreach my $cmd (qw(get set create delete)) {
527 next if $param->{command} && $cmd ne $param->{command};
528 my $method = $method_map->{$cmd};
529 my $uri_param = {};
530 my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
531 next if !$handler;
532 $found = 1;
533
534 if ($param->{verbose}) {
535 print $handler->usage_str(
536 $info->{name}, "pvesh $cmd $path", undef, $uri_param, 'full');
537
538 } else {
539 print "USAGE: " . $handler->usage_str(
540 $info->{name}, "pvesh $cmd $path", undef, $uri_param, 'short');
541 }
542 if ($param-> {returns}) {
543 my $schema = to_json($info->{returns}, {utf8 => 1, canonical => 1, pretty => 1 });
544 print "RETURNS: $schema\n";
545 }
546 }
547
548 if (!$found) {
549 if ($param->{command}) {
550 die "no '$param->{command}' handler for '$path'\n";
551 } else {
552 die "no such resource '$path'\n"
553 }
554 }
555
556 return undef;
557 }});
558
559 our $cmddef = {
560 usage => [ __PACKAGE__, 'usage', ['api_path']],
561 get => [ __PACKAGE__, 'get', ['api_path']],
562 ls => [ __PACKAGE__, 'ls', ['api_path']],
563 set => [ __PACKAGE__, 'set', ['api_path']],
564 create => [ __PACKAGE__, 'create', ['api_path']],
565 delete => [ __PACKAGE__, 'delete', ['api_path']],
566 };
567
568 1;