Dietmar Maurer 864a5845ec pvesh usage: correctly handle uri paramaeters, cleanups
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
2018-07-11 11:27:13 +02:00

447 lines
10 KiB
Perl
Executable File

#!/usr/bin/perl
package pvesh;
use strict;
use warnings;
use HTTP::Status qw(:constants :is status_message);
use String::ShellQuote;
use PVE::JSONSchema qw(get_standard_option);
use PVE::SafeSyslog;
use PVE::Cluster;
use PVE::INotify;
use PVE::RPCEnvironment;
use PVE::RESTHandler;
use PVE::CLIFormatter;
use PVE::CLIHandler;
use PVE::API2Tools;
use PVE::API2;
use JSON;
use base qw(PVE::CLIHandler);
my $disable_proxy = 0;
my $opt_nooutput = 0;
# compatibility code
my $optmatch;
do {
$optmatch = 0;
if ($ARGV[0]) {
if ($ARGV[0] eq '--noproxy') {
shift @ARGV;
$disable_proxy = 1;
$optmatch = 1;
} elsif ($ARGV[0] eq '--nooutput') {
# we use this when starting task in CLI (suppress printing upid)
# for example 'pvesh --nooutput create /nodes/localhost/stopall'
shift @ARGV;
$opt_nooutput = 1;
$optmatch = 1;
}
}
} while ($optmatch);
sub setup_environment {
PVE::RPCEnvironment->setup_default_cli_env();
}
sub complete_api_path {
my($text) = @_;
my ($dir, undef, $rest) = $text =~ m|^(.*/)?(([^/]*))?$|;
my $path = $dir // ''; # copy
$path =~ s|/+|/|g;
$path =~ s|^\/||;
$path =~ s|\/$||;
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) == 1) {
return [$res->[0], "$res->[0]/"];
}
return $res;
}
my $method_map = {
create => 'POST',
set => 'PUT',
get => 'GET',
delete => 'DELETE',
};
sub check_proxyto {
my ($info, $uri_param) = @_;
my $rpcenv = PVE::RPCEnvironment->get();
if ($info->{proxyto} || $info->{proxyto_callback}) {
my $node = PVE::API2Tools::resolve_proxyto(
$rpcenv, $info->{proxyto_callback}, $info->{proxyto}, $uri_param);
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, $path, $cmd, $param, $noout) = @_;
my $args = [];
foreach my $key (keys %$param) {
push @$args, "--$key", $param->{$key};
}
push @$args, '--quiet' if $noout;
my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
'pvesh', '--noproxy', $cmd, $path,
'--format', 'json'];
if (scalar(@$args)) {
my $cmdargs = [String::ShellQuote::shell_quote(@$args)];
push @$remcmd, @$cmdargs;
}
my $json = '';
PVE::Tools::run_command($remcmd, errmsg => "proxy handler failed",
outfunc => sub { $json .= shift });
return decode_json($json);
}
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;
}
# dynamically update schema definition
# like: pvesh <get|set|create|delete|help> <path>
sub extract_path_info {
my ($uri_param) = @_;
my $info;
my $test_path_properties = sub {
my ($method, $path) = @_;
(undef, $info) = PVE::API2->find_handler($method, $path, $uri_param);
};
if (defined(my $cmd = $ARGV[0])) {
if (my $method = $method_map->{$cmd}) {
if (my $path = $ARGV[1]) {
$test_path_properties->($method, $path);
}
} elsif ($cmd eq 'bashcomplete') {
my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT});
my $args = PVE::Tools::split_args($cmdline);
if (defined(my $cmd = $args->[1])) {
if (my $method = $method_map->{$cmd}) {
if (my $path = $args->[2]) {
$test_path_properties->($method, $path);
}
}
}
}
}
return $info;
}
my $path_properties = {};
my $path_returns = { type => 'null' };
my $api_path_property = {
description => "API path.",
type => 'string',
completion => sub {
my ($cmd, $pname, $cur, $args) = @_;
return complete_api_path($cur);
},
};
my $uri_param = {};
if (my $info = extract_path_info($uri_param)) {
foreach my $key (keys %{$info->{parameters}->{properties}}) {
next if defined($uri_param->{$key});
$path_properties->{$key} = $info->{parameters}->{properties}->{$key};
}
$path_returns = $info->{returns};
}
$path_properties->{format} = get_standard_option('pve-output-format');
$path_properties->{api_path} = $api_path_property;
$path_properties->{noproxy} = {
description => "Disable automatic proxying.",
type => 'boolean',
optional => 1,
};
$path_properties->{quiet} = {
description => "Suppress printing results.",
type => 'boolean',
optional => 1,
};
my $format_result = sub {
my ($data, $result_schema, $options) = @_;
return if $opt_nooutput || ($options->{format}//'') eq 'none';
PVE::CLIFormatter::print_api_result($data, $path_returns, undef, $options);
};
sub call_api_method {
my ($cmd, $param) = @_;
my $method = $method_map->{$cmd} || die "unable to map command '$cmd'";
my $path = PVE::Tools::extract_param($param, 'api_path');
die "missing API path\n" if !defined($path);
my $uri_param = {};
my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
if (!$handler || !$info) {
die "no '$cmd' handler for '$path'\n";
}
my ($node, $remip) = check_proxyto($info, $uri_param);
return proxy_handler($node, $remip, $path, $cmd, $param, $opt_nooutput) if $node;
foreach my $p (keys %$uri_param) {
$param->{$p} = $uri_param->{$p};
}
my $data = $handler->handle($info, $param);
return $data;
}
__PACKAGE__->register_method ({
name => 'get',
path => 'get',
method => 'GET',
description => "Call API GET on <api_path>.",
parameters => {
additionalProperties => 0,
properties => $path_properties,
},
returns => $path_returns,
code => sub {
my ($param) = @_;
return call_api_method('get', $param);
}});
__PACKAGE__->register_method ({
name => 'set',
path => 'set',
method => 'PUT',
description => "Call API PUT on <api_path>.",
parameters => {
additionalProperties => 0,
properties => $path_properties,
},
returns => $path_returns,
code => sub {
my ($param) = @_;
return call_api_method('set', $param);
}});
__PACKAGE__->register_method ({
name => 'create',
path => 'create',
method => 'POST',
description => "Call API POST on <api_path>.",
parameters => {
additionalProperties => 0,
properties => $path_properties,
},
returns => $path_returns,
code => sub {
my ($param) = @_;
return call_api_method('create', $param);
}});
__PACKAGE__->register_method ({
name => 'delete',
path => 'delete',
method => 'DELETE',
description => "Call API DELETE on <api_path>.",
parameters => {
additionalProperties => 0,
properties => $path_properties,
},
returns => $path_returns,
code => sub {
my ($param) = @_;
return call_api_method('delete', $param);
}});
__PACKAGE__->register_method ({
name => 'usage',
path => 'usage',
method => 'GET',
description => "print API usage information for <api_path>.",
parameters => {
additionalProperties => 0,
properties => {
api_path => $api_path_property,
verbose => {
description => "Verbose output format.",
type => 'boolean',
optional => 1,
},
returns => {
description => "Including schema for returned data.",
type => 'boolean',
optional => 1,
},
command => {
description => "API command.",
type => 'string',
enum => [ keys %$method_map ],
optional => 1,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my $path = $param->{api_path};
my $found = 0;
foreach my $cmd (qw(get set create delete)) {
next if $param->{command} && $cmd ne $param->{command};
my $method = $method_map->{$cmd};
my $uri_param = {};
my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
next if !$handler;
$found = 1;
if ($param->{verbose}) {
print $handler->usage_str(
$info->{name}, "pvesh $cmd $path", undef, $uri_param, 'full');
} else {
print "USAGE: " . $handler->usage_str(
$info->{name}, "pvesh $cmd $path", undef, $uri_param, 'short');
}
if ($param-> {returns}) {
my $schema = to_json($info->{returns}, {utf8 => 1, canonical => 1, pretty => 1 });
print "RETURNS: $schema\n";
}
}
if (!$found) {
if ($param->{command}) {
die "no '$param->{command}' handler for '$path'\n";
} else {
die "no such resource '$path'\n"
}
}
return undef;
}});
our $cmddef = {
usage => [ __PACKAGE__, 'usage', ['api_path']],
get => [ __PACKAGE__, 'get', ['api_path'], {}, $format_result ],
set => [ __PACKAGE__, 'set', ['api_path'], {}, $format_result ],
create => [ __PACKAGE__, 'create', ['api_path'], {}, $format_result ],
delete => [ __PACKAGE__, 'delete', ['api_path'], {}, $format_result ],
};
my $cmd = $ARGV[0];
__PACKAGE__->run_cli_handler();
__END__
=head1 NAME
pvesh - shell interface to the Promox VE API
=head1 SYNOPSIS
pvesh [get|set|create|delete|usage] [REST API path] [--verbose]
=head1 DESCRIPTION
pvesh provides a command line interface to the Proxmox VE REST API.
=head1 EXAMPLES
get the list of nodes in my cluster
pvesh get /nodes
get a list of available options for the datacenter
pvesh usage 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)