pvesh cleanup: use a handler class - PVE/CLI/pvesh.pm
This commit is contained in:
parent
cd16ae07f5
commit
bd0e50532b
505
PVE/CLI/pvesh.pm
Executable file
505
PVE/CLI/pvesh.pm
Executable file
@ -0,0 +1,505 @@
|
||||
package PVE::CLI::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) = @_;
|
||||
|
||||
my $args = [];
|
||||
foreach my $key (keys %$param) {
|
||||
next if $key eq 'quiet' || $key eq 'output-format'; # just to be sure
|
||||
push @$args, "--$key", $param->{$key};
|
||||
}
|
||||
|
||||
my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
|
||||
'pvesh', '--noproxy', $cmd, $path,
|
||||
'--output-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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
# 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 $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_properties->{api_path} = $api_path_property;
|
||||
$path_properties->{noproxy} = {
|
||||
description => "Disable automatic proxying.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
};
|
||||
|
||||
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 $stdopts = PVE::RESTHandler::extract_standard_output_properties($param);
|
||||
|
||||
$opt_nooutput = 1 if $stdopts->{quiet};
|
||||
|
||||
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 $data;
|
||||
my ($node, $remip) = check_proxyto($info, $uri_param);
|
||||
if ($node) {
|
||||
$data = proxy_handler($node, $remip, $path, $cmd, $param);
|
||||
} else {
|
||||
foreach my $p (keys %$uri_param) {
|
||||
$param->{$p} = $uri_param->{$p};
|
||||
}
|
||||
|
||||
$data = $handler->handle($info, $param);
|
||||
}
|
||||
|
||||
return if $opt_nooutput || $stdopts->{quiet};
|
||||
|
||||
PVE::CLIFormatter::print_api_result($data, $info->{returns}, undef, $stdopts);
|
||||
}
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'ls',
|
||||
path => 'ls',
|
||||
method => 'GET',
|
||||
description => "List child objects on <api_path>.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => PVE::RESTHandler::add_standard_output_properties($path_properties),
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $path = PVE::Tools::extract_param($param, 'api_path');
|
||||
|
||||
my $stdopts = PVE::RESTHandler::extract_standard_output_properties($param);
|
||||
|
||||
my $uri_param = {};
|
||||
my ($handler, $info) = PVE::API2->find_handler('GET', $path, $uri_param);
|
||||
if (!$handler || !$info) {
|
||||
die "no such resource '$path'\n";
|
||||
}
|
||||
|
||||
my $link = PVE::JSONSchema::method_get_child_link($info);
|
||||
die "resource '$path' does not define child links\n" if !$link;
|
||||
|
||||
my $res;
|
||||
|
||||
my ($node, $remip) = check_proxyto($info, $uri_param);
|
||||
if ($node) {
|
||||
$res = proxy_handler($node, $remip, $path, 'ls', $param);
|
||||
} else {
|
||||
foreach my $p (keys %$uri_param) {
|
||||
$param->{$p} = $uri_param->{$p};
|
||||
}
|
||||
|
||||
my $data = $handler->handle($info, $param);
|
||||
|
||||
my $children = extract_children($link, $data);
|
||||
|
||||
$res = [];
|
||||
foreach my $c (@$children) {
|
||||
my $item = { name => $c, capabilities => resource_cap("$path/$c")};
|
||||
push @$res, $item;
|
||||
}
|
||||
}
|
||||
|
||||
my $schema = { type => 'array', items => { type => 'object' }};
|
||||
$stdopts->{sort_key} = 'name';
|
||||
$stdopts->{noborder} //= 1;
|
||||
$stdopts->{noheader} //= 1;
|
||||
PVE::CLIFormatter::print_api_result($res, $schema, ['capabilities', 'name'], $stdopts);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'get',
|
||||
path => 'get',
|
||||
method => 'GET',
|
||||
description => "Call API GET on <api_path>.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => PVE::RESTHandler::add_standard_output_properties($path_properties),
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
call_api_method('get', $param);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'set',
|
||||
path => 'set',
|
||||
method => 'PUT',
|
||||
description => "Call API PUT on <api_path>.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => $path_properties,
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
call_api_method('set', $param);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'create',
|
||||
path => 'create',
|
||||
method => 'POST',
|
||||
description => "Call API POST on <api_path>.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => $path_properties,
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
call_api_method('create', $param);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'delete',
|
||||
path => 'delete',
|
||||
method => 'DELETE',
|
||||
description => "Call API DELETE on <api_path>.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => $path_properties,
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
call_api_method('delete', $param);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__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']],
|
||||
ls => [ __PACKAGE__, 'ls', ['api_path']],
|
||||
set => [ __PACKAGE__, 'set', ['api_path']],
|
||||
create => [ __PACKAGE__, 'create', ['api_path']],
|
||||
delete => [ __PACKAGE__, 'delete', ['api_path']],
|
||||
};
|
||||
|
||||
1;
|
@ -5,12 +5,11 @@ export NOVIEW=1
|
||||
include /usr/share/pve-doc-generator/pve-doc-generator.mk
|
||||
|
||||
SERVICES = pvestatd pveproxy pvedaemon spiceproxy
|
||||
CLITOOLS = vzdump pvesubscription pveceph pveam pvesr pvenode
|
||||
CLITOOLS = vzdump pvesubscription pveceph pveam pvesr pvenode pvesh
|
||||
|
||||
SCRIPTS = \
|
||||
${SERVICES} \
|
||||
${CLITOOLS} \
|
||||
pvesh \
|
||||
pvebanner \
|
||||
pveversion \
|
||||
pvemailforward.pl \
|
||||
@ -26,7 +25,6 @@ CLI_MANS = \
|
||||
pveversion.1 \
|
||||
pveupgrade.1 \
|
||||
pveperf.1 \
|
||||
pvesh.1 \
|
||||
pvereport.1 \
|
||||
|
||||
|
||||
@ -43,7 +41,6 @@ all: ${SERVICE_MANS} ${CLI_MANS} pvemailforward
|
||||
|
||||
pveversion.1.pod: pveversion
|
||||
pveupgrade.1.pod: pveupgrade
|
||||
pvesh.1.pod: pvesh
|
||||
pvereport.1.pod: pvereport
|
||||
|
||||
%.service-bash-completion:
|
||||
|
538
bin/pvesh
Executable file → Normal file
538
bin/pvesh
Executable file → Normal file
@ -1,542 +1,8 @@
|
||||
#!/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);
|
||||
use PVE::CLI::pvesh;
|
||||
|
||||
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) = @_;
|
||||
|
||||
my $args = [];
|
||||
foreach my $key (keys %$param) {
|
||||
next if $key eq 'quiet' || $key eq 'output-format'; # just to be sure
|
||||
push @$args, "--$key", $param->{$key};
|
||||
}
|
||||
|
||||
my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip",
|
||||
'pvesh', '--noproxy', $cmd, $path,
|
||||
'--output-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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
# 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 $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_properties->{api_path} = $api_path_property;
|
||||
$path_properties->{noproxy} = {
|
||||
description => "Disable automatic proxying.",
|
||||
type => 'boolean',
|
||||
optional => 1,
|
||||
};
|
||||
|
||||
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 $stdopts = PVE::RESTHandler::extract_standard_output_properties($param);
|
||||
|
||||
$opt_nooutput = 1 if $stdopts->{quiet};
|
||||
|
||||
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 $data;
|
||||
my ($node, $remip) = check_proxyto($info, $uri_param);
|
||||
if ($node) {
|
||||
$data = proxy_handler($node, $remip, $path, $cmd, $param);
|
||||
} else {
|
||||
foreach my $p (keys %$uri_param) {
|
||||
$param->{$p} = $uri_param->{$p};
|
||||
}
|
||||
|
||||
$data = $handler->handle($info, $param);
|
||||
}
|
||||
|
||||
return if $opt_nooutput || $stdopts->{quiet};
|
||||
|
||||
PVE::CLIFormatter::print_api_result($data, $info->{returns}, undef, $stdopts);
|
||||
}
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'ls',
|
||||
path => 'ls',
|
||||
method => 'GET',
|
||||
description => "List child objects on <api_path>.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => PVE::RESTHandler::add_standard_output_properties($path_properties),
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $path = PVE::Tools::extract_param($param, 'api_path');
|
||||
|
||||
my $stdopts = PVE::RESTHandler::extract_standard_output_properties($param);
|
||||
|
||||
my $uri_param = {};
|
||||
my ($handler, $info) = PVE::API2->find_handler('GET', $path, $uri_param);
|
||||
if (!$handler || !$info) {
|
||||
die "no such resource '$path'\n";
|
||||
}
|
||||
|
||||
my $link = PVE::JSONSchema::method_get_child_link($info);
|
||||
die "resource '$path' does not define child links\n" if !$link;
|
||||
|
||||
my $res;
|
||||
|
||||
my ($node, $remip) = check_proxyto($info, $uri_param);
|
||||
if ($node) {
|
||||
$res = proxy_handler($node, $remip, $path, 'ls', $param);
|
||||
} else {
|
||||
foreach my $p (keys %$uri_param) {
|
||||
$param->{$p} = $uri_param->{$p};
|
||||
}
|
||||
|
||||
my $data = $handler->handle($info, $param);
|
||||
|
||||
my $children = extract_children($link, $data);
|
||||
|
||||
$res = [];
|
||||
foreach my $c (@$children) {
|
||||
my $item = { name => $c, capabilities => resource_cap("$path/$c")};
|
||||
push @$res, $item;
|
||||
}
|
||||
}
|
||||
|
||||
my $schema = { type => 'array', items => { type => 'object' }};
|
||||
$stdopts->{sort_key} = 'name';
|
||||
$stdopts->{noborder} //= 1;
|
||||
$stdopts->{noheader} //= 1;
|
||||
PVE::CLIFormatter::print_api_result($res, $schema, ['capabilities', 'name'], $stdopts);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'get',
|
||||
path => 'get',
|
||||
method => 'GET',
|
||||
description => "Call API GET on <api_path>.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => PVE::RESTHandler::add_standard_output_properties($path_properties),
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
call_api_method('get', $param);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'set',
|
||||
path => 'set',
|
||||
method => 'PUT',
|
||||
description => "Call API PUT on <api_path>.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => $path_properties,
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
call_api_method('set', $param);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'create',
|
||||
path => 'create',
|
||||
method => 'POST',
|
||||
description => "Call API POST on <api_path>.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => $path_properties,
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
call_api_method('create', $param);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__PACKAGE__->register_method ({
|
||||
name => 'delete',
|
||||
path => 'delete',
|
||||
method => 'DELETE',
|
||||
description => "Call API DELETE on <api_path>.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => $path_properties,
|
||||
},
|
||||
returns => { type => 'null' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
call_api_method('delete', $param);
|
||||
|
||||
return undef;
|
||||
}});
|
||||
|
||||
__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']],
|
||||
ls => [ __PACKAGE__, 'ls', ['api_path']],
|
||||
set => [ __PACKAGE__, 'set', ['api_path']],
|
||||
create => [ __PACKAGE__, 'create', ['api_path']],
|
||||
delete => [ __PACKAGE__, 'delete', ['api_path']],
|
||||
};
|
||||
|
||||
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)
|
||||
PVE::CLI::pvesh->run_cli_handler();
|
||||
|
Loading…
Reference in New Issue
Block a user