File Coverage

blib/lib/Net/Amazon/Route53.pm
Criterion Covered Total %
statement 33 216 15.2
branch 0 64 0.0
condition 0 9 0.0
subroutine 11 21 52.3
pod 5 5 100.0
total 49 315 15.5


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