File Coverage

blib/lib/Clustericious/Client/Command.pm
Criterion Covered Total %
statement 67 147 45.5
branch 22 82 26.8
condition 16 72 22.2
subroutine 13 16 81.2
pod 1 1 100.0
total 119 318 37.4


line stmt bran cond sub pod time code
1             package Clustericious::Client::Command;
2              
3 9     9   22479 use strict;
  9         19  
  9         268  
4 9     9   47 use warnings;
  9         24  
  9         419  
5              
6             # ABSTRACT: Command line type processing for clients.
7             our $VERSION = '1.27'; # VERSION
8              
9              
10 9     9   61 use File::Basename qw/basename/;
  9         19  
  9         692  
11 9     9   75 use YAML::XS qw(Load Dump LoadFile);
  9         124  
  9         515  
12 9     9   55 use Log::Log4perl qw/:easy/;
  9         22  
  9         82  
13 9     9   7303 use Scalar::Util qw/blessed/;
  9         20  
  9         491  
14 9     9   2761 use Data::Rmap qw/rmap_ref/;
  9         11749  
  9         636  
15 9     9   68 use File::Temp;
  9         25  
  9         669  
16              
17 9     9   69 use Clustericious::Log;
  9         18  
  9         72  
18 9     9   895 use Clustericious::Client::Meta;
  9         23  
  9         14204  
