Dominik Csapak 8d365d994f fix #1212: allow options for delete in pvesh
we did not allow to use parameters in the delete handler in pvesh,
but we make use of them (e.g. force on snapshot deleting)

as the get/set and delete handler do the same, refactor the if/else
paths of them

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2016-11-25 15:24:22 +01:00

610 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 =~ m/^get|delete|set$/) {
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);
} 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)