]>
Commit | Line | Data |
---|---|---|
776de3bc | 1 | #!/usr/bin/perl |
aff192e6 DM |
2 | |
3 | # TODO: | |
4 | # implement persistent history ? | |
5 | ||
6 | use strict; | |
776de3bc | 7 | use warnings; |
aff192e6 DM |
8 | use Term::ReadLine; |
9 | use File::Basename; | |
10 | use Getopt::Long; | |
11 | use HTTP::Status qw(:constants :is status_message); | |
12 | use Text::ParseWords; | |
4114aeac | 13 | use String::ShellQuote; |
aff192e6 | 14 | use PVE::JSONSchema; |
1d9f1e8d | 15 | use PVE::SafeSyslog; |
aff192e6 DM |
16 | use PVE::Cluster; |
17 | use PVE::INotify; | |
18 | use PVE::RPCEnvironment; | |
3c54bc91 | 19 | use PVE::API2Tools; |
aff192e6 DM |
20 | use PVE::API2; |
21 | use JSON; | |
b9938a0b | 22 | |
aff192e6 DM |
23 | PVE::INotify::inotify_init(); |
24 | ||
25 | my $rpcenv = PVE::RPCEnvironment->init('cli'); | |
26 | ||
27 | $rpcenv->set_language($ENV{LANG}); | |
28 | $rpcenv->set_user('root@pam'); | |
29 | ||
1d9f1e8d DM |
30 | my $logid = $ENV{PVE_LOG_ID} || 'pvesh'; |
31 | initlog($logid); | |
32 | ||
aff192e6 DM |
33 | my $basedir = '/api2/json'; |
34 | ||
35 | my $cdir = ''; | |
36 | ||
aff192e6 DM |
37 | sub print_usage { |
38 | my $msg = shift; | |
39 | ||
40 | print STDERR "ERROR: $msg\n" if $msg; | |
41 | print STDERR "USAGE: pvesh [verifyapi]\n"; | |
46e067ac DM |
42 | print STDERR " pvesh CMD [OPTIONS]\n"; |
43 | ||
44 | } | |
aff192e6 | 45 | |
46e067ac | 46 | my $disable_proxy = 0; |
4b72af23 | 47 | my $opt_nooutput = 0; |
46e067ac DM |
48 | |
49 | my $cmd = shift; | |
40a9e2bf | 50 | |
4b72af23 DM |
51 | my $optmatch; |
52 | do { | |
53 | $optmatch = 0; | |
54 | if ($cmd) { | |
55 | if ($cmd eq '--noproxy') { | |
56 | $cmd = shift; | |
57 | $disable_proxy = 1; | |
58 | $optmatch = 1; | |
59 | } elsif ($cmd eq '--nooutput') { | |
60 | # we use this when starting task in CLI (suppress printing upid) | |
61 | # for example 'pvesh --nooutput create /nodes/localhost/stopall' | |
62 | $cmd = shift; | |
63 | $opt_nooutput = 1; | |
64 | $optmatch = 1; | |
65 | } | |
66 | } | |
67 | } while ($optmatch); | |
46e067ac DM |
68 | |
69 | if ($cmd) { | |
70 | if ($cmd eq 'verifyapi') { | |
71 | PVE::RESTHandler::validate_method_schemas(); | |
72 | exit 0; | |
73 | } elsif ($cmd eq 'ls' || $cmd eq 'get' || $cmd eq 'create' || | |
74 | $cmd eq 'set' || $cmd eq 'delete' ||$cmd eq 'help' ) { | |
4b72af23 | 75 | pve_command([ $cmd, @ARGV], $opt_nooutput); |
46e067ac DM |
76 | exit(0); |
77 | } else { | |
78 | print_usage ("unknown command '$cmd'"); | |
79 | exit (-1); | |
80 | } | |
aff192e6 DM |
81 | } |
82 | ||
83 | if (scalar (@ARGV) != 0) { | |
84 | print_usage (); | |
85 | exit (-1); | |
86 | } | |
87 | ||
88 | print "entering PVE shell - type 'help' for help\n"; | |
89 | ||
90 | my $term = new Term::ReadLine ('pvesh'); | |
91 | my $attribs = $term->Attribs; | |
92 | ||
93 | sub complete_path { | |
94 | my($text) = @_; | |
95 | ||
96 | my ($dir, undef, $rest) = $text =~ m|^(.*/)?(([^/]*))?$|; | |
97 | my $path = abs_path($cdir, $dir); | |
98 | ||
99 | my @res = (); | |
100 | ||
101 | my $di = dir_info($path); | |
102 | if (my $children = $di->{children}) { | |
103 | foreach my $c (@$children) { | |
104 | if ($c =~ /^\Q$rest/) { | |
105 | my $new = $dir ? "$dir$c" : $c; | |
106 | push @res, $new; | |
107 | } | |
108 | } | |
109 | } | |
110 | ||
111 | if (scalar(@res) == 0) { | |
112 | return undef; | |
113 | } elsif (scalar(@res) == 1) { | |
114 | return ($res[0], $res[0], "$res[0]/"); | |
115 | } | |
116 | ||
117 | # lcd : lowest common denominator | |
118 | my $lcd = ''; | |
119 | my $tmp = $res[0]; | |
120 | for (my $i = 1; $i <= length($tmp); $i++) { | |
121 | my $found = 1; | |
122 | foreach my $p (@res) { | |
123 | if (substr($tmp, 0, $i) ne substr($p, 0, $i)) { | |
124 | $found = 0; | |
125 | last; | |
126 | } | |
127 | } | |
128 | if ($found) { | |
129 | $lcd = substr($tmp, 0, $i); | |
130 | } else { | |
131 | last; | |
132 | } | |
133 | } | |
134 | ||
135 | return ($lcd, @res); | |
136 | }; | |
137 | ||
138 | # just to avoid an endless loop (called by attempted_completion_function) | |
139 | $attribs->{completion_entry_function} = sub { | |
140 | my($text, $state) = @_; | |
141 | return undef; | |
142 | }; | |
143 | ||
144 | $attribs->{attempted_completion_function} = sub { | |
145 | my ($text, $line, $start) = @_; | |
146 | ||
147 | my $prefix = substr($line, 0, $start); | |
148 | if ($prefix =~ /^\s*$/) { # first word (command completeion) | |
149 | $attribs->{completion_word} = [qw(help ls cd get set create delete quit)]; | |
150 | return $term->completion_matches($text, $attribs->{list_completion_function}); | |
151 | } | |
152 | ||
153 | if ($prefix =~ /^\s*\S+\s+$/) { # second word (path completion) | |
154 | return complete_path($text); | |
155 | } | |
156 | ||
157 | return (); | |
158 | }; | |
159 | ||
160 | sub abs_path { | |
161 | my ($current, $path) = @_; | |
162 | ||
163 | my $ret = $current; | |
164 | ||
165 | return $current if !defined($path); | |
166 | ||
167 | $ret = '' if $path =~ m|^\/|; | |
168 | ||
169 | foreach my $d (split (/\/+/ , $path)) { | |
170 | if ($d eq '.') { | |
171 | next; | |
172 | } elsif ($d eq '..') { | |
173 | $ret = dirname($ret); | |
174 | $ret = '' if $ret eq '.'; | |
175 | } else { | |
176 | $ret = "$ret/$d"; | |
177 | } | |
178 | } | |
179 | ||
180 | $ret =~ s|\/+|\/|g; | |
181 | $ret =~ s|^\/||; | |
182 | $ret =~ s|\/$||; | |
183 | ||
184 | return $ret; | |
185 | } | |
186 | ||
187 | my $read_password = sub { | |
188 | my $attribs = $term->Attribs; | |
189 | my $old = $attribs->{redisplay_function}; | |
190 | $attribs->{redisplay_function} = $attribs->{shadow_redisplay}; | |
191 | my $input = $term->readline('password: '); | |
192 | my $conf = $term->readline('Retype new password: '); | |
193 | $attribs->{redisplay_function} = $old; | |
613ac948 DC |
194 | |
195 | # remove password from history | |
196 | if ($term->Features->{autohistory}) { | |
197 | my $historyPosition = $term->where_history(); | |
198 | $term->remove_history($historyPosition); | |
199 | $term->remove_history($historyPosition - 1); | |
200 | } | |
201 | ||
aff192e6 DM |
202 | die "Passwords do not match.\n" if ($input ne $conf); |
203 | return $input; | |
204 | }; | |
205 | ||
206 | sub reverse_map_cmd { | |
207 | my $method = shift; | |
208 | ||
209 | my $mmap = { | |
210 | GET => 'get', | |
211 | PUT => 'set', | |
212 | POST => 'create', | |
213 | DELETE => 'delete', | |
214 | }; | |
215 | ||
216 | my $cmd = $mmap->{$method}; | |
217 | ||
218 | die "got strange value for method ('$method') - internal error" if !$cmd; | |
219 | ||
220 | return $cmd; | |
221 | } | |
222 | ||
223 | sub map_cmd { | |
224 | my $cmd = shift; | |
225 | ||
226 | my $mmap = { | |
227 | create => 'POST', | |
228 | set => 'PUT', | |
229 | get => 'GET', | |
230 | ls => 'GET', | |
231 | delete => 'DELETE', | |
232 | }; | |
233 | ||
234 | my $method = $mmap->{$cmd}; | |
235 | ||
236 | die "unable to map method" if !$method; | |
237 | ||
238 | return $method; | |
239 | } | |
240 | ||
241 | sub check_proxyto { | |
242 | my ($info, $uri_param) = @_; | |
243 | ||
3c54bc91 DM |
244 | if ($info->{proxyto} || $info->{proxyto_callback}) { |
245 | my $node = PVE::API2Tools::resolve_proxyto( | |
246 | $rpcenv, $info->{proxyto_callback}, $info->{proxyto}, $uri_param); | |
aff192e6 DM |
247 | |
248 | if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) { | |
46e067ac DM |
249 | die "proxy loop detected - aborting\n" if $disable_proxy; |
250 | my $remip = PVE::Cluster::remote_node_ip($node); | |
251 | return ($node, $remip); | |
aff192e6 DM |
252 | } |
253 | } | |
46e067ac DM |
254 | |
255 | return undef; | |
256 | } | |
257 | ||
258 | sub proxy_handler { | |
259 | my ($node, $remip, $dir, $cmd, $args) = @_; | |
260 | ||
4114aeac | 261 | my $cmdargs = [String::ShellQuote::shell_quote(@$args)]; |
46e067ac | 262 | my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip", |
4114aeac | 263 | 'pvesh', '--noproxy', $cmd, $dir, @$cmdargs]; |
46e067ac DM |
264 | |
265 | system(@$remcmd) == 0 || die "proxy handler failed\n"; | |
aff192e6 DM |
266 | } |
267 | ||
268 | sub call_method { | |
4b72af23 | 269 | my ($dir, $cmd, $args, $nooutput) = @_; |
aff192e6 DM |
270 | |
271 | my $method = map_cmd($cmd); | |
272 | ||
273 | my $uri_param = {}; | |
274 | my ($handler, $info) = PVE::API2->find_handler($method, $dir, $uri_param); | |
275 | if (!$handler || !$info) { | |
276 | die "no '$cmd' handler for '$dir'\n"; | |
277 | } | |
278 | ||
46e067ac DM |
279 | my ($node, $remip) = check_proxyto($info, $uri_param); |
280 | return proxy_handler($node, $remip, $dir, $cmd, $args) if $node; | |
aff192e6 DM |
281 | |
282 | my $data = $handler->cli_handler("$cmd $dir", $info->{name}, $args, [], $uri_param, $read_password); | |
283 | ||
4b72af23 DM |
284 | return if $nooutput; |
285 | ||
aff192e6 DM |
286 | warn "200 OK\n"; # always print OK status if successful |
287 | ||
f28d0dcf DM |
288 | if ($info && $info->{returns} && $info->{returns}->{type}) { |
289 | my $rtype = $info->{returns}->{type}; | |
290 | ||
291 | return if $rtype eq 'null'; | |
292 | ||
293 | if ($rtype eq 'string') { | |
165337e2 | 294 | print $data if $data; |
46e067ac | 295 | return; |
f28d0dcf DM |
296 | } |
297 | } | |
aff192e6 | 298 | |
b9938a0b | 299 | print to_json($data, {utf8 => 1, allow_nonref => 1, canonical => 1, pretty => 1 }); |
aff192e6 | 300 | |
46e067ac | 301 | return; |
aff192e6 DM |
302 | } |
303 | ||
304 | sub find_resource_methods { | |
305 | my ($path, $ihash) = @_; | |
306 | ||
307 | for my $method (qw(GET POST PUT DELETE)) { | |
308 | my $uri_param = {}; | |
435063bd DM |
309 | my $path_match; |
310 | my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param, \$path_match); | |
aff192e6 DM |
311 | if ($handler && $info && !$ihash->{$info}) { |
312 | $ihash->{$info} = { | |
435063bd | 313 | path => $path_match, |
aff192e6 DM |
314 | handler => $handler, |
315 | info => $info, | |
316 | uri_param => $uri_param, | |
317 | }; | |
318 | } | |
319 | } | |
320 | } | |
321 | ||
322 | sub print_help { | |
323 | my ($path, $opts) = @_; | |
324 | ||
325 | my $ihash = {}; | |
326 | ||
327 | find_resource_methods($path, $ihash); | |
328 | ||
329 | if (!scalar(keys(%$ihash))) { | |
330 | die "no such resource\n"; | |
331 | } | |
332 | ||
333 | my $di = dir_info($path); | |
334 | if (my $children = $di->{children}) { | |
335 | foreach my $c (@$children) { | |
336 | my $cp = abs_path($path, $c); | |
337 | find_resource_methods($cp, $ihash); | |
338 | } | |
339 | } | |
340 | ||
341 | foreach my $mi (sort { $a->{path} cmp $b->{path} } values %$ihash) { | |
342 | my $method = $mi->{info}->{method}; | |
343 | ||
344 | # we skip index methods for now. | |
345 | next if ($method eq 'GET') && PVE::JSONSchema::method_get_child_link($mi->{info}); | |
346 | ||
347 | my $path = $mi->{path}; | |
348 | $path =~ s|/+$||; # remove trailing slash | |
349 | ||
350 | my $cmd = reverse_map_cmd($method); | |
351 | ||
352 | print $mi->{handler}->usage_str($mi->{info}->{name}, "$cmd $path", [], $mi->{uri_param}, | |
353 | $opts->{verbose} ? 'full' : 'short', 1); | |
354 | print "\n\n" if $opts->{verbose}; | |
355 | } | |
356 | ||
357 | }; | |
358 | ||
359 | sub resource_cap { | |
360 | my ($path) = @_; | |
361 | ||
362 | my $res = ''; | |
363 | ||
364 | my ($handler, $info) = PVE::API2->find_handler('GET', $path); | |
365 | if (!($handler && $info)) { | |
366 | $res .= '--'; | |
367 | } else { | |
368 | if (PVE::JSONSchema::method_get_child_link($info)) { | |
369 | $res .= 'Dr'; | |
370 | } else { | |
371 | $res .= '-r'; | |
372 | } | |
373 | } | |
374 | ||
375 | ($handler, $info) = PVE::API2->find_handler('PUT', $path); | |
376 | if (!($handler && $info)) { | |
377 | $res .= '-'; | |
378 | } else { | |
379 | $res .= 'w'; | |
380 | } | |
381 | ||
382 | ($handler, $info) = PVE::API2->find_handler('POST', $path); | |
383 | if (!($handler && $info)) { | |
384 | $res .= '-'; | |
385 | } else { | |
386 | $res .= 'c'; | |
387 | } | |
388 | ||
389 | ($handler, $info) = PVE::API2->find_handler('DELETE', $path); | |
390 | if (!($handler && $info)) { | |
391 | $res .= '-'; | |
392 | } else { | |
393 | $res .= 'd'; | |
394 | } | |
395 | ||
396 | return $res; | |
397 | } | |
398 | ||
399 | sub extract_children { | |
400 | my ($lnk, $data) = @_; | |
401 | ||
402 | my $res = []; | |
403 | ||
404 | return $res if !($lnk && $data); | |
405 | ||
406 | my $href = $lnk->{href}; | |
407 | if ($href =~ m/^\{(\S+)\}$/) { | |
408 | my $prop = $1; | |
409 | ||
410 | foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) { | |
411 | next if !ref($elem); | |
412 | my $value = $elem->{$prop}; | |
413 | push @$res, $value; | |
414 | } | |
415 | } | |
416 | ||
417 | return $res; | |
418 | } | |
419 | ||
420 | sub dir_info { | |
421 | my ($path) = @_; | |
422 | ||
423 | my $res = { path => $path }; | |
424 | my $uri_param = {}; | |
425 | my ($handler, $info, $pm) = PVE::API2->find_handler('GET', $path, $uri_param); | |
426 | if ($handler && $info) { | |
427 | eval { | |
428 | my $data = $handler->handle($info, $uri_param); | |
429 | my $lnk = PVE::JSONSchema::method_get_child_link($info); | |
430 | $res->{children} = extract_children($lnk, $data); | |
431 | }; # ignore errors ? | |
432 | } | |
433 | return $res; | |
434 | } | |
435 | ||
436 | sub list_dir { | |
437 | my ($dir, $args) = @_; | |
438 | ||
439 | my $uri_param = {}; | |
440 | my ($handler, $info) = PVE::API2->find_handler('GET', $dir, $uri_param); | |
441 | if (!$handler || !$info) { | |
442 | die "no such resource\n"; | |
443 | } | |
444 | ||
aff192e6 DM |
445 | if (!PVE::JSONSchema::method_get_child_link($info)) { |
446 | die "resource does not define child links\n"; | |
447 | } | |
448 | ||
46e067ac DM |
449 | my ($node, $remip) = check_proxyto($info, $uri_param); |
450 | return proxy_handler($node, $remip, $dir, 'ls', $args) if $node; | |
451 | ||
452 | ||
aff192e6 DM |
453 | my $data = $handler->cli_handler("ls $dir", $info->{name}, $args, [], $uri_param, $read_password); |
454 | my $lnk = PVE::JSONSchema::method_get_child_link($info); | |
455 | my $children = extract_children($lnk, $data); | |
456 | ||
457 | foreach my $c (@$children) { | |
458 | my $cap = resource_cap(abs_path($dir, $c)); | |
459 | print "$cap $c\n"; | |
460 | } | |
461 | } | |
462 | ||
46e067ac | 463 | |
aff192e6 | 464 | sub pve_command { |
4b72af23 | 465 | my ($args, $nooutput) = @_; |
aff192e6 DM |
466 | |
467 | PVE::Cluster::cfs_update(); | |
468 | ||
469 | $rpcenv->init_request(); | |
470 | ||
aff192e6 DM |
471 | my $cmd = shift @$args; |
472 | ||
473 | if ($cmd eq 'cd') { | |
474 | ||
475 | my $path = shift @$args; | |
476 | ||
477 | die "usage: cd [dir]\n" if scalar(@$args); | |
478 | ||
479 | if (!defined($path)) { | |
480 | $cdir = ''; | |
481 | return; | |
482 | } else { | |
483 | my $new_dir = abs_path($cdir, $path); | |
484 | my ($handler, $info) = PVE::API2->find_handler('GET', $new_dir); | |
485 | die "no such resource\n" if !$handler; | |
486 | $cdir = $new_dir; | |
487 | } | |
488 | ||
489 | } elsif ($cmd eq 'help') { | |
490 | ||
491 | my $help_usage_error = sub { | |
492 | die "usage: help [path] [--verbose]\n"; | |
493 | }; | |
494 | ||
495 | my $opts = {}; | |
496 | ||
497 | &$help_usage_error() if !Getopt::Long::GetOptionsFromArray($args, $opts, 'verbose'); | |
498 | ||
499 | my $path; | |
500 | if (scalar(@$args) && $args->[0] !~ m/^\-/) { | |
501 | $path = shift @$args; | |
502 | } | |
503 | ||
504 | &$help_usage_error() if scalar(@$args); | |
505 | ||
506 | print "help [path] [--verbose]\n"; | |
507 | print "cd [path]\n"; | |
508 | print "ls [path]\n\n"; | |
509 | ||
510 | print_help(abs_path($cdir, $path), $opts); | |
511 | ||
512 | } elsif ($cmd eq 'ls') { | |
513 | my $path; | |
514 | if (scalar(@$args) && $args->[0] !~ m/^\-/) { | |
515 | $path = shift @$args; | |
516 | } | |
517 | ||
518 | list_dir(abs_path($cdir, $path), $args); | |
519 | ||
8d365d99 | 520 | } elsif ($cmd =~ m/^get|delete|set$/) { |
aff192e6 DM |
521 | |
522 | my $path; | |
523 | if (scalar(@$args) && $args->[0] !~ m/^\-/) { | |
524 | $path = shift @$args; | |
525 | } | |
526 | ||
527 | call_method(abs_path($cdir, $path), $cmd, $args); | |
528 | ||
529 | } elsif ($cmd eq 'create') { | |
530 | ||
531 | my $path; | |
532 | if (scalar(@$args) && $args->[0] !~ m/^\-/) { | |
533 | $path = shift @$args; | |
534 | } | |
535 | ||
4b72af23 | 536 | call_method(abs_path($cdir, $path), $cmd, $args, $nooutput); |
aff192e6 | 537 | |
aff192e6 DM |
538 | } else { |
539 | die "unknown command '$cmd'\n"; | |
540 | } | |
541 | ||
542 | } | |
543 | ||
544 | my $input; | |
545 | while (defined ($input = $term->readline("pve:/$cdir> "))) { | |
546 | chomp $input; | |
547 | ||
548 | next if $input =~ m/^\s*$/; | |
549 | ||
550 | if ($input =~ m/^\s*q(uit)?\s*$/) { | |
551 | exit (0); | |
552 | } | |
553 | ||
613ac948 DC |
554 | # add input to history if it gets not |
555 | # automatically added | |
556 | if (!$term->Features->{autohistory}) { | |
557 | $term->addhistory($input); | |
558 | } | |
aff192e6 DM |
559 | |
560 | eval { | |
46e067ac DM |
561 | my $args = [ shellwords($input) ]; |
562 | pve_command($args); | |
aff192e6 DM |
563 | }; |
564 | warn $@ if $@; | |
565 | } | |
f6b62dd2 EK |
566 | |
567 | __END__ | |
568 | ||
569 | =head1 NAME | |
570 | ||
571 | pvesh - shell interface to the Promox VE API | |
572 | ||
573 | =head1 SYNOPSIS | |
574 | ||
575 | pvesh [get|set|create|delete|help] [REST API path] [--verbose] | |
576 | ||
577 | =head1 DESCRIPTION | |
578 | ||
579 | pvesh provides a shell-like interface to the Proxmox VE REST API, in two different modes: | |
580 | ||
581 | =over | |
582 | ||
583 | =item interactive | |
584 | ||
585 | when called without parameters, pvesh starts an interactive client, where you can navigate | |
586 | in the different parts of the API | |
587 | ||
588 | =item command line | |
589 | ||
590 | when started with parameters pvesh will send a command to the corresponding REST url, and will | |
591 | return the JSON formatted output | |
592 | ||
593 | =back | |
594 | ||
595 | =head1 EXAMPLES | |
596 | ||
597 | get the list of nodes in my cluster | |
598 | ||
599 | pvesh get /nodes | |
600 | ||
601 | get a list of available options for the datacenter | |
602 | ||
603 | pvesh help cluster/options -v | |
604 | ||
605 | set the HTMl5 NoVNC console as the default console for the datacenter | |
606 | ||
607 | pvesh set cluster/options -console html5 | |
608 | ||
609 | =head1 SEE ALSO | |
610 | ||
611 | qm(1), pct(1) |