File Coverage

blib/lib/App/EC2Cssh.pm
Criterion Covered Total %
statement 33 127 25.9
branch 0 32 0.0
condition 0 9 0.0
subroutine 11 18 61.1
pod 0 2 0.0
total 44 188 23.4


line stmt bran cond sub pod time code
1             package App::EC2Cssh;
2             $App::EC2Cssh::VERSION = '0.006';
3 1     1   1011 use Moose;
  1         352934  
  1         6  
4              
5             =head1 NAME
6              
7             App::EC2Cssh - Package for ec2-cssh CLI application
8              
9             =head1 SYNOSPSIS
10              
11             See L<ec2-cssh>
12              
13             =cut
14              
15 1     1   5407 use autodie qw/:all/;
  1         11124  
  1         4  
16 1     1   15613 use Cwd;
  1         12  
  1         49  
17 1     1   4 use File::Spec;
  1         1  
  1         18  
18 1     1   663 use IO::Socket::SSL;
  1         54077  
  1         8  
19 1     1   965 use Net::Amazon::EC2;
  1         1440340  
  1         39  
20 1     1   567 use Safe;
  1         27135  
  1         48  
21 1     1   544 use Text::Template;
  1         2017  
  1         39  
22              
23 1     1   396 use IO::Pipe;
  1         813  
  1         26  
24 1     1   813 use AnyEvent;
  1         3765  
  1         33  
25              
26 1     1   401 use Log::Any qw/$log/;
  1         5711  
  1         6  
