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