From: Dietmar Maurer Date: Wed, 4 Jul 2018 10:44:59 +0000 (+0200) Subject: pvesh: complete rewrite using PVE::CLIHandler X-Git-Url: https://git.proxmox.com/?p=pve-manager.git;a=commitdiff_plain;h=46028e7432b01f32c5ce787c1588dc8b0e1ca330 pvesh: complete rewrite using PVE::CLIHandler Signed-off-by: Dietmar Maurer --- diff --git a/bin/pvesh b/bin/pvesh index ff7b8482..d3ab9954 100755 --- a/bin/pvesh +++ b/bin/pvesh @@ -2,12 +2,10 @@ use strict; use warnings; -use File::Basename; -use Getopt::Long; +use Data::Dumper; use HTTP::Status qw(:constants :is status_message); -use Text::ParseWords; use String::ShellQuote; -use PVE::JSONSchema; +use PVE::JSONSchema qw(get_standard_option); use PVE::SafeSyslog; use PVE::Cluster; use PVE::INotify; @@ -19,182 +17,78 @@ use PVE::API2Tools; use PVE::API2; use JSON; -PVE::INotify::inotify_init(); +use base qw(PVE::CLIHandler); -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 $output_format = 'text'; my $disable_proxy = 0; my $opt_nooutput = 0; -my $cmd = shift; - +# compatibility code my $optmatch; do { $optmatch = 0; - if ($cmd) { - if ($cmd eq '--noproxy') { - $cmd = shift; + if ($ARGV[0]) { + if ($ARGV[0] eq '--noproxy') { + shift @ARGV; $disable_proxy = 1; $optmatch = 1; - } elsif ($cmd eq '--nooutput') { + } elsif ($ARGV[0] eq '--nooutput') { # we use this when starting task in CLI (suppress printing upid) # for example 'pvesh --nooutput create /nodes/localhost/stopall' - $cmd = shift; + shift @ARGV; $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); - } +sub setup_environment { + PVE::RPCEnvironment->setup_default_cli_env(); } -sub complete_path { +sub complete_api_path { my($text) = @_; my ($dir, undef, $rest) = $text =~ m|^(.*/)?(([^/]*))?$|; - my $path = abs_path($cdir, $dir); - my @res = (); + 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; + 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); -}; - -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"; - } + if (scalar(@$res) == 1) { + return [$res->[0], "$res->[0]/"]; } - $ret =~ s|\/+|\/|g; - $ret =~ s|^\/||; - $ret =~ s|\/$||; - - return $ret; + return $res; } -my $param_mapping = sub { - my ($name) = @_; - - return [PVE::CLIHandler::get_standard_mapping('pve-password')]; +my $method_map = { + create => 'POST', + set => 'PUT', + get => 'GET', + delete => 'DELETE', }; -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) = @_; + 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); @@ -210,144 +104,29 @@ sub check_proxyto { } sub proxy_handler { - my ($node, $remip, $dir, $cmd, $args) = @_; - - my $cmdargs = [String::ShellQuote::shell_quote(@$args)]; - my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip", - 'pvesh', '--noproxy', $cmd, $dir, @$cmdargs]; - - 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, $param_mapping); - - 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; -} + my ($node, $remip, $path, $cmd, $param, $noout) = @_; -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, - }; - } + my $args = []; + foreach my $key (keys %$param) { + push @$args, "--$key", $param->{$key}; } -} -sub print_help { - my ($path, $opts) = @_; + push @$args, '--quiet' if $noout; - my $ihash = {}; + my $remcmd = ['ssh', '-o', 'BatchMode=yes', "root\@$remip", + 'pvesh', '--noproxy', $cmd, $path, + '--format', 'json']; - find_resource_methods($path, $ihash); - - if (!scalar(keys(%$ihash))) { - die "no such resource\n"; + if (scalar(@$args)) { + my $cmdargs = [String::ShellQuote::shell_quote(@$args)]; + push @$remcmd, @$cmdargs; } - 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); - } - } + my $json = ''; + PVE::Tools::run_command($remcmd, errmsg => "proxy handler failed", + outfunc => sub { $json .= shift }); - 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', $param_mapping); - 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; + return decode_json($json); } sub extract_children { @@ -387,113 +166,252 @@ sub dir_info { 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"; - } +# dynamically update schema definition +# like: pvesh - my ($node, $remip) = check_proxyto($info, $uri_param); - return proxy_handler($node, $remip, $dir, 'ls', $args) if $node; +sub extract_path_info { + my ($uri_param) = @_; + my $info; - my $data = $handler->cli_handler("ls $dir", $info->{name}, $args, [], $uri_param, $param_mapping); - my $lnk = PVE::JSONSchema::method_get_child_link($info); - my $children = extract_children($lnk, $data); + my $test_path_properties = sub { + my ($method, $path) = @_; + (undef, $info) = PVE::API2->find_handler($method, $path, $uri_param); + }; - foreach my $c (@$children) { - my $cap = resource_cap(abs_path($dir, $c)); - print "$cap $c\n"; + 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; +} -sub pve_command { - my ($args, $nooutput) = @_; - PVE::Cluster::cfs_update(); +my $path_properties = {}; +my $path_returns = { type => 'null' }; - $rpcenv->init_request(); +my $api_path_property = { + description => "API path.", + type => 'string', + completion => sub { + my ($cmd, $pname, $cur, $args) = @_; + return complete_api_path($cur); + }, +}; - my $cmd = shift @$args; +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}; +} - if ($cmd eq 'cd') { +$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 $path = shift @$args; +my $format_result = sub { + my ($data) = @_; - die "usage: cd [dir]\n" if scalar(@$args); + return if $opt_nooutput || $output_format eq 'none'; - 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; - } + my $options = PVE::CLIFormatter::query_terminal_options({}); - } elsif ($cmd eq 'help') { + PVE::CLIFormatter::print_api_result($output_format, $data, $path_returns, undef, $options); +}; - my $help_usage_error = sub { - die "usage: help [path] [--verbose]\n"; - }; +sub call_api_method { + my ($cmd, $param) = @_; - my $opts = {}; + my $method = $method_map->{$cmd} || die "unable to map command '$cmd'"; - &$help_usage_error() if !Getopt::Long::GetOptionsFromArray($args, $opts, 'verbose'); + my $path = PVE::Tools::extract_param($param, 'api_path'); + die "missing API path\n" if !defined($path); - my $path; - if (scalar(@$args) && $args->[0] !~ m/^\-/) { - $path = shift @$args; - } + if (my $format = PVE::Tools::extract_param($param, 'format')) { + $output_format = $format; + } - &$help_usage_error() if scalar(@$args); + $opt_nooutput = 1 if PVE::Tools::extract_param($param, 'quiet'); - print "help [path] [--verbose]\n"; - print "cd [path]\n"; - print "ls [path]\n\n"; + my $uri_param = {}; + my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param); + if (!$handler || !$info) { + die "no '$cmd' handler for '$path'\n"; + } - print_help(abs_path($cdir, $path), $opts); + my ($node, $remip) = check_proxyto($info, $uri_param); + return proxy_handler($node, $remip, $path, $cmd, $param, $opt_nooutput) if $node; - } elsif ($cmd eq 'ls') { - my $path; - if (scalar(@$args) && $args->[0] !~ m/^\-/) { - $path = shift @$args; - } + foreach my $p (keys %$uri_param) { + $param->{$p} = $uri_param->{$p}; + } - list_dir(abs_path($cdir, $path), $args); + my $data = $handler->handle($info, $param); - } elsif ($cmd =~ m/^get|delete|set$/) { + return $data; +} - my $path; - if (scalar(@$args) && $args->[0] !~ m/^\-/) { - $path = shift @$args; +__PACKAGE__->register_method ({ + name => 'get', + path => 'get', + method => 'GET', + description => "Call API GET on .", + 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 .", + 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 .", + 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 .", + 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 .", + parameters => { + additionalProperties => 0, + properties => { + api_path => $api_path_property, + verbose => { + description => "Verbose output format.", + type => 'boolean', + optional => 1, + }, + command => { + description => "API command.", + type => 'string', + enum => [ keys %$method_map ], + optional => 1, + }, + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + $opt_nooutput = 1; # we print directly + + 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 ($handler, $info) = PVE::API2->find_handler($method, $path); + next if !$handler; + $found = 1; + + if ($param->{verbose}) { + print $handler->usage_str( + $info->{name}, "pvesh $cmd $path", undef, {}, 'full'); + } else { + print "USAGE: " . $handler->usage_str( + $info->{name}, "pvesh $cmd $path", undef, {}, 'short'); + } } - call_method(abs_path($cdir, $path), $cmd, $args); - - } elsif ($cmd eq 'create') { - - my $path; - if (scalar(@$args) && $args->[0] !~ m/^\-/) { - $path = shift @$args; + if (!$found) { + if ($param->{command}) { + die "no '$param->{command}' handler for '$path'\n"; + } else { + die "no such resource '$path'\n" + } } - call_method(abs_path($cdir, $path), $cmd, $args, $nooutput); + return undef; + }}); + +our $cmddef = { + usage => [ __PACKAGE__, 'usage', ['api_path'], {}, $format_result ], + 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]; - } else { - die "unknown command '$cmd'\n"; - } +__PACKAGE__->run_cli_handler(); -} __END__ @@ -503,7 +421,7 @@ pvesh - shell interface to the Promox VE API =head1 SYNOPSIS -pvesh [get|set|create|delete|help] [REST API path] [--verbose] +pvesh [get|set|create|delete|usage] [REST API path] [--verbose] =head1 DESCRIPTION @@ -517,7 +435,7 @@ pvesh get /nodes get a list of available options for the datacenter -pvesh help cluster/options -v +pvesh usage cluster/options -v set the HTMl5 NoVNC console as the default console for the datacenter