565 lines
12 KiB
Perl
Executable File
565 lines
12 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
|
|
# TODO:
|
|
# implement persistent history ?
|
|
|
|
use strict;
|
|
use warnings;
|
|
use Term::ReadLine;
|
|
use File::Basename;
|
|
use Getopt::Long;
|
|
use HTTP::Status qw(:constants :is status_message);
|
|
use Text::ParseWords;
|
|
use PVE::JSONSchema;
|
|
use PVE::Cluster;
|
|
use PVE::INotify;
|
|
use PVE::RPCEnvironment;
|
|
use PVE::API2;
|
|
use JSON;
|
|
|
|
PVE::INotify::inotify_init();
|
|
|
|
my $rpcenv = PVE::RPCEnvironment->init('cli');
|
|
|
|
$rpcenv->set_language($ENV{LANG});
|
|
$rpcenv->set_user('root@pam');
|
|
|
|
my $basedir = '/api2/json';
|
|
|
|
my $cdir = '';
|
|
|
|
sub print_usage {
|
|
my $msg = shift;
|
|
|
|
print STDERR "ERROR: $msg\n" if $msg;
|
|
print STDERR "USAGE: pvesh [verifyapi]\n";
|
|
print STDERR " pvesh CMD [OPTIONS]\n";
|
|
|
|
}
|
|
|
|
my $disable_proxy = 0;
|
|
my $opt_nooutput = 0;
|
|
|
|
my $cmd = shift;
|
|
|
|
my $optmatch;
|
|
do {
|
|
$optmatch = 0;
|
|
if ($cmd) {
|
|
if ($cmd eq '--noproxy') {
|
|
$cmd = shift;
|
|
$disable_proxy = 1;
|
|
$optmatch = 1;
|
|
} elsif ($cmd eq '--nooutput') {
|
|
# we use this when starting task in CLI (suppress printing upid)
|
|
# for example 'pvesh --nooutput create /nodes/localhost/stopall'
|
|
$cmd = shift;
|
|
$opt_nooutput = 1;
|
|
$optmatch = 1;
|
|
}
|
|
}
|
|
} while ($optmatch);
|
|
|
|
if ($cmd) {
|
|
if ($cmd eq 'verifyapi') {
|
|
PVE::RESTHandler::validate_method_schemas();
|
|
exit 0;
|
|
} elsif ($cmd eq 'ls' || $cmd eq 'get' || $cmd eq 'create' ||
|
|
$cmd eq 'set' || $cmd eq 'delete' ||$cmd eq 'help' ) {
|
|
pve_command([ $cmd, @ARGV], $opt_nooutput);
|
|
exit(0);
|
|
} else {
|
|
print_usage ("unknown command '$cmd'");
|
|
exit (-1);
|
|
}
|
|
}
|
|
|
|
if (scalar (@ARGV) != 0) {
|
|
print_usage ();
|
|
exit (-1);
|
|
}
|
|
|
|
print "entering PVE shell - type 'help' for help\n";
|
|
|
|
my $term = new Term::ReadLine ('pvesh');
|
|
my $attribs = $term->Attribs;
|
|
|
|
sub complete_path {
|
|
my($text) = @_;
|
|
|
|
my ($dir, undef, $rest) = $text =~ m|^(.*/)?(([^/]*))?$|;
|
|
my $path = abs_path($cdir, $dir);
|
|
|
|
my @res = ();
|
|
|
|
my $di = dir_info($path);
|
|
if (my $children = $di->{children}) {
|
|
foreach my $c (@$children) {
|
|
if ($c =~ /^\Q$rest/) {
|
|
my $new = $dir ? "$dir$c" : $c;
|
|
push @res, $new;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (scalar(@res) == 0) {
|
|
return undef;
|
|
} elsif (scalar(@res) == 1) {
|
|
return ($res[0], $res[0], "$res[0]/");
|
|
}
|
|
|
|
# lcd : lowest common denominator
|
|
my $lcd = '';
|
|
my $tmp = $res[0];
|
|
for (my $i = 1; $i <= length($tmp); $i++) {
|
|
my $found = 1;
|
|
foreach my $p (@res) {
|
|
if (substr($tmp, 0, $i) ne substr($p, 0, $i)) {
|
|
$found = 0;
|
|
last;
|
|
}
|
|
}
|
|
if ($found) {
|
|
$lcd = substr($tmp, 0, $i);
|
|
} else {
|
|
last;
|
|
}
|
|
}
|
|
|
|
return ($lcd, @res);
|
|
};
|
|
|
|
# just to avoid an endless loop (called by attempted_completion_function)
|
|
$attribs->{completion_entry_function} = sub {
|
|
my($text, $state) = @_;
|
|
return undef;
|
|
};
|
|
|
|
$attribs->{attempted_completion_function} = sub {
|
|
my ($text, $line, $start) = @_;
|
|
|
|
my $prefix = substr($line, 0, $start);
|
|
if ($prefix =~ /^\s*$/) { # first word (command completeion)
|
|
$attribs->{completion_word} = [qw(help ls cd get set create delete quit)];
|
|
return $term->completion_matches($text, $attribs->{list_completion_function});
|
|
}
|
|
|
|
if ($prefix =~ /^\s*\S+\s+$/) { # second word (path completion)
|
|
return complete_path($text);
|
|
}
|
|
|
|
return ();
|
|
};
|
|
|
|
sub abs_path {
|
|
my ($current, $path) = @_;
|
|
|
|
my $ret = $current;
|
|
|
|
return $current if !defined($path);
|
|
|
|
$ret = '' if $path =~ m|^\/|;
|
|
|
|
foreach my $d (split (/\/+/ , $path)) {
|
|
if ($d eq '.') {
|
|
next;
|
|
} elsif ($d eq '..') {
|
|
$ret = dirname($ret);
|
|
$ret = '' if $ret eq '.';
|
|
} else {
|
|
$ret = "$ret/$d";
|
|
}
|
|
}
|
|
|
|
$ret =~ s|\/+|\/|g;
|
|
$ret =~ s|^\/||;
|
|
$ret =~ s|\/$||;
|
|
|
|
return $ret;
|
|
}
|
|
|
|
my $read_password = sub {
|
|
my $attribs = $term->Attribs;
|
|
my $old = $attribs->{redisplay_function};
|
|
$attribs->{redisplay_function} = $attribs->{shadow_redisplay};
|
|
my $input = $term->readline('password: ');
|
|
my $conf = $term->readline('Retype new password: ');
|
|
$attribs->{redisplay_function} = $old;
|
|
die "Passwords do not match.\n" if ($input ne $conf);
|
|
return $input;
|
|
};
|
|
|
|
sub reverse_map_cmd {
|
|
my $method = shift;
|
|
|
|
my $mmap = {
|
|
GET => 'get',
|
|
PUT => 'set',
|
|
POST => 'create',
|
|
DELETE => 'delete',
|
|
};
|
|
|
|
my $cmd = $mmap->{$method};
|
|
|
|
die "got strange value for method ('$method') - internal error" if !$cmd;
|
|
|
|
return $cmd;
|
|
}
|
|
|
|
sub map_cmd {
|
|
my $cmd = shift;
|
|
|
|
my $mmap = {
|
|
create => 'POST',
|
|
set => 'PUT',
|
|
get => 'GET',
|
|
ls => 'GET',
|
|
delete => 'DELETE',
|
|
};
|
|
|
|
my $method = $mmap->{$cmd};
|
|
|
|
die "unable to map method" if !$method;
|
|
|
|
return $method;
|
|
}
|
|
|
|
sub check_proxyto {
|
|
my ($info, $uri_param) = @_;
|
|
|
|
if ($info->{proxyto}) {
|
|
my $pn = $info->{proxyto};
|
|
my $node = $uri_param->{$pn};
|
|
die "proxy parameter '$pn' does not exists" if !$node;
|
|
|
|
if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) {
|
|
die "proxy loop detected - aborting\n" if $disable_proxy;
|
|
my $remip = PVE::Cluster::remote_node_ip($node);
|
|
return ($node, $remip);
|
|
}
|
|
}
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub proxy_handler {
|
|
my ($node, $remip, $dir, $cmd, $args) = @_;
|
|
|
|
my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
|
|
'pvesh', '--noproxy', $cmd, $dir, @$args];
|
|
|
|
system(@$remcmd) == 0 || die "proxy handler failed\n";
|
|
}
|
|
|
|
sub call_method {
|
|
my ($dir, $cmd, $args, $nooutput) = @_;
|
|
|
|
my $method = map_cmd($cmd);
|
|
|
|
my $uri_param = {};
|
|
my ($handler, $info) = PVE::API2->find_handler($method, $dir, $uri_param);
|
|
if (!$handler || !$info) {
|
|
die "no '$cmd' handler for '$dir'\n";
|
|
}
|
|
|
|
my ($node, $remip) = check_proxyto($info, $uri_param);
|
|
return proxy_handler($node, $remip, $dir, $cmd, $args) if $node;
|
|
|
|
my $data = $handler->cli_handler("$cmd $dir", $info->{name}, $args, [], $uri_param, $read_password);
|
|
|
|
return if $nooutput;
|
|
|
|
warn "200 OK\n"; # always print OK status if successful
|
|
|
|
if ($info && $info->{returns} && $info->{returns}->{type}) {
|
|
my $rtype = $info->{returns}->{type};
|
|
|
|
return if $rtype eq 'null';
|
|
|
|
if ($rtype eq 'string') {
|
|
print $data if $data;
|
|
return;
|
|
}
|
|
}
|
|
|
|
print to_json($data, {utf8 => 1, allow_nonref => 1, canonical => 1, pretty => 1 });
|
|
|
|
return;
|
|
}
|
|
|
|
sub find_resource_methods {
|
|
my ($path, $ihash) = @_;
|
|
|
|
for my $method (qw(GET POST PUT DELETE)) {
|
|
my $uri_param = {};
|
|
my $path_match;
|
|
my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param, \$path_match);
|
|
if ($handler && $info && !$ihash->{$info}) {
|
|
$ihash->{$info} = {
|
|
path => $path_match,
|
|
handler => $handler,
|
|
info => $info,
|
|
uri_param => $uri_param,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
sub print_help {
|
|
my ($path, $opts) = @_;
|
|
|
|
my $ihash = {};
|
|
|
|
find_resource_methods($path, $ihash);
|
|
|
|
if (!scalar(keys(%$ihash))) {
|
|
die "no such resource\n";
|
|
}
|
|
|
|
my $di = dir_info($path);
|
|
if (my $children = $di->{children}) {
|
|
foreach my $c (@$children) {
|
|
my $cp = abs_path($path, $c);
|
|
find_resource_methods($cp, $ihash);
|
|
}
|
|
}
|
|
|
|
foreach my $mi (sort { $a->{path} cmp $b->{path} } values %$ihash) {
|
|
my $method = $mi->{info}->{method};
|
|
|
|
# we skip index methods for now.
|
|
next if ($method eq 'GET') && PVE::JSONSchema::method_get_child_link($mi->{info});
|
|
|
|
my $path = $mi->{path};
|
|
$path =~ s|/+$||; # remove trailing slash
|
|
|
|
my $cmd = reverse_map_cmd($method);
|
|
|
|
print $mi->{handler}->usage_str($mi->{info}->{name}, "$cmd $path", [], $mi->{uri_param},
|
|
$opts->{verbose} ? 'full' : 'short', 1);
|
|
print "\n\n" if $opts->{verbose};
|
|
}
|
|
|
|
};
|
|
|
|
sub resource_cap {
|
|
my ($path) = @_;
|
|
|
|
my $res = '';
|
|
|
|
my ($handler, $info) = PVE::API2->find_handler('GET', $path);
|
|
if (!($handler && $info)) {
|
|
$res .= '--';
|
|
} else {
|
|
if (PVE::JSONSchema::method_get_child_link($info)) {
|
|
$res .= 'Dr';
|
|
} else {
|
|
$res .= '-r';
|
|
}
|
|
}
|
|
|
|
($handler, $info) = PVE::API2->find_handler('PUT', $path);
|
|
if (!($handler && $info)) {
|
|
$res .= '-';
|
|
} else {
|
|
$res .= 'w';
|
|
}
|
|
|
|
($handler, $info) = PVE::API2->find_handler('POST', $path);
|
|
if (!($handler && $info)) {
|
|
$res .= '-';
|
|
} else {
|
|
$res .= 'c';
|
|
}
|
|
|
|
($handler, $info) = PVE::API2->find_handler('DELETE', $path);
|
|
if (!($handler && $info)) {
|
|
$res .= '-';
|
|
} else {
|
|
$res .= 'd';
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
sub extract_children {
|
|
my ($lnk, $data) = @_;
|
|
|
|
my $res = [];
|
|
|
|
return $res if !($lnk && $data);
|
|
|
|
my $href = $lnk->{href};
|
|
if ($href =~ m/^\{(\S+)\}$/) {
|
|
my $prop = $1;
|
|
|
|
foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
|
|
next if !ref($elem);
|
|
my $value = $elem->{$prop};
|
|
push @$res, $value;
|
|
}
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
sub dir_info {
|
|
my ($path) = @_;
|
|
|
|
my $res = { path => $path };
|
|
my $uri_param = {};
|
|
my ($handler, $info, $pm) = PVE::API2->find_handler('GET', $path, $uri_param);
|
|
if ($handler && $info) {
|
|
eval {
|
|
my $data = $handler->handle($info, $uri_param);
|
|
my $lnk = PVE::JSONSchema::method_get_child_link($info);
|
|
$res->{children} = extract_children($lnk, $data);
|
|
}; # ignore errors ?
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
sub list_dir {
|
|
my ($dir, $args) = @_;
|
|
|
|
my $uri_param = {};
|
|
my ($handler, $info) = PVE::API2->find_handler('GET', $dir, $uri_param);
|
|
if (!$handler || !$info) {
|
|
die "no such resource\n";
|
|
}
|
|
|
|
if (!PVE::JSONSchema::method_get_child_link($info)) {
|
|
die "resource does not define child links\n";
|
|
}
|
|
|
|
my ($node, $remip) = check_proxyto($info, $uri_param);
|
|
return proxy_handler($node, $remip, $dir, 'ls', $args) if $node;
|
|
|
|
|
|
my $data = $handler->cli_handler("ls $dir", $info->{name}, $args, [], $uri_param, $read_password);
|
|
my $lnk = PVE::JSONSchema::method_get_child_link($info);
|
|
my $children = extract_children($lnk, $data);
|
|
|
|
foreach my $c (@$children) {
|
|
my $cap = resource_cap(abs_path($dir, $c));
|
|
print "$cap $c\n";
|
|
}
|
|
}
|
|
|
|
|
|
sub pve_command {
|
|
my ($args, $nooutput) = @_;
|
|
|
|
PVE::Cluster::cfs_update();
|
|
|
|
$rpcenv->init_request();
|
|
|
|
my $cmd = shift @$args;
|
|
|
|
if ($cmd eq 'cd') {
|
|
|
|
my $path = shift @$args;
|
|
|
|
die "usage: cd [dir]\n" if scalar(@$args);
|
|
|
|
if (!defined($path)) {
|
|
$cdir = '';
|
|
return;
|
|
} else {
|
|
my $new_dir = abs_path($cdir, $path);
|
|
my ($handler, $info) = PVE::API2->find_handler('GET', $new_dir);
|
|
die "no such resource\n" if !$handler;
|
|
$cdir = $new_dir;
|
|
}
|
|
|
|
} elsif ($cmd eq 'help') {
|
|
|
|
my $help_usage_error = sub {
|
|
die "usage: help [path] [--verbose]\n";
|
|
};
|
|
|
|
my $opts = {};
|
|
|
|
&$help_usage_error() if !Getopt::Long::GetOptionsFromArray($args, $opts, 'verbose');
|
|
|
|
my $path;
|
|
if (scalar(@$args) && $args->[0] !~ m/^\-/) {
|
|
$path = shift @$args;
|
|
}
|
|
|
|
&$help_usage_error() if scalar(@$args);
|
|
|
|
print "help [path] [--verbose]\n";
|
|
print "cd [path]\n";
|
|
print "ls [path]\n\n";
|
|
|
|
print_help(abs_path($cdir, $path), $opts);
|
|
|
|
} elsif ($cmd eq 'ls') {
|
|
my $path;
|
|
if (scalar(@$args) && $args->[0] !~ m/^\-/) {
|
|
$path = shift @$args;
|
|
}
|
|
|
|
list_dir(abs_path($cdir, $path), $args);
|
|
|
|
} elsif ($cmd eq 'get') {
|
|
|
|
my $path;
|
|
if (scalar(@$args) && $args->[0] !~ m/^\-/) {
|
|
$path = shift @$args;
|
|
}
|
|
|
|
call_method(abs_path($cdir, $path), $cmd, $args);
|
|
|
|
} elsif ($cmd eq 'create') {
|
|
|
|
my $path;
|
|
if (scalar(@$args) && $args->[0] !~ m/^\-/) {
|
|
$path = shift @$args;
|
|
}
|
|
|
|
call_method(abs_path($cdir, $path), $cmd, $args, $nooutput);
|
|
|
|
} elsif ($cmd eq 'delete') {
|
|
|
|
my $path = shift @$args;
|
|
|
|
die "usage: delete [path]\n" if scalar(@$args);
|
|
|
|
call_method(abs_path($cdir, $path), $cmd, $args);
|
|
|
|
} elsif ($cmd eq 'set') {
|
|
|
|
my $path;
|
|
if (scalar(@$args) && $args->[0] !~ m/^\-/) {
|
|
$path = shift @$args;
|
|
}
|
|
|
|
call_method(abs_path($cdir, $path), $cmd, $args);
|
|
|
|
} else {
|
|
die "unknown command '$cmd'\n";
|
|
}
|
|
|
|
}
|
|
|
|
my $input;
|
|
while (defined ($input = $term->readline("pve:/$cdir> "))) {
|
|
chomp $input;
|
|
|
|
next if $input =~ m/^\s*$/;
|
|
|
|
if ($input =~ m/^\s*q(uit)?\s*$/) {
|
|
exit (0);
|
|
}
|
|
|
|
$term->addhistory($input);
|
|
|
|
eval {
|
|
my $args = [ shellwords($input) ];
|
|
pve_command($args);
|
|
};
|
|
warn $@ if $@;
|
|
}
|