add experimental 'asciidoc' generator
[pve-common.git] / src / PVE / CLIHandler.pm
1 package PVE::CLIHandler;
2
3 use strict;
4 use warnings;
5 use Data::Dumper;
6
7 use PVE::SafeSyslog;
8 use PVE::Exception qw(raise raise_param_exc);
9 use PVE::RESTHandler;
10 use PVE::PodParser;
11
12 use base qw(PVE::RESTHandler);
13
14 my $cmddef;
15 my $exename;
16 my $cli_handler_class;
17
18 my $expand_command_name = sub {
19     my ($def, $cmd) = @_;
20
21     if (!$def->{$cmd}) {
22         my $expanded;
23         for my $k (keys(%$def)) {
24             if ($k =~ m/^$cmd/) {
25                 if ($expanded) {
26                     $expanded = undef; # more than one match
27                     last;
28                 } else {
29                     $expanded = $k;
30                 }
31             }
32         }
33         $cmd = $expanded if $expanded;
34     }
35     return $cmd;
36 };
37
38 my $complete_command_names = sub {
39     my $res = [];
40
41     return if ref($cmddef) ne 'HASH';
42
43     foreach my $cmd (keys %$cmddef) {
44         next if $cmd eq 'help';
45         push @$res, $cmd;
46     }
47
48     return $res;
49 };
50
51 __PACKAGE__->register_method ({
52     name => 'help', 
53     path => 'help',
54     method => 'GET',
55     description => "Get help about specified command.",
56     parameters => {
57         additionalProperties => 0,
58         properties => {
59             cmd => {
60                 description => "Command name",
61                 type => 'string',
62                 optional => 1,
63                 completion => $complete_command_names,
64             },
65             verbose => {
66                 description => "Verbose output format.",
67                 type => 'boolean',
68                 optional => 1,
69             },
70         },
71     },
72     returns => { type => 'null' },
73     
74     code => sub {
75         my ($param) = @_;
76
77         die "not initialized" if !($cmddef && $exename && $cli_handler_class);
78
79         my $cmd = $param->{cmd};
80
81         my $verbose = defined($cmd) && $cmd; 
82         $verbose = $param->{verbose} if defined($param->{verbose});
83
84         if (!$cmd) {
85             if ($verbose) {
86                 print_usage_verbose();
87             } else {            
88                 print_usage_short(\*STDOUT);
89             }
90             return undef;
91         }
92
93         $cmd = &$expand_command_name($cmddef, $cmd);
94
95         my ($class, $name, $arg_param, $uri_param) = @{$cmddef->{$cmd} || []};
96
97         raise_param_exc({ cmd => "no such command '$cmd'"}) if !$class;
98
99         my $pwcallback = $cli_handler_class->can('read_password');
100
101         my $str = $class->usage_str($name, "$exename $cmd", $arg_param, $uri_param,
102                                     $verbose ? 'full' : 'short', $pwcallback);
103         if ($verbose) {
104             print "$str\n";
105         } else {
106             print "USAGE: $str\n";
107         }
108
109         return undef;
110
111     }});
112
113 sub print_simple_asciidoc_synopsys {
114     my ($class, $name, $arg_param, $uri_param) = @_;
115
116     die "not initialized" if !$cli_handler_class;
117
118     my $pwcallback = $cli_handler_class->can('read_password');
119
120     my $synopsis = "*${name}* `help`\n\n";
121
122     $synopsis .= $class->usage_str($name, $name, $arg_param, $uri_param, 'asciidoc', $pwcallback);
123
124     return $synopsis;
125 }
126
127 sub print_asciidoc_synopsys {
128
129     die "not initialized" if !($cmddef && $exename && $cli_handler_class);
130
131     my $pwcallback = $cli_handler_class->can('read_password');
132
133     my $synopsis = "";
134
135     $synopsis .= "*${exename}* `<COMMAND> [ARGS] [OPTIONS]`\n\n";
136
137     my $oldclass;
138     foreach my $cmd (sort keys %$cmddef) {
139         my ($class, $name, $arg_param, $uri_param) = @{$cmddef->{$cmd}};
140         my $str = $class->usage_str($name, "$exename $cmd", $arg_param,
141                                     $uri_param, 'asciidoc', $pwcallback);
142         $synopsis .= "\n" if $oldclass && $oldclass ne $class;
143
144         $synopsis .= "$str\n\n";
145         $oldclass = $class;
146     }
147
148     $synopsis .= "\n";
149
150     return $synopsis;
151 }
152
153 sub print_simple_pod_manpage {
154     my ($podfn, $class, $name, $arg_param, $uri_param) = @_;
155
156     die "not initialized" if !$cli_handler_class;
157
158     my $pwcallback = $cli_handler_class->can('read_password');
159
160     my $synopsis = " $name help\n\n";
161     my $str = $class->usage_str($name, $name, $arg_param, $uri_param, 'long', $pwcallback);
162     $str =~ s/^USAGE://;
163     $str =~ s/\n/\n /g;
164     $synopsis .= $str;
165
166     my $parser = PVE::PodParser->new();
167     $parser->{include}->{synopsis} = $synopsis;
168     $parser->parse_from_file($podfn);
169 }
170
171 sub print_pod_manpage {
172     my ($podfn) = @_;
173
174     die "not initialized" if !($cmddef && $exename && $cli_handler_class);
175     die "no pod file specified" if !$podfn;
176
177     my $pwcallback = $cli_handler_class->can('read_password');
178
179     my $synopsis = "";
180     
181     $synopsis .= " $exename <COMMAND> [ARGS] [OPTIONS]\n\n";
182
183     my $style = 'full'; # or should we use 'short'?
184     my $oldclass;
185     foreach my $cmd (sorted_commands()) {
186         my ($class, $name, $arg_param, $uri_param) = @{$cmddef->{$cmd}};
187         my $str = $class->usage_str($name, "$exename $cmd", $arg_param,
188                                     $uri_param, $style, $pwcallback);
189         $str =~ s/^USAGE: //;
190
191         $synopsis .= "\n" if $oldclass && $oldclass ne $class;
192         $str =~ s/\n/\n /g;
193         $synopsis .= " $str\n\n";
194         $oldclass = $class;
195     }
196
197     $synopsis .= "\n";
198
199     my $parser = PVE::PodParser->new();
200     $parser->{include}->{synopsis} = $synopsis;
201     $parser->parse_from_file($podfn);
202 }
203
204 sub print_usage_verbose {
205
206     die "not initialized" if !($cmddef && $exename && $cli_handler_class);
207
208     my $pwcallback = $cli_handler_class->can('read_password');
209
210     print "USAGE: $exename <COMMAND> [ARGS] [OPTIONS]\n\n";
211
212     foreach my $cmd (sort keys %$cmddef) {
213         my ($class, $name, $arg_param, $uri_param) = @{$cmddef->{$cmd}};
214         my $str = $class->usage_str($name, "$exename $cmd", $arg_param, $uri_param,
215                                     'full', $pwcallback);
216         print "$str\n\n";
217     }
218 }
219
220 sub sorted_commands {   
221     return sort { ($cmddef->{$a}->[0] cmp $cmddef->{$b}->[0]) || ($a cmp $b)} keys %$cmddef;
222 }
223
224 sub print_usage_short {
225     my ($fd, $msg) = @_;
226
227     die "not initialized" if !($cmddef && $exename && $cli_handler_class);
228
229     my $pwcallback = $cli_handler_class->can('read_password');
230
231     print $fd "ERROR: $msg\n" if $msg;
232     print $fd "USAGE: $exename <COMMAND> [ARGS] [OPTIONS]\n";
233
234     my $oldclass;
235     foreach my $cmd (sorted_commands()) {
236         my ($class, $name, $arg_param, $uri_param) = @{$cmddef->{$cmd}};
237         my $str = $class->usage_str($name, "$exename $cmd", $arg_param, $uri_param, 'short', $pwcallback);
238         print $fd "\n" if $oldclass && $oldclass ne $class;
239         print $fd "       $str";
240         $oldclass = $class;
241     }
242 }
243
244 my $print_bash_completion = sub {
245     my ($cmddef, $simple_cmd, $bash_command, $cur, $prev) = @_;
246
247     my $debug = 0;
248
249     return if !(defined($cur) && defined($prev) && defined($bash_command));
250     return if !defined($ENV{COMP_LINE});
251     return if !defined($ENV{COMP_POINT});
252
253     my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT});
254     print STDERR "\nCMDLINE: $ENV{COMP_LINE}\n" if $debug;
255
256     my $args = PVE::Tools::split_args($cmdline);
257     my $pos = scalar(@$args) - 2;
258     $pos += 1 if $cmdline =~ m/\s+$/;
259
260     print STDERR "CMDLINE:$pos:$cmdline\n" if $debug;
261
262     return if $pos < 0;
263
264     my $print_result = sub {
265         foreach my $p (@_) {
266             print "$p\n" if $p =~ m/^$cur/;
267         }
268     };
269
270     my $cmd;
271     if ($simple_cmd) {
272         $cmd = $simple_cmd;
273     } else {
274         if ($pos == 0) {
275             &$print_result(keys %$cmddef);
276             return;
277         }
278         $cmd = $args->[1];
279     }
280
281     my $def = $cmddef->{$cmd};
282     return if !$def;
283
284     print STDERR "CMDLINE1:$pos:$cmdline\n" if $debug;
285
286     my $skip_param = {};
287
288     my ($class, $name, $arg_param, $uri_param) = @$def;
289     $arg_param //= [];
290     $uri_param //= {};
291
292     $arg_param = [ $arg_param ] if !ref($arg_param);
293
294     map { $skip_param->{$_} = 1; } @$arg_param;
295     map { $skip_param->{$_} = 1; } keys %$uri_param;
296
297     my $fpcount = scalar(@$arg_param);
298
299     my $info = $class->map_method_by_name($name);
300
301     my $schema = $info->{parameters};
302     my $prop = $schema->{properties};
303
304     my $print_parameter_completion = sub {
305         my ($pname) = @_;
306         my $d = $prop->{$pname};
307         if ($d->{completion}) {
308             my $vt = ref($d->{completion});
309             if ($vt eq 'CODE') {
310                 my $res = $d->{completion}->($cmd, $pname, $cur, $args);
311                 &$print_result(@$res);
312             }
313         } elsif ($d->{type} eq 'boolean') {
314             &$print_result('0', '1');
315         } elsif ($d->{enum}) {
316             &$print_result(@{$d->{enum}});
317         }
318     };
319
320     # positional arguments
321     $pos += 1 if $simple_cmd;
322     if ($fpcount && $pos <= $fpcount) {
323         my $pname = $arg_param->[$pos -1];
324         &$print_parameter_completion($pname);
325         return;
326     }
327
328     my @option_list = ();
329     foreach my $key (keys %$prop) {
330         next if $skip_param->{$key};
331         push @option_list, "--$key";
332     }
333
334     if ($cur =~ m/^-/) {
335         &$print_result(@option_list);
336         return;
337     }
338
339     if ($prev =~ m/^--?(.+)$/ && $prop->{$1}) {
340         my $pname = $1;
341         &$print_parameter_completion($pname);
342         return;
343     }
344
345     &$print_result(@option_list);
346 };
347
348 sub verify_api {
349     my ($class) = @_;
350
351     # simply verify all registered methods
352     PVE::RESTHandler::validate_method_schemas();
353 }
354
355 my $get_exe_name = sub {
356     my ($class) = @_;
357     
358     my $name = $class;
359     $name =~ s/^.*:://;
360     $name =~ s/_/-/g;
361
362     return $name;
363 };
364
365 sub generate_bash_completions {
366     my ($class) = @_;
367
368     # generate bash completion config
369
370     $exename = &$get_exe_name($class);
371
372     print <<__EOD__;
373 # $exename bash completion
374
375 # see http://tiswww.case.edu/php/chet/bash/FAQ
376 # and __ltrim_colon_completions() in /usr/share/bash-completion/bash_completion
377 # this modifies global var, but I found no better way
378 COMP_WORDBREAKS=\${COMP_WORDBREAKS//:}
379
380 complete -o default -C '$exename bashcomplete' $exename
381 __EOD__
382 }
383
384 sub find_cli_class_source {
385     my ($name) = @_;
386
387     my $filename;
388
389     $name =~ s/-/_/g;
390
391     my $cpath = "PVE/CLI/${name}.pm";
392     my $spath = "PVE/Service/${name}.pm";
393     foreach my $p (@INC) {
394         foreach my $s (($cpath, $spath)) {
395             my $testfn = "$p/$s";
396             if (-f $testfn) {
397                 $filename = $testfn;
398                 last;
399             }
400         }
401         last if defined($filename);
402     }
403
404     return $filename;
405 }
406
407 sub generate_pod_manpage {
408     my ($class, $podfn) = @_;
409
410     $cli_handler_class = $class;
411
412     $exename = &$get_exe_name($class);
413
414     $podfn = find_cli_class_source($exename) if !defined($podfn);
415
416     die "unable to find source for class '$class'" if !$podfn;
417
418     no strict 'refs';
419     my $def = ${"${class}::cmddef"};
420
421     if (ref($def) eq 'ARRAY') {
422         print_simple_pod_manpage($podfn, @$def);
423     } else {
424         $cmddef = $def;
425
426         $cmddef->{help} = [ __PACKAGE__, 'help', ['cmd'] ];
427
428         print_pod_manpage($podfn);
429     }
430 }
431
432 sub generate_asciidoc_synopsys {
433     my ($class) = @_;
434
435     $cli_handler_class = $class;
436
437     $exename = &$get_exe_name($class);
438
439     no strict 'refs';
440     my $def = ${"${class}::cmddef"};
441
442     if (ref($def) eq 'ARRAY') {
443         print_simple_asciidoc_synopsys(@$def);
444     } else {
445         $cmddef = $def;
446
447         $cmddef->{help} = [ __PACKAGE__, 'help', ['cmd'] ];
448
449         print_asciidoc_synopsys();
450     }
451 }
452
453 my $handle_cmd  = sub {
454     my ($def, $cmdname, $cmd, $args, $pwcallback, $podfn, $preparefunc) = @_;
455
456     $cmddef = $def;
457     $exename = $cmdname;
458
459     $cmddef->{help} = [ __PACKAGE__, 'help', ['cmd'] ];
460
461     if (!$cmd) { 
462         print_usage_short (\*STDERR, "no command specified");
463         exit (-1);
464     } elsif ($cmd eq 'verifyapi') {
465         PVE::RESTHandler::validate_method_schemas();
466         return;
467     } elsif ($cmd eq 'printmanpod') {
468         $podfn = find_cli_class_source($exename) if !defined($podfn);
469         print_pod_manpage($podfn);
470         return;
471     } elsif ($cmd eq 'bashcomplete') {
472         &$print_bash_completion($cmddef, 0, @$args);
473         return;
474     }
475
476     &$preparefunc() if $preparefunc;
477
478     $cmd = &$expand_command_name($cmddef, $cmd);
479
480     my ($class, $name, $arg_param, $uri_param, $outsub) = @{$cmddef->{$cmd} || []};
481
482     if (!$class) {
483         print_usage_short (\*STDERR, "unknown command '$cmd'");
484         exit (-1);
485     }
486
487     my $prefix = "$exename $cmd";
488     my $res = $class->cli_handler($prefix, $name, \@ARGV, $arg_param, $uri_param, $pwcallback);
489
490     &$outsub($res) if $outsub;
491 };
492
493 my $handle_simple_cmd = sub {
494     my ($def, $args, $pwcallback, $podfn, $preparefunc) = @_;
495
496     my ($class, $name, $arg_param, $uri_param, $outsub) = @{$def};
497     die "no class specified" if !$class;
498
499     if (scalar(@$args) >= 1) {
500         if ($args->[0] eq 'help') {
501             my $str = "USAGE: $name help\n";
502             $str .= $class->usage_str($name, $name, $arg_param, $uri_param, 'long', $pwcallback);
503             print STDERR "$str\n\n";
504             return;
505         } elsif ($args->[0] eq 'bashcomplete') {
506             shift @$args;
507             &$print_bash_completion({ $name => $def }, $name, @$args);
508             return;
509         } elsif ($args->[0] eq 'verifyapi') {
510             PVE::RESTHandler::validate_method_schemas();
511             return;
512         } elsif ($args->[0] eq 'printmanpod') {
513             $podfn = find_cli_class_source($name) if !defined($podfn);
514             print_simple_pod_manpage($podfn, @$def);
515             return;
516         }
517     }
518
519     &$preparefunc() if $preparefunc;
520
521     my $res = $class->cli_handler($name, $name, \@ARGV, $arg_param, $uri_param, $pwcallback);
522
523     &$outsub($res) if $outsub;
524 };
525
526 sub run_cli {
527     my ($class, $pwcallback, $podfn, $preparefunc) = @_;
528
529     # Note: "depreciated function run_cli - use run_cli_handler instead";
530     
531     die "password callback is no longer supported" if $pwcallback;
532
533     run_cli_handler($class, podfn => $podfn, prepare => $preparefunc);
534 }
535
536 sub run_cli_handler {
537     my ($class, %params) = @_;
538
539     $cli_handler_class = $class;
540
541     $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
542
543     foreach my $key (keys %params) {
544         next if $key eq 'podfn';
545         next if $key eq 'prepare';
546         next if $key eq 'no_init'; # used by lxc hooks
547         die "unknown parameter '$key'";
548     }
549
550     my $podfn = $params{podfn};
551     my $preparefunc = $params{prepare};
552     my $no_init = $params{no_init};
553
554     my $pwcallback = $class->can('read_password');
555
556     $exename = &$get_exe_name($class);
557
558     initlog($exename);
559
560     if ($class !~ m/^PVE::Service::/) {
561         die "please run as root\n" if $> != 0;
562
563         PVE::INotify::inotify_init() if !$no_init;
564
565         my $rpcenv = PVE::RPCEnvironment->init('cli');
566         $rpcenv->init_request() if !$no_init;
567         $rpcenv->set_language($ENV{LANG});
568         $rpcenv->set_user('root@pam');
569     }
570
571     no strict 'refs';
572     my $def = ${"${class}::cmddef"};
573
574     if (ref($def) eq 'ARRAY') {
575         &$handle_simple_cmd($def, \@ARGV, $pwcallback, $podfn, $preparefunc);
576     } else {
577         $cmddef = $def;
578         my $cmd = shift @ARGV;
579         &$handle_cmd($cmddef, $exename, $cmd, \@ARGV, $pwcallback, $podfn, $preparefunc);
580     }
581
582     exit 0;
583 }
584
585 1;