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