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