]> git.proxmox.com Git - pve-manager.git/blob - PVE/CLI/pvesh.pm
9b8e324550ed77d787f516f999a564d23f66ff17
[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
19 use base qw(PVE::CLIHandler);
20
21 my $disable_proxy = 0;
22 my $opt_nooutput = 0;
23
24 # compatibility code
25 my $optmatch;
26 do {
27 $optmatch = 0;
28 if ($ARGV[0]) {
29 if ($ARGV[0] eq '--noproxy') {
30 shift @ARGV;
31 $disable_proxy = 1;
32 $optmatch = 1;
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'
36 shift @ARGV;
37 $opt_nooutput = 1;
38 $optmatch = 1;
39 }
40 }
41 } while ($optmatch);
42
43 sub setup_environment {
44 PVE::RPCEnvironment->setup_default_cli_env();
45 }
46
47 sub complete_api_path {
48 my($text) = @_;
49
50 my ($dir, undef, $rest) = $text =~ m|^(.*/)?(([^/]*))?$|;
51
52 my $path = $dir // ''; # copy
53
54 $path =~ s|/+|/|g;
55 $path =~ s|^\/||;
56 $path =~ s|\/$||;
57
58 my $res = [];
59
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;
65 push @$res, $new;
66 }
67 }
68 }
69
70 if (scalar(@$res) == 1) {
71 return [$res->[0], "$res->[0]/"];
72 }
73
74 return $res;
75 }
76
77 my $method_map = {
78 create => 'POST',
79 set => 'PUT',
80 get => 'GET',
81 delete => 'DELETE',
82 };
83
84 sub check_proxyto {
85 my ($info, $uri_param) = @_;
86
87 my $rpcenv = PVE::RPCEnvironment->get();
88
89 if ($info->{proxyto} || $info->{proxyto_callback}) {
90 my $node = PVE::API2Tools::resolve_proxyto(
91 $rpcenv, $info->{proxyto_callback}, $info->{proxyto}, $uri_param);
92
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);
97 }
98 }
99
100 return undef;
101 }
102
103 sub proxy_handler {
104 my ($node, $remip, $path, $cmd, $param) = @_;
105
106 my $args = [];
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};
110 }
111
112 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
113 'pvesh', '--noproxy', $cmd, $path,
114 '--output-format', 'json'];
115
116 if (scalar(@$args)) {
117 my $cmdargs = [String::ShellQuote::shell_quote(@$args)];
118 push @$remcmd, @$cmdargs;
119 }
120
121 my $json = '';
122 PVE::Tools::run_command($remcmd, errmsg => "proxy handler failed",
123 outfunc => sub { $json .= shift });
124
125 return decode_json($json);
126 }
127
128 sub extract_children {
129 my ($lnk, $data) = @_;
130
131 my $res = [];
132
133 return $res if !($lnk && $data);
134
135 my $href = $lnk->{href};
136 if ($href =~ m/^\{(\S+)\}$/) {
137 my $prop = $1;
138
139 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
140 next if !ref($elem);
141 my $value = $elem->{$prop};
142 push @$res, $value;
143 }
144 }
145
146 return $res;
147 }
148
149 sub dir_info {
150 my ($path) = @_;
151
152 my $res = { path => $path };
153 my $uri_param = {};
154 my ($handler, $info, $pm) = PVE::API2->find_handler('GET', $path, $uri_param);
155 if ($handler && $info) {
156 eval {
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);
160 }; # ignore errors ?
161 }
162 return $res;
163 }
164
165 sub resource_cap {
166 my ($path) = @_;
167
168 my $res = '';
169
170 my ($handler, $info) = PVE::API2->find_handler('GET', $path);
171 if (!($handler && $info)) {
172 $res .= '--';
173 } else {
174 if (PVE::JSONSchema::method_get_child_link($info)) {
175 $res .= 'Dr';
176 } else {
177 $res .= '-r';
178 }
179 }
180
181 ($handler, $info) = PVE::API2->find_handler('PUT', $path);
182 if (!($handler && $info)) {
183 $res .= '-';
184 } else {
185 $res .= 'w';
186 }
187
188 ($handler, $info) = PVE::API2->find_handler('POST', $path);
189 if (!($handler && $info)) {
190 $res .= '-';
191 } else {
192 $res .= 'c';
193 }
194
195 ($handler, $info) = PVE::API2->find_handler('DELETE', $path);
196 if (!($handler && $info)) {
197 $res .= '-';
198 } else {
199 $res .= 'd';
200 }
201
202 return $res;
203 }
204
205 # dynamically update schema definition
206 # like: pvesh <get|set|create|delete|help> <path>
207
208 sub extract_path_info {
209 my ($uri_param) = @_;
210
211 my ($handler, $info);
212
213 my $test_path_properties = sub {
214 my ($method, $path) = @_;
215 ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
216 };
217
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";
224 exit(1);
225 }
226 }
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);
234 }
235 }
236 }
237 }
238 }
239
240 return $info;
241 }
242
243
244 my $path_properties = {};
245
246 my $api_path_property = {
247 description => "API path.",
248 type => 'string',
249 completion => sub {
250 my ($cmd, $pname, $cur, $args) = @_;
251 return complete_api_path($cur);
252 },
253 };
254
255 my $uri_param = {};
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};
260 }
261 }
262
263 $path_properties->{api_path} = $api_path_property;
264 $path_properties->{noproxy} = {
265 description => "Disable automatic proxying.",
266 type => 'boolean',
267 optional => 1,
268 };
269
270 my $extract_std_options = 1;
271
272 my $cond_add_standard_output_properties = sub {
273 my ($props) = @_;
274
275 foreach my $opt (keys %$PVE::RESTHandler::standard_output_options) {
276 if (defined($props->{$opt})) {
277 $extract_std_options = 0;
278 return $props;
279 }
280 }
281
282 return PVE::RESTHandler::add_standard_output_properties($props);
283 };
284
285 sub call_api_method {
286 my ($cmd, $param) = @_;
287
288 my $method = $method_map->{$cmd} || die "unable to map command '$cmd'";
289
290 my $path = PVE::Tools::extract_param($param, 'api_path');
291 die "missing API path\n" if !defined($path);
292
293 my $stdopts = $extract_std_options ?
294 PVE::RESTHandler::extract_standard_output_properties($param) : {};
295
296 $opt_nooutput = 1 if $stdopts->{quiet};
297
298 my $uri_param = {};
299 my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
300 if (!$handler || !$info) {
301 die "no '$cmd' handler for '$path'\n";
302 }
303
304 my $data;
305 my ($node, $remip) = check_proxyto($info, $uri_param);
306 if ($node) {
307 $data = proxy_handler($node, $remip, $path, $cmd, $param);
308 } else {
309 foreach my $p (keys %$uri_param) {
310 $param->{$p} = $uri_param->{$p};
311 }
312
313 $data = $handler->handle($info, $param);
314 }
315
316 return if $opt_nooutput || $stdopts->{quiet};
317
318 PVE::CLIFormatter::print_api_result($data, $info->{returns}, undef, $stdopts);
319 }
320
321 __PACKAGE__->register_method ({
322 name => 'ls',
323 path => 'ls',
324 method => 'GET',
325 description => "List child objects on <api_path>.",
326 parameters => {
327 additionalProperties => 0,
328 properties => $cond_add_standard_output_properties->($path_properties),
329 },
330 returns => { type => 'null' },
331 code => sub {
332 my ($param) = @_;
333
334 my $path = PVE::Tools::extract_param($param, 'api_path');
335
336 my $stdopts = PVE::RESTHandler::extract_standard_output_properties($param);
337
338 my $uri_param = {};
339 my ($handler, $info) = PVE::API2->find_handler('GET', $path, $uri_param);
340 if (!$handler || !$info) {
341 die "no such resource '$path'\n";
342 }
343
344 my $link = PVE::JSONSchema::method_get_child_link($info);
345 die "resource '$path' does not define child links\n" if !$link;
346
347 my $res;
348
349 my ($node, $remip) = check_proxyto($info, $uri_param);
350 if ($node) {
351 $res = proxy_handler($node, $remip, $path, 'ls', $param);
352 } else {
353 foreach my $p (keys %$uri_param) {
354 $param->{$p} = $uri_param->{$p};
355 }
356
357 my $data = $handler->handle($info, $param);
358
359 my $children = extract_children($link, $data);
360
361 $res = [];
362 foreach my $c (@$children) {
363 my $item = { name => $c, capabilities => resource_cap("$path/$c")};
364 push @$res, $item;
365 }
366 }
367
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);
373
374 return undef;
375 }});
376
377 __PACKAGE__->register_method ({
378 name => 'get',
379 path => 'get',
380 method => 'GET',
381 description => "Call API GET on <api_path>.",
382 parameters => {
383 additionalProperties => 0,
384 properties => $cond_add_standard_output_properties->($path_properties),
385 },
386 returns => { type => 'null' },
387 code => sub {
388 my ($param) = @_;
389
390 call_api_method('get', $param);
391
392 return undef;
393 }});
394
395 __PACKAGE__->register_method ({
396 name => 'set',
397 path => 'set',
398 method => 'PUT',
399 description => "Call API PUT on <api_path>.",
400 parameters => {
401 additionalProperties => 0,
402 properties => $cond_add_standard_output_properties->($path_properties),
403 },
404 returns => { type => 'null' },
405 code => sub {
406 my ($param) = @_;
407
408 call_api_method('set', $param);
409
410 return undef;
411 }});
412
413 __PACKAGE__->register_method ({
414 name => 'create',
415 path => 'create',
416 method => 'POST',
417 description => "Call API POST on <api_path>.",
418 parameters => {
419 additionalProperties => 0,
420 properties => $cond_add_standard_output_properties->($path_properties),
421 },
422 returns => { type => 'null' },
423 code => sub {
424 my ($param) = @_;
425
426 call_api_method('create', $param);
427
428 return undef;
429 }});
430
431 __PACKAGE__->register_method ({
432 name => 'delete',
433 path => 'delete',
434 method => 'DELETE',
435 description => "Call API DELETE on <api_path>.",
436 parameters => {
437 additionalProperties => 0,
438 properties => $cond_add_standard_output_properties->($path_properties),
439 },
440 returns => { type => 'null' },
441 code => sub {
442 my ($param) = @_;
443
444 call_api_method('delete', $param);
445
446 return undef;
447 }});
448
449 __PACKAGE__->register_method ({
450 name => 'usage',
451 path => 'usage',
452 method => 'GET',
453 description => "print API usage information for <api_path>.",
454 parameters => {
455 additionalProperties => 0,
456 properties => {
457 api_path => $api_path_property,
458 verbose => {
459 description => "Verbose output format.",
460 type => 'boolean',
461 optional => 1,
462 },
463 returns => {
464 description => "Including schema for returned data.",
465 type => 'boolean',
466 optional => 1,
467 },
468 command => {
469 description => "API command.",
470 type => 'string',
471 enum => [ keys %$method_map ],
472 optional => 1,
473 },
474 },
475 },
476 returns => { type => 'null' },
477 code => sub {
478 my ($param) = @_;
479
480 my $path = $param->{api_path};
481
482 my $found = 0;
483 foreach my $cmd (qw(get set create delete)) {
484 next if $param->{command} && $cmd ne $param->{command};
485 my $method = $method_map->{$cmd};
486 my $uri_param = {};
487 my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
488 next if !$handler;
489 $found = 1;
490
491 if ($param->{verbose}) {
492 print $handler->usage_str(
493 $info->{name}, "pvesh $cmd $path", undef, $uri_param, 'full');
494
495 } else {
496 print "USAGE: " . $handler->usage_str(
497 $info->{name}, "pvesh $cmd $path", undef, $uri_param, 'short');
498 }
499 if ($param-> {returns}) {
500 my $schema = to_json($info->{returns}, {utf8 => 1, canonical => 1, pretty => 1 });
501 print "RETURNS: $schema\n";
502 }
503 }
504
505 if (!$found) {
506 if ($param->{command}) {
507 die "no '$param->{command}' handler for '$path'\n";
508 } else {
509 die "no such resource '$path'\n"
510 }
511 }
512
513 return undef;
514 }});
515
516 our $cmddef = {
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']],
523 };
524
525 1;