File Coverage

blib/lib/Respite/CommandLine.pm
Criterion Covered Total %
statement 12 181 6.6
branch 0 118 0.0
condition 0 90 0.0
subroutine 4 22 18.1
pod 1 8 12.5
total 17 419 4.0


line stmt bran cond sub pod time code
1             package Respite::CommandLine;
2              
3             =head1 NAME
4              
5             Respite::CommandLine - Provide an easy way to get commandline abstraction of Respite::Base
6              
7             =cut
8              
9 1     1   1396 use strict;
  1         3  
  1         42  
10 1     1   5 use warnings;
  1         2  
  1         70  
11 1     1   6 use Throw qw(throw);
  1         2  
  1         5  
12 1     1   51 use Scalar::Util qw(blessed);
  1         2  
  1         4263  
13              
14             sub new {
15 0     0 0   my ($class, $args) = @_;
16 0 0         return bless {%{$args || {}}}, $class;
  0            
17             }
18              
19 0     0 1   sub api_meta { shift->{'api_meta'} }
20 0     0 0   sub dispatch_class { shift->{'dispatch_class'} }
21              
22             sub dispatch_factory { # this is identical to code in Respite::Server
23 0     0 0   my ($self, $preload) = @_;
24 0   0       return $self->{'dispatch_factory'} ||= do {
25 0   0       my $meta = $self->api_meta || $self->dispatch_class || throw "Missing one of api_meta or dispatch_class";
26 0 0         if (!ref $meta) {
    0          
27 0           (my $file = "$meta.pm") =~ s|::|/|g;
28 0 0 0       throw "Failed to load dispatch class", {class => $meta, file => $file, msg => $@} if !$meta->can('new') && !eval { require $file };
  0            
29 0 0         throw "Specified class does not have a run_method method", {class => $meta} if ! $meta->can('run_method');
30 0     0     sub { $meta->new(@_) };
  0            
31             } elsif ($meta->{'remote'}) {
32 0           require Respite::Client;
33 0 0   0     sub { Respite::Client->new({%{shift() || {}}, %$meta}) };
  0            
  0            
34             } else {
35 0           require Respite::Base;
36 0 0         Respite::Base->new({api_meta => $meta})->api_preload if $preload;
37 0 0   0     sub { Respite::Base->new({%{shift() || {}}, api_meta => $meta}) };
  0            
  0            
38             }
39             };
40             }
41              
42             ###----------------------------------------------------------------###
43              
44 0     0 0   sub run_commandline { shift->run(@_) }
45              
46             sub run {
47 0     0 0   my ($self, $args) = @_;
48 0 0         $self = $self->new($args) if ! ref($self);
49              
50 0           my $obj = $self->dispatch_factory->();
51 0   0       my $ARGV = $args->{'argv'} || $self->{'argv'} || \@ARGV;
52              
53 0   0       my $method = shift(@$ARGV) || return print $self->_pod($obj, {brief => 1});
54 0 0         return print $self->_pod($obj, {format => $1}) if $method =~ /^-{0,2}(help|h|pod|p)$/;
55 0           return print $self->_pod($obj, {method => $method, format => $_}) for grep {/^-{1,2}(help|h|pod|p)$/} @ARGV;
  0            
56              
57 0 0         throw "Odd number of args passed to commandline. If you want the last value to be undef pass a :null", {argv => $ARGV, _pretty=>1} if @$ARGV % 2;
58 0           my $req = {@$ARGV};
59 0 0         throw "Cannot use '' as a keyname - possible invalid args", {argv => $ARGV} if exists $req->{''};
60 0 0 0       foreach my $key (keys %$req) { $req->{$key} = __PACKAGE__->can("_$1")->() if $req->{$key} && $req->{$key} =~ /^:(null|true|false)$/ }
  0            
61 0 0 0       $req = Data::URIEncode::flat_to_complex($req) || {} if !$self->{'no_data_uriencode'} && eval { require Data::URIEncode };
  0   0        
62              
63 0           my $data = $self->_run_method($obj,$method, $req);
64 0 0         my $meta = $ENV{'SHOW_META'} ? $self->_run_method($obj,"${method}__meta", $req) : undef;
65 0           $self->print_data($data, $req, $meta);
66 0 0 0       exit(1) if ref($data) && $data->{'error'};
67             }
68              
69             sub run_method {
70 0     0 0   my ($self, $method, $args) = @_;
71 0 0         $self = $self->new($args) if ! ref($self);
72 0           my $obj = $self->dispatch_factory->();
73 0           return $self->_run_method($obj, $method, $args);
74             }
75              
76             sub _run_method {
77 0     0     my ($self, $obj, $method, $args, $extra) = @_;
78              
79 0 0         local $args->{'_c'} = ['commandline'] if $obj->can('config') ? !$obj->config(no_trace => undef) : 1;
    0          
80              
81 0 0 0       local $obj->{'remote_ip'} = local $obj->{'api_ip'} = ($ENV{'REALUSER'} || $ENV{'SUDO_USER'}) ? 'sudo' : 'cmdline';
82 0 0 0       local $obj->{'api_brand'} = $ENV{'BRAND'} || $ENV{'PROV'} if $obj->isa('Respite::Base') && ($ENV{'BRAND'} || $ENV{'PROV'});
      0        
      0        
83 0   0       local $obj->{'remote_user'} = $ENV{'REALUSER'} || $ENV{'SUDO_USER'} || $ENV{'REMOTE_USER'} || $ENV{'USER'} || (getpwuid($<))[0] || '-unknown-';
84 0 0 0       local $obj->{'token'} = $self->{'token'} || $ENV{'ADMIN_Respite_TOKEN'} if $self->{'token'} || $ENV{'ADMIN_Respite_TOKEN'};
      0        
85 0           local $obj->{'transport'} = 'cmdline';
86 0 0         $obj->commandline_init($method, $args, $self) if $obj->can('commandline_init');
87              
88             my $run = sub {
89 0 0   0     my $data = eval { $obj->can('run_method') ? $obj->run_method(@_) : $obj->$method($args, ($extra ? $extra : ())) };
  0 0          
90 0 0         $data = $@ if ! ref $data;
91 0 0 0       return !ref($data) ? {error => 'Commandline failed', msg => $data} : (blessed($data) && $data->can('data')) ? $data->data : $data;
    0          
92 0           };
93 0           my $ref = $run->($method, $args, $extra);
94 0           while ($ref->{'error'}) {
95 0 0 0       last if !$ref->{'type'} || $ref->{'type'} !~ /^token_\w+$/;
96 0 0         last if $self->{'no_token_retry'};
97 0           warn "Prompting for authorization and retry ($ref->{'type'}: $ref->{'error'})\n";
98 0 0         eval { require IO::Prompt } || throw "Please install IO::Prompt to authenticate from commandline", {msg => $@};
  0            
99 0   0       my $user = ''.IO::Prompt::prompt(" Web Auth Username: ", -d => $obj->{'remote_user'}) || $obj->{'remote_user'};
100 0   0       my $pass = ''.IO::Prompt::prompt(" Web Auth Password ($user): ", -e => '*') || throw "Cannot proceed without password";
101             my $key = !$obj->can('config') ? $config::config{'plaintext_public_key'}
102 0 0   0     : $obj->config(plaintext_public_key => sub { $obj->config(plaintext_public_key => sub { $obj->_configs->{'plaintext_public_key'} }, 'emp_auth') });
  0            
  0            
103 0 0         if (!$key) {
    0          
104 0           warn " Could not find plaintext_public_key in config - sending plaintext password\n";
105 0           } elsif (!eval { require Crypt::OpenSSL::RSA }) {
106 0           warn " (Crypt::OpenSSL::RSA is not installed - install to avoid sending plaintext password)\n";
107             } else {
108 0           my $c = Crypt::OpenSSL::RSA->new_public_key($key);
109 0           my $len = length($pass) + 1;
110 0           $pass = pack 'u*', $c->encrypt(pack "Z$len", $pass);
111 0           $pass = "RSA".length($pass).":$pass";
112             }
113 0           $obj->{'token'} = "$user/i:cmdline/$pass";
114 0           $ref = $run->(hello => {test_auth => 1});
115 0   0       $self->{'token'} = $obj->{'token'} = $ref->{'token'} || throw "Did not get a token back from successful test_auth", {data => $ref};
116 0           warn "\nexport ADMIN_Respite_TOKEN=$obj->{'token'}\n\n";
117 0           $ref = $run->($method, $args, $extra);
118             }
119 0           return $ref;
120             }
121              
122             sub print_data {
123 0     0 0   my ($self, $data, $args, $meta) = @_;
124 0 0 0       if ($ENV{'CSV'} and my @fields = grep {ref($data->{$_}) eq 'ARRAY' && ref($data->{$_}->[0]) eq 'HASH'} sort keys %$data) {
  0 0          
125 0           require Text::CSV_XS;
126 0           my $csv = Text::CSV_XS->new({eol => "\n"});
127 0           foreach my $field (@fields) {
128 0 0         print "----- $field -------------------------\n" if @fields > 1;
129 0 0         my @keys = sort {($a eq 'id') ? -1 : ($b eq 'id') ? 1 : $a cmp $b } keys %{ $data->{'rows'}->[0] };
  0 0          
  0            
130 0           $csv->print(\*STDOUT, \@keys);
131 0 0         $csv->print(\*STDOUT, [map {ref($_) eq 'ARRAY' ? join(",",@$_) : ref($_) eq 'HASH' ? join(",",%$_) : $_} @$_{@keys}]) for @{ $data->{'rows'} };
  0 0          
  0            
132             }
133 0           exit;
134             }
135 0 0 0       if ($ENV{'YAML'}) {
    0          
    0          
136 0 0         eval { require YAML } || throw "Could not load YAML for output", {msg => $@};
  0            
137 0           print YAML->new->Dump($data);
138 0           } elsif ($ENV{'JSON'} || ! eval { require Text::PrettyTable }) {
139 0 0         eval { require JSON } || throw "Could not load JSON for output", {msg => $@};
  0            
140 0           my $json = JSON->new->utf8->allow_nonref->convert_blessed->pretty->canonical;
141 0 0         print "meta = ".$json->encode($meta) if $ENV{'SHOW_META'};
142 0           print "args = ".$json->encode($args);
143 0           print "data = ".$json->encode($data);
144             } elsif ($ENV{'PERL'}) {
145 0 0         if (eval { require Data::Debug }) {
  0            
146 0           Data::Debug::debug($args, $data);
147             } else {
148 0           require Data::Dumper;
149 0 0         print Data::Dumper::Dumper($_) for $ENV{'SHOW_META'} ? $meta : (), $args, $data;
150             }
151             } else {
152 0           my $p = PrettyTable->new({auto_collapse => 1});
153 0 0         if ($ENV{'SHOW_META'}) {
154 0           print "Meta:\n";
155 0           print $p->tablify($meta);
156             }
157 0           print "Arguments:\n";
158 0           print $p->tablify($args);
159 0 0 0       if ((scalar(keys %$data) == 1 || $data->{'n_pages'} && $data->{'n_pages'} == 1) && $data->{'rows'}) {
      0        
160 0           print "Data Rows:\n";
161 0           print $p->tablify($data->{'rows'});
162             } else {
163 0           print "Data:\n";
164 0           print $p->tablify($data);
165             }
166             }
167             }
168              
169 0     0     sub _false { require JSON; JSON::false() }
  0            
