File Coverage

blib/lib/LiquidWeb/Storm/CLI.pm
Criterion Covered Total %
statement 24 268 8.9
branch 0 106 0.0
condition 0 26 0.0
subroutine 8 28 28.5
pod 5 18 27.7
total 37 446 8.3


line stmt bran cond sub pod time code
1             package LiquidWeb::Storm::CLI;
2              
3 1     1   413 use strict;
  1         2  
  1         50  
4              
5             require Exporter;
6             our @ISA = qw(Exporter);
7             our $VERSION = '1.03';
8              
9 1     1   563 use Getopt::Long;
  1         7254  
  1         4  
10 1     1   540 use HTTP::Request;
  1         17478  
  1         36  
11 1     1   527 use LWP::UserAgent;
  1         16256  
  1         26  
12 1     1   551 use Data::Dumper;
  1         4505  
  1         62  
13 1     1   362 use MIME::Base64;
  1         484  
  1         43  
14 1     1   463 use Text::ASCIITable;
  1         13213  
  1         43  
15 1     1   7 use JSON;
  1         2  
  1         6  
16              
17             sub new {
18 0     0 1   my $class = shift;
19              
20 0           my $self = bless {
21             lwhome => "$ENV{HOME}/.lw",
22             apiconfig => "$ENV{HOME}/.lw/config",
23             apisession => "$ENV{HOME}/.lw/session",
24             }, $class;
25              
26 0           my $options = $self->options;
27              
28 0           foreach my $method (qw/help list clean/) {
29 0 0         $self->$method if ($self->{$method});
30             }
31            
32 0           return $self;
33             }
34              
35              
36             sub configure {
37 0     0 0   my ($self, $args) = @_;
38              
39 0 0         mkdir $self->{lwhome} unless -d $self->{lwhome};
40              
41 0           print "LiquidWeb API User: ";
42 0           chomp($self->{configure}{username} = );
43 0           print "LiquidWeb API Secret: ";
44 0           chomp($self->{configure}{secret} = );
45 0           print "Default output type [json,perl,table]: ";
46 0           chomp($self->{configure}{output} = );
47              
48 0           print "Save auth credentials locally? [Y/N default No]: ";
49 0           chomp($self->{configure}{save} = );
50              
51 0           $self->fetchDocs;
52              
53 0 0         if (($self->{configure}{save} =~ m/y/i) ? 1 : 0) {
    0          
54 0   0       $self->{configure}{output} ||= 'json';
55 0 0         open my $session, '>', $self->{apiconfig} or die $!;
56 0           print $session "username=$self->{configure}{username}\nsecret=$self->{configure}{secret}\noutput=$self->{configure}{output}\n";
57 0           close $session;
58 0           exit;
59             }
60             else {
61 0           $self->{username} = $self->{configure}{username};
62 0           $self->{token} = $self->{configure}{secret};
63            
64 0           print "Enter a timeout in seconds for this session: ";
65 0           chomp($self->{configure}{timeout} = );
66             }
67              
68 0           return $self->{configure};
69             }
70              
71             sub fetchDocs {
72 0     0 0   my ($self, $args) = @_;
73              
74 0           my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
75              
76 0           foreach my $version (qw/v1 bleed/) {
77 0           my $req = HTTP::Request->new(GET => "https://www.liquidweb.com/storm/api/docs/$version/docs.json");
78              
79 0           my $response = $ua->request($req);
80 0 0 0       if ($response->code != 200 || $response->content =~ /error/) {
81 0           die($response->content);
82             }
83              
84 0           open my $doc, '>', "$self->{lwhome}/$version.json";
85 0           print $doc $response->content;
86             }
87             }
88            
89             sub options {
90 0     0 0   my $self = shift;
91              
92 0   0       $self->{options} ||= do {
93 0           my $options;
94              
95 0           my %hash = ();
96 0           foreach my $key (qw/output version command list/) {
97 0           $hash{"$key=s"} = \$options->{$key};
98             }
99 0           foreach my $key (qw/configure help clean/) {
100 0           $hash{$key} = \$options->{$key};
101             }
102 0           my ($inputs, $seen);
103 0           foreach my $version (qw/v1 bleed/) {
104 0           my $docs = do { open my $doc, '<', "$self->{lwhome}/$version.json"; local $/; <$doc> };
  0            
  0            
  0            
105              
106 0 0         do {
107 0           $docs = $self->parser->decode($docs);
108 0           foreach my $class (keys %$docs) {
109 0           foreach my $method (keys %{$docs->{$class}{__methods}}) {
  0            
110 0           foreach my $input (keys %{$docs->{$class}{__methods}{$method}{__input}}) {
  0            
111 0           my $value = $docs->{$class}{__methods}{$method}{__input}{$input}{type};
112 0 0 0       do {
113 0 0         if ($value eq 'BOOLEAN') {
    0          
    0          
    0          
    0          
114 0           $hash{$input} = \$options->{$input};
115             }
116             elsif ($value =~ 'HASH') {
117 0           $hash{"$input=s%"} = \$options->{$input};
118             }
119             elsif ($value =~ 'ARRAY') {
120 0           $hash{"$input=s@"} = \$options->{$input};
121             }
122             elsif ($value =~ 'INT') {
123 0           $hash{"$input=i"} = \$options->{$input};
124             }
125             elsif ($value =~ 'FLOAT') {
126 0           $hash{"$input=f"} = \$options->{$input};
127             }
128             else {
129 0           $hash{"$input=s"} = \$options->{$input};
130             }
131             } unless (!$value || $seen->{$input}++);
132             }
133             }
134             }
135             } unless (not $docs);
136             }
137              
138 0           Getopt::Long::GetOptions(%hash);
139              
140 0 0         if (delete $options->{configure}) {
141 0           $self->configure;
142            
143 0 0         if (!$self->{configure}{save}) {
144 0           $options->{command} = 'account.auth.token';
145             }
146            
147 0 0         if (my $timeout = $self->{configure}{timeout}) {
148 0           $options->{timeout} = $timeout;
149             }
150             }
151              
152 0           foreach my $key (qw/help list command version output clean/) {
153 0 0         if (my $value = delete $options->{$key}) {
154 0           $self->{$key} = $value;
155             }
156             }
157              
158 0           foreach my $key (keys %$options) {
159 0 0         delete $options->{$key} if not defined $options->{$key};
160             }
161              
162 0           $options;
163             };
164              
165 0           return $self->{options};
166             }
167              
168             sub clean {
169 0     0 0   my ($self, $args) = @_;
170              
171 0           foreach my $file (qw/apiconfig apisession/) {
172 0 0         if (-e $self->{$file}) {
173 0 0         unlink $self->{$file} or die "error removing $self->{$file}: $!";
174             }
175             }
176 0           print "Successly removed sensitive data\n";
177            
178 0           exit;
179             }
180              
181             sub list {
182 0     0 1   my ($self, $args) = @_;
183              
184 0           my $commands = $self->commands;
185 0           foreach my $version (keys %$commands) {
186 0           foreach my $command (sort { $a cmp $b } keys %{$commands->{$version}}) {
  0            
  0            
187 0 0         if ($self->{list} eq 'all') {
    0          
188 0           print "$command => $version\n";
189             }
190             elsif($command =~ /^$self->{list}/) {
191 0           print "$command => $version\n";
192             }
193             }
194             }
195 0           exit;
196             }
197              
198             sub commands {
199 0     0 0   my ($self, $args) = @_;
200              
201 0           my $commands;
202 0           foreach my $version (qw/v1 bleed/) {
203 0           my $docs = do {
204 0           open my $doc, '<', "$self->{lwhome}/$version.json";
205 0           local $/; <$doc>
206 0           };
207 0 0         do {
208 0           $docs = $self->parser->decode($docs);
209 0           foreach my $class (keys %$docs) {
210 0           my $transform = $class;
211 0           $transform =~ s/\//\./g;
212 0           foreach my $method (keys %{$docs->{$class}{__methods}}) {
  0            
213 0           $commands->{$version}{lc $transform .'.'.$method}++;
214             }
215             }
216             } unless (not $docs);
217             }
218              
219 0           return $commands;
220             }
221              
222             sub version {
223 0     0 0   my ($self, $args) = @_;
224              
225 0 0         my $version = ($self->commands->{bleed}{$self->{command}}) ? 'bleed' : ($self->commands->{v1}{$self->{command}}) ? 'v1' : $self->{version};
    0          
226              
227 0           return $version;
228             }
229              
230             sub buildUrl {
231 0     0 0   my ($self, $args) = @_;
232              
233 0   0       my $version = $self->{version} || $self->version;
234              
235 0           my $request = join('/', 'https://api.stormondemand.com', $version, split(/\./, $self->{command})) . '?encoding=JSON';
236              
237 0           return $request
238             }
239              
240             sub help {
241 0     0 1   my $self = shift;
242              
243 0 0         unless ($self->{command}) {
244 0           my $usage=<
245             Usage: lw-cli [OPTION].. [PARAM]...[PARAM]
246              
247             --help displays this message
248             --configure configures the preferences for your client and syncs the database.
249             enter in values via interactive prompt, that can be reconfigured by running again.
250             each time configure mode is run, the methods database is retrieved from the api.
251             --list lists available commands on the public api server.
252             with 'all' argument, --list lists all available api commands. You can specify partial commands ie. --list=billing or --list=billing.invoice
253             --version specifies the version you want to use.
254             currently only supports [v2 or bleed]. If no version is specified a lookup is performed on bleed, then v2 and uses
255             the version it finds first.
256             --output specifies the api response output type.
257             available types are [ json, text, perl, table ]. default output type is specified --during configure.
258              
259             --clean removes locally stored session and saved authentication credentials.
260              
261             report bugs to bug-LiquidWeb-Storm-CLI\@rt.cpan.org
262              
263             LiquidWeb homepage: http://www.liquidweb.com
264             General help using software: perldoc LiquidWeb::Storm::CLI
265             support email: bug-LiquidWeb-Storm-CLI\@rt.cpan.org
266             USAGE
267 0           print $usage;
268 0           exit;
269             }
270 0           my $version = $self->version;
271              
272 0           my $content = do {
273 0 0         open my $docs, '<', "$self->{lwhome}/$version.json" or
274             die "Method: $self->{command} not found\n";
275 0           local $/; <$docs>
276 0           };
277              
278 0           $content = $self->parser->decode($content);
279              
280 0           foreach my $key (keys %$content) {
281 0           $content->{lc $key} = delete $content->{$key};
282             }
283              
284 0           my @parts = split(/\./,$self->{command});
285 0           my $method = pop @parts;
286            
287 0           my $query = join('/', @parts);
288              
289 0           my $doc = $content->{$query}{__methods}{$method};
290            
291 0           my $params = $self->generateText($doc->{__input});
292              
293 0           my $pod =<
294             NAME
295              
296             $self->{command}
297              
298             DESCRIPTION
299              
300             $doc->{__description}
301              
302             PARAMETERS
303              
304             $params
305              
306             POD
307              
308 0           print $pod;
309              
310 0           exit;
311             }
312              
313             sub output {
314 0     0 0   my $self = shift;
315              
316 0   0       $self->{output} ||= do {
317 0           open my $config, '<', $self->{apiconfig};
318 0           my $content = do { local $/; <$config> };
  0            
  0            
319 0           my @lines = split /\n/, $content;
320              
321 0           foreach my $line (@lines) {
322 0 0         if ($line =~ m/^([output-]+)(\s*?)=(\s*?)(.*?)$/) {
323 0           $self->{output} = $4;
324             }
325             }
326              
327 0           $self->{output};
328             };
329              
330 0           return $self->{output};
331              
332             }
333              
334             sub auth {
335 0     0 1   my $self = shift;
336              
337 0 0 0       do {
338 0 0         if (-e $self->{apiconfig}) {
    0          
339 0           open my $config, '<', $self->{apiconfig};
340 0           my $content = do { local $/; <$config> };
  0            
  0            
341 0           my @lines = split /\n/, $content;
342 0           foreach my $line (@lines) {
343 0 0         if ($line =~ m/^([secret-]+)(\s*?)=(\s*?)(.*?)$/) {
344 0           $self->{secret} = $4;
345             }
346 0 0         if ($line =~ m/^([username-]+)(\s*?)=(\s*?)(.*?)$/) {
347 0           $self->{username} = $4;
348             }
349             }
350             }
351             elsif (-e $self->{apisession}) {
352 0 0         open my $session, '<', $self->{apisession} or die $!;
353 0           my $cookie = do { local $/; <$session>; };
  0            
  0            
354 0           ($self->{username},$self->{secret}) = split(':', decode_base64($cookie));
355             }
356             } unless ($self->{configure}{username} && $self->{configure}{secret});
357              
358             return {
359 0 0         username => $self->{configure}{username} ? $self->{configure}{username} : $self->{username},
    0          
360             secret => $self->{configure}{secret} ? $self->{configure}{secret} : $self->{secret},
361             };
362             }
363              
364             sub tokenize {
365 0     0 0   my ($self, $args) = @_;
366              
367 0 0         open my $session, '>', $self->{apisession} or die $!;
368 0           print $session encode_base64("$self->{username}:$args->{token}");
369 0           close $session;
370              
371 0           return $self;
372             }
373              
374             sub display {
375 0     0 0   my ($self, $args) = @_;
376              
377 0           my $content;
378 0 0         if ($self->output eq 'perl') {
    0          
    0          
379 0           print Data::Dumper->Dump([$args->{content}]);
380             }
381             elsif ($self->output eq 'table') {
382 0           print $self->generateTable($args->{content});
383             }
384             elsif ($self->output eq 'text') {
385 0           print $self->generateText($args->{content});
386             }
387             else {
388 0           print $self->parser->encode($args->{content});
389             }
390            
391 0           return $content;
392             }
393              
394             sub generateText {
395 0     0 0   my ($self, $content) = @_;
396              
397 0           my ($inner, $text);
398             $inner = sub {
399 0     0     my $ref = shift;
400 0           my $key = shift;
401              
402 0 0         if (ref $ref eq 'ARRAY'){
    0          
    0          
403 0 0         $text .= sprintf("%s\n",($key) ? $key : '');
404 0           $inner->($_) for @{$ref};
  0            
405             }
406             elsif (ref $ref eq 'HASH') {
407 0 0         $text .= sprintf("%s\n", ($key) ? $key : '');
408 0           for my $k (sort keys %{$ref}) {
  0            
409 0           $inner->($ref->{$k},$k);
410             }
411             }
412             elsif ($key) {
413 0 0         $text .= sprintf("\t%s: %s\n", $key, $ref ? $ref : 'undef');
414             }
415             else {
416 0           $text .= sprintf("\t\t%s\n", $ref);
417             }
418 0           };
419              
420 0           $inner->($_) for $content;
421              
422 0           return $text;
423             }
424              
425             sub generateTable {
426 0     0 0   my ($self, $content) = @_;
427              
428 0           my $t;
429 0           my $table = Text::ASCIITable->new({
430             allowANSI => 1,
431             headingText => $self->{command}
432             });
433             $t = sub {
434 0     0     my $ref = shift;
435 0           my $key = shift;
436            
437 0           my @keys;
438 0 0         if (ref $ref eq 'ARRAY') {
    0          
439 0           $table->addRow($ref);
440 0           $t->($_) for sort @{$ref};
  0            
441             }
442             elsif (ref $ref eq 'HASH') {
443 0 0         if (exists $ref->{items}) {
444 0           for my $k (sort %{$ref}) { $t->($ref->{$k},$k); }
  0            
  0            
445             }
446             else {
447 0           my (@values, @keys);
448 0           foreach my $key (sort keys %$ref) {
449 0           push @keys, $key;
450 0           push @values, $ref->{$key};
451             }
452 0           $table->setCols(\@keys);
453 0           $table->addRow(\@values);
454             }
455             }
456 0           };
457 0           $t->($_) for $content;
458              
459 0           return $table;
460             }
461            
462             sub parser {
463 0     0 0   my ($self, $args) = @_;
464            
465 0   0       $self->{parser} ||= do {
466 0           JSON->new->utf8(1);
467             };
468 0           return $self->{parser};
469             }
470            
471             sub execute {
472 0     0 1   my ($self, $args) = @_;
473              
474 0 0         die "usage: lw-cli --command=class.subclass.method --param=value\n" unless ($self->{command});
475              
476 0           my $req = HTTP::Request->new(POST => $self->buildUrl);
477              
478 0           $req->content($self->parser->encode({ params => $self->options }));
479              
480 0           $req->authorization_basic($self->auth->{username}, $self->auth->{secret});
481              
482 0 0         my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => $self->options->{'no-verify-ssl'} ? 0 : 1 });
483 0           $ua->timeout($self->{apitimeout});
484              
485 0           my $response = $ua->request($req);
486              
487 0 0 0       if ($response->code != 200 || $response->content =~ /error/) {
488 0           die($response->content);
489             }
490              
491 0           my $content = $self->parser->decode($response->content);
492              
493 0 0         if ($content->{token}) {
494 0           $self->tokenize({ token => $content->{token} });
495             }
496              
497 0           $self->display({ content => $content });
498              
499 0           return $content;
500             }
501              
502             1;
503              
504             __END__