File Coverage

blib/lib/Net/Amazon/Utils.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Net::Amazon::Utils;
2              
3 1     1   49544 use v5.10.0;
  1         4  
  1         380  
4 1     1   13 use strict;
  1         2  
  1         47  
5 1     1   6 use warnings FATAL => 'all';
  1         6  
  1         55  
6 1     1   7 use Carp;
  1         2  
  1         79  
7 1     1   4453 use LWP::UserAgent;
  1         127562  
  1         38  
8 1     1   1255 use LWP::Protocol::https;
  1         202142  
  1         49  
9 1     1   10 use HTTP::Message;
  1         2  
  1         26  
10 1     1   481 use XML::Simple;
  0            
  0            
11              
12             =head1 NAME
13              
14             Net::Amazon::Utils - Implementation of a set of utilities to help in developing Amazon web service modules in Perl.
15              
16             =head1 VERSION
17              
18             Version 0.21
19              
20             =cut
21              
22             our $VERSION = '0.21';
23              
24             =head1 SYNOPSIS
25              
26             This module implements a set of helpers that should be of aid to
27             programming client to Amazon RESTful webservices.
28              
29             Loosely based in com.amazonaws.regions.Region at L
30              
31             use Net::Amazon::Utils;
32              
33             my $utils = Net::Amazon::Utils->new();
34              
35             # get a list of all regions
36             my @all_regions = $utils->get_regions();
37              
38             # get a list of all services abbreviations
39             my @all_services = $utils->get_services();
40              
41             # get all endpoints for ec2
42             my @service_endpoints = $utils->get_service_endpoints( 'ec2' );
43              
44             my $endpoint_uri;
45              
46             # check that ec2 exists in region us-west-1
47             if ( $utils->is_service_supported( 'ec2', 'us-west-1' ) ) {
48             # check that http is supported by the end point
49             if ( $utils->get_http_support( 'ec2', 'us-west-1' ) ) {
50             # get the first http endpoint for ec2 in region us-west-1
51             $endpoint_uri =($utils->get_endpoint_uris( 'Http', 'ec2', 'us-west-1' ))[0];
52             #... use LWP to POST, send get comments
53             #... use Net::Amazon::EC2
54             }
55             }
56              
57             # get endpoints for ec2 with http support on two given regions
58             my @some_endpoints = $utils->get_http_support( 'ec2', 'us-west-1', 'us-east-1' );
59              
60             # check ec2 is supported on all us regions
61             my @us_regions = grep( /^us/, $utils->get_regions );
62             my @us_endpoints;
63             if ( $utils->is_service_supported( 'ec2', @us_regions ) ) {
64             # get endpoints for ec2 with http support on all us regions
65             @us_endpoints = $utils->get_http_support( 'ec2', @us_regions );
66             # choose a random one and give you images a spin
67             # ...
68             }
69              
70             =head1 SUBROUTINES/METHODS
71              
72             =head2 new( [ $no_cache = 0 ], [ $no_inet = 1 ] )
73              
74             Spawns a blessed Net::Amazon::Utils minion.
75              
76             $no_cache means regions will be reloaded with each call to a function and will likely be deprecated.
77             $no_inet means regions should never be fetched from the Internet unless forced by fetch_region_update.
78              
79             =cut
80              
81             sub new {
82             my ( $class, $no_cache, $no_inet ) = @_;
83              
84             $no_inet = 1 unless defined $no_inet;
85             $no_cache = 0 unless defined $no_cache;
86              
87             my $self = {
88             remote_region_file => 'https://raw.githubusercontent.com/aws/aws-sdk-android-v2/master/src/com/amazonaws/regions/regions.xml',
89             # do not cache regions between calls, does not affect Internet caching, defaults to false.
90             no_cache => $no_cache,
91             # do not load updated file from the Internet, defaults to true.
92             no_inet => $no_inet,
93             # be well behaved and tell who we are.
94             # use more reasonable 21st century Internet timeout
95             # do not accept redirects
96             ua => LWP::UserAgent->new(
97             agent => __PACKAGE__ . '/' . $VERSION,
98             timeout => 30,
99             max_redirect => 0,
100             ),
101             };
102              
103             bless $self, $class;
104              
105             return $self;
106             }
107              
108             =head2 fetch_region_update
109              
110             Fetch regions file from the internet even if no_inet was specified when
111             intanciating the object.
112              
113             =cut
114              
115             sub fetch_region_update {
116             my ( $self ) = @_;
117              
118             if ( $self->{no_cache} ) {
119             # Cached regions will not be fetched
120             carp 'Fetching updated region update is useless unless no_cache is false. Still I will comply to your orders because you are intelligent.';
121             $self->_load_regions( 1 );
122             } else {
123             # Backup and restore Internet connection selection.
124             my $old_no_inet = $self->{no_inet};
125             # Force loading
126             $self->_load_regions( 1 );
127             $self->{no_inet} = $old_no_inet;
128             }
129             }
130              
131             =head2 get_domain
132              
133             Currently returns 'amazonaws.com' which is the only supported domain.
134              
135             =cut
136              
137             sub get_domain {
138             return 'amazonaws.com';
139             }
140              
141             =head2 get_regions
142              
143             Returns a list of regions abbreviations, i.g., us-west-1, us-east-1, eu-west-1, sa-east-1.
144              
145             =cut
146              
147             sub get_regions {
148             my ( $self ) = @_;
149             my @regions;
150              
151             $self->_load_regions();
152              
153             return keys %{$self->{regions}->{Regions}};
154              
155             $self->_unload_regions();
156             }
157              
158             =head2 get_services
159              
160             Returns a list of services abbreviations, i.g., ec2, sqs, glacier.
161              
162             =cut
163              
164             sub get_services {
165             my ( $self ) = @_;
166              
167             $self->_load_regions();
168              
169             return keys %{$self->{regions}->{Services}};
170              
171             $self->_unload_regions();
172             }
173              
174             =head2 get_service_endpoints
175              
176             Returns a list of the available services endpoints.
177              
178             =cut
179              
180             sub get_service_endpoints {
181             my ( $self, $service ) = @_;
182              
183             croak 'A service must be specified' unless defined $service;
184              
185             $self->_load_regions();
186              
187             my @service_endpoints;
188              
189             unless ( defined $self->{regions}->{ServiceEndpoints} ) {
190             foreach my $region ( keys %{$self->{regions}->{Regions}} ) {
191             push @service_endpoints, $self->{regions}->{Regions}->{$region}->{Endpoint}->{$service}->{Hostname}
192             if (
193             defined $self->{regions}->{Regions}->{$region}->{Endpoint}->{$service}
194             );
195             }
196             $self->{regions}->{ServiceEndpoints} = \@service_endpoints;
197             }
198              
199             return @{$self->{regions}->{ServiceEndpoints}};
200              
201             $self->_unload_regions();
202             }
203              
204             =head2 get_http_support( $service, [ @regions ] )
205              
206             Returns a list of the available http services endpoints for a service abbreviation
207             as returned by get_services.
208             A region or list of regions can be specified to narrow down the results.
209              
210             =cut
211              
212             sub get_http_support {
213             my ( $self, $service, @regions ) = @_;
214              
215             return $self->get_protocol_support( 'Http', $service, @regions );
216             }
217              
218             =head2 get_https_support( $service, [ @regions ] )
219              
220             @regions is an optional list of regions to narrow down the results.
221              
222             Returns a list of the available https services endpoints for a service abbreviation
223             as returned by get_services.
224              
225             =cut
226              
227             sub get_https_support {
228             my ( $self, $service, @regions ) = @_;
229              
230             return $self->get_protocol_support( 'Https', $service, @regions );
231             }
232              
233             =head2 get_protocol_support( $protocol, $service, [ @regions ] )
234              
235             @regions is an optional list of regions to narrow down the results.
236              
237             Returns a list of the available services endpoints for a service abbreviation as
238             returned by get_services for a given protocol. Protocols should be cased accordingly.
239              
240             =cut
241              
242             sub get_protocol_support {
243             my ( $self, $protocol, $service, @regions ) = @_;
244              
245             croak 'A protocol must be specified' unless defined $protocol;
246             croak 'A service must be specified' unless defined $service;
247              
248             $self->_load_regions();
249              
250             @regions = keys %{$self->{regions}->{Regions}} unless ( @regions );
251              
252             my $regions_key = join('||', sort @regions);
253              
254             my @protocol_support;
255              
256             unless ( defined $self->{regions}->{$protocol . 'Support'}->{$service}->{$regions_key} ) {
257             foreach my $region ( @regions ) {
258             push @protocol_support, $self->{regions}->{Regions}->{$region}->{Endpoint}->{$service}->{Hostname}
259             if (
260             defined $self->{regions}->{Regions}->{$region}->{Endpoint}->{$service} &&
261             $self->_is_true(
262             $self->{regions}->{Regions}->{$region}->{Endpoint}->{$service}->{$protocol}
263             )
264             );
265             }
266             $self->{regions}->{$protocol . 'Support'}->{$service}->{$regions_key} = \@protocol_support;
267             }
268              
269             return @{$self->{regions}->{$protocol . 'Support'}->{$service}->{$regions_key}};
270              
271             $self->_unload_regions();
272              
273             }
274              
275             =head2 get_service_endpoint( $protocol, $service, @regions )
276              
277             $protocol is a protocol as returned by get_known_protocols.
278             $service is a service abbreviation as returned by get_services.
279             @regions is a list of regions as returned by get_regions.
280              
281             Returns the list of endpoints for the specified protocol and service on a list of regions.
282              
283             =cut
284              
285             sub get_service_endpoint {
286             my ( $self, $protocol, $service, @regions ) = @_;
287              
288             croak 'A protocol must be specified' unless defined $protocol;
289             croak 'A service must be specified' unless defined $service;
290             croak 'At least one region must be specified' unless @regions;
291              
292             $self->_load_regions();
293              
294             my @endpoints;
295              
296             foreach my $region ( @regions ) {
297             push @endpoints, $self->{regions}->{Regions}->{$region}->{Endpoint}->{$service}->{Hostname}
298             if (
299             $self->_is_true(
300             $self->{regions}->{Regions}->{$region}->{Endpoint}->{$service}->{$protocol}
301             )
302             );
303             }
304              
305             $self->_unload_regions();
306              
307             return @endpoints;
308             }
309              
310             =head2 is_service_supported( $service, @regions )
311              
312             $service is a service abbreviation as returned by get_services.
313             @regions is a list of regions as returned by get_regions.
314              
315             Returns true if the service is supported in all listed regions.
316              
317             =cut
318              
319             sub is_service_supported {
320             my ( $self, $service, @regions ) = @_;
321             my $support = 1;
322              
323             croak 'A service must be specified' unless defined $service;
324             croak 'At least one region must be specified' unless @regions;
325              
326             $self->_load_regions();
327              
328             foreach my $region ( @regions ) {
329             my $supported_in_this_region = 0;
330             foreach my $protocol ( $self->get_known_protocols() ) {
331             $supported_in_this_region ||= $self->_is_true( $self->{regions}->{Regions}->{$region}->{Endpoint}->{$service}->{$protocol} );
332             last if $supported_in_this_region;
333             }
334             $support &&= $supported_in_this_region;
335             last unless $support;
336             }
337              
338             $self->_unload_regions();
339              
340             return $support;
341             }
342              
343             =head2 has_http_endpoint( $service, @regions )
344              
345             $service is a service abbreviation as returned by get_services.
346             @regions is a list of regions as returned by get_regions.
347              
348             Returns true if an http endpoint exists for the service on the region or list or regions
349              
350             =cut
351              
352             sub has_http_endpoint {
353             my ( $self, $service, @regions ) = @_;
354              
355             $self->_load_regions();
356              
357             return $self->has_protocol_endpoint( 'Http', $service, @regions );
358              
359             $self->_unload_regions();
360             }
361              
362             =head2 has_https_endpoint( $service, @regions )
363              
364             $service is a service abbreviation as returned by get_services.
365             @regions is a list of regions as returned by get_regions.
366              
367             Returns true if an https endpoint exists for the service on the region or list or regions
368              
369             =cut
370              
371             sub has_https_endpoint {
372             my ( $self, $service, @regions ) = @_;
373              
374             $self->_load_regions();
375              
376             return $self->has_protocol_endpoint( 'Https', $service, @regions );
377              
378             $self->_unload_regions();
379             }
380              
381             =head2 has_protocol_endpoint( $protocol, $service, @regions )
382              
383             $protocol is a protocol as returned by get_known_protocols.
384             $service is a service abbreviation as returned by get_services.
385             @regions is a list of regions as returned by get_regions.
386              
387             Returns true if an endpoint of the specified protocol exists for the service on the region or list or regions
388              
389             =cut
390              
391             sub has_protocol_endpoint {
392             my ( $self, $protocol, $service, @regions ) = @_;
393              
394             croak 'A protocol must be specified.' unless $protocol;
395             croak 'A service must be specified' unless defined $service;
396             croak 'At least one region must be specified' unless @regions;
397              
398             $self->_load_regions();
399              
400             my $has_protocol = 1;
401              
402             foreach my $region ( @regions ) {
403             $has_protocol &&= $self->_is_true( $self->{regions}->{Regions}->{$region}->{Endpoint}->{$service}->{$protocol} );
404             last unless $has_protocol;
405             }
406              
407             $self->_unload_regions();
408              
409             return $has_protocol;
410             }
411              
412             =head2 get_known_protocols
413              
414             Returns a list of known endpoint protocols, e.g. Http, Https (note casing).
415              
416             =cut
417              
418             sub get_known_protocols {
419             my ( $self ) = @_;
420              
421             return @{$self->{regions}->{Protocols}};
422             }
423              
424             =head2 set_known_protocols ( @protocols )
425              
426             Sets the list of known protocols. Should not be used unless Net::Amazon::Utils::Regions is really
427             outdated or you are blatantly galant and brave, probably reckless.
428             Remember to properly case protocols and rerun test including your set protocols.
429              
430             Returns the newly set protocols.
431              
432             =cut
433              
434             sub set_known_protocols {
435             my ( $self, @protocols) = @_;
436              
437             croak 'Protocols must be specified.' unless @protocols;
438              
439             $self->{regions}->{Protocols} = \@protocols;
440              
441             return @protocols;
442             }
443              
444             =head2 reset_known_protocols
445              
446             Sets the list of known protocols to Net::Amazon::Utils::Regions defaults.
447             Should fix bad set_known_protocols.
448              
449             =cut
450              
451             sub reset_known_protocols {
452             my ( $self) = @_;
453              
454             $self->set_known_protocols( 'Http', 'Https' );
455             }
456              
457             =head2 get_endpoint_uris( $protocol, $service, @regions )
458              
459             $protocol is a protocol as returned by get_known_protocols.
460             $service is a service abbreviation as returned by get_services.
461             @regions is a list of regions as returned by get_regions.
462              
463             Returns a list of protocol://service.region.domain URIs usable for RESTful fidling.
464              
465             =cut
466              
467             sub get_endpoint_uris {
468             my ( $self, $protocol, $service, @regions ) = @_;
469              
470             croak 'A protocol must be specified.' unless $protocol;
471             croak 'A service must be specified' unless defined $service;
472             croak 'At least one region must be specified' unless @regions;
473              
474             $self->_load_regions();
475              
476             my @endpoint_uris;
477             my $domain = $self->get_domain();
478              
479             foreach my $region ( @regions ) {
480             if ( defined $self->_is_true( $self->{regions}->{Regions}->{$region}->{Endpoint}->{$service}->{$protocol} ) ) {
481             push @endpoint_uris, "\L$protocol\E://$service.$region.$domain";
482             } else {
483             croak "An endpoint does not exist for $service in $region with protocol $protocol.";
484             }
485             }
486              
487             return @endpoint_uris;
488              
489             $self->_unload_regions();
490             }
491              
492             =head1 Internal Functions
493              
494             =head2 _load_regions( [$force] )
495              
496             Loads regions from local cached file or the Internet performing reasonable formatting.
497              
498             $force, does what it should when set.
499              
500             If Internet fails local cached file is used.
501             If loading of new region definitions fail, old regions remain unaffected.
502              
503             =cut
504              
505             sub _load_regions {
506             my ( $self, $force ) = @_;
507              
508             if ( $force || !defined $self->{regions} ) {
509             my $error;
510              
511             my $new_regions;
512             if ( $self->{no_inet} ) {
513             eval {
514             require Net::Amazon::Utils::Regions;
515             $new_regions = Net::Amazon::Utils::Regions::get_regions_data();
516             };
517             if ( $@ ) {
518             carp "Processing XML failed with error $@";
519             $error = 1;
520             }
521             } else {
522             my $response = $self->{ua}->get( $self->{remote_region_file},
523             'Accept-Encoding' => scalar HTTP::Message::decodable,
524             'If-None-Match' => $self->{region_etag} );
525             if ( $response->is_success ) {
526             # Store etag for later tests
527             $self->{region_etag} = $response->header( 'Etag' );
528             # This should be a big file...
529             my $content = $response->decoded_content;
530             carp "Size of region file looks suspiciously small." if ( length $content < 10000 );
531             eval {
532             my @xml_options = ( KeyAttr => { Region => 'Name', Endpoint=>'ServiceName', Service => 'Name' } );
533             $new_regions = XML::Simple::XMLin( $content, @xml_options );
534              
535             # Check that some "trustable" regions and services exist.
536             unless ( defined $new_regions &&
537             defined $new_regions->{Regions} &&
538             defined $new_regions->{Regions}->{Region}->{'us-east-1'} &&
539             defined $new_regions->{Regions}->{Region}->{'us-west-1'} &&
540             defined $new_regions->{Regions}->{Region}->{'us-west-2'} &&
541             defined $new_regions->{Services} &&
542             defined $new_regions->{Services}->{Service}->{ec2} &&
543             defined $new_regions->{Services}->{Service}->{sqs} &&
544             defined $new_regions->{Services}->{Service}->{glacier}
545             ) {
546             croak "Region file format cannot be trusted.";
547             }
548             };
549             if ( $@ ) {
550             carp "Processing XML failed with error $@";
551             $error = 1;
552             }
553             } else {
554             unless ( $response->code() eq '304' ) {
555             carp "Getting updated regions failed with " . $response->status_line;
556             $error = 1;
557             }
558             }
559             }
560             # Retry locally on errors
561             if ( $error ) {
562             my $old_no_inet = $self->{no_inet};
563             carp "Getting regions file from Internet failed will use local cache. Check your Internet connection...";
564             $self->{no_inet} = 1;
565             $self->_load_regions();
566             $self->{no_inet} = $old_no_inet
567             }
568             $new_regions->{Regions} = $new_regions->{Regions}->{Region};
569             $new_regions->{Services} = $new_regions->{Services}->{Service};
570              
571             $self->{regions} = $new_regions if ( defined $new_regions );
572             # Create a set of correct protocols for this set
573             $self->reset_known_protocols();
574             }
575             }
576              
577             =head2 _unload_regions
578              
579             Unloads regions recovering memory unless object has been instantiated with
580             cache_regions set to any true value.
581              
582             =cut
583              
584             sub _unload_regions {
585             my ( $self ) = @_;
586              
587             $self->_force_unload_regions unless $self->{cache_regions};
588             }
589              
590             =head2 _force_unload_regions
591              
592             Unloads regions recovering memory.
593              
594             =cut
595              
596             sub _force_unload_regions {
597             my ( $self ) = @_;
598              
599             $self->{regions} = undef;
600             }
601              
602             =head2 _get_remote_regions_file_uri
603              
604             Returns the uri of the remote regions.xml file.
605              
606             =cut
607              
608             sub _get_remote_regions_file_uri {
609             my ( $self ) = @_;
610              
611             return $self->{remote_region_file};
612             }
613              
614             =head2 get_regions_file_raw
615              
616             Returns the full structure (plus possibly cached queries) of the interpreted regions.xml file.
617              
618             =cut
619              
620             sub _get_regions_file_raw {
621             my ( $self ) = @_;
622              
623             $self->_load_regions();
624              
625             return $self->{regions};
626              
627             $self->_unload_regions();
628             }
629              
630             =head2 _is_true
631              
632             Converts a supposed truth into a true Perl true value if the value should be true perlishly speaking.
633              
634             Returns a true value on strings that should be true in regions.xml parlance.
635              
636             =cut
637              
638             sub _is_true {
639             my ( $self, $supposed_truth ) = @_;
640              
641             return $supposed_truth eq 'true';
642             }
643              
644             =head1 AUTHOR
645              
646             Gonzalo Barco, C<< >>
647              
648             =head1 TODO
649              
650             =over 4
651              
652             =item * Online tests that endpoints are actually there.
653              
654             =item * Better return values when scalar is expected.
655              
656             =item * Probably helpers for assembling and signing requests to actual endpoints.
657              
658             =back
659              
660             =head1 BUGS
661              
662             Please report any bugs or feature requests to C, or through
663             the web interface at L. I will be notified, and then you'll
664             automatically be notified of progress on your bug as I make changes.
665              
666             =head1 SUPPORT
667              
668             You can find documentation for this module with the perldoc command.
669              
670             perldoc Net::Amazon::Utils
671              
672             You can also look for information at:
673              
674             =over 4
675              
676             =item * RT: CPAN's request tracker (report bugs here)
677              
678             L
679              
680             =item * AnnoCPAN: Annotated CPAN documentation
681              
682             L
683              
684             =item * CPAN Ratings
685              
686             L
687              
688             =item * Search CPAN
689              
690             L
691              
692             =back
693              
694             =head1 ACKNOWLEDGEMENTS
695              
696             =head1 LICENSE AND COPYRIGHT
697              
698             Copyright 2014 Gonzalo Barco.
699              
700             This program is free software; you can redistribute it and/or modify it
701             under the terms of either: the GNU General Public License as published
702             by the Free Software Foundation; or the Artistic License.
703              
704             See http://dev.perl.org/licenses/ for more information.
705              
706             =cut
707              
708             1; # End of Net::Amazon::Utils