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 push @$args, "--$key", $_ for split(/\0/, $param->{$key});
115 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
116 'pvesh', '--noproxy', $cmd, $path,
117 '--output-format', 'json'];
119 if (scalar(@$args)) {
120 my $cmdargs = [String
::ShellQuote
::shell_quote
(@$args)];
121 push @$remcmd, @$cmdargs;
125 PVE
::Tools
::run_command
($remcmd, errmsg
=> "proxy handler failed",
126 outfunc
=> sub { $res .= shift });
128 my $decoded_json = eval { decode_json
($res) };
130 return $res; # do not error, '' (null) is valid too
132 return $decoded_json;
135 sub extract_children
{
136 my ($lnk, $data) = @_;
140 return $res if !($lnk && $data);
142 my $href = $lnk->{href
};
143 if ($href =~ m/^\{(\S+)\}$/) {
146 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
148 my $value = $elem->{$prop};
159 my $res = { path
=> $path };
161 my ($handler, $info, $pm) = PVE
::API2-
>find_handler('GET', $path, $uri_param);
162 if ($handler && $info) {
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);
177 my ($handler, $info) = PVE
::API2-
>find_handler('GET', $path);
178 if (!($handler && $info)) {
181 if (PVE
::JSONSchema
::method_get_child_link
($info)) {
188 ($handler, $info) = PVE
::API2-
>find_handler('PUT', $path);
189 if (!($handler && $info)) {
195 ($handler, $info) = PVE
::API2-
>find_handler('POST', $path);
196 if (!($handler && $info)) {
202 ($handler, $info) = PVE
::API2-
>find_handler('DELETE', $path);
203 if (!($handler && $info)) {
212 # dynamically update schema definition
213 # like: pvesh <get|set|create|delete|help> <path>
215 sub extract_path_info
{
216 my ($uri_param) = @_;
218 my ($handler, $info);
220 my $test_path_properties = sub {
221 my ($method, $path) = @_;
222 ($handler, $info) = PVE
::API2-
>find_handler($method, $path, $uri_param);
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";
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);
251 my $path_properties = {};
253 my $api_path_property = {
254 description
=> "API path.",
257 my ($cmd, $pname, $cur, $args) = @_;
258 return complete_api_path
($cur);
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};
270 $path_properties->{api_path
} = $api_path_property;
271 $path_properties->{noproxy
} = {
272 description
=> "Disable automatic proxying.",
277 my $extract_std_options = 1;
279 my $cond_add_standard_output_properties = sub {
282 my $keys = [ grep { !defined($props->{$_}) } keys %$PVE::RESTHandler
::standard_output_options
];
284 return PVE
::RESTHandler
::add_standard_output_properties
($props, $keys);
287 my $handle_streamed_response = sub {
289 my ($fh, $path, $encoding, $type) =
290 $download->@{'fh', 'path', 'content-encoding', 'content-type'};
292 die "{download} returned but neither fh nor path given\n"
293 if !defined($fh) && !defined($path);
295 die "unknown 'content-encoding' $encoding\n"
296 if defined($encoding) && $encoding ne 'gzip';
298 die "unknown 'content-type' $type\n"
299 if defined($type) && $type !~ qw
!^(text
/plain)|(application/json
)$!;
301 if (defined($path)) {
302 open($fh, '<', $path)
303 or die "open stream path '$path' for reading failed: $!\n";
309 if (defined($encoding)) {
311 gunzip
(\
$data => \
$out);
315 if (defined($type) && $type eq 'application/json') {
316 $data = decode_json
($data)->{data
};
322 sub call_api_method
{
323 my ($cmd, $param) = @_;
325 my $method = $method_map->{$cmd} || die "unable to map command '$cmd'";
327 my $path = PVE
::Tools
::extract_param
($param, 'api_path');
328 die "missing API path\n" if !defined($path);
330 my $stdopts = $extract_std_options ?
331 PVE
::RESTHandler
::extract_standard_output_properties
($param) : {};
333 $opt_nooutput = 1 if $stdopts->{quiet
};
336 my ($handler, $info) = PVE
::API2-
>find_handler($method, $path, $uri_param);
337 if (!$handler || !$info) {
338 die "no '$cmd' handler for '$path'\n";
342 my ($node, $remip) = check_proxyto
($info, $uri_param, $param);
344 $data = proxy_handler
($node, $remip, $path, $cmd, $param);
346 foreach my $p (keys %$uri_param) {
347 $param->{$p} = $uri_param->{$p};
350 $data = $handler->handle($info, $param);
352 $data = &$handle_streamed_response($data->{download
})
353 if ref($data) eq 'HASH' && ref($data->{download
}) eq 'HASH';
356 return if $opt_nooutput || $stdopts->{quiet
};
358 PVE
::CLIFormatter
::print_api_result
($data, $info->{returns
}, undef, $stdopts);
361 __PACKAGE__-
>register_method ({
365 description
=> "List child objects on <api_path>.",
367 additionalProperties
=> 0,
368 properties
=> $cond_add_standard_output_properties->($path_properties),
370 returns
=> { type
=> 'null' },
374 my $path = PVE
::Tools
::extract_param
($param, 'api_path');
376 my $stdopts = PVE
::RESTHandler
::extract_standard_output_properties
($param);
379 my ($handler, $info) = PVE
::API2-
>find_handler('GET', $path, $uri_param);
380 if (!$handler || !$info) {
381 die "no such resource '$path'\n";
384 my $link = PVE
::JSONSchema
::method_get_child_link
($info);
385 die "resource '$path' does not define child links\n" if !$link;
389 my ($node, $remip) = check_proxyto
($info, $uri_param, $param);
391 $res = proxy_handler
($node, $remip, $path, 'ls', $param);
393 foreach my $p (keys %$uri_param) {
394 $param->{$p} = $uri_param->{$p};
397 my $data = $handler->handle($info, $param);
399 my $children = extract_children
($link, $data);
402 foreach my $c (@$children) {
403 my $item = { name
=> $c, capabilities
=> resource_cap
("$path/$c")};
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);
417 __PACKAGE__-
>register_method ({
421 description
=> "Call API GET on <api_path>.",
423 additionalProperties
=> 0,
424 properties
=> $cond_add_standard_output_properties->($path_properties),
426 returns
=> { type
=> 'null' },
430 call_api_method
('get', $param);
435 __PACKAGE__-
>register_method ({
439 description
=> "Call API PUT on <api_path>.",
441 additionalProperties
=> 0,
442 properties
=> $cond_add_standard_output_properties->($path_properties),
444 returns
=> { type
=> 'null' },
448 call_api_method
('set', $param);
453 __PACKAGE__-
>register_method ({
457 description
=> "Call API POST on <api_path>.",
459 additionalProperties
=> 0,
460 properties
=> $cond_add_standard_output_properties->($path_properties),
462 returns
=> { type
=> 'null' },
466 call_api_method
('create', $param);
471 __PACKAGE__-
>register_method ({
475 description
=> "Call API DELETE on <api_path>.",
477 additionalProperties
=> 0,
478 properties
=> $cond_add_standard_output_properties->($path_properties),
480 returns
=> { type
=> 'null' },
484 call_api_method
('delete', $param);
489 __PACKAGE__-
>register_method ({
493 description
=> "print API usage information for <api_path>.",
495 additionalProperties
=> 0,
497 api_path
=> $api_path_property,
499 description
=> "Verbose output format.",
504 description
=> "Including schema for returned data.",
509 description
=> "API command.",
511 enum
=> [ keys %$method_map ],
516 returns
=> { type
=> 'null' },
520 my $path = $param->{api_path
};
523 foreach my $cmd (qw(get set create delete)) {
524 next if $param->{command
} && $cmd ne $param->{command
};
525 my $method = $method_map->{$cmd};
527 my ($handler, $info) = PVE
::API2-
>find_handler($method, $path, $uri_param);
531 if ($param->{verbose
}) {
532 print $handler->usage_str(
533 $info->{name
}, "pvesh $cmd $path", undef, $uri_param, 'full');
536 print "USAGE: " . $handler->usage_str(
537 $info->{name
}, "pvesh $cmd $path", undef, $uri_param, 'short');
539 if ($param-> {returns
}) {
540 my $schema = to_json
($info->{returns
}, {utf8
=> 1, canonical
=> 1, pretty
=> 1 });
541 print "RETURNS: $schema\n";
546 if ($param->{command
}) {
547 die "no '$param->{command}' handler for '$path'\n";
549 die "no such resource '$path'\n"
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']],