File Coverage

blib/lib/Net/RackSpace/CloudServers.pm
Criterion Covered Total %
statement 26 28 92.8
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 36 38 94.7


line stmt bran cond sub pod time code
1             package Net::RackSpace::CloudServers;
2              
3             BEGIN {
4 2     2   60431 $Net::RackSpace::CloudServers::VERSION = '0.14';
5             }
6 2     2   18 use warnings;
  2         4  
  2         58  
7 2     2   12 use strict;
  2         3  
  2         68  
8 2     2   1695 use Any::Moose;
  2         116896  
  2         13  
9 2     2   1084 use Any::Moose ('::Util::TypeConstraints');
  2         3  
  2         9  
10 2     2   1729 use Net::RackSpace::CloudServers::Flavor;
  2         6  
  2         58  
11 2     2   1110 use Net::RackSpace::CloudServers::Server;
  2         5  
  2         67  
12 2     2   14 use Net::RackSpace::CloudServers::Image;
  2         3  
  2         41  
13 2     2   1293 use Net::RackSpace::CloudServers::Limits;
  2         4  
  2         54  
14 2     2   2334 use LWP::ConnCache::MaxKeepAliveRequests;
  0            
  0            
15             use LWP::UserAgent::Determined;
16             use JSON;
17             use YAML;
18             use Carp;
19              
20             our $DEBUG = 0;
21              
22             has 'user' => ( is => 'ro', isa => 'Str', required => 1 );
23             has 'key' => ( is => 'ro', isa => 'Str', required => 1 );
24             has 'timeout' => ( is => 'ro', isa => 'Num', required => 0, default => 30 );
25             has 'ua' => ( is => 'rw', isa => 'LWP::UserAgent', required => 0 );
26              
27             # This module currently supports only US and UK
28             subtype ValidLocation => as 'Str' => where { $_ eq 'US' or $_ eq 'UK' };
29              
30             # The two locations have different API endpoints
31             our %api_endpoint_by_location = (
32             US => 'https://auth.api.rackspacecloud.com/v1.0',
33             UK => 'https://lon.auth.api.rackspacecloud.com/v1.0',
34             );
35              
36             # So this module can work on either the US or the UK versions
37             # of the API, defaulting to the current default
38             has 'location' =>
39             ( is => 'rw', isa => 'ValidLocation', required => 1, default => 'US' );
40              
41             has 'limits' => (
42             is => 'rw',
43             isa => 'Net::RackSpace::CloudServers::Limits',
44             lazy => 1,
45             required => 1,
46             default => sub {
47             my ($self) = @_;
48             return Net::RackSpace::CloudServers::Limits->new( cloudservers => $self,
49             );
50             }
51             );
52              
53             has 'server_management_url' => (
54             is => 'rw',
55             isa => 'Str',
56             required => 0,
57             );
58             has 'storage_url' => (
59             is => 'rw',
60             isa => 'Str',
61             required => 0,
62             );
63             has 'cdn_management_url' => ( is => 'rw', isa => 'Str', required => 0 );
64             has 'token' => ( is => 'rw', isa => 'Str', required => 0 );
65              
66             no Any::Moose;
67             __PACKAGE__->meta->make_immutable();
68              
69             # copied from Net::Mosso::CloudFiles
70             sub BUILD {
71             my $self = shift;
72             my $ua = LWP::UserAgent::Determined->new(
73             keep_alive => 10,
74             requests_redirectable => [qw(GET HEAD DELETE PUT)],
75             );
76             $ua->timing('1,2,4,8,16,32');
77             $ua->conn_cache(
78             LWP::ConnCache::MaxKeepAliveRequests->new(
79             total_capacity => 10,
80             max_keep_alive_requests => 990,
81             )
82             );
83             my $http_codes_hr = $ua->codes_to_determinate();
84             $http_codes_hr->{422} = 1; # used by cloudfiles for upload data corruption
85             $ua->timeout( $self->timeout );
86             $ua->env_proxy;
87             $self->ua($ua);
88             $self->_authenticate;
89             }
90              
91             sub _authenticate {
92             my $self = shift;
93             my $request = HTTP::Request->new(
94             'GET',
95             $api_endpoint_by_location{ $self->location },
96             [
97             'X-Auth-User' => $self->user,
98             'X-Auth-Key' => $self->key,
99             ]
100             );
101             my $response = $self->_request($request);
102             confess 'Unauthorized' if $response->code == 401;
103             confess 'Unknown error ' . $response->code . "\n" . $response->content
104             if $response->code != 204;
105              
106             my $server_management_url = $response->header('X-Server-Management-Url')
107             || confess 'Missing server management url';
108             $self->server_management_url($server_management_url);
109             my $token = $response->header('X-Auth-Token')
110             || confess 'Missing auth token';
111             $self->token($token);
112              
113             # From the docs:
114             # The URLs specified in X-Storage-Url and X-CDN-Management-Url
115             # are specific to the Cloud Files product and may be ignored
116             # for purposes of interacting with Cloud Servers.
117              
118             my $storage_url = $response->header('X-Storage-Url')
119             || confess 'Missing storage url';
120             $self->storage_url($storage_url);
121             my $cdn_management_url = $response->header('X-CDN-Management-Url')
122             || confess 'Missing CDN management url';
123             $self->storage_url($cdn_management_url);
124             }
125              
126             sub _request {
127             my ( $self, $request, $filename ) = @_;
128             warn "Requesting ", $request->as_string if $DEBUG;
129             my $response = $self->ua->request( $request, $filename );
130             warn "Requested, got ", $response->as_string if $DEBUG;
131              
132             # From the docs:
133             # Authentication tokens are typically valid for 24 hours.
134             # Applications should be designed to re-authenticate after
135             # receiving a 401 Unauthorized response.
136              
137             if ( $response->code == 401 && $request->header('X-Auth-Token') ) {
138              
139             # http://trac.cyberduck.ch/ticket/2876
140             # Be warned that the token will expire over time (possibly as short
141             # as an hour). The application should trap a 401 (Unauthorized)
142             # response on a given request (to either storage or cdn system)
143             # and then re-authenticate to obtain an updated token.
144             warn "Need reauthentication" if $DEBUG;
145             $self->_authenticate;
146             $request->header( 'X-Auth-Token', $self->token );
147             warn $request->as_string if $DEBUG;
148             warn "Re-requesting" if $DEBUG;
149             $response = $self->ua->request( $request, $filename );
150             warn $response->as_string if $DEBUG;
151             }
152              
153             # From the docs:
154             # In the event you exceed the thresholds established for your account,
155             # a 413 Rate Control HTTP response will be returned with a
156             # Reply-After header to notify the client when they can attempt to
157             # try again.
158             if ( $response->code == 413 ) {
159             my $when = $response->header('Reply-After');
160             if ( !defined $when ) {
161             $when = 'in about 10 mins';
162             }
163             else {
164             $when = 'at ' . $when;
165             }
166             confess "Cannot execute request as rate control limit exceeded; retry ",
167             $when;
168             }
169              
170             return $response;
171             }
172              
173             sub get_server {
174             my $self = shift;
175             my $id = shift;
176             my $detail = shift;
177             my $uri = (
178             ( defined $detail && $detail )
179             ? ( defined $id ? '/servers/' . $id : '/servers/detail' )
180             : ( defined $id ? '/servers/' . $id : '/servers' )
181             );
182             my $request = HTTP::Request->new(
183             'GET',
184             $self->server_management_url . $uri,
185             [ 'X-Auth-Token' => $self->token ]
186             );
187             my $response = $self->_request($request);
188             return if scalar grep { $response->code eq $_ } ( 204, 404 );
189             confess 'Unknown error' . $response->code
190             unless scalar grep { $response->code eq $_ } ( 200, 203 );
191             my @servers;
192             my $hash_response = from_json( $response->content );
193             warn Dump($hash_response) if $DEBUG;
194              
195             # {"servers":[{"name":"test00","id":12345}]}
196              
197             confess 'response does not contain key "servers"'
198             if ( !defined $id && !defined $hash_response->{servers} );
199             confess 'response does not contain key "server"'
200             if ( defined $id && !defined $hash_response->{server} );
201             confess 'response does not contain arrayref of "servers"'
202             if ( !defined $id && ref $hash_response->{servers} ne 'ARRAY' );
203             confess 'response does not contain hashref of "server"'
204             if ( defined $id && ref $hash_response->{server} ne 'HASH' );
205              
206             return map {
207             Net::RackSpace::CloudServers::Server->new(
208             cloudservers => $self,
209             id => $_->{id},
210             name => $_->{name},
211             name => $_->{name},
212             imageid => $_->{imageId},
213             flavorid => $_->{flavorId},
214             hostid => $_->{hostId},
215             status => $_->{status},
216             progress => $_->{progress},
217             public_address => $_->{addresses}->{public},
218             private_address => $_->{addresses}->{private},
219             metadata => $_->{metadata},
220             adminpass => $_->{adminPass},
221             )
222             } @{ $hash_response->{servers} } if ( !defined $id );
223              
224             my $hserver = $hash_response->{server};
225             return Net::RackSpace::CloudServers::Server->new(
226             cloudservers => $self,
227             id => $hserver->{id},
228             name => $hserver->{name},
229             imageid => $hserver->{imageId},
230             flavorid => $hserver->{flavorId},
231             hostid => $hserver->{hostId},
232             status => $hserver->{status},
233             progress => $hserver->{progress},
234             public_address => $hserver->{addresses}->{public},
235             private_address => $hserver->{addresses}->{private},
236             metadata => $hserver->{metadata},
237             adminpass => $hserver->{adminPass}
238             );
239             }
240              
241             sub get_server_detail {
242             my $self = shift;
243             my $id = shift;
244             return $self->get_server( $id, 1 );
245             }
246              
247             sub get_flavor {
248             my $self = shift;
249             my $id = shift;
250             my $detail = shift;
251             my $uri = (
252             ( defined $detail && $detail )
253             ? ( defined $id ? '/flavors/' . $id : '/flavors/detail' )
254             : ( defined $id ? '/flavors/' . $id : '/flavors' )
255             );
256             my $request = HTTP::Request->new(
257             'GET',
258             $self->server_management_url . $uri,
259             [ 'X-Auth-Token' => $self->token ]
260             );
261             my $response = $self->_request($request);
262             return if $response->code == 204;
263             confess 'Unknown error ' . $response->code
264             unless scalar grep { $response->code eq $_ } ( 200, 203 );
265             my $hash_response = from_json( $response->content );
266             warn Dump($hash_response) if $DEBUG;
267              
268             confess 'response does not contain key "flavors"'
269             if ( !defined $id && !defined $hash_response->{flavors} );
270             confess 'response does not contain key "flavor"'
271             if ( defined $id && !defined $hash_response->{flavor} );
272             confess 'response does not contain arrayref of "flavors"'
273             if ( !defined $id && ref $hash_response->{flavors} ne 'ARRAY' );
274             confess 'response does not contain hashref of "flavor"'
275             if ( defined $id && ref $hash_response->{flavor} ne 'HASH' );
276              
277             return map {
278             Net::RackSpace::CloudServers::Flavor->new(
279             cloudservers => $self,
280             id => $_->{id},
281             name => $_->{name},
282             ram => $_->{ram},
283             disk => $_->{disk},
284             )
285             } @{ $hash_response->{flavors} } if ( !defined $id );
286             return Net::RackSpace::CloudServers::Flavor->new(
287             cloudservers => $self,
288             id => $hash_response->{flavor}->{id},
289             name => $hash_response->{flavor}->{name},
290             ram => $hash_response->{flavor}->{ram},
291             disk => $hash_response->{flavor}->{disk},
292             );
293             }
294              
295             sub get_flavor_detail {
296             my $self = shift;
297             my $id = shift;
298             return $self->get_flavor( $id, 1 );
299             }
300              
301             sub get_image {
302             my $self = shift;
303             my $id = shift;
304             my $detail = shift;
305             my $uri = (
306             ( defined $detail && $detail )
307             ? ( defined $id ? '/images/' . $id : '/images/detail' )
308             : ( defined $id ? '/images/' . $id : '/images' )
309             );
310             $uri .= '?changes_since=0';
311             my $request = HTTP::Request->new(
312             'GET',
313             $self->server_management_url . $uri,
314             [ 'X-Auth-Token' => $self->token ]
315             );
316             my $response = $self->_request($request);
317             return if $response->code == 204;
318             confess 'Unknown error' . $response->code
319             unless scalar grep { $response->code eq $_ } ( 200, 203 );
320             my $hash_response = from_json( $response->content );
321             warn Dump($hash_response) if $DEBUG;
322              
323             confess 'response does not contain key "images"'
324             if ( !defined $id && !defined $hash_response->{images} );
325             confess 'response does not contain key "image"'
326             if ( defined $id && !defined $hash_response->{image} );
327             confess 'response does not contain arrayref of "images"'
328             if ( !defined $id && ref $hash_response->{images} ne 'ARRAY' );
329             confess 'response does not contain hashref of "image"'
330             if ( defined $id && ref $hash_response->{image} ne 'HASH' );
331              
332             return map {
333             Net::RackSpace::CloudServers::Image->new(
334             cloudservers => $self,
335             id => $_->{id},
336             name => $_->{name},
337             serverid => $_->{serverId},
338             updated => $_->{updated},
339             created => $_->{created},
340             status => $_->{status},
341             progress => $_->{progress},
342             )
343             } @{ $hash_response->{images} } if ( !defined $id );
344             return Net::RackSpace::CloudServers::Image->new(
345             cloudservers => $self,
346             id => $hash_response->{image}->{id},
347             name => $hash_response->{image}->{name},
348             serverid => $hash_response->{image}->{serverId},
349             updated => $hash_response->{image}->{updated},
350             created => $hash_response->{image}->{created},
351             status => $hash_response->{image}->{status},
352             progress => $hash_response->{image}->{progress},
353             );
354             }
355              
356             sub get_image_detail {
357             my $self = shift;
358             my $id = shift;
359             return $self->get_image( $id, 1 );
360             }
361              
362             sub delete_image {
363             my $self = shift;
364             my $id = shift;
365             my $request = HTTP::Request->new(
366             'DELETE',
367             $self->server_management_url . '/images/' . $id,
368             [
369             'X-Auth-Token' => $self->token,
370             'Content-Type' => 'application/json',
371             ],
372             );
373             my $response = $self->_request($request);
374             confess 'Unknown error' . $response->code
375             unless scalar grep { $response->code eq $_ } ( 200, 204 );
376             return;
377             }
378              
379             =head1 NAME
380              
381             Net::RackSpace::CloudServers - Interface to RackSpace CloudServers via API
382              
383             =head1 VERSION
384              
385             version 0.14
386              
387             =head1 SYNOPSIS
388              
389             use Net::RackSpace::CloudServers;
390             my $cs = Net::RackSpace::CloudServers->new(
391             user => 'myusername', key => 'mysecretkey',
392             # location => 'UK',
393             );
394             # list my servers;
395             my @servers = $cs->get_server;
396             foreach my $server ( @servers ) {
397             print 'Have server ', $server->name, ' id ', $server->id, "\n";
398             }
399              
400             =head1 METHODS
401              
402             =head2 new / BUILD
403              
404             The constructor logs you into CloudServers:
405              
406             my $cs = Net::RackSpace::CloudServers->new(
407             user => 'myusername', key => 'mysecretkey',
408             # location => 'US',
409             );
410              
411             The C<location> parameter can be either C<US> or C<UK>, as the APIs have the same interface and just
412             different endpoints. This is all handled transparently. The default is to use the C<US> API endpoint.
413              
414             =head2 get_server
415              
416             Lists all the servers linked to the account. If no ID is passed as parameter, returns an array of
417             L<Net::RackSpace::CloudServers::Server> object containing only B<id> and B<name> set.
418             If an ID is passed as parameter, it will return a L<Net::RackSpace::CloudServers::Server> object
419             containing B<id>, B<name>, B<imageid>, etc. See L<Net::RackSpace::CloudServers::Server> for details.
420              
421             my @servers = $cs->get_server; # all servers, id/name
422             my $test_server = $cs->get_server(1); # ID 1, detailed
423              
424             =head2 get_server_detail
425              
426             Lists more details about all the servers and returns them as a L<Net::RackSpace::CloudServers::Server> object:
427              
428             my @servers = $cs->get_server_detail; # all servers, id/name
429             my $test_server = $cs->get_server_detail(1); # ID 1, detailed
430              
431             =head2 limits
432              
433             Lists all the limits currently set for the account, and returns them as a L<Net::RackSpace::CloudServers::Limits> object:
434              
435             my $limits = $cs->limits;
436              
437             =head2 get_flavor
438              
439             Lists all the flavors able to be used. If no ID is passed as parameter, returns an array of
440             L<Net::RackSpace::CloudServers::Flavor> object containing only B<id> and B<name> set.
441             If an ID is passed as parameter, it will return a L<Net::RackSpace::CloudServers::Flavor> object
442             containing B<id>, B<name>, B<ram> and B<disk>.
443              
444             my @flavors = $cs->get_flavor;
445             foreach (@flavors) { print $_->id, ' ', $_->name, "\n" }
446              
447             my $f1 = $cs->get_flavor(1);
448             print join(' ', $f1->id, $f1->name, $f1->ram, $f1->disk), "\n";
449              
450             =head2 get_flavor_detail
451              
452             Lists details of all the flavors able to be used. If no ID is passed as parameter, returns an
453             array of L<Net::RackSpace::CloudServers::Flavor>. All details are returned back: B<id>, B<name>,
454             B<ram> and B<disk>. If an ID is passed as parameter, it will return a L<Net::RackSpace::CloudServers::Flavor>
455             object with all details filled in.
456              
457             =head2 get_image
458              
459             Lists all the server/backup images able to be used. If no ID is passed as parameter, returns an
460             array of L<Net::RackSpace::CloudServers::Image> object containing only B<id> and B<name> set.
461             If an ID is passed as parameter, it will return a L<Net::RackSpace::CloudServers::Image> object
462             with all the available attributes set.
463              
464             my @images = $cs->get_image;
465             foreach (@images) { print $_->id, ' ', $_->name, "\n" }
466              
467             my $f1 = $cs->get_image(1);
468             print join(' ', $f1->id, $f1->name, $f1->updated, $f1->status), "\n";
469              
470             =head2 get_image_detail
471              
472             Lists details of all the server/backup images able to be used. If no ID is passed as parameter,
473             returns an array of L<Net::RackSpace::CloudServers::Image>. All details are returned back: B<id>, B<name>,
474             B<serverid>, B<updated>, B<created>, B<status> and B<progress>. If an ID is passed as parameter,
475             it will return a L<Net::RackSpace::CloudServers::Image> object with all details filled in.
476              
477             =head2 delete_image
478              
479             Deletes a previously created backup image. Needs the image ID passed as parameter, returns undef
480             in case of success or confess()es in case of error.
481              
482             =head1 AUTHOR
483              
484             Marco Fontani, C<< <mfontani at cpan.org> >>
485              
486             =head1 BUGS
487              
488             Please report any bugs or feature requests to C<bug-net-rackspace-cloudservers at rt.cpan.org>, or through
489             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net-RackSpace-CloudServers>. I will be notified, and then you'll
490             automatically be notified of progress on your bug as I make changes.
491              
492             =head1 SUPPORT
493              
494             You can find documentation for this module with the perldoc command.
495              
496             perldoc Net::RackSpace::CloudServers
497              
498             You can also look for information at:
499              
500             =over 4
501              
502             =item * RT: CPAN's request tracker
503              
504             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net-RackSpace-CloudServers>
505              
506             =item * AnnoCPAN: Annotated CPAN documentation
507              
508             L<http://annocpan.org/dist/Net-RackSpace-CloudServers>
509              
510             =item * CPAN Ratings
511              
512             L<http://cpanratings.perl.org/d/Net-RackSpace-CloudServers>
513              
514             =item * Search CPAN
515              
516             L<http://search.cpan.org/dist/Net-RackSpace-CloudServers/>
517              
518             =back
519              
520             =head1 ACKNOWLEDGEMENTS
521              
522             Léon Brocard for L<Net::Mosso::CloudFiles>
523              
524             =head1 COPYRIGHT & LICENSE
525              
526             Copyright 2009 Marco Fontani, all rights reserved.
527              
528             This program is free software; you can redistribute it and/or modify it
529             under the same terms as Perl itself.
530              
531             =cut
532              
533             1; # End of Net::RackSpace::CloudServers