170 0     0     sub _null { undef }
171 0     0     sub _true { require JSON; JSON::true() }
  0            
172              
173             sub _pod {
174 0     0     my ($self, $obj, $args) = @_;
175 0           my $class = ref($obj);
176 0   0       my $script = $args->{'script'} || $0;
177 0   0       my $meth = $args->{'method'} || 'methodname';
178 0           my $out = "=head1 NAME\n\n"
179             ."$script - commandline interface to $class methods\n\n"
180             ."=head1 SYNOPSIS\n\n"
181             ." $script $meth\n\n"
182             ." $script $meth --help\n\n"
183             ." $script $meth methods # brief list of methods \n\n"
184             ." $script $meth key1 value1 key2 value2\n\n"
185             ." $script $meth key1:0 arrayvalue1 key1:1 arrayvalue2\n\n"
186             ." JSON=1 $script $meth key1 value1 key2 value2\n\n"
187             ." YAML=1 $script $meth key1 value1 key2 value2\n\n"
188             ." PERL=1 $script $meth key1 value1 key2 value2\n\n"
189             ." CSV=1 $script $meth key1 value1 key2 value2 (only works for fields that are arrays of hashes)\n\n"
190             ." SHOW_META=1 $script $meth key1 value1 key2 value2 (includes meta information for $meth)\n\n"
191             ."Arguments for the hashref should be passed on the commandline as"
192             ." simple key value pairs. If the arguments are more complex, you can"
193             ." pass values in any of the ways that L supports.\n\n"
194             ."=head1 METHODS\n\n";
195 0 0         if ($args->{'brief'}) {
196 0           $out .= join(", ", sort keys %{ $self->_run_method($obj, methods => {})->{'methods'} })."\n\n";
  0            
197             } else {
198 0 0         my $methods = ($_ = $args->{'method'}) ? {$_ => $self->_run_method($obj, "${_}__meta", {_flat => 1})} : $self->_run_method($obj, methods => {meta => 1, _flat => 1})->{'methods'};
199 0           foreach my $meth (sort keys %$methods) {
200 0           my $m = $methods->{$meth};
201 0           $out .= "=head2 C<$meth>\n\n";
202 0 0         $out .= "$m->{'desc'}\n\n" if $m->{'desc'};
203 0           $out .= "=over 4\n\n";
204 0           my $args = $m->{'args'}; my %uk;
  0            
205 0 0 0       foreach my $field (grep {!$uk{$_}++} (map {split /\s*,\s*/} ref($args->{'group order'}) ? @{$args->{'group order'}} : $args->{'group order'}||()), grep {!/^group /} sort keys %$args) {
  0            
  0            
  0            
  0            
206 0           $out .= "=item C<$field>\n\n";
207 0 0 0       Data::Debug::debug($meth, $field, $args->{$field}) if ! ref($args->{$field}) && eval {require Data::Debug};
  0            
208 0 0         $out .= "(required)\n\n" if $args->{$field}->{'required'};
209 0 0         $out .= "$args->{$field}->{'desc'}\n\n" if $args->{$field}->{'desc'};
210             }
211 0           $out .= "=back\n\n";
212             }
213             }
214 0           $out .= "=cut\n";
215 0 0 0       if (!$args->{'format'} || ($args->{'format'} && $args->{'format'} =~ /h/)) {
      0        
216 0           require Pod::Text;
217 0           require IO::String;
218 0   0       my $cols = $ENV{'COLUMNS'} || eval {
219             require IO::Interactive;
220             die if ! IO::Interactive::is_interactive(*STDOUT);
221             require Term::ReadKey; (Term::ReadKey::GetTerminalSize(\*STDOUT))[0]
222             } || 80;
223 0           Pod::Text->new(width => $cols)->parse_from_file(IO::String->new($out), IO::String->new(my $txt));
224 0           return $txt;
225             }
226 0           return $out;
227             }
228              
229             1;