File Coverage

lib/VM/EC2/Dispatch.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package VM::EC2::Dispatch;
2              
3 7     7   26 use strict;
  7         8  
  7         211  
4              
5 7     7   1526 use XML::Simple;
  0            
  0            
6             use URI::Escape;
7              
8             =head1 NAME
9              
10             VM::EC2::Dispatch - Create Perl objects from AWS XML requests
11              
12             =head1 SYNOPSIS
13              
14             use VM::EC2;
15              
16             VM::EC2::Dispatch->register('DescribeRegions'=>\&mysub);
17              
18             VM::EC2::Dispatch->replace('DescribeRegions'=>'My::Type');
19            
20             sub mysub {
21             my ($parsed_xml_object,$ec2) = @_;
22             my $payload = $parsed_xml_object->{regionInfo}
23             return My::Type->new($payload,$ec2);
24             }
25              
26             =head1 DESCRIPTION
27              
28             This class handles turning the XML response to AWS requests into perl
29             objects. Only one method is likely to be useful to developers, the
30             replace() class method. This allows you to replace the handlers
31             used to map the response onto objects.
32              
33             =head2 VM::EC2::Dispatch->replace($request_name => \&sub)
34              
35             =head2 VM::EC2::Dispatch->replace($request_name => 'Class::Name')
36              
37             =head2 VM::EC2::Dispatch->replace($request_name => 'method_name,arg1,arg2,...')
38              
39             Before invoking a VM::EC2 request you wish to customize, call the
40             replace() method with two arguments. The first argument is the
41             name of the request you wish to customize, such as
42             "DescribeVolumes". The second argument is either a code reference, a
43             VM::EC2::Dispatch method name and arguments (separated by commas), or
44             a class name.
45              
46             In the case of a code reference as the second argument, the subroutine
47             you provide will be invoked with four arguments consisting of the
48             parsed XML response, the VM::EC2 object, the XML namespace string from
49             the request, and the Amazon-assigned request ID. In practice, only the
50             first two arguments are useful.
51              
52             In the case of a string containing a classname, the class will be
53             loaded if it needs to be, and then its new() method invoked as
54             follows:
55              
56             Your::Class->new($parsed_xml,$ec2,$xmlns,$requestid)
57              
58             Your new() method should return one or more objects. It is suggested
59             that you subclass VM::EC2::Generic and use the inherited new() method
60             to store the parsed XML and EC2 object. See the code for
61             L for a simple template.
62              
63             If the second argument is neither a code reference nor a classname, it
64             will be treated as a VM::EC2::Dispatch method name and its arguments,
65             separated by commas. The method will be invoked as follows:
66              
67             $dispatch->$method_name($raw_xml,$ec2,$arg1,$arg2,$arg3,...)
68              
69             There are two methods currently defined for this purpose, boolean(),
70             and fetch_items(), which handle the preprocessing of several common
71             XML representations of EC2 data. Note that in this form, the RAW XML
72             is passed in, not the parsed data structure.
73              
74             The parsed XML response is generated by the XML::Simple module using
75             these options:
76              
77             $parser = XML::Simple->new(ForceArray => ['item', 'member'],
78             KeyAttr => ['key'],
79             SuppressEmpty => undef);
80             $parsed = $parser->XMLin($raw_xml)
81              
82             In general, this will give you a hash of hashes. Any tag named 'item'
83             or 'member' will be forced to point to an array reference, and any tag
84             named "key" will be flattened as described in the XML::Simple
85             documentation.
86              
87             A simple way to examine the raw parsed XML is to invoke any
88             VM::EC2::Object's as_string method:
89              
90             my ($i) = $ec2->describe_instances;
91             print $i->as_string;
92              
93             This will give you a Data::Dumper representation of the XML after it
94             has been parsed. Look at the calls to VM::EC2::Dispatch->register() in
95             the various VM/EC2/REST/*.pm modules for many examples of how this
96             works.
97              
98             Note that the replace() method was called add_override() in previous
99             versions of this module. add_override() is recognized as an alias for
100             backward compatibility.
101              
102             =head2 VM::EC2::Dispatch->register($request_name1 => \&sub1,$request_name2 => \&sub2,...)
103              
104             Similar to replace() but if the request name is already registered
105             does not overwrite it. You may provide multiple request=>handler pairs.
106              
107             =head1 OBJECT CREATION METHODS
108              
109             The following methods perform simple pre-processing of the parsed XML
110             (a hash of hashes) before passing the modified data structure to the
111             designated object class. They are used as the second argument to
112             VM::EC2::Dispatch->register().
113              
114             =cut
115             ;
116              
117             my $REGISTRATION = {};
118             VM::EC2::Dispatch->register(Error => 'VM::EC2::Error');
119             *add_override = \&replace; # backward compatibility
120              
121             # Not clear that you ever need to instantiate this object as it has
122             # no instance data.
123             sub new {
124             my $class = shift;
125             my $self= bless {},ref $class || $class;
126             return $self;
127             }
128              
129             sub replace {
130             my $self = shift;
131             while (my ($request_name,$object_creator) = splice(@_,0,2)) {
132             $REGISTRATION->{$request_name} = $object_creator;
133             }
134             }
135              
136             sub register {
137             my $self = shift;
138             while (my ($request_name,$object_creator) = splice(@_,0,2)) {
139             $REGISTRATION->{$request_name} ||= $object_creator;
140             }
141             }
142              
143             # new way
144             sub content2objects {
145             my $self = shift;
146             my ($action,$content,$ec2) = @_;
147              
148             my $handler = $REGISTRATION->{$action} || 'VM::EC2::Generic';
149             my ($method,@params) = split /,/,$handler;
150              
151             if (ref $handler eq 'CODE') {
152             my $parsed = $self->new_xml_parser->XMLin($content);
153             my $req_id_tag = $parsed->{requestId} ? 'requestId' : 'RequestId';
154             $handler->($parsed,$ec2,@{$parsed}{'xmlns',$req_id_tag});
155             }
156             elsif ($self->can($method)) {
157             return $self->$method($content,$ec2,@params);
158             }
159             else {
160             load_module($handler);
161             my $parser = $self->new();
162             $parser->parse($content,$ec2,$handler);
163             }
164             }
165              
166             sub parser {
167             my $self = shift;
168             return $self->{xml_parser} ||= $self->new_xml_parser;
169             }
170              
171             sub parse {
172             my $self = shift;
173             my ($content,$ec2,$class) = @_;
174             $self = $self->new unless ref $self;
175             my $parsed = $self->parser->XMLin($content);
176             return $self->create_objects($parsed,$ec2,$class);
177             }
178              
179             sub new_xml_parser {
180             my $self = shift;
181             my $nokey = shift;
182             return XML::Simple->new(ForceArray => ['item', 'member'],
183             KeyAttr => $nokey ? [] : ['key'],
184             SuppressEmpty => undef,
185             );
186             }
187              
188             =head2 $bool = $dispatch->boolean($raw_xml,$ec2,$tag)
189              
190             This is used for XML responses like this:
191              
192            
193             59dbff89-35bd-4eac-99ed-be587EXAMPLE
194             true
195            
196              
197             It looks inside the structure for the tag named $tag ("return" if not
198             provided), and returns a true value if the contents equals "true".
199              
200             Pass it to replace() like this:
201              
202             VM::EC2::Dispatch->replace(DeleteVolume => 'boolean,return';
203              
204             or, since "return" is the default tag:
205              
206             VM::EC2::Dispatch->replace(DeleteVolume => 'boolean';
207              
208             =cut
209              
210             sub boolean {
211             my $self = shift;
212             my ($content,$ec2,$tag) = @_;
213             my $parsed = $self->new_xml_parser()->XMLin($content);
214             $tag ||= 'return';
215             return $parsed->{$tag} eq 'true';
216             }
217              
218             =head2 @list = $dispatch->elb_member_list($raw_xml,$ec2,$tag)
219              
220             This is used for XML responses from the ELB API such as this:
221              
222            
223            
224            
225             us-west-2a
226             us-west-2b
227            
228            
229            
230             02eadcfc-fc38-11e1-a1bf-9de31EXAMPLE
231            
232            
233              
234             It looks inside the Result structure for the tag named $tag and returns the
235             list wrapped in member elements. In this case the tag is 'AvailabilityZones'
236             and the return value would be:
237             ( 'us-west-2a', 'us-west-2b' )
238              
239             If $embedded_tag is passed, then it is used for XML responses such as this,
240             where the member list has an embedded tag:
241              
242            
243            
244            
245            
246             i-12345678
247            
248            
249             i-90abcdef
250            
251            
252            
253            
254             f4f12596-fc3b-11e1-be5a-f71ecEXAMPLE
255            
256            
257              
258             It looks inside the Result structure for the tag named $tag and returns the
259             list wrapped in a member element plus the embedded tag. In this case the
260             tag is 'Instances', the embedded tag is 'InstanceId' and the return value would
261             be: ( 'i-12345678', 'i-90abcdef' )
262              
263             =cut
264              
265             sub elb_member_list {
266             my $self = shift;
267             my ($content,$ec2,$tag,$embedded_tag) = @_;
268             my $parsed = $self->new_xml_parser()->XMLin($content);
269             my ($result_key) = grep /Result$/,keys %$parsed;
270             return $embedded_tag ? map { $_->{$embedded_tag} } @{$parsed->{$result_key}{$tag}{member}} :
271             @{$parsed->{$result_key}{$tag}{member}};
272             }
273              
274             # identical to fetch_one, except looks inside the (APICallName)Result tag that
275             # ELB and RDS API calls return
276             sub fetch_one_result {
277             my $self = shift;
278             my ($content,$ec2,$tag,$class,$nokey) = @_;
279             load_module($class);
280             my $parser = $self->new_xml_parser($nokey);
281             my $parsed = $parser->XMLin($content);
282             my ($result_key) = grep /Result$/,keys %$parsed;
283             my $obj = $parsed->{$result_key}{$tag} or return;
284             return $class->new($obj,$ec2,@{$parsed}{'xmlns','RequestId'});
285             }
286              
287             sub fetch_one {
288             my $self = shift;
289             my ($content,$ec2,$tag,$class,$nokey) = @_;
290             load_module($class);
291             my $parser = $self->new_xml_parser($nokey);
292             my $parsed = $parser->XMLin($content);
293             my $obj = $parsed->{$tag} or return;
294             return $class->new($obj,$ec2,@{$parsed}{'xmlns','requestId'});
295             }
296              
297             =head2 @objects = $dispatch->fetch_items($raw_xml,$ec2,$container_tag,$object_class,$nokey)
298              
299             This is used for XML responses like this:
300              
301            
302             59dbff89-35bd-4eac-99ed-be587EXAMPLE
303            
304            
305             gsg-keypair
306            
307             1f:51:ae:28:bf:89:e9:d8:1f:25:5d:37:2d:7d:b8:ca:9f:f5:f1:6f
308            
309            
310            
311             default-keypair
312            
313             0a:93:bb:e8:c2:89:e9:d8:1f:42:5d:37:1d:8d:b8:0a:88:f1:f1:1a
314            
315            
316            
317            
318              
319             It looks inside the structure for the tag named $container_tag, pulls
320             out the items that are stored under and then passes the parsed
321             contents to $object_class->new(). The optional $nokey argument is used
322             to suppress XML::Simple's default flattening behavior turning tags
323             named "key" into hash keys.
324              
325             Pass it to replace() like this:
326              
327             VM::EC2::Dispatch->replace(DescribeVolumes => 'fetch_items,volumeSet,VM::EC2::Volume')
328              
329             =cut
330              
331             sub fetch_items {
332             my $self = shift;
333             my ($content,$ec2,$tag,$class,$nokey) = @_;
334             load_module($class);
335             my $parser = $self->new_xml_parser($nokey);
336             my $parsed = $parser->XMLin($content);
337             my $list = $parsed->{$tag}{item} or return;
338             return map {$class->new($_,$ec2,@{$parsed}{'xmlns','requestId'})} @$list;
339             }
340              
341             =head2 @objects = $dispatch->fetch_members($raw_xml,$ec2,$container_tag,$object_class,$nokey)
342              
343             Used for XML responses from ELB API calls which contain a key that is the name
344             of the API call with 'Result' appended. All these XML responses contain
345             'member' as the item delimter instead of 'item'
346              
347             =cut
348              
349             sub fetch_members {
350             my $self = shift;
351             my ($content,$ec2,$tag,$class,$nokey) = @_;
352             load_module($class);
353             my $parser = $self->new_xml_parser($nokey);
354             my $parsed = $parser->XMLin($content);
355             my ($result_key) = grep /Result$/,keys %$parsed;
356             my $list = $parsed->{$result_key}{$tag}{member} or return;
357             return map {$class->new($_,$ec2,@{$parsed}{'xmlns','RequestId'})} @$list;
358             }
359              
360             =head2 @objects = $dispatch->fetch_items_iterator($raw_xml,$ec2,$container_tag,$object_class,$token_name)
361              
362             This is used for requests that have a -max_results argument. In this
363             case, the response will have a nextToken field, which can be used to
364             fetch the "next page" of results.
365              
366             The $token_name is some unique identifying token. It will be turned
367             into two temporary EC2 instance variables, one named
368             "${token_name}_token", which contains the nextToken value, and the
369             other "${token_name}_stop", which flags the caller that no more
370             results will be forthcoming.
371              
372             This must all be coordinated with the request subroutine. See how
373             describe_instance_status() and describe_spot_price_history() do it.
374              
375             =cut
376              
377             sub fetch_items_iterator {
378             my $self = shift;
379             my ($content,$ec2,$tag,$class,$base_name) = @_;
380             my $token = "${base_name}_token";
381             my $stop = "${base_name}_stop";
382              
383             load_module($class);
384             my $parser = $self->new_xml_parser();
385             my $parsed = $parser->XMLin($content);
386             my $list = $parsed->{$tag}{item} or return;
387              
388             if ($ec2->{$token} && !$parsed->{nextToken}) {
389             delete $ec2->{$token};
390             $ec2->{$stop}++;
391             } else {
392             $ec2->{$token} = $parsed->{nextToken};
393             }
394             return map {$class->new($_,$ec2,@{$parsed}{'xmlns','requestId'})} @$list;
395             }
396              
397             sub create_objects {
398             my $self = shift;
399             my ($parsed,$ec2,$class) = @_;
400             return $class->new($parsed,$ec2,@{$parsed}{'xmlns','requestId'});
401             }
402              
403             sub create_error_object {
404             my $self = shift;
405             my ($content,$ec2,$API_call) = @_;
406             my $class = $REGISTRATION->{Error};
407             eval "require $class; 1" || die $@ unless $class->can('new');
408             my $parsed = $self->new_xml_parser->XMLin($content);
409             if (defined $API_call) {
410             $parsed->{Errors}{Error}{Message} =~ s/\.$//;
411             $parsed->{Errors}{Error}{Message} .= ", at API call '$API_call'";
412             }
413             return $class->new($parsed->{Errors}{Error},$ec2,@{$parsed}{'xmlns','RequestID'});
414             }
415              
416             # alternate method used for ELB, RDS calls
417             sub create_alt_error_object {
418             my $self = shift;
419             my ($content,$ec2) = @_;
420             my $class = 'VM::EC2::Error';
421             eval "require $class; 1" || die $@ unless $class->can('new');
422             my $parsed = $self->new_xml_parser->XMLin($content);
423             return $class->new($parsed->{Error},$ec2,@{$parsed}{'xmlns','RequestId'});
424             }
425              
426             # not a method!
427             sub load_module {
428             my $class = shift;
429             eval "require $class; 1" || die $@ unless $class->can('new');
430             }
431              
432             =head1 EXAMPLE OF USING OVERRIDE TO SUBCLASS VM::EC2::Volume
433              
434             The author decided that a volume object should not be able to delete
435             itself; you disagree with that decision. Let's subclass
436             VM::EC2::Volume to add a delete() method.
437              
438             First subclass the VM::EC2::Volume class:
439              
440             package MyVolume;
441             use base 'VM::EC2::Volume';
442              
443             sub delete {
444             my $self = shift;
445             $self->ec2->delete_volume($self);
446             }
447              
448             Now subclass VM::EC2 to add the appropriate overrides to the new() method:
449              
450             package MyEC2;
451             use base 'VM::EC2';
452              
453             sub new {
454             my $class = shift;
455             VM::EC2::Dispatch->replace(CreateVolume =>'MyVolume');
456             VM::EC2::Dispatch->replace(DescribeVolumes=>'fetch_items,volumeSet,MyVolume');
457             return $class->SUPER::new(@_);
458             }
459              
460             Now we can test it out:
461              
462             use MyEC2;
463             # find all volumes that are "available" and not in-use
464             my @vol = $ec2->describe_volumes({status=>'available'});
465             for my $vol (@vol) {
466             $vol->delete && print "$vol deleted\n"
467             }
468            
469             =head1 SEE ALSO
470              
471             L
472             L
473             L
474             L
475             L
476             L
477             L
478             L
479             L
480             L
481             L
482             L
483             L
484             L
485             L
486             L
487             L
488             L
489             L
490             L
491             L
492             L
493             L
494              
495             =head1 AUTHOR
496              
497             Lincoln Stein Elincoln.stein@gmail.comE.
498              
499             Copyright (c) 2011 Ontario Institute for Cancer Research
500              
501             This package and its accompanying libraries is free software; you can
502             redistribute it and/or modify it under the terms of the GPL (either
503             version 1, or at your option, any later version) or the Artistic
504             License 2.0. Refer to LICENSE for the full license text. In addition,
505             please see DISCLAIMER.txt for disclaimers of warranty.
506              
507             =cut
508              
509             1;
510