27              
28             # Config stuff.
29             has 'config' => ( is => 'ro', isa => 'HashRef', lazy_build => 1);
30             has 'config_file' => ( is => 'ro' , isa => 'Maybe[Str]');
31             has 'config_files' => ( is => 'ro' , isa => 'ArrayRef[Str]' , lazy_build => 1);
32              
33             # Run options stuff
34             has 'set' => ( is => 'ro' , isa => 'Str', required => 1 );
35             has 'demux_command' => ( is => 'ro', isa => 'Maybe[Str]', required => 0, predicate => 'has_demux_command' );
36              
37             # Operational stuff.
38             has 'ec2' => ( is => 'ro', isa => 'Net::Amazon::EC2', lazy_build => 1);
39              
40              
41             sub _build_config{
42 0     0     my ($self) = @_;
43 0           my $config = {};
44 0           foreach my $file (reverse @{$self->config_files()} ){
  0            
45 0           $log->info("Loading $file..");
46 0           my $file_config = do $file;
47              
48 0 0         my $ec2_config = { %{ $config->{ec2_config} || {} } , %{ $file_config->{ec2_config} || {} } };
  0 0          
  0            
49 0 0         my $ec2_sets = { %{ $config->{ec2_sets} || {} } , %{ $file_config->{ec2_sets} || {} } };
  0 0          
  0            
50 0           $config = { %{$config} , %{$file_config} , 'ec2_config' => $ec2_config , ec2_sets => $ec2_sets };
  0            
  0            
51             }
52              
53 0           $log->info("Available sets: " .( join(', ', sort keys %{$config->{ec2_sets}})));
  0            
54 0           return $config;
55             }
56              
57             sub _build_config_files{
58 0     0     my ($self) = @_;
59             my @candidates = (
60             ( $self->config_file() ? $self->config_file() : () ),
61             File::Spec->catfile( getcwd() , '.ec2cssh.conf' ),
62 0 0         File::Spec->catfile( $ENV{HOME} , '.ec2cssh.conf' ),
63             File::Spec->catfile( '/' , 'etc' , 'ec2ssh.conf' )
64             );
65 0           my @files = ();
66 0           foreach my $candidate ( @candidates ){
67 0 0         if( -r $candidate ){
68 0           $log->info("Found config file '$candidate'");
69 0           push @files , $candidate;
70             }
71             }
72 0 0         unless( @files ){
73 0           die "Cannot find any config files amongst ".join(', ' , @candidates )."\n";
74             }
75 0           return \@files;
76             }
77              
78             sub _build_ec2{
79 0     0     my ($self) = @_;
80              
81             # Hack so we never verify Amazon's host. Whilst still keeping HTTPS
82 0     0     IO::Socket::SSL::set_defaults( SSL_verify_callback => sub{ return 1; } );
  0            
83 0 0         my $ec2 = Net::Amazon::EC2->new({ %{ $self->config()->{ec2_config} || die "No ec2_config in config\n" } , ssl => 1 } );
  0            
84 0           return $ec2;
85             }
86              
87             sub main{
88 0     0 0   my ($self) = @_;
89              
90 0           my @hosts;
91 0           my %hostnames = ();
92 0           $log->info("Listing instances for set='".$self->set()."'");
93              
94 0           my $set_config = {};
95 0 0         if( $self->set() ){
96 0   0       $set_config = $self->config()->{ec2_sets}->{$self->set()} || die "No ec2_set '".$self->set()."' defined in config\n";
97             }
98              
99 0           my $reservation_infos = $self->ec2->describe_instances( %{ $set_config } ) ;
  0            
100 0           foreach my $ri ( @$reservation_infos ){
101 0           my $instances = $ri->instances_set();
102 0           foreach my $instance ( @$instances ){
103 0           my $host = $instance->dns_name();
104 0 0         unless( $host ){
105 0           $log->warn("Instance ".$instance->instance_id()." does not have a dns_name. Skipping");
106 0           next;
107             }
108 0           $log->debug("Adding host $host");
109 0           push @hosts , $host;
110              
111 0 0         if( my $tagset = $instance->tag_set() ){
112 0           foreach my $tag ( @$tagset ){
113 0   0       $log->trace("Host has tag: ".$tag->key().':'.( $tag->value() // 'UNDEF' ));
114 0 0         if( $tag->key() eq 'Name' ){
115 0           $log->debug("Host $host name is ".$tag->value());
116 0           $hostnames{$host} = $tag->value();
117             }
118             }
119             }
120             }
121             }
122              
123 0           $log->info("Got ".scalar( @hosts )." hosts");
124 0 0         if( $self->has_demux_command() ){
125 0           return $self->do_demux_command( \@hosts , \%hostnames );
126             }
127              
128             # No demux command, just carry on using the configured command for multiple hosts.
129             my $tmpl = Text::Template->new( TYPE => 'STRING',
130 0   0       SOURCE => $self->config()->{command} || die "Missing command in config\n"
131             );
132 0 0         unless( $tmpl->compile() ){
133 0           die "Cannot compile template from '".$self->config()->{command}."' ERROR:".$Text::Template::ERROR."\n";
134             }
135              
136 0           my $command = $tmpl->fill_in( SAFE => Safe->new(),
137             HASH => {
138             hosts => \@hosts,
139             hostnames => \%hostnames,
140             }
141             );
142 0           $log->info("Will do '".substr($command, 0, 80)."..'");
143 0 0         if( $log->is_debug() ){
144 0           $log->debug($command);
145             }
146 0           my $sys_return = system( $command );
147 0           $log->info("Done (returned $sys_return)");
148 0           return $sys_return;
149             }
150              
151             $| = 1;
152              
153             sub do_demux_command{
154 0     0 0   my ($self, $hosts, $hostnames) = @_;
155              
156 0           $log->info("Will do ".$self->demux_command()." on each of the hosts");
157              
158 0           my $tmpl = Text::Template->new( TYPE => 'STRING',
159             SOURCE => $self->demux_command() );
160              
161 0           my @finished = ();
162 0           foreach my $host ( @$hosts ){
163 0           my $hostname = $hostnames->{$host};
164 0           my $command = $tmpl->fill_in( HASH => { host => $host , hostname => $hostname } );
165 0           $log->debug("Will do ".$command);
166 0           my $io_h = IO::Pipe->new()->reader( $command );
167 0           my $w;
168 0   0       my $toprint = $hostname || $host;
169 0           my $finished = AnyEvent->condvar();
170 0           push @finished , $finished;
171             $w = AnyEvent->io( fh => $io_h,
172             poll => 'r',
173             cb => sub{
174 0     0     my $line = <$io_h>;
175 0 0         unless( $line ){
176 0           undef $w;
177 0           $finished->send();
178 0           return;
179             }
180 0           print "$toprint: ".$line;
181 0           });
182             }
183              
184 0           map{ $_->recv() } @finished;
  0            
185 0           return 0;
186             }
187              
188             __PACKAGE__->meta->make_immutable();