#!/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 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 .", 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, }, 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) = @_; $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'); } 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'], {}, $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]; __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)