File Coverage

blib/lib/Archer/Shell.pm
Criterion Covered Total %
statement 94 135 69.6
branch 27 52 51.9
condition 6 11 54.5
subroutine 15 18 83.3
pod 0 8 0.0
total 142 224 63.3


line stmt bran cond sub pod time code
1             package Archer::Shell;
2 1     1   781 use strict;
  1         2  
  1         43  
3 1     1   5 use warnings;
  1         3  
  1         44  
4 1     1   1173 use Net::SSH;
  1         31682  
  1         56  
5 1     1   1319 use Term::ReadLine;
  1         3946  
  1         19  
6 1     1   38 use POSIX;
  1         2  
  1         10  
7 1     1   14578 use File::HomeDir;
  1         10507  
  1         79  
8 1     1   11 use Path::Class;
  1         13  
  1         58  
9 1     1   7 use List::MoreUtils qw/uniq/;
  1         2  
  1         2792  
10              
11             sub new {
12 1     1 0 58 my ( $class, $args ) = @_;
13              
14 1         7 return bless {%$args}, $class;
15             }
16              
17             sub run_loop {
18 0     0 0 0 my ( $self, ) = @_;
19              
20             # initialize parallel manager.
21 0   0     0 $self->{parallel} = $self->{context}->{config}->{global}->{parallel}
22             || 'Archer::Parallel::ForkManager';
23 0 0       0 $self->{parallel}->use or die $@;
24              
25             # initialize readline library.
26 0         0 my $term = Term::ReadLine->new('Archer');
27              
28 0         0 my $HISTFILE = file( File::HomeDir->my_home, "/.archer_shell_history" );
29 0         0 my $HISTSIZE = 256;
30              
31             # this won't work with Term::ReadLine::Perl
32             # If there is Term::ReadLine::Gnu, be sure to do : export "PERL_RL=Gnu o=0"
33 0         0 eval { $term->stifle_history($HISTSIZE); };
  0         0  
34              
35 0 0       0 if ($@) {
36 0         0 $self->{context}
37             ->log( 'debug' => "You will need Term::ReadLine::Gnu" );
38             }
39             else {
40 0 0       0 if ( -f $HISTFILE ) {
41 0 0       0 $term->ReadHistory($HISTFILE)
42             or $self->{context}
43             ->log( 'warn' => "cannot read history file: $!" );
44             }
45             }
46              
47 0         0 while ( defined( my $line = $term->readline('archer> ') ) ) {
48 0 0       0 next if $line =~ /^\s*$/;
49 0         0 $self->catch_run($line);
50             }
51              
52 0         0 print "\n";
53              
54 0         0 eval { $term->WriteHistory($HISTFILE); };
  0         0  
55 0 0       0 if ($@) {
56 0         0 $self->{context}
57             ->log( 'debug' => "perlsh: cannot write history file: $!" );
58             }
59              
60             }
61              
62             sub catch_run {
63 8     8 0 7352 my ( $self, $cmd ) = @_;
64              
65 8 100       73 if ( $cmd =~ /^on\s+/ ) {
    100          
    50          
    50          
    100          
66 1 50       9 if ( $cmd =~ /^on\s(.*)\sdo\s(.*)$/ ) {
67 1         24 $self->process_host( $1, $2 );
68             }
69             else {
70 0         0 print "[WARNING] error in your syntax, see help\n";
71             }
72             }
73             elsif ( $cmd =~ /^with\s+/ ) {
74 2 50       12 if ( $cmd =~ /^with\s(.*)\sdo\s(.*)$/ ) {
75 2         10 $self->process_role( $1, $2 );
76             }
77             else {
78 0         0 print "[WARNING] error in your syntax, see help\n";
79             }
80             }
81             elsif ( $cmd =~ /^help/ ) {
82 0         0 $self->help();
83             }
84             elsif ( $cmd =~ /^(quit|exit)/ ) {
85 0         0 print "bye bye\n";
86 0         0 exit;
87             }
88             elsif ( $cmd =~ /^!/ ) {
89 4 50       24 if ( $cmd =~ /^!(\w+)\s?(on|with)?\s?(.*)?$/ ) {
90 4         9 my $task = $1;
91 4         10 my $action = $2;
92 4         7 my $machines = $3;
93 4 50 66     34 if ( defined $action
      66        
94             && defined $machines
95             && length($machines) < 1 )
96             {
97 0         0 return print "[WARNING] error in your syntax, see help\n";
98             }
99 4         60 my $executed = 0;
100 4         6 my %valid_host = map {$_=>1} @{$self->{servers}};
  16         43  
  4         13  
101 4         8 for my $plugin ( @{ $self->{config}->{tasks}->{process} } ) {
  4         17  
102 8 100       24 next if $plugin->{name} ne $task;
103 4         6 $executed = 1;
104 4 100       10 if ( defined $action ) {
105 3 100       7 if ( $action eq "on" ) {
106 1         5 my @hosts = split " ", $machines;
107 1         11 for my $host (uniq @hosts) {
108 2 100       10 $self->process_task( $plugin, $host ) if $valid_host{$host};
109             }
110             }
111             else {
112 2         7 my @roles = split " ", $machines;
113 2         7 my $server_tree = $self->{config}->{projects}->{$self->{context}->{project}};
114 2         4 for my $role (@roles) {
115 3         22 for my $host ( @{ $server_tree->{$role} } ) {
  3         9  
116 6 50       81 $self->process_task( $plugin, $host ) if $valid_host{$host};
117             }
118             }
119             }
120             }
121             else {
122 1         2 for my $host (@{$self->{servers}}) {
  1         2  
123 4         141 $self->process_task( $plugin, $host );
124             }
125             }
126             }
127 4 50       121 if ( $executed == 0 ) {
128 0         0 print "[WARNING] unable to find the requested task: $task\n";
129             }
130             }
131             else {
132 0         0 print "[WARNING] error in your syntax\n";
133             }
134             }
135             else {
136 1         5 $self->process_command($cmd);
137             }
138             }
139              
140             sub process_host {
141 1     1 0 6 my ( $self, $hosts, $cmd ) = @_;
142              
143 1         7 my @hosts = split /\s/, $hosts;
144              
145             # check if hosts are in our config.
146 1         3 my %valid_host = map {$_=>1} @{$self->{servers}};
  4         13  
  1         4  
147 1         3 @hosts = grep { $valid_host{$_} } @hosts;
  3         9  
148              
149 1 50       5 if (@hosts) {
150 1         4 $self->process_command( $cmd, \@hosts );
151             }
152             }
153              
154             sub process_role {
155 2     2 0 8 my ( $self, $roles, $cmd ) = @_;
156              
157 2         9 my @roles = split /\s/, $roles;
158 2         4 my @hosts = ();
159 2         4 my @inexistant = ();
160 2         9 my $server_tree = $self->{config}->{projects}->{$self->{context}->{project}};
161              
162 2         6 for my $role (@roles) {
163 3 50       14 if ( !defined $server_tree->{$role} ) {
164 0         0 push( @inexistant, $role );
165 0         0 next;
166             }
167 3         5 for my $host ( @{ $server_tree->{$role} } ) {
  3         9  
168 6         16 push @hosts, $host;
169             }
170             }
171 2 50       7 if (@inexistant) {
172 0         0 print "[WARNING] inexisting role(s) for "
173             . join( ' ', @inexistant ) . "\n";
174             }
175 2         7 $self->process_command( $cmd, \@hosts );
176             }
177              
178             sub process_command {
179 4     4 0 10 my ( $self, $cmd, $hosts ) = @_;
180 4         34 my $manager = $self->{parallel}->new;
181              
182 4   66     19 $hosts ||= $self->{servers};
183 4         5 $hosts = [ sort( uniq(@{$hosts}) ) ];
  4         36  
184              
185             $manager->run(
186             { elems => $hosts,
187             callback => sub {
188 11     11   19 my $server = shift;
189 11         33 $self->callback( $server, $cmd );
190             },
191 4         41 num => $self->{context}->{parallel_num},
192             }
193             );
194             }
195              
196             sub process_task {
197 11     11 0 21 my ( $self, $plugin, $host ) = @_;
198 11 50       58 my $class = ($plugin->{module} =~ /^\+(.+)$/) ? $1 : "Archer::Plugin::$plugin->{module}";
199 11 50       57 $class->use or die $@;
200 11         139 $class->new(
201             { config => $plugin->{config},
202             project => $self->{context}->{project},
203             server => $host
204             }
205             )->run( $self->{context} );
206             }
207              
208             sub callback {
209 0     0     my ( $self, $server, $cmd ) = @_;
210              
211 0           Net::SSH::sshopen2( $server, *READER, *WRITER, $cmd );
212 0           while () {
213 0           chomp;
214 0           print "[$server] $_\n";
215             }
216 0           close READER;
217 0           close WRITER;
218             }
219              
220             sub help {
221 0     0 0   my ($self) = @_;
222 0           my $help = <
223             To quit, just type quit, exit, or press ctrl-D.
224             This shell is still experimental.
225              
226             execute a command on all servers, just type it directly, like:
227              
228             archer> ping
229              
230             To execute a command on a specific set of servers, specify an 'on' clause.
231             Note that if you specify more than one host name, they must be
232             space-delimited.
233              
234             archer> on app1.foo.com app2.foo.com do ping
235              
236             To execute a command on all servers matching a set of roles:
237              
238             archer> with web db do ping
239              
240             To execute an Archer task, prefix the name with a bang, by default it
241             will be executed only on the role applyed to this task.
242              
243             archer> !restart
244              
245             To execute an Archer task on a specific set of servers:
246              
247             archer> !restart on app1.foo.com app2.foo.com
248              
249             To execute an Archer task on all servers matching a set of roles:
250              
251             archer> !restart with web db
252              
253             HELP
254 0           print $help;
255             }
256              
257             1;
258             __END__