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
;
19 use base
qw(PVE::CLIHandler);
21 my $disable_proxy = 0;
29 if ($ARGV[0] eq '--noproxy') {
33 } elsif ($ARGV[0] eq '--nooutput') {
34 # we use this when starting task in CLI (suppress printing upid)
35 # for example 'pvesh --nooutput create /nodes/localhost/stopall'
43 sub setup_environment
{
44 PVE
::RPCEnvironment-
>setup_default_cli_env();
47 sub complete_api_path
{
50 my ($dir, undef, $rest) = $text =~ m
|^(.*/)?(([^/]*))?
$|;
52 my $path = $dir // ''; # copy
60 my $di = dir_info
($path);
61 if (my $children = $di->{children
}) {
62 foreach my $c (@$children) {
63 if ($c =~ /^\Q$rest/) {
64 my $new = $dir ?
"$dir$c" : $c;
70 if (scalar(@$res) == 1) {
71 return [$res->[0], "$res->[0]/"];
85 my ($info, $uri_param) = @_;
87 my $rpcenv = PVE
::RPCEnvironment-
>get();
89 if ($info->{proxyto
} || $info->{proxyto_callback
}) {
90 my $node = PVE
::API2Tools
::resolve_proxyto
(
91 $rpcenv, $info->{proxyto_callback
}, $info->{proxyto
}, $uri_param);
93 if ($node ne 'localhost' && ($node ne PVE
::INotify
::nodename
())) {
94 die "proxy loop detected - aborting\n" if $disable_proxy;
95 my $remip = PVE
::Cluster
::remote_node_ip
($node);
96 return ($node, $remip);
104 my ($node, $remip, $path, $cmd, $param) = @_;
107 foreach my $key (keys %$param) {
108 next if $key eq 'quiet' || $key eq 'output-format'; # just to be sure
109 push @$args, "--$key", $param->{$key};
112 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
113 'pvesh', '--noproxy', $cmd, $path,
114 '--output-format', 'json'];
116 if (scalar(@$args)) {
117 my $cmdargs = [String
::ShellQuote
::shell_quote
(@$args)];
118 push @$remcmd, @$cmdargs;
122 PVE
::Tools
::run_command
($remcmd, errmsg
=> "proxy handler failed",
123 outfunc
=> sub { $json .= shift });
125 return decode_json
($json);
128 sub extract_children
{
129 my ($lnk, $data) = @_;
133 return $res if !($lnk && $data);
135 my $href = $lnk->{href
};
136 if ($href =~ m/^\{(\S+)\}$/) {
139 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
141 my $value = $elem->{$prop};
152 my $res = { path
=> $path };
154 my ($handler, $info, $pm) = PVE
::API2-
>find_handler('GET', $path, $uri_param);
155 if ($handler && $info) {
157 my $data = $handler->handle($info, $uri_param);
158 my $lnk = PVE
::JSONSchema
::method_get_child_link
($info);
159 $res->{children
} = extract_children
($lnk, $data);
170 my ($handler, $info) = PVE
::API2-
>find_handler('GET', $path);
171 if (!($handler && $info)) {
174 if (PVE
::JSONSchema
::method_get_child_link
($info)) {
181 ($handler, $info) = PVE
::API2-
>find_handler('PUT', $path);
182 if (!($handler && $info)) {
188 ($handler, $info) = PVE
::API2-
>find_handler('POST', $path);
189 if (!($handler && $info)) {
195 ($handler, $info) = PVE
::API2-
>find_handler('DELETE', $path);
196 if (!($handler && $info)) {
205 # dynamically update schema definition
206 # like: pvesh <get|set|create|delete|help> <path>
208 sub extract_path_info
{
209 my ($uri_param) = @_;
211 my ($handler, $info);
213 my $test_path_properties = sub {
214 my ($method, $path) = @_;
215 ($handler, $info) = PVE
::API2-
>find_handler($method, $path, $uri_param);
218 if (defined(my $cmd = $ARGV[0])) {
219 if (my $method = $method_map->{$cmd}) {
220 if (my $path = $ARGV[1]) {
221 $test_path_properties->($method, $path);
222 if (!defined($handler)) {
223 print STDERR
"No '$cmd' handler defined for '$path'\n";
227 } elsif ($cmd eq 'bashcomplete') {
228 my $cmdline = substr($ENV{COMP_LINE
}, 0, $ENV{COMP_POINT
});
229 my $args = PVE
::Tools
::split_args
($cmdline);
230 if (defined(my $cmd = $args->[1])) {
231 if (my $method = $method_map->{$cmd}) {
232 if (my $path = $args->[2]) {
233 $test_path_properties->($method, $path);
244 my $path_properties = {};
246 my $api_path_property = {
247 description
=> "API path.",
250 my ($cmd, $pname, $cur, $args) = @_;
251 return complete_api_path
($cur);
256 if (my $info = extract_path_info
($uri_param)) {
257 foreach my $key (keys %{$info->{parameters
}->{properties
}}) {
258 next if defined($uri_param->{$key});
259 $path_properties->{$key} = $info->{parameters
}->{properties
}->{$key};
263 $path_properties->{api_path
} = $api_path_property;
264 $path_properties->{noproxy
} = {
265 description
=> "Disable automatic proxying.",
270 my $extract_std_options = 1;
272 my $cond_add_standard_output_properties = sub {
275 foreach my $opt (keys %$PVE::RESTHandler
::standard_output_options
) {
276 if (defined($props->{$opt})) {
277 $extract_std_options = 0;
282 return PVE
::RESTHandler
::add_standard_output_properties
($props);
285 sub call_api_method
{
286 my ($cmd, $param) = @_;
288 my $method = $method_map->{$cmd} || die "unable to map command '$cmd'";
290 my $path = PVE
::Tools
::extract_param
($param, 'api_path');
291 die "missing API path\n" if !defined($path);
293 my $stdopts = $extract_std_options ?
294 PVE
::RESTHandler
::extract_standard_output_properties
($param) : {};
296 $opt_nooutput = 1 if $stdopts->{quiet
};
299 my ($handler, $info) = PVE
::API2-
>find_handler($method, $path, $uri_param);
300 if (!$handler || !$info) {
301 die "no '$cmd' handler for '$path'\n";
305 my ($node, $remip) = check_proxyto
($info, $uri_param);
307 $data = proxy_handler
($node, $remip, $path, $cmd, $param);
309 foreach my $p (keys %$uri_param) {
310 $param->{$p} = $uri_param->{$p};
313 $data = $handler->handle($info, $param);
316 return if $opt_nooutput || $stdopts->{quiet
};
318 PVE
::CLIFormatter
::print_api_result
($data, $info->{returns
}, undef, $stdopts);
321 __PACKAGE__-
>register_method ({
325 description
=> "List child objects on <api_path>.",
327 additionalProperties
=> 0,
328 properties
=> $cond_add_standard_output_properties->($path_properties),
330 returns
=> { type
=> 'null' },
334 my $path = PVE
::Tools
::extract_param
($param, 'api_path');
336 my $stdopts = PVE
::RESTHandler
::extract_standard_output_properties
($param);
339 my ($handler, $info) = PVE
::API2-
>find_handler('GET', $path, $uri_param);
340 if (!$handler || !$info) {
341 die "no such resource '$path'\n";
344 my $link = PVE
::JSONSchema
::method_get_child_link
($info);
345 die "resource '$path' does not define child links\n" if !$link;
349 my ($node, $remip) = check_proxyto
($info, $uri_param);
351 $res = proxy_handler
($node, $remip, $path, 'ls', $param);
353 foreach my $p (keys %$uri_param) {
354 $param->{$p} = $uri_param->{$p};
357 my $data = $handler->handle($info, $param);
359 my $children = extract_children
($link, $data);
362 foreach my $c (@$children) {
363 my $item = { name
=> $c, capabilities
=> resource_cap
("$path/$c")};
368 my $schema = { type
=> 'array', items
=> { type
=> 'object' }};
369 $stdopts->{sort_key
} = 'name';
370 $stdopts->{noborder
} //= 1;
371 $stdopts->{noheader
} //= 1;
372 PVE
::CLIFormatter
::print_api_result
($res, $schema, ['capabilities', 'name'], $stdopts);
377 __PACKAGE__-
>register_method ({
381 description
=> "Call API GET on <api_path>.",
383 additionalProperties
=> 0,
384 properties
=> $cond_add_standard_output_properties->($path_properties),
386 returns
=> { type
=> 'null' },
390 call_api_method
('get', $param);
395 __PACKAGE__-
>register_method ({
399 description
=> "Call API PUT on <api_path>.",
401 additionalProperties
=> 0,
402 properties
=> $cond_add_standard_output_properties->($path_properties),
404 returns
=> { type
=> 'null' },
408 call_api_method
('set', $param);
413 __PACKAGE__-
>register_method ({
417 description
=> "Call API POST on <api_path>.",
419 additionalProperties
=> 0,
420 properties
=> $cond_add_standard_output_properties->($path_properties),
422 returns
=> { type
=> 'null' },
426 call_api_method
('create', $param);
431 __PACKAGE__-
>register_method ({
435 description
=> "Call API DELETE on <api_path>.",
437 additionalProperties
=> 0,
438 properties
=> $cond_add_standard_output_properties->($path_properties),
440 returns
=> { type
=> 'null' },
444 call_api_method
('delete', $param);
449 __PACKAGE__-
>register_method ({
453 description
=> "print API usage information for <api_path>.",
455 additionalProperties
=> 0,
457 api_path
=> $api_path_property,
459 description
=> "Verbose output format.",
464 description
=> "Including schema for returned data.",
469 description
=> "API command.",
471 enum
=> [ keys %$method_map ],
476 returns
=> { type
=> 'null' },
480 my $path = $param->{api_path
};
483 foreach my $cmd (qw(get set create delete)) {
484 next if $param->{command
} && $cmd ne $param->{command
};
485 my $method = $method_map->{$cmd};
487 my ($handler, $info) = PVE
::API2-
>find_handler($method, $path, $uri_param);
491 if ($param->{verbose
}) {
492 print $handler->usage_str(
493 $info->{name
}, "pvesh $cmd $path", undef, $uri_param, 'full');
496 print "USAGE: " . $handler->usage_str(
497 $info->{name
}, "pvesh $cmd $path", undef, $uri_param, 'short');
499 if ($param-> {returns
}) {
500 my $schema = to_json
($info->{returns
}, {utf8
=> 1, canonical
=> 1, pretty
=> 1 });
501 print "RETURNS: $schema\n";
506 if ($param->{command
}) {
507 die "no '$param->{command}' handler for '$path'\n";
509 die "no such resource '$path'\n"
517 usage
=> [ __PACKAGE__
, 'usage', ['api_path']],
518 get
=> [ __PACKAGE__
, 'get', ['api_path']],
519 ls
=> [ __PACKAGE__
, 'ls', ['api_path']],
520 set
=> [ __PACKAGE__
, 'set', ['api_path']],
521 create
=> [ __PACKAGE__
, 'create', ['api_path']],
522 delete => [ __PACKAGE__
, 'delete', ['api_path']],