X-Git-Url: https://git.proxmox.com/?p=pve-client.git;a=blobdiff_plain;f=PVE%2FAPIClient%2FHelpers.pm;h=7e4447dd8ab6c9b3a070ec4cedf810dd7bb97e21;hp=8bf11bb56bff41a03e8b21ae38c06baa2be5f664;hb=a00bef992d68c429798d031ecd6dec7935cb381b;hpb=635c05119ae8f8214bec5c110a9ec2667202bf5a diff --git a/PVE/APIClient/Helpers.pm b/PVE/APIClient/Helpers.pm index 8bf11bb..7e4447d 100644 --- a/PVE/APIClient/Helpers.pm +++ b/PVE/APIClient/Helpers.pm @@ -3,75 +3,128 @@ package PVE::APIClient::Helpers; use strict; use warnings; -use Data::Dumper; +use Storable; use JSON; +use File::Path qw(make_path); + +use PVE::APIClient::JSONSchema; use PVE::APIClient::Exception qw(raise); +use PVE::APIClient::CLIFormatter; +use PVE::APIClient::CLIHandler; +use PVE::APIClient::PTY; use Encode::Locale; use Encode; use HTTP::Status qw(:constants); my $pve_api_definition; -my $pve_api_path_hash; -my $pve_api_definition_fn = "/usr/share/pve-client/pve-api-definition.js"; +my $pve_api_definition_fn = "/usr/share/pve-client/pve-api-definition.dat"; + +our $method_map = { + create => 'POST', + set => 'PUT', + get => 'GET', + delete => 'DELETE', +}; -my $build_pve_api_path_hash; -$build_pve_api_path_hash = sub { +my $__real_remove_formats; $__real_remove_formats = sub { + my ($properties) = @_; + + foreach my $pname (keys %$properties) { + if (my $d = $properties->{$pname}) { + if (defined(my $format = $d->{format})) { + if (ref($format)) { + $__real_remove_formats->($format); + } elsif (!PVE::APIClient::JSONSchema::get_format($format)) { + # simply remove unknown format definitions + delete $d->{format}; + } + } + } + } +}; + +my $remove_unknown_formats; $remove_unknown_formats = sub { my ($tree) = @_; my $class = ref($tree); - return $tree if !$class; + return if !$class; if ($class eq 'ARRAY') { - foreach my $el (@$tree) { - $build_pve_api_path_hash->($el); - } + foreach my $el (@$tree) { + $remove_unknown_formats->($el); + } } elsif ($class eq 'HASH') { - if (defined($tree->{leaf}) && defined(my $path = $tree->{path})) { - $pve_api_path_hash->{$path} = $tree; + if (my $info = $tree->{info}) { + for my $method (qw(GET PUT PUSH DELETE)) { + next if !$info->{$method}; + my $properties = $info->{$method}->{parameters}->{properties}; + $__real_remove_formats->($properties) if $properties; + } } - foreach my $k (keys %$tree) { - $build_pve_api_path_hash->($tree->{$k}); + if ($tree->{children}) { + $remove_unknown_formats->($tree->{children}); } } + return; }; sub get_api_definition { if (!defined($pve_api_definition)) { - local $/; open(my $fh, '<', $pve_api_definition_fn) || die "unable to open '$pve_api_definition_fn' - $!\n"; - my $json_text = <$fh>; - $pve_api_definition = decode_json($json_text); + $pve_api_definition = Storable::fd_retrieve($fh); - $build_pve_api_path_hash->($pve_api_definition); + $remove_unknown_formats->($pve_api_definition); } - return $pve_api_definition; } -sub lookup_api_method { - my ($path, $method, $noerr) = @_; - - get_api_definition(); # make sure API data is loaded +my $map_path_to_info = sub { + my ($child_list, $stack, $uri_param) = @_; - my $info = $pve_api_path_hash->{$path}; + while (defined(my $comp = shift @$stack)) { + foreach my $child (@$child_list) { + my $text = $child->{text}; - if (!$info) { - return undef if $noerr; - die "unable to find API info for path '$path'\n"; + if ($text eq $comp) { + # found + } elsif ($text =~ m/^\{(\S+)\}$/) { + # found + $uri_param->{$1} = $comp; + } else { + next; # text next child + } + if ($child->{leaf} || !scalar(@$stack)) { + return $child; + } else { + $child_list = $child->{children}; + last; # test next path component + } + } } + return undef; +}; + +sub find_method_info { + my ($path, $method, $uri_param, $noerr) = @_; + + $uri_param //= {}; - my $data = $info->{info}->{$method}; + my $stack = [ grep { length($_) > 0 } split('\/+' , $path)]; # skip empty fragments - if (!$data) { + my $api = get_api_definition(); + + my $child = scalar(@$stack) ? $map_path_to_info->($api->{children}, $stack, $uri_param) : $api; + + if (!($child && $child->{info} && $child->{info}->{$method})) { return undef if $noerr; die "unable to find API method '$method' for path '$path'\n"; } - return $data; + return $child->{info}->{$method}; } sub complete_api_call_options { @@ -118,10 +171,18 @@ sub complete_api_call_options { &$print_result(@option_list); } +sub merge_api_definition_properties { + my ($path, $method, $properties) = @_; + + my $info = PVE::APIClient::Helpers::find_method_info($path, $method); + + return { %{$info->{parameters}->{properties}}, %$properties }; +} + sub complete_api_path { my ($text) = @_; - get_api_definition(); # make sure API data is loaded + my $api = get_api_definition(); # make sure API data is loaded $text =~ s!^/!!; @@ -130,21 +191,208 @@ sub complete_api_path { my $info; if (!defined($dir)) { $dir = ''; - $info = { children => $pve_api_definition }; + $info = $api; } else { - $info = $pve_api_path_hash->{"/$dir"}; + my $stack = [ grep { length($_) > 0 } split('\/+' , $dir)]; # skip empty fragments + $info = $map_path_to_info->($api->{children}, $stack, {}); } + my $res = []; if ($info) { + my $prefix = length($dir) ? "/$dir/" : '/'; if (my $children = $info->{children}) { foreach my $c (@$children) { - if ($c->{path} =~ m!\Q$dir/$rest!) { - print "$c->{path}\n"; - print "$c->{path}/\n"if $c->{children}; + my $ctext = $c->{text}; + push @$res, "${prefix}$ctext" if $ctext =~ m/^\Q$rest/; + if ($ctext =~ m/^\{(\S+)\}$/) { + if (length($rest) && $rest ne $ctext) { + push @$res, "$prefix$rest"; + } + } + } + } + } + + if (scalar(@$res) == 1) { + $res = [$res->[0], "$res->[0]/"]; + } + + return $res; +} + +# test for command lines with api calls (or similar bash completion calls): +# example1: pveclient api get remote1 /cluster +sub extract_path_info { + my ($uri_param) = @_; + + my $info; + + my $test_path_properties = sub { + my ($args) = @_; + + return if scalar(@$args) < 5; + return if $args->[1] ne 'api'; + + my $path = $args->[4]; + if (my $method = $method_map->{$args->[2]}) { + $info = find_method_info($path, $method, $uri_param, 1); + } + }; + + if (defined(my $cmd = $ARGV[0])) { + if ($cmd eq 'api') { + $test_path_properties->([$0, @ARGV]); + } elsif ($cmd eq 'bashcomplete') { + my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT}); + my $args = PVE::APIClient::Tools::split_args($cmdline); + $test_path_properties->($args); + } + } + + return $info; +} + +sub get_vmid_resource { + my ($conn, $vmid) = @_; + + my $resources = $conn->get('api2/json/cluster/resources', {type => 'vm'}); + + my $resource; + for my $tmp (@$resources) { + if ($tmp->{vmid} eq $vmid) { + $resource = $tmp; + last; + } + } + + if (!defined($resource)) { + die "\"$vmid\" not found"; + } + + return $resource; +} + +sub poll_task { + my ($conn, $node, $upid, $quiet) = @_; + + my $path = "api2/json/nodes/$node/tasks/$upid/status"; + + my $task_status; + my $last_line = 0; + while(1) { + if (!$quiet) { + my $path = "api2/json/nodes/$node/tasks/$upid/log"; + my $task_log = $conn->get($path, {start => $last_line}); + + my $printme = ''; + for my $li (@$task_log) { + if ($li->{t} eq 'no content') { + next; } + $printme .= $li->{t} . "\n"; + $last_line = $li->{n}; + } + + if ($printme ne '') { + print $printme; } } + + $task_status = $conn->get($path, {}); + + if ($task_status->{status} eq "stopped") { + last; + } + + sleep(2); + } + + if ($task_status->{exitstatus} ne "OK") { + die $task_status->{exitstatus}; } + + return $task_status->{exitstatus}; +} + +sub configuration_directory { + + my $home = $ENV{HOME} // ''; + my $xdg = $ENV{XDG_CONFIG_HOME} // ''; + + my $subdir = "pveclient"; + + return "$xdg/$subdir" if length($xdg); + + return "$home/.config/$subdir" if length($home); + + die "neither XDG_CONFIG_HOME nor HOME environment variable set\n"; +} + +my $ticket_cache_filename = "/.tickets"; + +sub ticket_cache_lookup { + my ($remote) = @_; + + my $dir = configuration_directory(); + my $filename = "$dir/$ticket_cache_filename"; + + my $data = {}; + eval { $data = from_json(PVE::APIClient::Tools::file_get_contents($filename)); }; + # ignore errors + + my $ticket = $data->{$remote}; + return undef if !defined($ticket); + + my $min_age = - 60; + my $max_age = 3600*2 - 60; + + if ($ticket =~ m/:([a-fA-F0-9]{8})::/) { + my $ttime = hex($1); + my $ctime = time(); + my $age = $ctime - $ttime; + + return $ticket if ($age > $min_age) && ($age < $max_age); + } + + return undef; +} + +sub ticket_cache_update { + my ($remote, $ticket) = @_; + + my $dir = configuration_directory(); + my $filename = "$dir/$ticket_cache_filename"; + + my $code = sub { + make_path($dir); + my $data = {}; + if (-f $filename) { + my $raw = PVE::APIClient::Tools::file_get_contents($filename); + eval { $data = from_json($raw); }; + # ignore errors + } + $data->{$remote} = $ticket; + + PVE::APIClient::Tools::file_set_contents($filename, to_json($data), 0600); + }; + + PVE::APIClient::Tools::lock_file($filename, undef, $code); + die $@ if $@; +} + +sub extract_even_elements { + my ($list) = @_; + + my $ind = 0; + return [ grep { ($ind++ % 2) == 0 } @$list ]; +} + +sub print_ordered_result { + my ($property_list, $data, $result_schema, $options) = @_; + + my $param_order = extract_even_elements($property_list); + + PVE::APIClient::CLIFormatter::print_api_result($data, $result_schema, $param_order, $options); } 1;