dcec3dc922f592b56562ce2f16438ebfa084124e
[pve-client.git] / PVE / APIClient / Helpers.pm
1 package PVE::APIClient::Helpers;
2
3 use strict;
4 use warnings;
5
6 use Storable;
7 use JSON;
8 use File::Path qw(make_path);
9
10 use PVE::APIClient::Exception qw(raise);
11 use Encode::Locale;
12 use Encode;
13 use HTTP::Status qw(:constants);
14
15 my $pve_api_definition;
16 my $pve_api_path_hash;
17
18 my $pve_api_definition_fn = "/usr/share/pve-client/pve-api-definition.dat";
19
20 my $method_map = {
21     create => 'POST',
22     set => 'PUT',
23     get => 'GET',
24     delete => 'DELETE',
25 };
26
27 my $build_pve_api_path_hash;
28 $build_pve_api_path_hash = sub {
29     my ($tree) = @_;
30
31     my $class = ref($tree);
32     return $tree if !$class;
33
34     if ($class eq 'ARRAY') {
35         foreach my $el (@$tree) {
36             $build_pve_api_path_hash->($el);
37         }
38     } elsif ($class eq 'HASH') {
39         if (defined($tree->{leaf}) && defined(my $path = $tree->{path})) {
40             $pve_api_path_hash->{$path} = $tree;
41         }
42         foreach my $k (keys %$tree) {
43             $build_pve_api_path_hash->($tree->{$k});
44         }
45     }
46 };
47
48 my $default_output_format = 'text';
49 my $client_output_format =  $default_output_format;
50
51 sub set_output_format {
52     my ($format) = @_;
53
54     if (!defined($format)) {
55         $client_output_format =  $default_output_format;
56     } else {
57         $client_output_format =  $format;
58     }
59 }
60
61 sub get_output_format {
62     return $client_output_format;
63 }
64
65 sub print_result {
66     my ($data, $result_schema) = @_;
67
68     my $format = get_output_format();
69
70     return if $result_schema->{type} eq 'null';
71
72     # TODO: implement different output formats ($format)
73
74     if ($format eq 'json') {
75         print to_json($data, {utf8 => 1, allow_nonref => 1, canonical => 1, pretty => 1 });
76     } elsif ($format eq 'text') {
77         my $type = $result_schema->{type};
78         if ($type eq 'object') {
79             die "implement me";
80         } elsif ($type eq 'array') {
81             my $item_type = $result_schema->{items}->{type};
82             if ($item_type eq 'object') {
83                 die "implement me";
84             } elsif ($item_type eq 'array') {
85                 die "implement me";
86             } else {
87                 foreach my $el (@$data) {
88                     print "$el\n"
89                 }
90             }
91         } else {
92             print "$data\n";
93         }
94     } else {
95         die "internal error: unknown output format"; # should not happen
96     }
97 }
98
99 sub get_api_definition {
100
101     if (!defined($pve_api_definition)) {
102         open(my $fh, '<',  $pve_api_definition_fn) ||
103             die "unable to open '$pve_api_definition_fn' - $!\n";
104         $pve_api_definition = Storable::fd_retrieve($fh);
105         $build_pve_api_path_hash->($pve_api_definition);
106     }
107
108     return $pve_api_definition;
109 }
110
111 my $map_path_to_info = sub {
112     my ($child_list, $stack, $uri_param) = @_;
113
114     while (defined(my $comp = shift @$stack)) {
115         foreach my $child (@$child_list) {
116             my $text = $child->{text};
117
118             if ($text eq $comp) {
119                 # found
120             } elsif ($text =~ m/^\{(\S+)\}$/) {
121                 # found
122                 $uri_param->{$1} = $comp;
123             } else {
124                 next; # text next child
125             }
126             if ($child->{leaf} || !scalar(@$stack)) {
127                 return $child;
128             } else {
129                 $child_list = $child->{children};
130                 last; # test next path component
131             }
132         }
133     }
134     return undef;
135 };
136
137 sub find_method_info {
138     my ($path, $method, $uri_param, $noerr) = @_;
139
140     $uri_param //= {};
141
142     my $stack = [ grep { length($_) > 0 }  split('\/+' , $path)]; # skip empty fragments
143
144     my $child = $map_path_to_info->(get_api_definition(), $stack, $uri_param);
145
146     if (!($child && $child->{info} && $child->{info}->{$method})) {
147         return undef if $noerr;
148         die "unable to find API method '$method' for path '$path'\n";
149     }
150
151     return $child->{info}->{$method};
152 }
153
154 sub complete_api_call_options {
155     my ($cmd, $prop, $prev, $cur, $args) = @_;
156
157     my $print_result = sub {
158         foreach my $p (@_) {
159             print "$p\n" if $p =~ m/^$cur/;
160         }
161     };
162
163     my $print_parameter_completion = sub {
164         my ($pname) = @_;
165         my $d = $prop->{$pname};
166         if ($d->{completion}) {
167             my $vt = ref($d->{completion});
168             if ($vt eq 'CODE') {
169                 my $res = $d->{completion}->($cmd, $pname, $cur, $args);
170                 &$print_result(@$res);
171             }
172         } elsif ($d->{type} eq 'boolean') {
173             &$print_result('0', '1');
174         } elsif ($d->{enum}) {
175             &$print_result(@{$d->{enum}});
176         }
177     };
178
179     my @option_list = ();
180     foreach my $key (keys %$prop) {
181         push @option_list, "--$key";
182     }
183
184     if ($cur =~ m/^-/) {
185         &$print_result(@option_list);
186         return;
187     }
188
189     if ($prev =~ m/^--?(.+)$/ && $prop->{$1}) {
190         my $pname = $1;
191         &$print_parameter_completion($pname);
192         return;
193     }
194
195     &$print_result(@option_list);
196 }
197
198 sub complete_api_path {
199     my ($text) = @_;
200
201     get_api_definition(); # make sure API data is loaded
202
203     $text =~ s!^/!!;
204
205     my ($dir, $rest) = $text =~ m|^(?:(.*)/)?(?:([^/]*))?$|;
206
207     my $info;
208     if (!defined($dir)) {
209         $dir = '';
210         $info = { children => $pve_api_definition };
211     } else {
212         my $stack = [ grep { length($_) > 0 }  split('\/+' , $dir)]; # skip empty fragments
213         $info = $map_path_to_info->($pve_api_definition, $stack, {});
214     }
215
216     my $res = [];
217     if ($info) {
218         my $prefix = length($dir) ? "/$dir/" : '/';
219         if (my $children = $info->{children}) {
220             foreach my $c (@$children) {
221                 my $ctext = $c->{text};
222                 if ($ctext =~ m/^\{(\S+)\}$/) {
223                     push @$res, "$prefix$ctext";
224                     push @$res, "$prefix$ctext/";
225                     if (length($rest)) {
226                         push @$res, "$prefix$rest";
227                         push @$res, "$prefix$rest/";
228                     }
229                 } elsif ($ctext =~ m/^\Q$rest/) {
230                     push @$res, "$prefix$ctext";
231                     push @$res, "$prefix$ctext/" if $c->{children};
232                 }
233             }
234         }
235     }
236
237     return $res;
238 }
239
240 # test for command lines with api calls (or similar bash completion calls):
241 # example1: pveclient api get remote1 /cluster
242 sub extract_path_info {
243     my ($uri_param) = @_;
244
245     my $info;
246
247     my $test_path_properties = sub {
248         my ($args) = @_;
249
250         return if scalar(@$args) < 5;
251         return if $args->[1] ne 'api';
252
253         my $path = $args->[4];
254         if (my $method = $method_map->{$args->[2]}) {
255             $info = find_method_info($path, $method, $uri_param, 1);
256         }
257     };
258
259     if (defined(my $cmd = $ARGV[0])) {
260         if ($cmd eq 'api') {
261             $test_path_properties->([$0, @ARGV]);
262         } elsif ($cmd eq 'bashcomplete') {
263             my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT});
264             my $args = PVE::APIClient::Tools::split_args($cmdline);
265             $test_path_properties->($args);
266         }
267     }
268
269     return $info;
270 }
271
272 sub get_vmid_resource {
273     my ($conn, $vmid) = @_;
274
275     my $resources = $conn->get('api2/json/cluster/resources', {type => 'vm'});
276
277     my $resource;
278     for my $tmp (@$resources) {
279         if ($tmp->{vmid} eq $vmid) {
280             $resource = $tmp;
281             last;
282         }
283     }
284
285     if (!defined($resource)) {
286         die "\"$vmid\" not found";
287     }
288
289     return $resource;
290 }
291
292 sub poll_task {
293     my ($conn, $node, $upid) = @_;
294
295     my $path = "api2/json/nodes/$node/tasks/$upid/status";
296
297     my $task_status;
298     while(1) {
299         $task_status = $conn->get($path, {});
300
301         if ($task_status->{status} eq "stopped") {
302             last;
303         }
304
305         sleep(10);
306     }
307
308     return $task_status->{exitstatus};
309 }
310
311 sub configuration_directory {
312
313     my $home = $ENV{HOME} // '';
314     my $xdg = $ENV{XDG_CONFIG_HOME} // '';
315
316     my $subdir = "pveclient";
317
318     return "$xdg/$subdir" if length($xdg);
319
320     return "$home/.config/$subdir" if length($home);
321
322     die "neither XDG_CONFIG_HOME nor HOME environment variable set\n";
323 }
324
325 my $ticket_cache_filename = "/.tickets";
326
327 sub ticket_cache_lookup {
328     my ($remote) = @_;
329
330     my $dir = configuration_directory();
331     my $filename = "$dir/$ticket_cache_filename";
332
333     my $data = {};
334     eval { $data = from_json(PVE::APIClient::Tools::file_get_contents($filename)); };
335     # ignore errors
336
337     my $ticket = $data->{$remote};
338     return undef if !defined($ticket);
339
340     my $min_age = - 60;
341     my $max_age = 3600*2 - 60;
342
343     if ($ticket =~ m/:([a-fA-F0-9]{8})::/) {
344         my $ttime = hex($1);
345         my $ctime = time();
346         my $age = $ctime - $ttime;
347
348         return $ticket if ($age > $min_age) && ($age < $max_age);
349     }
350
351     return undef;
352 }
353
354 sub ticket_cache_update {
355     my ($remote, $ticket) = @_;
356
357     my $dir = configuration_directory();
358     my $filename = "$dir/$ticket_cache_filename";
359
360     my $code = sub {
361         make_path($dir);
362         my $data = {};
363         if (-f $filename) {
364             my $raw = PVE::APIClient::Tools::file_get_contents($filename);
365             eval { $data = from_json($raw); };
366             # ignore errors
367         }
368         $data->{$remote} = $ticket;
369
370         PVE::APIClient::Tools::file_set_contents($filename, to_json($data), 0600);
371     };
372
373     PVE::APIClient::Tools::lock_file($filename, undef, $code);
374     die $@ if $@;
375 }
376
377
378 1;