8 use HTTP
::Status
qw(:constants :is status_message);
15 use PMG
::RESTEnvironment
;
23 PVE
::INotify
::inotify_init
();
25 my $rpcenv = PMG
::RESTEnvironment-
>init('cli');
27 $rpcenv->set_language($ENV{LANG
});
28 $rpcenv->set_user('root@pam');
29 my $ticket = PMG
::Ticket
::assemble_ticket
('root@pam');
31 my $logid = $ENV{PVE_LOG_ID
} || 'pmgsh';
34 my $basedir = '/api2/json';
41 print STDERR
"ERROR: $msg\n" if $msg;
42 print STDERR
"USAGE: pmgsh [verifyapi]\n";
43 print STDERR
" pmgsh CMD [OPTIONS]\n";
47 my $disable_proxy = 0;
56 if ($cmd eq '--noproxy') {
60 } elsif ($cmd eq '--nooutput') {
61 # we use this when starting task in CLI (suppress printing upid)
70 if ($cmd eq 'verifyapi') {
71 PVE
::RESTHandler
::validate_method_schemas
();
73 } elsif ($cmd eq 'ls' || $cmd eq 'get' || $cmd eq 'create' ||
74 $cmd eq 'set' || $cmd eq 'delete' ||$cmd eq 'help' ) {
75 pmg_command
([ $cmd, @ARGV], $opt_nooutput);
78 print_usage
("unknown command '$cmd'");
83 if (scalar (@ARGV) != 0) {
88 print "entering PMG shell - type 'help' for help\n";
90 my $term = new Term
::ReadLine
('pmgsh');
91 my $attribs = $term->Attribs;
96 my ($dir, undef, $rest) = $text =~ m
|^(.*/)?(([^/]*))?
$|;
97 my $path = abs_path
($cdir, $dir);
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;
111 if (scalar(@res) == 0) {
113 } elsif (scalar(@res) == 1) {
114 return ($res[0], $res[0], "$res[0]/");
117 # lcd : lowest common denominator
120 for (my $i = 1; $i <= length($tmp); $i++) {
122 foreach my $p (@res) {
123 if (substr($tmp, 0, $i) ne substr($p, 0, $i)) {
129 $lcd = substr($tmp, 0, $i);
138 # just to avoid an endless loop (called by attempted_completion_function)
139 $attribs->{completion_entry_function
} = sub {
140 my($text, $state) = @_;
144 $attribs->{attempted_completion_function
} = sub {
145 my ($text, $line, $start) = @_;
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
});
153 if ($prefix =~ /^\s*\S+\s+$/) { # second word (path completion)
154 return complete_path
($text);
161 my ($current, $path) = @_;
165 return $current if !defined($path);
167 $ret = '' if $path =~ m
|^\
/|;
169 foreach my $d (split (/\/+/ , $path)) {
172 } elsif ($d eq '..') {
173 $ret = dirname
($ret);
174 $ret = '' if $ret eq '.';
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;
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);
202 die "Passwords do not match.\n" if ($input ne $conf);
206 sub reverse_map_cmd
{
216 my $cmd = $mmap->{$method};
218 die "got strange value for method ('$method') - internal error" if !$cmd;
234 my $method = $mmap->{$cmd};
236 die "unable to map method" if !$method;
242 my ($info, $uri_param) = @_;
244 if ($info->{proxyto
}) {
245 my $pn = $info->{proxyto
};
247 if ($pn eq 'master') {
248 $node = PMG
::Cluster
::get_master_node
();
250 $node = $uri_param->{$pn};
251 die "proxy parameter '$pn' does not exists" if !$node;
254 if ($node ne 'localhost' && ($node ne PVE
::INotify
::nodename
())) {
255 die "proxy loop detected - aborting\n" if $disable_proxy;
256 my $remip = PMG
::Cluster
::remote_node_ip
($node);
257 return ($node, $remip);
265 my ($node, $remip, $dir, $cmd, $args) = @_;
267 my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
268 'pmgsh', '--noproxy', $cmd, $dir, @$args];
270 system(@$remcmd) == 0 || die "proxy handler failed\n";
274 my ($dir, $cmd, $args, $nooutput) = @_;
276 my $method = map_cmd
($cmd);
279 my ($handler, $info) = PMG
::API2-
>find_handler($method, $dir, $uri_param);
280 if (!$handler || !$info) {
281 die "no '$cmd' handler for '$dir'\n";
284 my ($node, $remip) = check_proxyto
($info, $uri_param);
285 return proxy_handler
($node, $remip, $dir, $cmd, $args) if $node;
287 my $data = $handler->cli_handler("$cmd $dir", $info->{name
}, $args, [], $uri_param, $read_password);
291 warn "200 OK\n"; # always print OK status if successful
293 if ($info && $info->{returns
} && $info->{returns
}->{type
}) {
294 my $rtype = $info->{returns
}->{type
};
296 return if $rtype eq 'null';
298 if ($rtype eq 'string') {
299 print $data if $data;
304 print to_json
($data, {utf8
=> 1, allow_nonref
=> 1, canonical
=> 1, pretty
=> 1 });
309 sub find_resource_methods
{
310 my ($path, $ihash) = @_;
312 for my $method (qw(GET POST PUT DELETE)) {
315 my ($handler, $info) = PMG
::API2-
>find_handler($method, $path, $uri_param, \
$path_match);
316 if ($handler && $info && !$ihash->{$info}) {
321 uri_param
=> $uri_param,
328 my ($path, $opts) = @_;
332 find_resource_methods
($path, $ihash);
334 if (!scalar(keys(%$ihash))) {
335 die "no such resource\n";
338 my $di = dir_info
($path);
339 if (my $children = $di->{children
}) {
340 foreach my $c (@$children) {
341 my $cp = abs_path
($path, $c);
342 find_resource_methods
($cp, $ihash);
346 foreach my $mi (sort { $a->{path
} cmp $b->{path
} } values %$ihash) {
347 my $method = $mi->{info
}->{method};
349 # we skip index methods for now.
350 next if ($method eq 'GET') && PVE
::JSONSchema
::method_get_child_link
($mi->{info
});
352 my $path = $mi->{path
};
353 $path =~ s
|/+$||; # remove trailing slash
355 my $cmd = reverse_map_cmd
($method);
357 print $mi->{handler
}->usage_str($mi->{info
}->{name
}, "$cmd $path", [], $mi->{uri_param
},
358 $opts->{verbose
} ?
'full' : 'short', 1);
359 print "\n\n" if $opts->{verbose
};
369 my ($handler, $info) = PMG
::API2-
>find_handler('GET', $path);
370 if (!($handler && $info)) {
373 if (PVE
::JSONSchema
::method_get_child_link
($info)) {
380 ($handler, $info) = PMG
::API2-
>find_handler('PUT', $path);
381 if (!($handler && $info)) {
387 ($handler, $info) = PMG
::API2-
>find_handler('POST', $path);
388 if (!($handler && $info)) {
394 ($handler, $info) = PMG
::API2-
>find_handler('DELETE', $path);
395 if (!($handler && $info)) {
404 sub extract_children
{
405 my ($lnk, $data) = @_;
409 return $res if !($lnk && $data);
411 my $href = $lnk->{href
};
412 if ($href =~ m/^\{(\S+)\}$/) {
415 foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
417 my $value = $elem->{$prop};
428 my $res = { path
=> $path };
430 my ($handler, $info, $pm) = PMG
::API2-
>find_handler('GET', $path, $uri_param);
431 if ($handler && $info) {
433 my $data = $handler->handle($info, $uri_param);
434 my $lnk = PVE
::JSONSchema
::method_get_child_link
($info);
435 $res->{children
} = extract_children
($lnk, $data);
442 my ($dir, $args) = @_;
445 my ($handler, $info) = PMG
::API2-
>find_handler('GET', $dir, $uri_param);
446 if (!$handler || !$info) {
447 die "no such resource\n";
450 if (!PVE
::JSONSchema
::method_get_child_link
($info)) {
451 die "resource does not define child links\n";
454 my ($node, $remip) = check_proxyto
($info, $uri_param);
455 return proxy_handler
($node, $remip, $dir, 'ls', $args) if $node;
458 my $data = $handler->cli_handler("ls $dir", $info->{name
}, $args, [], $uri_param, $read_password);
459 my $lnk = PVE
::JSONSchema
::method_get_child_link
($info);
460 my $children = extract_children
($lnk, $data);
462 foreach my $c (@$children) {
463 my $cap = resource_cap
(abs_path
($dir, $c));
469 my ($args, $nooutput) = @_;
471 $rpcenv->init_request();
473 $rpcenv->set_ticket($ticket);
475 my $cmd = shift @$args;
479 my $path = shift @$args;
481 die "usage: cd [dir]\n" if scalar(@$args);
483 if (!defined($path)) {
487 my $new_dir = abs_path
($cdir, $path);
488 my ($handler, $info) = PMG
::API2-
>find_handler('GET', $new_dir);
489 die "no such resource\n" if !$handler;
493 } elsif ($cmd eq 'help') {
495 my $help_usage_error = sub {
496 die "usage: help [path] [--verbose]\n";
501 &$help_usage_error() if !Getopt
::Long
::GetOptionsFromArray
($args, $opts, 'verbose');
504 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
505 $path = shift @$args;
508 &$help_usage_error() if scalar(@$args);
510 print "help [path] [--verbose]\n";
512 print "ls [path]\n\n";
514 print_help
(abs_path
($cdir, $path), $opts);
516 } elsif ($cmd eq 'ls') {
518 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
519 $path = shift @$args;
522 list_dir
(abs_path
($cdir, $path), $args);
524 } elsif ($cmd =~ m/^get|delete|set$/) {
527 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
528 $path = shift @$args;
531 call_method
(abs_path
($cdir, $path), $cmd, $args);
533 } elsif ($cmd eq 'create') {
536 if (scalar(@$args) && $args->[0] !~ m/^\-/) {
537 $path = shift @$args;
540 call_method
(abs_path
($cdir, $path), $cmd, $args, $nooutput);
543 die "unknown command '$cmd'\n";
549 while (defined ($input = $term->readline("pmg:/$cdir> "))) {
552 next if $input =~ m/^\s*$/;
554 if ($input =~ m/^\s*q(uit)?\s*$/) {
558 # add input to history if it gets not
559 # automatically added
560 if (!$term->Features->{autohistory
}) {
561 $term->addhistory($input);
565 my $args = [ shellwords
($input) ];
575 pmgsh - shell interface to the Promox Mail Gateway API
579 pmgsh [get|set|create|delete|help] [REST API path] [--verbose]
583 pmgsh provides a shell-like interface to the Proxmox Mail Gateway API, in
590 when called without parameters, pmgsh starts an interactive client,
591 where you can navigate in the different parts of the API
595 when started with parameters pmgsh will send a command to the
596 corresponding REST url, and will return the JSON formatted output
602 get the list of nodes in my cluster