File Coverage

blib/lib/App/EC2Cssh.pm
Criterion Covered Total %
statement 33 118 27.9
branch 0 30 0.0
condition 0 6 0.0
subroutine 11 18 61.1
pod 0 2 0.0
total 44 174 25.2


line stmt bran cond sub pod time code
1             package App::EC2Cssh;
2             $App::EC2Cssh::VERSION = '0.005';
3 1     1   960 use Moose;
  1         295216  
  1         5  
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   5290 use autodie qw/:all/;
  1         11058  
  1         5  
16 1     1   15955 use Cwd;
  1         12  
  1         50  
17 1     1   3 use File::Spec;
  1         1  
  1         20  
18 1     1   655 use IO::Socket::SSL;
  1         56295  
  1         10  
19 1     1   932 use Net::Amazon::EC2;
  1         1452133  
  1         38  
20 1     1   600 use Safe;
  1         27207  
  1         45  
21 1     1   559 use Text::Template;
  1         2010  
  1         39  
22              
23 1     1   374 use IO::Pipe;
  1         755  
  1         23  
24 1     1   837 use AnyEvent;
  1         3695  
  1         31  
25              
26 1     1   437 use Log::Any qw/$log/;
  1         5584  
  1         5  
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           $log->info("Listing instances for set='".$self->set()."'");
92              
93 0           my $set_config = {};
94 0 0         if( $self->set() ){
95 0   0       $set_config = $self->config()->{ec2_sets}->{$self->set()} || die "No ec2_set '".$self->set()."' defined in config\n";
96             }
97              
98 0           my $reservation_infos = $self->ec2->describe_instances( %{ $set_config } ) ;
  0            
99 0           foreach my $ri ( @$reservation_infos ){
100 0           my $instances = $ri->instances_set();
101 0           foreach my $instance ( @$instances ){
102 0 0         if( my $tagset = $instance->tag_set() ){
103 0           foreach my $tag ( @$tagset ){
104 0   0       $log->trace("Host has tag: ".$tag->key().':'.( $tag->value() // 'UNDEF' ));
105             }
106             }
107 0 0         if( my $host = $instance->dns_name() ){
108 0           $log->debug("Adding host $host");
109 0           push @hosts , $host;
110             }else{
111 0           $log->warn("Instance ".$instance->instance_id()." does not have a dns_name. Skipping");
112             }
113             }
114             }
115              
116 0           $log->info("Got ".scalar( @hosts )." hosts");
117 0 0         if( $self->has_demux_command() ){
118 0           return $self->do_demux_command( \@hosts );
119             }
120              
121             # No demux command, just carry on using the configured command for multiple hosts.
122             my $tmpl = Text::Template->new( TYPE => 'STRING',
123 0   0       SOURCE => $self->config()->{command} || die "Missing command in config\n"
124             );
125 0 0         unless( $tmpl->compile() ){
126 0           die "Cannot compile template from '".$self->config()->{command}."' ERROR:".$Text::Template::ERROR."\n";
127             }
128              
129 0           my $command = $tmpl->fill_in( SAFE => Safe->new(),
130             HASH => {
131             hosts => \@hosts
132             }
133             );
134 0           $log->info("Will do '".substr($command, 0, 80)."..'");
135 0 0         if( $log->is_debug() ){
136 0           $log->debug($command);
137             }
138 0           my $sys_return = system( $command );
139 0           $log->info("Done (returned $sys_return)");
140 0           return $sys_return;
141             }
142              
143             $| = 1;
144              
145             sub do_demux_command{
146 0     0 0   my ($self, $hosts) = @_;
147              
148 0           my $tmpl = Text::Template->new( TYPE => 'STRING',
149             SOURCE => $self->demux_command() );
150              
151 0           my @finished = ();
152 0           foreach my $host ( @$hosts ){
153 0           my $command = $tmpl->fill_in( HASH => { host => $host } );
154 0           $log->info("Will do ".$command);
155 0           my $io_h = IO::Pipe->new()->reader( $command );
156 0           my $w;
157 0           my $finished = AnyEvent->condvar();
158 0           push @finished , $finished;
159             $w = AnyEvent->io( fh => $io_h,
160             poll => 'r',
161             cb => sub{
162 0     0     my $line = <$io_h>;
163 0 0         unless( $line ){
164 0           undef $w;
165 0           $finished->send();
166 0           return;
167             }
168 0           print $host.": ".$line;
169 0           });
170             }
171              
172 0           map{ $_->recv() } @finished;
  0            
173 0           return 0;
174             }
175              
176             __PACKAGE__->meta->make_immutable();