1#
2# (c) Jan Gehring <jan.gehring@gmail.com>
3#
4# vim: set ts=2 sw=2 tw=0:
5# vim: set expandtab:
6
7#
8# Some of the code is based on Net::Amazon::EC2
9#
10
11package Rex::Cloud::Amazon;
12
13use 5.010001;
14use strict;
15use warnings;
16
17our $VERSION = '1.13.4'; # VERSION
18
19use Rex::Logger;
20use Rex::Cloud::Base;
21use AWS::Signature4;
22use HTTP::Request::Common;
23use Digest::HMAC_SHA1;
24use base qw(Rex::Cloud::Base);
25use LWP::UserAgent;
26use XML::Simple;
27use Carp;
28
29BEGIN {
30  use Rex::Require;
31  HTTP::Date->use(qw(time2isoz));
32  MIME::Base64->use(qw(encode_base64 decode_base64));
33}
34
35use Data::Dumper;
36
37sub new {
38  my $that  = shift;
39  my $proto = ref($that) || $that;
40  my $self  = {@_};
41
42  bless( $self, $proto );
43
44  #$self->{"__version"} = "2009-11-30";
45  $self->{"__version"}           = "2011-05-15";
46  $self->{"__signature_version"} = 1;
47  $self->{"__endpoint"}          = "us-east-1.ec2.amazonaws.com";
48
49  Rex::Logger::debug(
50    "Creating new Amazon Object, with endpoint: " . $self->{"__endpoint"} );
51  Rex::Logger::debug( "Using API Version: " . $self->{"__version"} );
52
53  return $self;
54}
55
56sub signer {
57  my ($self) = @_;
58  return AWS::Signature4->new(
59    -access_key => $self->{__access_key},
60    -secret_key => $self->{__secret_access_key}
61  );
62}
63
64sub set_auth {
65  my ( $self, $access_key, $secret_access_key ) = @_;
66
67  $self->{"__access_key"}        = $access_key;
68  $self->{"__secret_access_key"} = $secret_access_key;
69}
70
71sub set_endpoint {
72  my ( $self, $endpoint ) = @_;
73  Rex::Logger::debug("Setting new endpoint to $endpoint");
74  $self->{'__endpoint'} = $endpoint;
75}
76
77sub timestamp {
78  my $t = time2isoz();
79  chop($t);
80  $t .= ".000Z";
81  $t =~ s/\s+/T/g;
82  return $t;
83}
84
85sub run_instance {
86  my ( $self, %data ) = @_;
87
88  Rex::Logger::debug("Trying to start a new Amazon instance with data:");
89  Rex::Logger::debug( "  $_ -> " . ( $data{$_} ? $data{$_} : "undef" ) )
90    for keys %data;
91
92  my $security_groups;
93
94  if ( ref( $data{security_group} ) eq "ARRAY" ) {
95    $security_groups = $data{security_group};
96  }
97  elsif ( exists $data{security_groups} ) {
98    $security_groups = $data{security_groups};
99  }
100  else {
101    $security_groups = $data{security_group};
102  }
103
104  my %security_group = ();
105  if ( ref($security_groups) eq "ARRAY" ) {
106    my $i = 0;
107    for my $sg ( @{$security_groups} ) {
108      $security_group{"SecurityGroup.$i"} = $sg;
109      $i++;
110    }
111  }
112  elsif ( !exists $data{options}->{SubnetId} ) {
113    $security_group{SecurityGroup} = $security_groups || "default";
114  }
115
116  my %more_options = %{ $data{options} || {} };
117
118  my $xml = $self->_request(
119    "RunInstances",
120    ImageId                      => $data{"image_id"},
121    MinCount                     => 1,
122    MaxCount                     => 1,
123    KeyName                      => $data{"key"},
124    InstanceType                 => $data{"type"} || "m1.small",
125    "Placement.AvailabilityZone" => $data{"zone"} || "",
126    %security_group,
127    %more_options,
128  );
129
130  my $ref = $self->_xml($xml);
131
132  if ( exists $data{"name"} ) {
133    $self->add_tag(
134      id    => $ref->{"instancesSet"}->{"item"}->{"instanceId"},
135      name  => "Name",
136      value => $data{"name"}
137    );
138  }
139
140  my ($info) =
141    grep { $_->{"id"} eq $ref->{"instancesSet"}->{"item"}->{"instanceId"} }
142    $self->list_instances();
143
144  while ( $info->{"state"} ne "running" ) {
145    Rex::Logger::debug("Waiting for instance to be created...");
146    ($info) =
147      grep { $_->{"id"} eq $ref->{"instancesSet"}->{"item"}->{"instanceId"} }
148      $self->list_instances();
149    sleep 1;
150  }
151
152  if ( exists $data{"volume"} ) {
153    $self->attach_volume(
154      volume_id   => $data{"volume"},
155      instance_id => $ref->{"instancesSet"}->{"item"}->{"instanceId"},
156      name        => "/dev/sdh",                                      # default for new instances
157    );
158  }
159
160  return $info;
161}
162
163sub attach_volume {
164  my ( $self, %data ) = @_;
165
166  Rex::Logger::debug("Trying to attach a new volume");
167
168  $self->_request(
169    "AttachVolume",
170    VolumeId   => $data{"volume_id"},
171    InstanceId => $data{"instance_id"},
172    Device     => $data{"name"} || "/dev/sdh"
173  );
174}
175
176sub detach_volume {
177  my ( $self, %data ) = @_;
178
179  Rex::Logger::debug("Trying to detach a volume");
180
181  $self->_request( "DetachVolume", VolumeId => $data{"volume_id"}, );
182}
183
184sub delete_volume {
185  my ( $self, %data ) = @_;
186
187  Rex::Logger::debug("Trying to delete a volume");
188
189  $self->_request( "DeleteVolume", VolumeId => $data{"volume_id"}, );
190}
191
192sub terminate_instance {
193  my ( $self, %data ) = @_;
194
195  Rex::Logger::debug("Trying to terminate an instance");
196
197  $self->_request( "TerminateInstances",
198    "InstanceId.1" => $data{"instance_id"} );
199}
200
201sub start_instance {
202  my ( $self, %data ) = @_;
203
204  Rex::Logger::debug("Trying to start an instance");
205
206  $self->_request( "StartInstances", "InstanceId.1" => $data{instance_id} );
207
208  my ($info) =
209    grep { $_->{"id"} eq $data{"instance_id"} } $self->list_instances();
210
211  while ( $info->{"state"} ne "running" ) {
212    Rex::Logger::debug("Waiting for instance to be started...");
213    ($info) =
214      grep { $_->{"id"} eq $data{"instance_id"} } $self->list_instances();
215    sleep 5;
216  }
217
218}
219
220sub stop_instance {
221  my ( $self, %data ) = @_;
222
223  Rex::Logger::debug("Trying to stop an instance");
224
225  $self->_request( "StopInstances", "InstanceId.1" => $data{instance_id} );
226
227  my ($info) =
228    grep { $_->{"id"} eq $data{"instance_id"} } $self->list_instances();
229
230  while ( $info->{"state"} ne "stopped" ) {
231    Rex::Logger::debug("Waiting for instance to be stopped...");
232    ($info) =
233      grep { $_->{"id"} eq $data{"instance_id"} } $self->list_instances();
234    sleep 5;
235  }
236
237}
238
239sub add_tag {
240  my ( $self, %data ) = @_;
241
242  Rex::Logger::debug( "Adding a new tag: "
243      . $data{id} . " -> "
244      . $data{name} . " -> "
245      . $data{value} );
246
247  $self->_request(
248    "CreateTags",
249    "ResourceId.1" => $data{"id"},
250    "Tag.1.Key"    => $data{"name"},
251    "Tag.1.Value"  => $data{"value"}
252  );
253}
254
255sub create_volume {
256  my ( $self, %data ) = @_;
257
258  Rex::Logger::debug("Creating a new volume");
259
260  my $xml = $self->_request(
261    "CreateVolume",
262    "Size"             => $data{"size"} || 1,
263    "AvailabilityZone" => $data{"zone"},
264  );
265
266  my $ref = $self->_xml($xml);
267
268  return $ref->{"volumeId"};
269
270  my ($info) = grep { $_->{"id"} eq $ref->{"volumeId"} } $self->list_volumes();
271
272  while ( $info->{"status"} ne "available" ) {
273    Rex::Logger::debug("Waiting for volume to become ready...");
274    ($info) = grep { $_->{"id"} eq $ref->{"volumeId"} } $self->list_volumes();
275    sleep 1;
276  }
277
278}
279
280sub list_volumes {
281  my ($self) = @_;
282
283  my $xml = $self->_request("DescribeVolumes");
284  my $ref = $self->_xml($xml);
285
286  return unless ($ref);
287  return unless ( exists $ref->{"volumeSet"}->{"item"} );
288  if ( ref( $ref->{"volumeSet"}->{"item"} ) eq "HASH" ) {
289    $ref->{"volumeSet"}->{"item"} = [ $ref->{"volumeSet"}->{"item"} ];
290  }
291
292  my @volumes;
293  for my $vol ( @{ $ref->{"volumeSet"}->{"item"} } ) {
294    push(
295      @volumes,
296      {
297        id          => $vol->{"volumeId"},
298        status      => $vol->{"status"},
299        zone        => $vol->{"availabilityZone"},
300        size        => $vol->{"size"},
301        attached_to => $vol->{"attachmentSet"}->{"item"}->{"instanceId"},
302      }
303    );
304  }
305
306  return @volumes;
307}
308
309sub _make_instance_map {
310  my ( $self, $instance_set ) = @_;
311  return (
312    ip           => $_[1]->{"ipAddress"},
313    id           => $_[1]->{"instanceId"},
314    image_id     => $_[1]->{"imageId"},
315    architecture => $_[1]->{"architecture"},
316    type         => $_[1]->{"instanceType"},
317    dns_name     => $_[1]->{"dnsName"},
318    state        => $_[1]->{"instanceState"}->{"name"},
319    launch_time  => $_[1]->{"launchTime"},
320    (
321      name => exists( $instance_set->{"tagSet"}->{"item"}->{"value"} )
322      ? $instance_set->{"tagSet"}->{"item"}->{"value"}
323      : $instance_set->{"tagSet"}->{"item"}->{"Name"}->{"value"}
324    ),
325    private_ip => $_[1]->{"privateIpAddress"},
326    (
327      security_group => ref $_[1]->{"groupSet"}->{"item"} eq 'ARRAY' ? join ',',
328      map { $_->{groupName} } @{ $_[1]->{"groupSet"}->{"item"} }
329      : $_[1]->{"groupSet"}->{"item"}->{"groupName"}
330    ),
331    (
332      security_groups => ref $_[1]->{"groupSet"}->{"item"} eq 'ARRAY'
333      ? [ map { $_->{groupName} } @{ $_[1]->{"groupSet"}->{"item"} } ]
334      : [ $_[1]->{"groupSet"}->{"item"}->{"groupName"} ]
335    ),
336    (
337      tags => {
338        map {
339          if ( ref $instance_set->{"tagSet"}->{"item"}->{$_} eq 'HASH' ) {
340            $_ => $instance_set->{"tagSet"}->{"item"}->{$_}->{value};
341          }
342          else {
343            $instance_set->{"tagSet"}->{"item"}->{key} =>
344              $instance_set->{"tagSet"}->{"item"}->{value};
345          }
346        } keys %{ $instance_set->{"tagSet"}->{"item"} }
347      }
348    ),
349  );
350}
351
352sub list_instances {
353  my ($self) = @_;
354
355  my @ret;
356
357  my $xml = $self->_request("DescribeInstances");
358  my $ref = $self->_xml($xml);
359
360  return unless ($ref);
361  return unless ( exists $ref->{"reservationSet"} );
362  return unless ( exists $ref->{"reservationSet"}->{"item"} );
363
364  if ( ref $ref->{"reservationSet"}->{"item"} eq "HASH" ) {
365
366    # if only one instance is returned, turn it to an array
367    $ref->{"reservationSet"}->{"item"} = [ $ref->{"reservationSet"}->{"item"} ];
368  }
369
370  for my $instance_set ( @{ $ref->{"reservationSet"}->{"item"} } ) {
371
372    # push(@ret, $instance_set);
373    my $isi = $instance_set->{"instancesSet"}->{"item"};
374    if ( ref $isi eq 'HASH' ) {
375      push( @ret, { $self->_make_instance_map($isi) } );
376    }
377    elsif ( ref $isi eq 'ARRAY' ) {
378      for my $iset (@$isi) {
379        push( @ret, { $self->_make_instance_map($iset) } );
380      }
381    }
382  }
383
384  return @ret;
385}
386
387sub list_running_instances {
388  my ($self) = @_;
389
390  return grep { $_->{"state"} eq "running" } $self->list_instances();
391}
392
393sub get_regions {
394  my ($self) = @_;
395
396  my $content = $self->_request("DescribeRegions");
397  my %items =
398    ( $content =~
399      m/<regionName>([^<]+)<\/regionName>\s+<regionEndpoint>([^<]+)<\/regionEndpoint>/gsim
400    );
401
402  return %items;
403}
404
405sub get_availability_zones {
406  my ($self) = @_;
407
408  my $xml = $self->_request("DescribeAvailabilityZones");
409  my $ref = $self->_xml($xml);
410
411  my @zones;
412  for my $item ( @{ $ref->{"availabilityZoneInfo"}->{"item"} } ) {
413    push(
414      @zones,
415      {
416        zone_name   => $item->{"zoneName"},
417        region_name => $item->{"regionName"},
418        zone_state  => $item->{"zoneState"},
419      }
420    );
421  }
422
423  return @zones;
424}
425
426sub _request {
427  my ( $self, $action, %args ) = @_;
428
429  my $ua = LWP::UserAgent->new;
430  $ua->timeout(300);
431  $ua->env_proxy;
432  my %param = $self->_sign( $action, %args );
433
434  Rex::Logger::debug( "Sending request to: https://" . $self->{'__endpoint'} );
435  Rex::Logger::debug( "  $_ -> " . $param{$_} ) for keys %param;
436
437  my $req = POST( "https://" . $self->{__endpoint}, [%param] );
438  $self->signer->sign($req);
439
440  #my $res = $ua->post( "https://" . $self->{'__endpoint'}, \%param );
441  my $res = $ua->request($req);
442
443  if ( $res->code >= 500 ) {
444    Rex::Logger::info( "Error on request", "warn" );
445    Rex::Logger::debug( $res->content );
446    return;
447  }
448
449  else {
450    my $ret;
451    eval {
452      no warnings;
453      $ret = $res->content;
454      Rex::Logger::debug($ret);
455      use warnings;
456    };
457
458    return $ret;
459  }
460}
461
462sub _sign {
463  my ( $self, $action, %o_args ) = @_;
464
465  my %args;
466  for my $key ( keys %o_args ) {
467    next unless $key;
468    next unless $o_args{$key};
469
470    $args{$key} = $o_args{$key};
471  }
472
473  $args{Action}  = $action;
474  $args{Version} = $self->{__version};
475
476  return %args;
477
478  my %sign_hash = (
479    AWSAccessKeyId   => $self->{"__access_key"},
480    Action           => $action,
481    Timestamp        => $self->timestamp(),
482    Version          => $self->{"__version"},
483    SignatureVersion => $self->{"__signature_version"},
484    %args
485  );
486
487  my $sign_this;
488  foreach my $key ( sort { lc($a) cmp lc($b) } keys %sign_hash ) {
489    $sign_this .= $key . $sign_hash{$key};
490  }
491
492  Rex::Logger::debug("Signed: $sign_this");
493
494  my $encoded = $self->_hash($sign_this);
495
496  my %params = (
497    Action           => $action,
498    SignatureVersion => $self->{"__signature_version"},
499    AWSAccessKeyId   => $self->{"__access_key"},
500    Timestamp        => $self->timestamp(),
501    Version          => $self->{"__version"},
502    Signature        => $encoded,
503    %args
504  );
505
506  return %params;
507}
508
509sub _hash {
510  my ( $self, $query_string ) = @_;
511
512  my $hashed = Digest::HMAC_SHA1->new( $self->{"__secret_access_key"} );
513  $hashed->add($query_string);
514
515  return encode_base64( $hashed->digest, "" );
516}
517
518sub _xml {
519  my ( $self, $xml ) = @_;
520
521  my $x   = XML::Simple->new;
522  my $res = $x->XMLin($xml);
523  if ( defined $res->{"Errors"} ) {
524    if ( ref( $res->{"Errors"} ) ne "ARRAY" ) {
525      $res->{"Errors"} = [ $res->{"Errors"} ];
526    }
527
528    my @error_msg = ();
529    for my $error ( @{ $res->{"Errors"} } ) {
530      push( @error_msg,
531            $error->{"Error"}->{"Message"}
532          . " (Code: "
533          . $error->{"Error"}->{"Code"}
534          . ")" );
535    }
536
537    confess( join( "\n", @error_msg ) );
538  }
539
540  return $res;
541}
542
5431;
544