we assumed that Term::ReadLine does not put input into the history automatically, but it does this so if this feature is enabled, we do not have to add the inputs manually, and we have to delete the password from the history Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
627 lines
13 KiB
Perl
Executable File
627 lines
13 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::SafeSyslog;
|
|
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 $logid = $ENV{PVE_LOG_ID} || 'pvesh';
|
|
initlog($logid);
|
|
|
|
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;
|
|
|
|
# remove password from history
|
|
if ($term->Features->{autohistory}) {
|
|
my $historyPosition = $term->where_history();
|
|
$term->remove_history($historyPosition);
|
|
$term->remove_history($historyPosition - 1);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
# add input to history if it gets not
|
|
# automatically added
|
|
if (!$term->Features->{autohistory}) {
|
|
$term->addhistory($input);
|
|
}
|
|
|
|
eval {
|
|
my $args = [ shellwords($input) ];
|
|
pve_command($args);
|
|
};
|
|
warn $@ if $@;
|
|
}
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
pvesh - shell interface to the Promox VE API
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
pvesh [get|set|create|delete|help] [REST API path] [--verbose]
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
pvesh provides a shell-like interface to the Proxmox VE REST API, in two different modes:
|
|
|
|
=over
|
|
|
|
=item interactive
|
|
|
|
when called without parameters, pvesh starts an interactive client, where you can navigate
|
|
in the different parts of the API
|
|
|
|
=item command line
|
|
|
|
when started with parameters pvesh will send a command to the corresponding REST url, and will
|
|
return the JSON formatted output
|
|
|
|
=back
|
|
|
|
=head1 EXAMPLES
|
|
|
|
get the list of nodes in my cluster
|
|
|
|
pvesh get /nodes
|
|
|
|
get a list of available options for the datacenter
|
|
|
|
pvesh help cluster/options -v
|
|
|
|
set the HTMl5 NoVNC console as the default console for the datacenter
|
|
|
|
pvesh set cluster/options -console html5
|
|
|
|
=head1 SEE ALSO
|
|
|
|
qm(1), pct(1)
|