1 package PVE
::CLI
::pvesh
;
5 use HTTP
::Status
qw(:constants :is status_message);
6 use String
::ShellQuote
;
7 use PVE
::JSONSchema
qw(get_standard_option);
11 use PVE
::RPCEnvironment
;
13 use PVE
::CLIFormatter
;
18 use IO
::Uncompress
::Gunzip
qw(gunzip);
20 use base
qw(PVE::CLIHandler);
22 my $disable_proxy = 0;
30 if ($ARGV[0] eq '--noproxy') {
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'
44 sub setup_environment
{
45 PVE
::RPCEnvironment-
>setup_default_cli_env();
48 sub complete_api_path
{
51 my ($dir, undef, $rest) = $text =~ m
|^(.*/)?(([^/]*))?
$|;
53 my $path = $dir // ''; # copy
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;
71 if (scalar(@$res) == 1) {
72 return [$res->[0], "$res->[0]/"];
86 my ($info, $uri_param, $params) = @_;
88 my $rpcenv = PVE
::RPCEnvironment-
>get();
90 my $all_params = { %$uri_param, %$params };
92 if ($info->{proxyto
} || $info->{proxyto_callback
}) {
93 my $node = PVE
::API2Tools
::resolve_proxyto
(
94 $rpcenv, $info->{proxyto_callback
}, $info->{proxyto
}, $all_params);
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);
107 my ($node, $remip, $path, $cmd, $param) = @_;
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}->@*;
115 push @$args, "--$key", $_ for split(/\0/, $param->{$key});
119 my @ssh_tunnel_cmd = ('ssh', '-o', 'BatchMode=yes', "root\@$remip");
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;
128 PVE
::Tools
::run_command
(
129 [ @ssh_tunnel_cmd, '--', @pvesh_cmd ],
130 errmsg
=> "proxy handler failed",
131 outfunc
=> sub { $res .= shift },
134 my $decoded_json = eval { decode_json
($res) };
136 return $res; # do not error, '' (null) is valid too
138 return $decoded_json;
141 sub extract_children
{
142 my ($lnk, $data) = @_;
146 return $res if !($lnk && $data);
148 my $href = $lnk->{href
};
149 if ($href =~ m/^\{(\S+)\}$/) {
152 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
154 my $value = $elem->{$prop};
165 my $res = { path
=> $path };
167 my ($handler, $info, $pm) = PVE
::API2-
>find_handler('GET', $path, $uri_param);
168 if ($handler && $info) {
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);
183 my ($handler, $info) = PVE
::API2-
>find_handler('GET', $path);
184 if (!($handler && $info)) {
187 if (PVE
::JSONSchema
::method_get_child_link
($info)) {
194 ($handler, $info) = PVE
::API2-
>find_handler('PUT', $path);
195 if (!($handler && $info)) {
201 ($handler, $info) = PVE
::API2-
>find_handler('POST', $path);
202 if (!($handler && $info)) {
208 ($handler, $info) = PVE
::API2-
>find_handler('DELETE', $path);
209 if (!($handler && $info)) {
218 # dynamically update schema definition
219 # like: pvesh <get|set|create|delete|help> <path>
221 sub extract_path_info
{
222 my ($uri_param) = @_;
224 my ($handler, $info);
226 my $test_path_properties = sub {
227 my ($method, $path) = @_;
228 ($handler, $info) = PVE
::API2-
>find_handler($method, $path, $uri_param);
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";
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);
257 my $path_properties = {};
259 my $api_path_property = {
260 description
=> "API path.",
263 my ($cmd, $pname, $cur, $args) = @_;
264 return complete_api_path
($cur);
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};
276 $path_properties->{api_path
} = $api_path_property;
277 $path_properties->{noproxy
} = {
278 description
=> "Disable automatic proxying.",
283 my $extract_std_options = 1;
285 my $cond_add_standard_output_properties = sub {
288 my $keys = [ grep { !defined($props->{$_}) } keys %$PVE::RESTHandler
::standard_output_options
];
290 return PVE
::RESTHandler
::add_standard_output_properties
($props, $keys);
293 my $handle_streamed_response = sub {
295 my ($fh, $path, $encoding, $type) =
296 $download->@{'fh', 'path', 'content-encoding', 'content-type'};
298 die "{download} returned but neither fh nor path given\n" if !defined($fh) && !defined($path);
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
)$!;
303 if (defined($path)) {
304 open($fh, '<', $path) or die "open stream path '$path' for reading failed - $!\n";
310 if (defined($encoding)) {
312 gunzip
(\
$data => \
$out);
316 if (defined($type) && $type eq 'application/json') {
317 $data = decode_json
($data)->{data
};
323 sub call_api_method
{
324 my ($cmd, $param) = @_;
326 my $method = $method_map->{$cmd} || die "unable to map command '$cmd'";
328 my $path = PVE
::Tools
::extract_param
($param, 'api_path');
329 die "missing API path\n" if !defined($path);
331 my $stdopts = $extract_std_options
332 ? PVE
::RESTHandler
::extract_standard_output_properties
($param)
335 $opt_nooutput = 1 if $stdopts->{quiet
};
338 my ($handler, $info) = PVE
::API2-
>find_handler($method, $path, $uri_param);
339 if (!$handler || !$info) {
340 die "no '$cmd' handler for '$path'\n";
344 my ($node, $remip) = check_proxyto
($info, $uri_param, $param);
346 $data = proxy_handler
($node, $remip, $path, $cmd, $param);
348 foreach my $p (keys %$uri_param) {
349 $param->{$p} = $uri_param->{$p};
352 $data = $handler->handle($info, $param);
354 if (ref($data) eq 'HASH' && ref($data->{download
}) eq 'HASH') {
355 $data = $handle_streamed_response->($data->{download
})
359 return if $opt_nooutput || $stdopts->{quiet
};
361 PVE
::CLIFormatter
::print_api_result
($data, $info->{returns
}, undef, $stdopts);
364 __PACKAGE__-
>register_method ({
368 description
=> "List child objects on <api_path>.",
370 additionalProperties
=> 0,
371 properties
=> $cond_add_standard_output_properties->($path_properties),
373 returns
=> { type
=> 'null' },
377 my $path = PVE
::Tools
::extract_param
($param, 'api_path');
379 my $stdopts = PVE
::RESTHandler
::extract_standard_output_properties
($param);
382 my ($handler, $info) = PVE
::API2-
>find_handler('GET', $path, $uri_param);
383 if (!$handler || !$info) {
384 die "no such resource '$path'\n";
387 my $link = PVE
::JSONSchema
::method_get_child_link
($info);
388 die "resource '$path' does not define child links\n" if !$link;
392 my ($node, $remip) = check_proxyto
($info, $uri_param, $param);
394 $res = proxy_handler
($node, $remip, $path, 'ls', $param);
396 foreach my $p (keys %$uri_param) {
397 $param->{$p} = $uri_param->{$p};
400 my $data = $handler->handle($info, $param);
402 my $children = extract_children
($link, $data);
405 foreach my $c (@$children) {
406 my $item = { name
=> $c, capabilities
=> resource_cap
("$path/$c")};
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);
420 __PACKAGE__-
>register_method ({
424 description
=> "Call API GET on <api_path>.",
426 additionalProperties
=> 0,
427 properties
=> $cond_add_standard_output_properties->($path_properties),
429 returns
=> { type
=> 'null' },
433 call_api_method
('get', $param);
438 __PACKAGE__-
>register_method ({
442 description
=> "Call API PUT on <api_path>.",
444 additionalProperties
=> 0,
445 properties
=> $cond_add_standard_output_properties->($path_properties),
447 returns
=> { type
=> 'null' },
451 call_api_method
('set', $param);
456 __PACKAGE__-
>register_method ({
460 description
=> "Call API POST on <api_path>.",
462 additionalProperties
=> 0,
463 properties
=> $cond_add_standard_output_properties->($path_properties),
465 returns
=> { type
=> 'null' },
469 call_api_method
('create', $param);
474 __PACKAGE__-
>register_method ({
478 description
=> "Call API DELETE on <api_path>.",
480 additionalProperties
=> 0,
481 properties
=> $cond_add_standard_output_properties->($path_properties),
483 returns
=> { type
=> 'null' },
487 call_api_method
('delete', $param);
492 __PACKAGE__-
>register_method ({
496 description
=> "print API usage information for <api_path>.",
498 additionalProperties
=> 0,
500 api_path
=> $api_path_property,
502 description
=> "Verbose output format.",
507 description
=> "Including schema for returned data.",
512 description
=> "API command.",
514 enum
=> [ keys %$method_map ],
519 returns
=> { type
=> 'null' },
523 my $path = $param->{api_path
};
526 foreach my $cmd (qw(get set create delete)) {
527 next if $param->{command
} && $cmd ne $param->{command
};
528 my $method = $method_map->{$cmd};
530 my ($handler, $info) = PVE
::API2-
>find_handler($method, $path, $uri_param);
534 if ($param->{verbose
}) {
535 print $handler->usage_str(
536 $info->{name
}, "pvesh $cmd $path", undef, $uri_param, 'full');
539 print "USAGE: " . $handler->usage_str(
540 $info->{name
}, "pvesh $cmd $path", undef, $uri_param, 'short');
542 if ($param-> {returns
}) {
543 my $schema = to_json
($info->{returns
}, {utf8
=> 1, canonical
=> 1, pretty
=> 1 });
544 print "RETURNS: $schema\n";
549 if ($param->{command
}) {
550 die "no '$param->{command}' handler for '$path'\n";
552 die "no such resource '$path'\n"
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']],