19              
20             sub _usage {
21 0     0   0 my $class = shift;
22 0         0 my $client = shift;
23 0         0 my $msg = shift;
24 0         0 my $routes = Clustericious::Client::Meta->routes(ref $client);
25 0         0 my $objects = Clustericious::Client::Meta->objects(ref $client);
26 0 0       0 print STDERR $msg,"\n" if $msg;
27 0         0 print STDERR "Usage:\n";
28 0         0 my $name = basename($0);
29 0 0 0     0 print STDERR <<EOPRINT if $routes && @$routes;
30 0         0 @{[ join "\n", map " $name [opts] $_->[0] $_->[1]", @$routes ]}
31             EOPRINT
32 0 0 0     0 print STDERR <<EOPRINT if $objects && @$objects;
33             $name [opts] <object>
34             $name [opts] <object> <keys>
35             $name [opts] search <object> [--key value]
36             $name [opts] create <object> [<filename list>]
37             $name [opts] update <object> <keys> [<filename>]
38             $name [opts] delete <object> <keys>
39              
40             where "opts" are as described in Clustericious::Log::CommandLine, or
41             may be "--remote <remote>" to specify a remote to use from the
42             config file (see Clustericious::Client).
43              
44             and "<object>" may be one of the following :
45 0         0 @{[ join "\n", map " $_->[0] $_->[1]", @$objects ]}
46              
47             EOPRINT
48              
49 0         0 exit 0;
50             }
51              
52              
53             our $Ssh = "ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o PasswordAuthentication=no";
54             sub _expand_remote_glob {
55             # Given a glob, e.g. omidev.gsfc.nasa.gov:/devsips/app/*/doc/Description.txt
56             # Return a list of filenames with the host prepended to each one, e.g.
57             # omidev.gsfc.nasa.gov:/devsips/app/foo-1/doc/Description.txt
58             # omidev.gsfc.nasa.gov:/devsips/app/bar-2/doc/Description.txt
59 0     0   0 my $pattern = shift;
60 0 0       0 return ( $pattern ) unless $pattern =~ /^(\S+):(.*)$/;
61 0         0 my ($host,$file) = ( $1, $2 );
62 0 0       0 return ( $pattern ) unless $file =~ /[*?]/;
63 0         0 INFO "Remote glob : $host:$file";
64 0         0 my $errs = File::Temp->new();
65 0         0 my @filenames = `$Ssh $host ls $file 2>$errs`;
66 0 0       0 LOGDIE "Error ssh $host ls $file returned (code $?)".`tail -2 $errs` if $?;
67 0         0 return map "$host:$_", @filenames;
68             }
69              
70             sub _load_yaml {
71             # _load_yaml can take a local filename or a remote ssh host + filename and
72             # returns parsed yaml content.
73 0     0   0 my $filename = shift;
74              
75 0 0       0 unless ($filename =~ /^(\S+):(.*)$/) {
76 0         0 INFO "Loading $filename";
77 0 0       0 my $parsed = LoadFile($filename) or LOGDIE "Invalid YAML : $filename\n";
78 0         0 return $parsed;
79             }
80              
81 0         0 my ($host,$file) = ($1,$2);
82 0         0 INFO "Loading remote file $file from $host";
83 0         0 my $errs = File::Temp->new();
84 0         0 my $content = `$Ssh $host cat $file 2>$errs`;
85 0 0       0 if ($?) {
86 0         0 LOGDIE "Error (code $?) running ssh $host cat $file : ".`tail -2 $errs`;
87             }
88 0 0       0 my $parsed = Load($content) or do {
89 0         0 ERROR "Invalid YAML: $filename";
90 0         0 return;
91             };
92 0         0 return $parsed;
93             }
94              
95             sub run {
96 6     6 1 13416 my $class = shift;
97 6         13 my $client = shift;
98 6 50       19 my @args = @_ ? @_ : @ARGV;
99              
100 6 50 33     33 return $class->_usage($client) if !$args[0] || $args[0] =~ /help/;
101              
102             # Preprocessing for any common args, e.g. --remote
103 6         8 my $arg;
104             ARG :
105 6         22 while ($arg = shift @args) {
106 6         12 for ($arg) {
107 6 50       12 /--remote/ and do {
108 0         0 my $remote = shift @args;
109 0         0 TRACE "Using remote $remote";
110 0         0 $client->remote($remote);
111 0         0 next ARG;
112             };
113 6         12 last ARG;
114             }
115             }
116              
117 6 50       15 my $method = $arg or $class->_usage($client);
118              
119             # Map some alternative command line forms.
120 6         7 my $try_stdin;
121 6 50       13 if ( $method eq 'create' ) {
122 0 0       0 $method = shift @args or $class->_usage( $client, "Missing <object>" );
123 0         0 $try_stdin = 1;
124             }
125              
126 6 50       11 if ( $method =~ /^(delete|search)$/ ) { # e.g. search -> app_search
127 0         0 $method = ( shift @args ) . '_' . $method;
128             }
129              
130 6 50       24 unless ($client->can($method)) {
131 0         0 $class->_usage($client, "Unrecognized argument : $method");
132 0         0 return;
133             }
134              
135 6         38 my $meta = Clustericious::Client::Meta::Route->new(
136             route_name => $method,
137             client_class => ref $client
138             );
139              
140 6 100       49 if ($meta->get('args')) {
141             # No heuristics for args.
142              
143 5         112 my $obj = $client->$method({ command_line => 1 }, @args);
144              
145 3 50       38 ERROR $client->errorstring if $client->has_error;
146              
147             # Copied from below, until that code is deprecated.
148 3 50 33     57 if ( blessed($obj) && $obj->isa("Mojo::Transaction") ) {
    50 33        
    50 33        
      33        
      33        
149 0 0       0 if ( my $res = $obj->success ) {
150 0         0 print $res->code," ",$res->default_message,"\n";
151             } else {
152 0         0 my ( $message, $code ) = $obj->error;
153 0 0       0 ERROR $code if $code;
154 0         0 ERROR $message;
155             }
156             } elsif (ref $obj eq 'HASH' && keys %$obj == 1 && $obj->{text}) {
157 0         0 print $obj->{text};
158             } elsif ($client->tx && $client->tx->req->method eq 'POST' && $meta->get("quiet_post")) {
159 0         0 my $msg = $client->res->code." ".$client->res->default_message;
160 0         0 my $got = $client->res->json;
161 0 0 0     0 if ($got && ref $got eq 'HASH' and keys %$got==1 && $got->{text}) {
      0        
      0        
162 0         0 $msg .= " ($got->{text})";
163             }
164 0         0 INFO $msg;
165             } else {
166 3         22 print _prettyDump($obj);
167             }
168 3         23 return;
169             }
170              
171             # Code below here should be deprecated, these are various heuristics for argument processing.
172              
173 1         3 my @extra_args = ( '/dev/null' );
174 1         3 my $have_filenames;
175              
176             # Assume we have files and/or remote globs
177 1 50 33     4 if ( !$meta->get('dont_read_files') && @args > 0 && ( -r $_[-1] || $_[-1] =~ /^\S+:/ ) ) {
    50 33        
      33        
      33        
      33        
178 0         0 $have_filenames = 1;
179 0         0 @extra_args = ();
180 0         0 while (my $arg = pop @args) {
181 0 0       0 if ($arg =~ /^\S+:/) {
    0          
182 0         0 push @extra_args, _expand_remote_glob($arg);
183             } elsif (-e $arg) {
184 0         0 push @extra_args, $arg;
185             } else {
186 0         0 LOGDIE "Do not know how to interpret argument : $arg";
187             }
188             }
189             } elsif ( $try_stdin && (-r STDIN) && @args==0) {
190 0         0 my $content = join '', <STDIN>;
191 0         0 $content = Load($content);
192 0 0       0 LOGDIE "Invalid yaml content in $method" unless $content;
193 0         0 push @args, $content;
194             }
195              
196             # Finally, run :
197 1         4 for my $arg (@extra_args) {
198 1         2 my $obj;
199 1 50       3 if ($have_filenames) {
200 0         0 $obj = $client->$method(@args, _load_yaml($arg));
201             } else {
202 1         27 $obj = $client->$method(@args);
203             }
204 1 50       20 ERROR $client->errorstring if $client->errorstring;
205 1 50       9 next unless $obj;
206              
207 1 50 33     14 if ( blessed($obj) && $obj->isa("Mojo::Transaction") ) {
    50 33        
    50 33        
      33        
      33        
208 0 0       0 if ( my $res = $obj->success ) {
209 0         0 print $res->code," ",$res->default_message,"\n";
210             } else {
211 0         0 my ( $message, $code ) = $obj->error;
212 0 0       0 ERROR $code if $code;
213 0         0 ERROR $message;
214             }
215             } elsif (ref $obj eq 'HASH' && keys %$obj == 1 && $obj->{text}) {
216 0         0 print $obj->{text};
217             } elsif ($client->tx && $client->tx->req->method eq 'POST' && $meta->get("quiet_post")) {
218 0         0 my $msg = $client->res->code." ".$client->res->default_message;
219 0         0 my $got = $client->res->json;
220 0 0 0     0 if ($got && ref $got eq 'HASH' and keys %$got==1 && $got->{text}) {
      0        
      0        
221 0         0 $msg .= " ($got->{text})";
222             }
223 0         0 INFO $msg;
224             } else {
225 1         9 print _prettyDump($obj);
226             }
227             }
228 1         6 return;
229             }
230              
231             sub _prettyDump {
232 4     4   6 my $what = shift;
233 4 50   12   28 rmap_ref { $_ = $_->iso8601() if ref($_) eq 'DateTime' } $what;
  12         704  
234 4         539 return Dump($what);
235             }
236              
237              
238             1;
239              
240             __END__
241              
242             =pod
243              
244             =encoding UTF-8
245              
246             =head1 NAME
247              
248             Clustericious::Client::Command - Command line type processing for clients.
249              
250             =head1 VERSION
251              
252             version 1.27
253              
254             =head1 SYNOPSIS
255              
256             # in fooclient :
257              
258             use Foo::Client;
259             use Clustericious::Client::Command;
260              
261             Clustericious::Client::Command->run(Foo::Client->new, @ARGV);
262              
263             Then
264              
265             fooclient status
266             fooclient --trace root status
267             fooclient version
268             fooclient foobject 31
269             fooclient foobject_search --color beige
270              
271             =head1 DESCRIPTION
272              
273             This will try to take command line arguments and call the right client
274             methods.
275              
276             Calling C<fooclient bar baz> is equivalent to
277             C<Foo::Client-E<gt>new()-E<gt>bar("baz")>.
278              
279             =head1 CAVEATS
280              
281             There are currently a few heuristics used when one of the arguments
282             is a filename (i.e. is it a YAML file that should be parsed and send
283             as a hashref, or a filename that should be PUT? Should STDIN be
284             used?). These need to be formalized and documented.
285              
286             =head1 METHODS
287              
288             =head2 run
289              
290             Clustericious::Client::Command->run(Some::Clustericious::Client->new, @ARGV);
291              
292             =head1 AUTHOR
293              
294             Original author: Brian Duggan
295              
296             Current maintainer: Graham Ollis E<lt>plicease@cpan.orgE<gt>
297              
298             Contributors:
299              
300             Curt Tilmes
301              
302             Yanick Champoux
303              
304             =head1 COPYRIGHT AND LICENSE
305              
306             This software is copyright (c) 2013 by NASA GSFC.
307              
308             This is free software; you can redistribute it and/or modify it under
309             the same terms as the Perl 5 programming language system itself.
310              
311             =cut