File Coverage

blib/lib/Net/Amazon/Route53.pm
Criterion Covered Total %
statement 36 220 16.3
branch 0 66 0.0
condition 0 9 0.0
subroutine 12 22 54.5
pod 5 5 100.0
total 53 322 16.4


line stmt bran cond sub pod time code
1 2     2   968 use strict;
  2         8  
  2         44  
2 2     2   7 use warnings;
  2         4  
  2         69  
3              
4             package Net::Amazon::Route53;
5             $Net::Amazon::Route53::VERSION = '0.173450';
6 2     2   1048 use LWP::UserAgent;
  2         78043  
  2         54  
7 2     2   12 use HTTP::Request;
  2         3  
  2         42  
8 2     2   711 use Digest::HMAC_SHA1;
  2         7864  
  2         77  
9 2     2   742 use MIME::Base64;
  2         955  
  2         97  
10 2     2   945 use XML::Bare;
  2         24850  
  2         93  
11 2     2   866 use HTML::Entities;
  2         9099  
  2         152  
12 2     2   894 use Moo;
  2         17703  
  2         8  
13 2     2   3258 use Types::Standard qw(InstanceOf Str);
  2         133069  
  2         19  
14              
15 2     2   2310 use Net::Amazon::Route53::HostedZone;
  2         7  
  2         73  
16 2     2   888 use Net::Amazon::Route53::ResourceRecordSet::Change;
  2         6  
  2         4350  
