49d43b94593c2f802b336271033d4df962024dea
[pve-common.git] / src / PVE / RESTHandler.pm
1 package PVE::RESTHandler;
2
3 use strict;
4 no strict 'refs'; # our autoload requires this
5 use warnings;
6 use PVE::SafeSyslog;
7 use PVE::Exception qw(raise raise_param_exc);
8 use PVE::JSONSchema;
9 use PVE::Tools;
10 use HTTP::Status qw(:constants :is status_message);
11 use Text::Wrap;
12 use Clone qw(clone);
13
14 my $method_registry = {};
15 my $method_by_name = {};
16 my $method_path_lookup = {};
17
18 our $AUTOLOAD;  # it's a package global
19
20 our $standard_output_options = {
21     'output-format' => PVE::JSONSchema::get_standard_option('pve-output-format'),
22     noheader => {
23         description => "Do not show column headers (for 'text' format).",
24         type => 'boolean',
25         optional => 1,
26         default => 0,
27     },
28     noborder => {
29         description => "Do not draw borders (for 'text' format).",
30         type => 'boolean',
31         optional => 1,
32         default => 0,
33     },
34     quiet => {
35         description => "Suppress printing results.",
36         type => 'boolean',
37         optional => 1,
38     },
39     'human-readable' => {
40         description => "Call output rendering functions to produce human readable text.",
41         type => 'boolean',
42         optional => 1,
43         default => 1,
44     }
45 };
46
47 sub api_clone_schema {
48     my ($schema, $no_typetext) = @_;
49
50     my $res = {};
51     my $ref = ref($schema);
52     die "not a HASH reference" if !($ref && $ref eq 'HASH');
53
54     foreach my $k (keys %$schema) {
55         my $d = $schema->{$k};
56         if ($k ne 'properties') {
57             $res->{$k} = ref($d) ? clone($d) : $d;
58             next;
59         }
60         # convert indexed parameters like -net\d+ to -net[n]
61         foreach my $p (keys %$d) {
62             my $pd = $d->{$p};
63             if ($p =~ m/^([a-z]+)(\d+)$/) {
64                 my ($name, $idx) = ($1, $2);
65                 if ($idx == 0 && defined($d->{"${name}1"})) {
66                     $p = "${name}[n]";
67                 } elsif ($idx > 0 && defined($d->{"${name}0"})) {
68                     next; # only handle once for -xx0, but only if -xx0 exists
69                 }
70             }
71             my $tmp = ref($pd) ? clone($pd) : $pd;
72             # NOTE: add typetext property for more complex types, to
73             # make the web api viewer code simpler
74             if (!$no_typetext && !(defined($tmp->{enum}) || defined($tmp->{pattern}))) {
75                 my $typetext = PVE::JSONSchema::schema_get_type_text($tmp);
76                 if ($tmp->{type} && ($tmp->{type} ne $typetext)) {
77                     $tmp->{typetext} = $typetext;
78                 }
79             }
80             $res->{$k}->{$p} = $tmp;
81         }
82     }
83
84     return $res;
85 }
86
87 sub api_dump_full {
88     my ($tree, $index, $class, $prefix, $raw_dump) = @_;
89
90     $prefix = '' if !$prefix;
91
92     my $ma = $method_registry->{$class};
93
94     foreach my $info (@$ma) {
95
96         my $path = "$prefix/$info->{path}";
97         $path =~ s/\/+$//;
98
99         if ($info->{subclass}) {
100             api_dump_full($tree, $index, $info->{subclass}, $path, $raw_dump);
101         } else {
102             next if !$path;
103
104             # check if method is unique
105             my $realpath = $path;
106             $realpath =~ s/\{[^\}]+\}/\{\}/g;
107             my $fullpath = "$info->{method} $realpath";
108             die "duplicate path '$realpath'" if $index->{$fullpath};
109             $index->{$fullpath} = $info;
110
111             # insert into tree
112             my $treedir = $tree;
113             my $res;
114             my $sp = '';
115             foreach my $dir (split('/', $path)) {
116                 next if !$dir;
117                 $sp .= "/$dir";
118                 $res = (grep { $_->{text} eq $dir } @$treedir)[0];
119                 if ($res) {
120                     $res->{children} = [] if !$res->{children};
121                     $treedir = $res->{children};
122                 } else {
123                     $res = {
124                         path => $sp,
125                         text => $dir,
126                         children => [],
127                     };
128                     push @$treedir, $res;
129                     $treedir = $res->{children};
130                 }
131             }
132
133             if ($res) {
134                 my $data = {};
135                 foreach my $k (keys %$info) {
136                     next if $k eq 'code' || $k eq "match_name" || $k eq "match_re" ||
137                         $k eq "path";
138
139                     my $d = $info->{$k};
140
141                     if ($raw_dump) {
142                         $data->{$k} = $d;
143                     } else {
144                         if ($k eq 'parameters') {
145                             $data->{$k} = api_clone_schema($d);
146                         } elsif ($k eq 'returns') {
147                             $data->{$k} = api_clone_schema($d, 1);
148                         } else {
149                             $data->{$k} = ref($d) ? clone($d) : $d;
150                         }
151                     }
152                 } 
153                 $res->{info}->{$info->{method}} = $data;
154             };
155         }
156     }
157 };
158
159 sub api_dump_cleanup_tree {
160     my ($tree) = @_;
161
162     foreach my $rec (@$tree) {
163         delete $rec->{children} if $rec->{children} && !scalar(@{$rec->{children}});
164         if ($rec->{children}) {
165             $rec->{leaf} = 0;
166             api_dump_cleanup_tree($rec->{children});
167         } else {
168             $rec->{leaf} = 1;
169         }
170     }
171
172 }
173
174 # api_dump_remove_refs: prepare API tree for use with to_json($tree)
175 sub api_dump_remove_refs {
176     my ($tree) = @_;
177
178     my $class = ref($tree);
179     return $tree if !$class;
180
181     if ($class eq 'ARRAY') {
182         my $res = [];
183         foreach my $el (@$tree) {
184             push @$res, api_dump_remove_refs($el);
185         }
186         return $res;
187     } elsif ($class eq 'HASH') {
188         my $res = {};
189         foreach my $k (keys %$tree) {
190             if (my $itemclass = ref($tree->{$k})) {
191                 if ($itemclass eq 'CODE') {
192                     next if $k eq 'completion';
193                 }
194                 $res->{$k} = api_dump_remove_refs($tree->{$k});
195             } else {
196                 $res->{$k} = $tree->{$k};
197             }
198         }
199         return $res;
200     } elsif ($class eq 'Regexp') {
201         return "$tree"; # return string representation
202     } else {
203         die "unknown class '$class'\n";
204     }
205 }
206
207 sub api_dump {
208     my ($class, $prefix, $raw_dump) = @_;
209
210     my $tree = [];
211
212     my $index = {};
213     api_dump_full($tree, $index, $class, $prefix, $raw_dump);
214     api_dump_cleanup_tree($tree);
215     return $tree;
216 };
217
218 sub validate_method_schemas {
219
220     foreach my $class (keys %$method_registry) {
221         my $ma = $method_registry->{$class};
222
223         foreach my $info (@$ma) {
224             PVE::JSONSchema::validate_method_info($info);
225         }
226     }
227 }
228
229 sub register_method {
230     my ($self, $info) = @_;
231
232     my $match_re = [];
233     my $match_name = [];
234
235     my $errprefix;
236
237     my $method;
238     if ($info->{subclass}) {
239         $errprefix = "register subclass $info->{subclass} at ${self}/$info->{path} -";
240         $method = 'SUBCLASS';
241     } else {
242         $errprefix = "register method ${self}/$info->{path} -";
243         $info->{method} = 'GET' if !$info->{method};
244         $method = $info->{method};
245     }
246
247     $method_path_lookup->{$self} = {} if !defined($method_path_lookup->{$self});
248     my $path_lookup = $method_path_lookup->{$self};
249
250     die "$errprefix no path" if !defined($info->{path});
251     
252     foreach my $comp (split(/\/+/, $info->{path})) {
253         die "$errprefix path compoment has zero length\n" if $comp eq '';
254         my ($name, $regex);
255         if ($comp =~ m/^\{(\w+)(:(.*))?\}$/) {
256             $name = $1;
257             $regex = $3 ? $3 : '\S+';
258             push @$match_re, $regex;
259             push @$match_name, $name;
260         } else {
261             $name = $comp;
262             push @$match_re, $name;
263             push @$match_name, undef;
264         }
265
266         if ($regex) {
267             $path_lookup->{regex} = {} if !defined($path_lookup->{regex});      
268
269             my $old_name = $path_lookup->{regex}->{match_name};
270             die "$errprefix found changed regex match name\n"
271                 if defined($old_name) && ($old_name ne $name);
272             my $old_re = $path_lookup->{regex}->{match_re};
273             die "$errprefix found changed regex\n"
274                 if defined($old_re) && ($old_re ne $regex);
275             $path_lookup->{regex}->{match_name} = $name;
276             $path_lookup->{regex}->{match_re} = $regex;
277             
278             die "$errprefix path match error - regex and fixed items\n"
279                 if defined($path_lookup->{folders});
280
281             $path_lookup = $path_lookup->{regex};
282             
283         } else {
284             $path_lookup->{folders}->{$name} = {} if !defined($path_lookup->{folders}->{$name});        
285
286             die "$errprefix path match error - regex and fixed items\n"
287                 if defined($path_lookup->{regex});
288
289             $path_lookup = $path_lookup->{folders}->{$name};
290         }
291     }
292
293     die "$errprefix duplicate method definition\n" 
294         if defined($path_lookup->{$method});
295
296     if ($method eq 'SUBCLASS') {
297         foreach my $m (qw(GET PUT POST DELETE)) {
298             die "$errprefix duplicate method definition SUBCLASS and $m\n" if $path_lookup->{$m};
299         }
300     }
301     $path_lookup->{$method} = $info;
302
303     $info->{match_re} = $match_re;
304     $info->{match_name} = $match_name;
305
306     $method_by_name->{$self} = {} if !defined($method_by_name->{$self});
307
308     if ($info->{name}) {
309         die "$errprefix method name already defined\n"
310             if defined($method_by_name->{$self}->{$info->{name}});
311
312         $method_by_name->{$self}->{$info->{name}} = $info;
313     }
314
315     push @{$method_registry->{$self}}, $info;
316 }
317
318 sub DESTROY {}; # avoid problems with autoload
319
320 sub AUTOLOAD {
321     my ($this) = @_;
322
323     # also see "man perldiag"
324  
325     my $sub = $AUTOLOAD;
326     (my $method = $sub) =~ s/.*:://;
327
328     my $info = $this->map_method_by_name($method);
329
330     *{$sub} = sub {
331         my $self = shift;
332         return $self->handle($info, @_);
333     };
334     goto &$AUTOLOAD;
335 }
336
337 sub method_attributes {
338     my ($self) = @_;
339
340     return $method_registry->{$self};
341 }
342
343 sub map_method_by_name {
344     my ($self, $name) = @_;
345
346     my $info = $method_by_name->{$self}->{$name};
347     die "no such method '${self}::$name'\n" if !$info;
348
349     return $info;
350 }
351
352 sub map_path_to_methods {
353     my ($class, $stack, $uri_param, $pathmatchref) = @_;
354
355     my $path_lookup = $method_path_lookup->{$class};
356
357     # Note: $pathmatchref can be used to obtain path including
358     # uri patterns like '/cluster/firewall/groups/{group}'.
359     # Used by pvesh to display help
360     if (defined($pathmatchref)) {
361         $$pathmatchref = '' if !$$pathmatchref;
362     }
363
364     while (defined(my $comp = shift @$stack)) {
365         return undef if !$path_lookup; # not registerd?
366         if ($path_lookup->{regex}) {
367             my $name = $path_lookup->{regex}->{match_name};
368             my $regex = $path_lookup->{regex}->{match_re};
369
370             return undef if $comp !~ m/^($regex)$/;
371             $uri_param->{$name} = $1;
372             $path_lookup = $path_lookup->{regex};
373             $$pathmatchref .= '/{' . $name . '}' if defined($pathmatchref);
374         } elsif ($path_lookup->{folders}) {
375             $path_lookup = $path_lookup->{folders}->{$comp};
376             $$pathmatchref .= '/' . $comp if defined($pathmatchref);
377         } else {
378             die "internal error";
379         }
380  
381         return undef if !$path_lookup;
382
383         if (my $info = $path_lookup->{SUBCLASS}) {
384             $class = $info->{subclass};
385
386             my $fd = $info->{fragmentDelimiter};
387
388             if (defined($fd)) {
389                 # we only support the empty string '' (match whole URI)
390                 die "unsupported fragmentDelimiter '$fd'" 
391                     if $fd ne '';
392
393                 $stack = [ join ('/', @$stack) ] if scalar(@$stack) > 1;
394             }
395             $path_lookup = $method_path_lookup->{$class};
396         }
397     }
398
399     return undef if !$path_lookup;
400
401     return ($class, $path_lookup);
402 }
403
404 sub find_handler {
405     my ($class, $method, $path, $uri_param, $pathmatchref) = @_;
406
407     my $stack = [ grep { length($_) > 0 }  split('\/+' , $path)]; # skip empty fragments
408
409     my ($handler_class, $path_info);
410     eval {
411         ($handler_class, $path_info) = $class->map_path_to_methods($stack, $uri_param, $pathmatchref);
412     };
413     my $err = $@;
414     syslog('err', $err) if $err;
415
416     return undef if !($handler_class && $path_info);
417
418     my $method_info = $path_info->{$method};
419
420     return undef if !$method_info;
421
422     return ($handler_class, $method_info);
423 }
424
425 sub handle {
426     my ($self, $info, $param) = @_;
427
428     my $func = $info->{code};
429
430     if (!($info->{name} && $func)) {
431         raise("Method lookup failed ('$info->{name}')\n",
432               code => HTTP_INTERNAL_SERVER_ERROR);
433     }
434
435     if (my $schema = $info->{parameters}) {
436         # warn "validate ". Dumper($param}) . "\n" . Dumper($schema);
437         PVE::JSONSchema::validate($param, $schema);
438         # untaint data (already validated)
439         my $extra = delete $param->{'extra-args'};
440         while (my ($key, $val) = each %$param) {
441             ($param->{$key}) = $val =~ /^(.*)$/s;
442         }
443         $param->{'extra-args'} = [map { /^(.*)$/ } @$extra] if $extra;
444     }
445
446     my $result = &$func($param);
447
448     # todo: this is only to be safe - disable?
449     if (my $schema = $info->{returns}) {
450         PVE::JSONSchema::validate($result, $schema, "Result verification failed\n");
451     }
452
453     return $result;
454 }
455
456 # format option, display type and description
457 # $name: option name
458 # $display_name: for example "-$name" of "<$name>", pass undef to use "$name:"
459 # $phash: json schema property hash
460 # $format: 'asciidoc', 'short', 'long' or 'full'
461 # $style: 'config', 'config-sub', 'arg' or 'fixed'
462 # $mapdef: parameter mapping ({ desc => XXX, func => sub {...} })
463 my $get_property_description = sub {
464     my ($name, $style, $phash, $format, $mapdef) = @_;
465
466     my $res = '';
467
468     $format = 'asciidoc' if !defined($format);
469
470     my $descr = $phash->{description} || "no description available";
471
472     if ($phash->{verbose_description} &&
473         ($style eq 'config' || $style eq 'config-sub')) {
474         $descr = $phash->{verbose_description};
475     }
476
477     chomp $descr;
478
479     my $type_text = PVE::JSONSchema::schema_get_type_text($phash, $style);
480
481     if ($mapdef && $phash->{type} eq 'string') {
482         $type_text = $mapdef->{desc};
483     }
484
485     if ($format eq 'asciidoc') {
486
487         if ($style eq 'config') {
488             $res .= "`$name`: ";
489         } elsif ($style eq 'config-sub') {
490             $res .= "`$name`=";
491         } elsif ($style eq 'arg') {
492             $res .= "`--$name` ";
493         } elsif ($style eq 'fixed') {
494             $res .= "`<$name>`: ";
495         } else {
496             die "unknown style '$style'";
497         }
498
499         $res .= "`$type_text` " if $type_text;
500
501         if (defined(my $dv = $phash->{default})) {
502             $res .= "('default =' `$dv`)";
503         }
504
505         if ($style eq 'config-sub') {
506             $res .= ";;\n\n";
507         } else {
508             $res .= "::\n\n";
509         }
510
511         my $wdescr = $descr;
512         chomp $wdescr;
513         $wdescr =~ s/^$/+/mg;
514
515         $res .= $wdescr . "\n";
516
517         if (my $req = $phash->{requires}) {
518             my $tmp .= ref($req) ? join(', ', @$req) : $req;
519             $res .= "+\nNOTE: Requires option(s): `$tmp`\n";
520         }
521         $res .= "\n";
522
523     } elsif ($format eq 'short' || $format eq 'long' || $format eq 'full') {
524
525         my $defaulttxt = '';
526         if (defined(my $dv = $phash->{default})) {
527             $defaulttxt = "   (default=$dv)";
528         }
529
530         my $display_name;
531         if ($style eq 'config') {
532             $display_name = "$name:";
533         } elsif ($style eq 'arg') {
534             $display_name = "-$name";
535         } elsif ($style eq 'fixed') {
536             $display_name = "<$name>";
537         } else {
538             die "unknown style '$style'";
539         }
540
541         my $tmp = sprintf "  %-10s %s$defaulttxt\n", $display_name, "$type_text";
542         my $indend = "             ";
543
544         $res .= Text::Wrap::wrap('', $indend, ($tmp));
545         $res .= "\n",
546         $res .= Text::Wrap::wrap($indend, $indend, ($descr)) . "\n\n";
547
548         if (my $req = $phash->{requires}) {
549             my $tmp = "Requires option(s): ";
550             $tmp .= ref($req) ? join(', ', @$req) : $req;
551             $res .= Text::Wrap::wrap($indend, $indend, ($tmp)). "\n\n";
552         }
553
554     } else {
555         die "unknown format '$format'";
556     }
557
558     return $res;
559 };
560
561 # translate parameter mapping definition
562 # $mapping_array is a array which can contain:
563 #   strings ... in that case we assume it is a parameter name, and
564 #      we want to load that parameter from a file
565 #   [ param_name, func, desc] ... allows you to specify a arbitrary
566 #      mapping func for any param
567 #
568 # Returns: a hash indexed by parameter_name,
569 # i.e.  { param_name => { func => .., desc => ... } }
570 my $compute_param_mapping_hash = sub {
571     my ($mapping_array) = @_;
572
573     my $res = {};
574
575     return $res if !defined($mapping_array);
576
577     foreach my $item (@$mapping_array) {
578         my ($name, $func, $desc, $interactive);
579         if (ref($item) eq 'ARRAY') {
580             ($name, $func, $desc, $interactive) = @$item;
581         } elsif (ref($item) eq 'HASH') {
582             # just use the hash
583             $res->{$item->{name}} = $item;
584             next;
585         } else {
586             $name = $item;
587             $func = sub { return PVE::Tools::file_get_contents($_[0]) };
588         }
589         $desc //= '<filepath>';
590         $res->{$name} = { desc => $desc, func => $func, interactive => $interactive };
591     }
592
593     return $res;
594 };
595
596 # generate usage information for command line tools
597 #
598 # $info        ... method info
599 # $prefix      ... usually something like "$exename $cmd" ('pvesm add')
600 # $arg_param   ... list of parameters we want to get as ordered arguments 
601 #                  on the command line (or single parameter name for lists)
602 # $fixed_param ... do not generate and info about those parameters
603 # $format:
604 #   'long'     ... default (text, list all options)
605 #   'short'    ... command line only (text, one line)
606 #   'full'     ... text, include description
607 #   'asciidoc' ... generate asciidoc for man pages (like 'full')
608 # $param_cb    ... mapping for string parameters to file path parameters
609 # $formatter_properties  ... additional property definitions (passed to output formatter)
610 sub getopt_usage {
611     my ($info, $prefix, $arg_param, $fixed_param, $format, $param_cb, $formatter_properties) = @_;
612
613     $format = 'long' if !$format;
614
615     my $schema = $info->{parameters};
616     my $name = $info->{name};
617     my $prop = { %{$schema->{properties}} }; # copy
618
619     my $has_output_format_option = $formatter_properties->{'output-format'} ? 1 : 0;
620
621     if ($formatter_properties) {
622         foreach my $key (keys %$formatter_properties) {
623             if (!$standard_output_options->{$key}) {
624                 $prop->{$key} = $formatter_properties->{$key};
625             }
626         }
627     }
628
629     # also remove $standard_output_options from $prop (pvesh, pveclient)
630     if ($prop->{'output-format'}) {
631         $has_output_format_option = 1;
632         foreach my $key (keys %$prop) {
633             if ($standard_output_options->{$key}) {
634                 delete $prop->{$key};
635             }
636         }
637     }
638
639     my $out = '';
640
641     my $arg_hash = {};
642
643     my $args = '';
644
645     $arg_param = [ $arg_param ] if $arg_param && !ref($arg_param);
646
647     foreach my $p (@$arg_param) {
648         next if !$prop->{$p}; # just to be sure
649         my $pd = $prop->{$p};
650
651         $arg_hash->{$p} = 1;
652         $args .= " " if $args;
653         if ($pd->{format} && $pd->{format} =~ m/-list/) {
654             $args .= "{<$p>}";
655         } else {
656             $args .= $pd->{optional} ? "[<$p>]" : "<$p>";
657         }
658     }
659
660     my $argdescr = '';
661     foreach my $k (@$arg_param) {
662         next if defined($fixed_param->{$k}); # just to be sure
663         next if !$prop->{$k}; # just to be sure
664         $argdescr .= $get_property_description->($k, 'fixed', $prop->{$k}, $format);
665     }
666
667     my $idx_param = {}; # -vlan\d+ -scsi\d+
668
669     my $opts = '';
670     foreach my $k (sort keys %$prop) {
671         next if $arg_hash->{$k};
672         next if defined($fixed_param->{$k});
673
674         my $type_text = $prop->{$k}->{type} || 'string';
675
676         my $param_map = {};
677
678         if (defined($param_cb)) {
679             my $mapping = $param_cb->($name);
680             $param_map = $compute_param_mapping_hash->($mapping);
681             next if $k eq 'password' && $param_map->{$k} && !$prop->{$k}->{optional};
682         }
683
684         my $base = $k;
685         if ($k =~ m/^([a-z]+)(\d+)$/) {
686             my ($name, $idx) = ($1, $2);
687             next if $idx_param->{$name};
688             if ($idx == 0 && defined($prop->{"${name}1"})) {
689                 $idx_param->{$name} = 1;
690                 $base = "${name}[n]";
691             }
692         }
693
694
695         $opts .= $get_property_description->($base, 'arg', $prop->{$k}, $format, $param_map->{$k});
696
697         if (!$prop->{$k}->{optional}) {
698             $args .= " " if $args;
699             $args .= "--$base <$type_text>"
700         }
701     } 
702
703     if ($format eq 'asciidoc') {
704         $out .= "*${prefix}*";
705         $out .= " `$args`" if $args;
706         $out .= " `[OPTIONS]`" if $opts;
707         $out .= " `[FORMAT_OPTIONS]`" if $has_output_format_option;
708         $out .= "\n";
709     } else {
710         $out .= "USAGE: " if $format ne 'short';
711         $out .= "$prefix $args";
712         $out .= " [OPTIONS]" if $opts;
713         $out .= " [FORMAT_OPTIONS]" if $has_output_format_option;
714         $out .= "\n";
715     }
716
717     return $out if $format eq 'short';
718
719     if ($info->{description}) {
720         if ($format eq 'asciidoc') {
721             my $desc = Text::Wrap::wrap('', '', ($info->{description}));
722             $out .= "\n$desc\n\n";
723         } elsif ($format eq 'full') {
724             my $desc = Text::Wrap::wrap('  ', '  ', ($info->{description}));
725             $out .= "\n$desc\n\n";
726         }
727     }
728
729     $out .= $argdescr if $argdescr;
730
731     $out .= $opts if $opts;
732
733     return $out;
734 }
735
736 sub usage_str {
737     my ($self, $name, $prefix, $arg_param, $fixed_param, $format, $param_cb, $formatter_properties) = @_;
738
739     my $info = $self->map_method_by_name($name);
740
741     return getopt_usage($info, $prefix, $arg_param, $fixed_param, $format, $param_cb, $formatter_properties);
742 }
743
744 # generate docs from JSON schema properties
745 sub dump_properties {
746     my ($prop, $format, $style, $filterFn) = @_;
747
748     my $raw = '';
749
750     $style //= 'config';
751     
752     my $idx_param = {}; # -vlan\d+ -scsi\d+
753
754     foreach my $k (sort keys %$prop) {
755         my $phash = $prop->{$k};
756
757         next if defined($filterFn) && &$filterFn($k, $phash);
758         next if $phash->{alias};
759
760         my $base = $k;
761         if ($k =~ m/^([a-z]+)(\d+)$/) {
762             my ($name, $idx) = ($1, $2);
763             next if $idx_param->{$name};
764             if ($idx == 0 && defined($prop->{"${name}1"})) {
765                 $idx_param->{$name} = 1;
766                 $base = "${name}[n]";
767             }
768         }
769
770         $raw .= $get_property_description->($base, $style, $phash, $format);
771
772         next if $style ne 'config';
773
774         my $prop_fmt = $phash->{format};
775         next if !$prop_fmt;
776
777         if (ref($prop_fmt) ne 'HASH') {
778             $prop_fmt = PVE::JSONSchema::get_format($prop_fmt);
779         }
780
781         next if !(ref($prop_fmt) && (ref($prop_fmt) eq 'HASH'));
782
783         $raw .= dump_properties($prop_fmt, $format, 'config-sub')
784         
785     }
786
787     return $raw;
788 }
789
790 my $replace_file_names_with_contents = sub {
791     my ($param, $param_map) = @_;
792
793     while (my ($k, $d) = each %$param_map) {
794         next if $d->{interactive}; # handled by the JSONSchema's get_options code
795         $param->{$k} = $d->{func}->($param->{$k})
796             if defined($param->{$k});
797     }
798
799     return $param;
800 };
801
802 sub add_standard_output_properties {
803     my ($propdef, $list) = @_;
804
805     $propdef //= {};
806
807     $list //= [ keys %$standard_output_options ];
808
809     my $res = { %$propdef }; # copy
810
811     foreach my $opt (@$list) {
812         die "no such standard output option '$opt'\n" if !defined($standard_output_options->{$opt});
813         die "detected overwriten standard CLI parameter '$opt'\n" if defined($res->{$opt});
814         $res->{$opt} = $standard_output_options->{$opt};
815     }
816
817     return $res;
818 }
819
820 sub extract_standard_output_properties {
821     my ($data) = @_;
822
823     my $options = {};
824     foreach my $opt (keys %$standard_output_options) {
825         $options->{$opt} = delete $data->{$opt} if defined($data->{$opt});
826     }
827
828     return $options;
829 }
830
831 sub cli_handler {
832     my ($self, $prefix, $name, $args, $arg_param, $fixed_param, $param_cb, $formatter_properties) = @_;
833
834     my $info = $self->map_method_by_name($name);
835     my $res;
836     my $fmt_param = {};
837
838     eval {
839         my $param_map = {};
840         $param_map = $compute_param_mapping_hash->($param_cb->($name)) if $param_cb;
841         my $schema = { %{$info->{parameters}} }; # copy
842         $schema->{properties} = { %{$schema->{properties}}, %$formatter_properties } if $formatter_properties;
843         my $param = PVE::JSONSchema::get_options($schema, $args, $arg_param, $fixed_param, $param_map);
844
845         if ($formatter_properties) {
846             foreach my $opt (keys %$formatter_properties) {
847                 $fmt_param->{$opt} = delete $param->{$opt} if defined($param->{$opt});
848             }
849         }
850
851         if (defined($param_map)) {
852             $replace_file_names_with_contents->($param, $param_map);
853         }
854
855         $res = $self->handle($info, $param);
856     };
857     if (my $err = $@) {
858         my $ec = ref($err);
859
860         die $err if !$ec || $ec ne "PVE::Exception" || !$err->is_param_exc();
861         
862         $err->{usage} = $self->usage_str($name, $prefix, $arg_param, $fixed_param, 'short', $param_cb, $formatter_properties);
863
864         die $err;
865     }
866
867     return wantarray ? ($res, $fmt_param) : $res;
868 }
869
870 # utility methods
871 # note: this modifies the original hash by adding the id property
872 sub hash_to_array {
873     my ($hash, $idprop) = @_;
874
875     my $res = [];
876     return $res if !$hash;
877
878     foreach my $k (keys %$hash) {
879         $hash->{$k}->{$idprop} = $k;
880         push @$res, $hash->{$k};
881     }
882
883     return $res;
884 }
885
886 1;