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