17              
18             # ABSTRACT: Interface to Amazon's Route 53
19              
20             =head2 SYNOPSIS
21              
22             use strict;
23             use warnings;
24             use Net::Amazon::Route53;
25             my $route53 = Net::Amazon::Route53->new( id => '...', key => '...' );
26             my @zones = $route53->get_hosted_zones;
27             for my $zone ( @zones ) {
28             # use the Net::Amazon::Route53::HostedZone object
29             }
30              
31             =cut
32              
33             =head2 ATTRIBUTES
34              
35             =cut
36              
37             =head3 id
38              
39             The Amazon id, needed to contact Amazon's Route 53.
40              
41             =head3 key
42              
43             The Amazon key, needed to contact Amazon's Route 53.
44              
45             =cut
46              
47             has 'id' => ( is => 'rw', isa => Str, required => 1, );
48             has 'key' => ( is => 'rw', isa => Str, required => 1, );
49              
50             =head3 ua
51              
52             Internal user agent object used to perform requests to
53             Amazon's Route 53
54              
55             =cut
56              
57             has 'ua' => (
58             is => 'rw',
59             isa => InstanceOf['LWP::UserAgent'],
60             required => 1,
61             default => sub {
62             my $self = shift;
63             LWP::UserAgent->new(
64             keep_alive => 10,
65             requests_redirectable => [qw(GET HEAD DELETE PUT)],
66             );
67             },
68             );
69              
70             =head2 METHODS
71              
72             =cut
73              
74             =head3 C
75              
76             my $hr_xml_response = $self->request( $method, $url );
77              
78             Requests something from Amazon Route 53, signing the request. Uses
79             L internally, and returns the hashref obtained from the
80             request. Dies on error, showing the request's error given by the API.
81              
82             =cut
83              
84             sub request
85             {
86 0     0 1   my $self = shift;
87 0           my $method = shift;
88 0           my $uri = shift;
89              
90 0 0         return unless $method;
91 0 0 0       return unless ( $method eq 'get' or $method eq 'post' or $method eq 'delete' );
      0        
92 0 0         return unless $uri;
93              
94             # Get amazon server's date
95 0           my $date = do {
96 0           my $rc = $self->ua->get('https://route53.amazonaws.com/date');
97 0           $rc->header('date');
98             };
99              
100             # Create signed request
101 0           my $hmac = Digest::HMAC_SHA1->new( $self->key );
102 0           $hmac->add($date);
103 0           my $signature = encode_base64( $hmac->digest, '' );
104              
105 0           my %options = (
106             'Date' => $date,
107             'X-Amzn-Authorization' =>
108             sprintf( "AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=HmacSHA1,Signature=%s", $self->id, $signature ),
109             @_
110             );
111 0           my $content = delete $options{Content};
112             my $request = HTTP::Request->new(
113             uc $method,
114             $uri,
115 0 0         [ map { $_ => $options{$_} } keys %options],
  0            
116             $content ? $content : undef,
117             );
118 0           my $rc = $self->ua->request( $request );
119 0 0         die "Could not perform request $method on $uri: "
    0          
120             . $rc->status_line . "\n"
121             . $rc->decoded_content . "\n"
122             . "Original request: "
123             . ( defined $content ? $content : '' ) . "\n"
124             unless $rc->is_success;
125             #use YAML;warn "\n\nmethod $method to $uri @_: " . Dump($rc);
126 0           my $resp = XML::Bare::xmlin( $rc->decoded_content );
127 0 0         die "Error: $resp->{Error}{Code}\n" if ( exists $resp->{Error} );
128 0           return $resp;
129             }
130              
131             =head3 C
132              
133             my $route53 = Net::Amazon::Route53->new( key => '...', id => '...' );
134             my @zones = $route53->get_hosted_zones();
135             my $zone = $route53->get_hosted_zones( 'example.com.' );
136              
137             Gets one or more L objects,
138             representing the zones associated with the account.
139              
140             Takes an optional parameter indicating the name of the wanted hosted zone.
141              
142             =cut
143              
144             sub get_hosted_zones
145             {
146 0     0 1   my $self = shift;
147 0           my $which = shift;
148 0           my $start_marker = '';
149 0           my @zones;
150 0           while (1) {
151 0           my $resp =
152             $self->request( 'get', 'https://route53.amazonaws.com/2010-10-01/hostedzone?maxitems=100' . $start_marker );
153 0 0         if($resp->{HostedZones}) {
154 0 0         push @zones, ( ref $resp->{HostedZones}{HostedZone} eq 'ARRAY' ? @{ $resp->{HostedZones}{HostedZone} } : $resp->{HostedZones}{HostedZone} );
  0            
155             }
156 0 0         last if $resp->{IsTruncated} eq 'false';
157 0           $start_marker = '?marker=' . $resp->{NextMarker};
158             }
159 0           my @o_zones;
160 0           for my $zone (@zones) {
161             push @o_zones,
162             Net::Amazon::Route53::HostedZone->new(
163             route53 => $self,
164 0           ( map { lc($_) => $zone->{$_} } qw/Id Name CallerReference/ ),
165 0 0 0       comment => (exists $zone->{Config} and ref $zone->{Config} eq 'HASH') ? $zone->{Config}{Comment} : '',
166             );
167             }
168 0 0         @o_zones = grep { $_->name eq $which } @o_zones if $which;
  0            
169 0           return @o_zones;
170             }
171              
172             =head3 batch_create
173              
174             my $route53 = Net::Amazon::Route53->new( key => '...', id => '...' );
175             my @records = record_generating_subroutine(); # returning an array of Net::Amazon::Route53::ResourceRecordSets
176             my $change = $route53->batch_create(\@records); # Or ->batch_create(\@records,1) if you want to wait
177              
178             Turns an arrayref of L objects into
179             one big create request. All records must belong to the same hosted zone.
180              
181             Takes an optional boolean parameter, C, to indicate whether the request
182             should return straightaway (default, or when C is C<0>) or it should wait
183             until the request is C according to the Change's status.
184              
185             Returns a L object representing the change
186             requested.
187              
188             =cut
189              
190             sub batch_create
191             {
192 0     0 1   my $self = shift;
193 0           my $batch = shift;
194 0           my $wait = shift;
195 0 0         $wait = 0 if !defined $wait;
196              
197 0 0         die "Your batch is not an arrayref" unless ref($batch) eq 'ARRAY';
198             my @invalid =
199 0           grep { !( $_->isa("Net::Amazon::Route53::ResourceRecordSet") ) }
  0            
200             @$batch;
201 0 0         die
202             "Your batch is not an arrayref of Net::Amazon::Route53::ResourceRecordSets"
203             if scalar(@invalid);
204              
205 0           my $hostedzone_id = $batch->[0]->hostedzone->id;
206 0           my @wrong_zone = grep { $_->hostedzone->id ne $hostedzone_id } @$batch;
  0            
207 0 0         die
208             "Your batch contains records from different hosted zones"
209             if scalar(@wrong_zone);
210              
211 0           $hostedzone_id =~ s/^\///g;
212              
213 0           my $batch_xml = $self->_batch_request_header;
214              
215 0           for my $rr (@$batch) {
216 0 0         $rr->name =~ /\.$/ or die "Zone name needs to end in a dot, to be created\n";
217 0           my $change_xml = $self->_get_create_xml($rr);
218 0           $batch_xml .= $change_xml;
219             }
220              
221 0           $batch_xml .= $self->_batch_request_footer;
222              
223 0           my $resp = $self->request(
224             'post',
225             'https://route53.amazonaws.com/2010-10-01/' . $hostedzone_id . '/rrset',
226             Content => $batch_xml
227             );
228             my $change = Net::Amazon::Route53::Change->new(
229             route53 => $self,
230 0           ( map { lc($_) => decode_entities($resp->{ChangeInfo}{$_}) } qw/Id Status SubmittedAt/ ),
  0            
231             );
232 0           $change->refresh();
233 0 0         return $change if !$wait;
234 0           while ( lc( $change->status ) ne 'insync' ) {
235 0           sleep 2;
236 0           $change->refresh();
237             }
238 0           return $change;
239             }
240              
241             =head3 atomic_update
242              
243             my $route53 = Net::Amazon::Route53->new( key => '...', id => '...' );
244             my $hosted_zone = $route_53->get_hosted_zones("example.com.");
245             my $old_records = $hosted_zone->resource_record_sets;
246             my $new_records = record_generating_subroutine();
247             my $change = $route53->atomic_update($old_records,$new_records);
248             # Or ->atomic_update($ref1,$ref2,1) if you want to wait
249              
250             Be warned: B. Give it the arrayref of records
251             currently in your zone and an arrayref of records representing the desired
252             state of your zone, and it will create, change, and delete the current records
253             in the zone to match the set you submitted.
254              
255             B.
256              
257             This method discovers which records needs to be deleted/created, e.g., changed,
258             which ones need simply to be created for the first time, and
259             B.
260             It's an "all-in-one, all-at-once" update for all the records in your zone.
261             This, and the fact that it is destructive, is why it is called
262             C.
263              
264             Takes an optional boolean parameter, C, to indicate whether the request
265             should return straightaway (default, or when C is C<0>) or it should wait
266             until the request is C according to the Change's status.
267              
268             Returns a L object representing the change
269             requested.
270              
271             =cut
272              
273             sub atomic_update {
274 0     0 1   my $self = shift;
275 0           my $original = shift;
276 0           my $new = shift;
277 0           my $wait = shift;
278 0 0         $wait = 0 if !defined $wait;
279              
280 0           for my $rrset (($original,$new)) {
281 0 0         die "A record set is not an arrayref" unless ref($rrset) eq 'ARRAY';
282             my @invalid =
283 0           grep { !( $_->isa("Net::Amazon::Route53::ResourceRecordSet") ) }
  0            
284             @$rrset;
285 0 0         die
286             "A record set is not an arrayref of Net::Amazon::Route53::ResourceRecordSets"
287             if scalar(@invalid);
288             }
289              
290 0           my $hostedzone_id = $original->[0]->hostedzone->id;
291 0           my @wrong_zone = grep { $_->hostedzone->id ne $hostedzone_id } (@$original,@$new);
  0            
292 0 0         die
293             "A record set contains records from different hosted zones"
294             if scalar(@wrong_zone);
295              
296 0           $hostedzone_id =~ s/^\///g;
297              
298 0           my %original = map { $_->name . '-' . $_->type => 1 } @$original;
  0            
299 0           my %new = map { $_->name . '-' . $_->type => 1 } @$new;
  0            
300 0           my %new_records = map { $_->name . '-' . $_->type => $_ } @$new;
  0            
301 0           my @creates = grep { !( defined $original{ $_->name . '-' . $_->type } ) } @$new;
  0            
302 0           my @deletions = grep { !( defined $new{ $_->name . '-' . $_->type } ) } @$original;
  0            
303 0           my %deleted = map { $_->name . '-' . $_->type => 1 } @deletions;
  0            
304 0           my @changes = grep { defined $new{ $_->name . '-' . $_->type } }
305 0           grep { !( defined $deleted{ $_->name . '-' . $_->type } ) }
  0            
306             @$original;
307             my @change_objects = map {
308             Net::Amazon::Route53::ResourceRecordSet::Change->new(
309             route53 => $_->route53,
310             hostedzone => $_->hostedzone,
311             name => $_->name,
312             ttl => $_->ttl,
313             type => $_->type,
314             original_values => $_->values,
315 0           values => $new_records{ $_->name . '-' . $_->type }->values
316             )
317             }
318             grep {
319 0           join( ',', @{$_->values} ) ne
  0            
320 0           join( ',', @{$new_records{ $_->name . '-' . $_->type }->values} )
  0            
321             } @changes;
322              
323 0           my $batch_xml = $self->_batch_request_header;
324              
325             # Do not attempt to push an empty changeset
326 0 0         return Net::Amazon::Route53::Change->new(
327             route53 => $self,
328             status => 'NOOP'
329             ) if @change_objects + @deletions + @creates < 1;
330              
331 0           for my $rr (@change_objects) {
332 0 0         $rr->name =~ /\.$/ or die "Zone name needs to end in a dot, to be changed\n";
333 0           my $change_xml = $self->_get_change_xml($rr);
334 0           $batch_xml .= $change_xml;
335             }
336              
337 0           for my $rr (@deletions) {
338 0 0         $rr->name =~ /\.$/ or die "Zone name needs to end in a dot, to be deleted\n";
339 0           my $change_xml = $self->_get_delete_xml($rr);
340 0           $batch_xml .= $change_xml;
341             }
342              
343 0           for my $rr (@creates) {
344 0 0         $rr->name =~ /\.$/ or die "Zone name needs to end in a dot, to be created\n";
345 0           my $change_xml = $self->_get_create_xml($rr);
346 0           $batch_xml .= $change_xml;
347             }
348              
349 0           $batch_xml .= $self->_batch_request_footer;
350              
351 0           my $resp = $self->request(
352             'post',
353             'https://route53.amazonaws.com/2010-10-01/' . $hostedzone_id . '/rrset',
354             Content => $batch_xml
355             );
356             my $change = Net::Amazon::Route53::Change->new(
357             route53 => $self,
358 0           ( map { lc($_) => decode_entities($resp->{ChangeInfo}{$_}) } qw/Id Status SubmittedAt/ ),
  0            
359             );
360 0           $change->refresh();
361 0 0         return $change if !$wait;
362 0           while ( lc( $change->status ) ne 'insync' ) {
363 0           sleep 2;
364 0           $change->refresh();
365             }
366 0           return $change;
367             }
368              
369             =head3 batch_change
370              
371             my $route53 = Net::Amazon::Route53->new( key => '...', id => '...' );
372             my $hosted_zone = $route_53->get_hosted_zones("example.com.");
373             my $recordset_changes = recordset_changes_generating_subroutine();
374             my $change = $route53->batch_change($recordset_changes);
375             # Or ->batch_change($recordset_changes,1) if you want to wait
376              
377             This method takes an arrayref of
378             L objects and the optional
379             C argument, and makes one big request to change all the records at once.
380              
381             =cut
382              
383             sub batch_change {
384 0     0 1   my $self = shift;
385 0           my $batch = shift;
386 0           my $wait = shift;
387 0 0         $wait = 0 if !defined $wait;
388              
389 0 0         die "Your batch is not an arrayref" unless ref($batch) eq 'ARRAY';
390             my @invalid =
391 0           grep { !( $_->isa("Net::Amazon::Route53::ResourceRecordSet::Change") ) }
  0            
392             @$batch;
393 0 0         die
394             "Your batch is not an arrayref of Net::Amazon::Route53::ResourceRecordSet::Changes"
395             if scalar(@invalid);
396              
397 0           my $hostedzone_id = $batch->[0]->hostedzone->id;
398 0           my @wrong_zone = grep { $_->hostedzone->id ne $hostedzone_id } @$batch;
  0            
399 0 0         die
400             "Your batch contains records from different hosted zones"
401             if scalar(@wrong_zone);
402              
403 0           $hostedzone_id =~ s/^\///g;
404              
405 0           my $batch_xml = $self->_batch_request_header;
406              
407 0           for my $rr (@$batch) {
408 0 0         $rr->name =~ /\.$/ or die "Zone name needs to end in a dot, to be created\n";
409 0           my $change_xml = $self->_get_change_xml($rr);
410 0           $batch_xml .= $change_xml;
411             }
412              
413 0           $batch_xml .= $self->_batch_request_footer;
414              
415 0           my $resp = $self->request(
416             'post',
417             'https://route53.amazonaws.com/2010-10-01/' . $hostedzone_id . '/rrset',
418             Content => $batch_xml
419             );
420             my $change = Net::Amazon::Route53::Change->new(
421             route53 => $self,
422 0           ( map { lc($_) => decode_entities($resp->{ChangeInfo}{$_}) } qw/Id Status SubmittedAt/ ),
  0            
423             );
424 0           $change->refresh();
425 0 0         return $change if !$wait;
426 0           while ( lc( $change->status ) ne 'insync' ) {
427 0           sleep 2;
428 0           $change->refresh();
429             }
430 0           return $change;
431             }
432              
433             =head3 _get_create_xml
434              
435             Private method for xml templating. Takes an
436             L object and returns the xml
437             to create that single record.
438              
439             =cut
440              
441             sub _get_create_xml {
442 0     0     my ($self,$record) = @_;
443 0           my $create_xml_str = <<'ENDXML';
444            
445             CREATE
446            
447             %s
448             %s
449             %s
450            
451             %s
452            
453            
454            
455             ENDXML
456              
457             my $create_xml = sprintf( $create_xml_str,
458 0           map { $_ }
459             $record->name, $record->type, $record->ttl,
460             join( "\n", map {
461 0           "" . $_ . ""
462 0           } @{ $record->values } ) );
  0            
463              
464 0           return $create_xml;
465             }
466              
467             =head3 _get_delete_xml
468              
469             Private method for xml templating. Takes an
470             L object and returns the xml to delete
471             that single record.
472              
473             =cut
474              
475             sub _get_delete_xml {
476 0     0     my ($self,$record) = @_;
477 0           my $delete_xml_str = <<'ENDXML';
478            
479             DELETE
480            
481             %s
482             %s
483             %s
484            
485             %s
486            
487            
488            
489             ENDXML
490              
491             my $delete_xml = sprintf( $delete_xml_str,
492 0           map { $_ }
493             $record->name, $record->type, $record->ttl,
494             join( "\n", map {
495 0           "" . $_ . ""
496 0           } @{ $record->values } ) );
  0            
497              
498 0           return $delete_xml;
499             }
500              
501             =head3 _get_change_xml
502              
503             Private method for xml templating. Takes an
504             L object and returns the xml
505             to change, i.e., delete and create, that single record.
506              
507             =cut
508              
509             sub _get_change_xml {
510 0     0     my ($self,$record) = @_;
511 0           my $change_xml_str = <<'ENDXML';
512            
513             DELETE
514            
515             %s
516             %s
517             %s
518            
519             %s
520            
521            
522            
523            
524             CREATE
525            
526             %s
527             %s
528             %s
529            
530             %s
531            
532            
533            
534             ENDXML
535              
536             my $change_xml = sprintf( $change_xml_str,
537 0           (map { $_ } ( $record->name, $record->type, $record->ttl, ) ),
538 0           join( "\n", map { "" . $_ . "" } @{ $record->original_values } ),
  0            
539 0           (map { $_ } ( $record->name, $record->type, $record->ttl, ) ),
540 0           join( "\n", map { "" . $_ . "" } @{ $record->values } ) );
  0            
  0            
541 0           return $change_xml;
542             }
543              
544             =head3 _batch_request_header
545              
546             Private method for xml templating. Returns a header string.
547              
548             =cut
549              
550             sub _batch_request_header {
551 0     0     my $self = shift;
552 0           my $header = <<'ENDXML';
553            
554            
555            
556             Batch changeset
557            
558             ENDXML
559 0           return $header;
560             }
561              
562             =head3 _batch_request_footer
563              
564             Private method for xml templating. Returns a footer string.
565              
566             =cut
567              
568             sub _batch_request_footer {
569 0     0     my $self = shift;
570 0           my $footer = <<'ENDXML';
571            
572            
573            
574             ENDXML
575 0           return $footer;
576             }
577              
578             =head1 SEE ALSO
579              
580             L
581             L
582              
583             =cut
584              
585             =head1 AUTHOR
586              
587             Marco FONTANI
588              
589             =head1 CONTRIBUTORS
590              
591             Daiji Hirata
592             Amiri Barksdale
593             Chris Weyl
594             Jason
595             Ulrich Kautz
596              
597             =head1 COPYRIGHT AND LICENSE
598              
599             This software is copyright (c) 2011 by Marco FONTANI.
600              
601             This is free software; you can redistribute it and/or modify it under
602             the same terms as the Perl 5 programming language system itself.
603              
604             =cut
605              
606             1;