]> git.proxmox.com Git - pve-manager.git/blob - bin/pvesh
test: replication: adapt test data
[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 String::ShellQuote;
14 use PVE::JSONSchema;
15 use PVE::SafeSyslog;
16 use PVE::Cluster;
17 use PVE::INotify;
18 use PVE::RPCEnvironment;
19 use PVE::API2Tools;
20 use PVE::API2;
21 use JSON;
22
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
30 my $logid = $ENV{PVE_LOG_ID} || 'pvesh';
31 initlog($logid);
32
33 my $basedir = '/api2/json';
34
35 my $cdir = '';
36
37 sub print_usage {
38 my $msg = shift;
39
40 print STDERR "ERROR: $msg\n" if $msg;
41 print STDERR "USAGE: pvesh [verifyapi]\n";
42 print STDERR " pvesh CMD [OPTIONS]\n";
43
44 }
45
46 my $disable_proxy = 0;
47 my $opt_nooutput = 0;
48
49 my $cmd = shift;
50
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);
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' ) {
75 pve_command([ $cmd, @ARGV], $opt_nooutput);
76 exit(0);
77 } else {
78 print_usage ("unknown command '$cmd'");
79 exit (-1);
80 }
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;
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
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
244 if ($info->{proxyto} || $info->{proxyto_callback}) {
245 my $node = PVE::API2Tools::resolve_proxyto(
246 $rpcenv, $info->{proxyto_callback}, $info->{proxyto}, $uri_param);
247
248 if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) {
249 die "proxy loop detected - aborting\n" if $disable_proxy;
250 my $remip = PVE::Cluster::remote_node_ip($node);
251 return ($node, $remip);
252 }
253 }
254
255 return undef;
256 }
257
258 sub proxy_handler {
259 my ($node, $remip, $dir, $cmd, $args) = @_;
260
261 my $cmdargs = [String::ShellQuote::shell_quote(@$args)];
262 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
263 'pvesh', '--noproxy', $cmd, $dir, @$cmdargs];
264
265 system(@$remcmd) == 0 || die "proxy handler failed\n";
266 }
267
268 sub call_method {
269 my ($dir, $cmd, $args, $nooutput) = @_;
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
279 my ($node, $remip) = check_proxyto($info, $uri_param);
280 return proxy_handler($node, $remip, $dir, $cmd, $args) if $node;
281
282 my $data = $handler->cli_handler("$cmd $dir", $info->{name}, $args, [], $uri_param, $read_password);
283
284 return if $nooutput;
285
286 warn "200 OK\n"; # always print OK status if successful
287
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') {
294 print $data if $data;
295 return;
296 }
297 }
298
299 print to_json($data, {utf8 => 1, allow_nonref => 1, canonical => 1, pretty => 1 });
300
301 return;
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 = {};
309 my $path_match;
310 my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param, \$path_match);
311 if ($handler && $info && !$ihash->{$info}) {
312 $ihash->{$info} = {
313 path => $path_match,
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
445 if (!PVE::JSONSchema::method_get_child_link($info)) {
446 die "resource does not define child links\n";
447 }
448
449 my ($node, $remip) = check_proxyto($info, $uri_param);
450 return proxy_handler($node, $remip, $dir, 'ls', $args) if $node;
451
452
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
463
464 sub pve_command {
465 my ($args, $nooutput) = @_;
466
467 PVE::Cluster::cfs_update();
468
469 $rpcenv->init_request();
470
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
520 } elsif ($cmd =~ m/^get|delete|set$/) {
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
536 call_method(abs_path($cdir, $path), $cmd, $args, $nooutput);
537
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
554 # add input to history if it gets not
555 # automatically added
556 if (!$term->Features->{autohistory}) {
557 $term->addhistory($input);
558 }
559
560 eval {
561 my $args = [ shellwords($input) ];
562 pve_command($args);
563 };
564 warn $@ if $@;
565 }
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)