File Coverage

blib/lib/VMware/vCloudDirector/API.pm
Criterion Covered Total %
statement 9 11 81.8
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 13 15 86.6


line stmt bran cond sub pod time code
1             package VMware::vCloudDirector::API;
2              
3             # ABSTRACT: Module to do stuff!
4              
5 1     1   718 use strict;
  1         3  
  1         27  
6 1     1   5 use warnings;
  1         2  
  1         28  
7 1     1   10 use v5.10; # needed for state variable
  1         3  
8              
9             our $VERSION = '0.007'; # VERSION
10             our $AUTHORITY = 'cpan:NIGELM'; # AUTHORITY
11              
12 1     1   145 use Moose;
  0            
  0            
13             use Method::Signatures;
14             use MooseX::Types::Path::Tiny qw(Path);
15             use MooseX::Types::URI qw(Uri);
16             use LWP::UserAgent;
17             use MIME::Base64;
18             use Mozilla::CA;
19             use Path::Tiny;
20             use Ref::Util qw(is_plain_hashref);
21             use Scalar::Util qw(looks_like_number);
22             use Syntax::Keyword::Try 0.04; # Earlier versions throw errors
23             use VMware::vCloudDirector::Error;
24             use VMware::vCloudDirector::Object;
25             use XML::Fast qw();
26             use Data::Dump qw(pp);
27              
28             # ------------------------------------------------------------------------
29              
30              
31             has hostname => ( is => 'ro', isa => 'Str', required => 1 );
32             has username => ( is => 'ro', isa => 'Str', required => 1 );
33             has password => ( is => 'ro', isa => 'Str', required => 1 );
34             has orgname => ( is => 'ro', isa => 'Str', required => 1, default => 'System' );
35             has ssl_verify => ( is => 'ro', isa => 'Bool', default => 1 );
36             has debug => ( is => 'rw', isa => 'Int', default => 0, );
37             has timeout => ( is => 'rw', isa => 'Int', default => 120 ); # Defaults to 120 seconds
38             has _debug_trace_directory =>
39             ( is => 'ro', isa => Path, coerce => 1, predicate => '_has_debug_trace_directory' );
40              
41             has default_accept_header => (
42             is => 'ro',
43             isa => 'Str',
44             lazy => 1,
45             builder => '_build_default_accept_header',
46             clearer => '_clear_default_accept_header',
47             );
48              
49             has _base_url => (
50             is => 'ro',
51             isa => Uri,
52             lazy => 1,
53             builder => '_build_base_url',
54             writer => '_set_base_url',
55             clearer => '_clear_base_url',
56             );
57              
58             has ssl_ca_file => (
59             is => 'ro',
60             isa => Path,
61             coerce => 1,
62             lazy => 1,
63             builder => '_build_ssl_ca_file'
64             );
65              
66             method _build_ssl_ca_file () { return path( Mozilla::CA::SSL_ca_file() ); }
67             method _build_base_url () { return URI->new( sprintf( 'https://%s/', $self->hostname ) ); }
68             method _build_default_accept_header () { return ( 'application/*+xml;version=' . $self->api_version ); }
69             method _debug (@parameters) { warn join( '', '# ', @parameters, "\n" ) if ( $self->debug ); }
70              
71             # ------------------------------------------------------------------------
72              
73             method BUILD ($args) {
74              
75             # deal with setting debug if needed
76             my $env_debug = $ENV{VCLOUD_API_DEBUG};
77             if ( defined($env_debug) ) {
78             $self->debug($env_debug) if ( looks_like_number($env_debug) );
79             }
80             }
81              
82             # ------------------------------------------------------------------------
83             has _ua => (
84             is => 'ro',
85             isa => 'LWP::UserAgent',
86             lazy => 1,
87             clearer => '_clear_ua',
88             builder => '_build_ua'
89             );
90              
91             has _ua_module_version => (
92             is => 'ro',
93             isa => 'Str',
94             default => sub { our $VERSION //= '0.00'; sprintf( '%s/%s', __PACKAGE__, $VERSION ) }
95             );
96              
97             method _build_ua () {
98             return LWP::UserAgent->new(
99             agent => $self->_ua_module_version . ' ',
100             cookie_jar => {},
101             ssl_opts => { verify_hostname => $self->ssl_verify, SSL_ca_file => $self->ssl_ca_file },
102             timeout => $self->timeout
103             );
104             }
105              
106             # ------------------------------------------------------------------------
107             method _decode_xml_response ($response) {
108             try {
109             my $xml = $response->decoded_content;
110             return unless ( defined($xml) and length($xml) );
111             return XML::Fast::xml2hash($xml);
112             }
113             catch {
114             VMware::vCloudDirector::Error->throw(
115             { message => "XML decode failed - " . join( ' ', $@ ),
116             response => $response
117             }
118             );
119             }
120             }
121              
122             # ------------------------------------------------------------------------
123             method _encode_xml_content ($hash) {
124             return XML::Hash::XS::hash2xml( $hash, method => 'LX' );
125             }
126              
127             # ------------------------------------------------------------------------
128             method _request ($method, $url, $content?, $headers?) {
129             my $uri = URI->new_abs( $url, $self->_base_url );
130             $self->_debug("API: _request [$method] $uri") if ( $self->debug );
131              
132             my $request = HTTP::Request->new( $method => $uri );
133              
134             # build headers
135             if ( defined $content && length($content) ) {
136             $request->content($content);
137             $request->header( 'Content-Length', length($content) );
138             }
139             else {
140             $request->header( 'Content-Length', 0 );
141             }
142              
143             # add any supplied headers
144             my $seen_accept;
145             if ( defined($headers) ) {
146             foreach my $h_name ( keys %{$headers} ) {
147             $request->header( $h_name, $headers->{$h_name} );
148             $seen_accept = 1 if ( lc($h_name) eq 'accept' );
149             }
150             }
151              
152             # set accept header
153             $request->header( 'Accept', $self->default_accept_header ) unless ($seen_accept);
154              
155             # set auth header
156             $request->header( 'x-vcloud-authorization', $self->authorization_token )
157             if ( $self->has_authorization_token );
158              
159             # do request
160             my $response;
161             try { $response = $self->_ua->request($request); }
162             catch {
163             VMware::vCloudDirector::Error->throw(
164             { message => "$method request bombed",
165             uri => $uri,
166             request => $request,
167             }
168             );
169             }
170              
171             # if _debug_trace_directory is set - we dump info from each request out into
172             # a pair of files, one with the dumped response object, the other with the content
173             if ( $self->_has_debug_trace_directory ) {
174             state $xcount = 0;
175             die "No trace directory - " . $self->_debug_trace_directory
176             unless ( $self->_debug_trace_directory->is_dir );
177             $self->_debug_trace_directory->child( sprintf( '%06d.txt', ++$xcount ) )
178             ->spew( pp($response) );
179             $self->_debug_trace_directory->child( sprintf( '%06d.xml', $xcount ) )
180             ->spew( $response->decoded_content );
181             }
182              
183             # Throw if this went wrong
184             if ( $response->is_error ) {
185             my $message = "$method request failed [$uri] - ";
186             try {
187             my $decoded_response = $self->_decode_xml_response($response);
188             $message .=
189             ( exists( $decoded_response->{Error}{'-message'} ) )
190             ? $decoded_response->{Error}{'-message'}
191             : 'Unknown after decode';
192             }
193             catch { $message .= 'Unknown'; }
194             VMware::vCloudDirector::Error->throw(
195             { message => $message,
196             uri => $uri,
197             request => $request,
198             response => $response
199             }
200             );
201             }
202              
203             return $response;
204             }
205              
206             # ------------------------------------------------------------------------
207              
208              
209             has api_version => (
210             is => 'ro',
211             isa => 'Str',
212             lazy => 1,
213             clearer => '_clear_api_version',
214             builder => '_build_api_version'
215             );
216             has _url_login => (
217             is => 'rw',
218             isa => Uri,
219             lazy => 1,
220             clearer => '_clear_url_login',
221             builder => '_build_url_login'
222             );
223             has _raw_version => (
224             is => 'rw',
225             isa => 'HashRef',
226             lazy => 1,
227             clearer => '_clear_raw_version',
228             builder => '_build_raw_version'
229             );
230             has _raw_version_full => (
231             is => 'rw',
232             isa => 'HashRef',
233             lazy => 1,
234             clearer => '_clear_raw_version_full',
235             builder => '_build_raw_version_full'
236             );
237              
238             method _build_api_version () { return $self->_raw_version->{Version}; }
239             method _build_url_login () { return URI->new( $self->_raw_version->{LoginUrl} ); }
240              
241             method _build_raw_version () {
242             my $hash = $self->_raw_version_full;
243             my $version = 0;
244             my $version_block;
245             for my $verblock ( @{ $hash->{SupportedVersions}{VersionInfo} } ) {
246             next if ( ( $verblock->{-deprecated} || '' ) eq 'true' );
247             if ( $verblock->{Version} > $version ) {
248             $version_block = $verblock;
249             $version = $verblock->{Version};
250             }
251             }
252              
253             $self->_debug("API: version used: $version") if ( $self->debug );
254             die "No valid version block seen" unless ($version_block);
255              
256             return $version_block;
257             }
258              
259             method _build_raw_version_full () {
260             my $response = $self->_request( 'GET', '/api/versions', undef, { Accept => 'text/xml' } );
261             return $self->_decode_xml_response($response);
262             }
263              
264             # ------------------------ ------------------------------------------------
265              
266              
267             has authorization_token => (
268             is => 'ro',
269             isa => 'Str',
270             writer => '_set_authorization_token',
271             clearer => '_clear_authorization_token',
272             predicate => 'has_authorization_token'
273             );
274              
275             has current_session => (
276             is => 'ro',
277             isa => 'VMware::vCloudDirector::Object',
278             clearer => '_clear_current_session',
279             predicate => 'has_current_session',
280             lazy => 1,
281             builder => '_build_current_session'
282             );
283              
284             method _build_current_session () {
285             my $login_id = join( '@', $self->username, $self->orgname );
286             my $encoded_auth = 'Basic ' . MIME::Base64::encode( join( ':', $login_id, $self->password ) );
287             $self->_debug("API: attempting login as: $login_id") if ( $self->debug );
288             my $response =
289             $self->_request( 'POST', $self->_url_login, undef, { Authorization => $encoded_auth } );
290              
291             # if we got here then it succeeded, since we throw on failure
292             my $token = $response->header('x-vcloud-authorization');
293             $self->_set_authorization_token($token);
294             $self->_debug("API: authentication token: $token") if ( $self->debug );
295              
296             # we also reset the base url to match the login URL
297             ## $self->_set_base_url( $self->_url_login->clone->path('') );
298              
299             my ($session) = $self->_build_returned_objects($response);
300             return $session;
301             }
302              
303             method login () { return $self->current_session; }
304              
305             method logout () {
306             if ( $self->has_current_session ) {
307              
308             # just do this - it might fail, but little you can do now
309             try { $self->DELETE( $self->_url_login ); }
310             catch { warn "DELETE of session failed: ", @_; }
311             }
312             $self->_clear_api_data;
313             }
314              
315             # ------------------------------------------------------------------------
316             method _build_returned_objects ($response) {
317              
318             if ( $response->is_success ) {
319             $self->_debug("API: building objects") if ( $self->debug );
320              
321             my $hash = $self->_decode_xml_response($response);
322             unless ( defined($hash) ) {
323             $self->_debug("API: returned null object") if ( $self->debug );
324             return;
325             }
326              
327             # See if this is a list of things, in which case root element will
328             # be ThingList and it will have a set of Thing in it
329             my @top_keys = keys %{$hash};
330             my $top_key = $top_keys[0];
331             my $thing_type = substr( $top_key, 0, -4 );
332             if ( ( scalar(@top_keys) == 1 )
333             and ( substr( $top_key, -4, 4 ) eq 'List' )
334             and is_plain_hashref( $hash->{$top_key} )
335             and ( exists( $hash->{$top_key}{$thing_type} ) ) ) {
336             my @thing_objects;
337             $self->_debug("API: building a set of [$thing_type] objects") if ( $self->debug );
338             foreach my $thing ( $self->_listify( $hash->{$top_key}{$thing_type} ) ) {
339             my $object = VMware::vCloudDirector::Object->new(
340             hash => { $thing_type => $thing },
341             api => $self,
342             _partial_object => 1
343             );
344             push @thing_objects, $object;
345             }
346             return @thing_objects;
347             }
348              
349             # was not a list of things, so just objectify the one thing here
350             else {
351             $self->_debug("API: building a single [$top_key] object") if ( $self->debug );
352             return VMware::vCloudDirector::Object->new( hash => $hash, api => $self );
353             }
354             }
355              
356             # there was an error here - so bomb out
357             else {
358             VMware::vCloudDirector::Error->throw(
359             { message => 'Error reponse passed to object builder', response => $response } );
360             }
361             }
362              
363             # ------------------------------------------------------------------------
364              
365              
366             method GET ($url) {
367             $self->current_session; # ensure/force valid session in place
368             my $response = $self->_request( 'GET', $url );
369             return $self->_build_returned_objects($response);
370             }
371              
372             method GET_hash ($url) {
373             $self->current_session; # ensure/force valid session in place
374             my $response = $self->_request( 'GET', $url );
375             return $self->_decode_xml_response($response);
376             }
377              
378             method PUT ($url, $xml_hash) {
379             $self->current_session; # ensure/force valid session in place
380             my $content = is_plain_hashref($xml_hash) ? $self->_encode_xml_content($xml_hash) : $xml_hash;
381             my $response = $self->_request( 'PUT', $url );
382             return $self->_build_returned_objects($response);
383             }
384              
385             method POST ($url, $xml_hash) {
386             $self->current_session; # ensure/force valid session in place
387             my $content = is_plain_hashref($xml_hash) ? $self->_encode_xml_content($xml_hash) : $xml_hash;
388             my $response = $self->_request( 'POST', $url );
389             return $self->_build_returned_objects($response);
390             }
391              
392             method DELETE ($url) {
393             $self->current_session; # ensure/force valid session in place
394             my $response = $self->_request( 'DELETE', $url );
395             return $self->_build_returned_objects($response);
396             }
397              
398             # ------------------------------------------------------------------------
399              
400              
401             has query_uri => (
402             is => 'ro',
403             isa => Uri,
404             lazy => 1,
405             builder => '_build_query_uri',
406             clearer => '_clear_query_uri',
407             );
408              
409             method _build_query_uri () {
410             my @links = $self->current_session->find_links( rel => 'down', type => 'queryList' );
411             VMware::vCloudDirector::Error->throw('Cannot find single query URL')
412             unless ( scalar(@links) == 1 );
413             return $links[0]->href;
414             }
415              
416             # ------------------------------------------------------------------------
417              
418              
419             method _clear_api_data () {
420             $self->_clear_default_accept_header;
421             $self->_clear_base_url;
422             $self->_clear_ua;
423             $self->_clear_api_version;
424             $self->_clear_url_login;
425             $self->_clear_raw_version;
426             $self->_clear_raw_version_full;
427             $self->_clear_authorization_token;
428             $self->_clear_current_session;
429             $self->_clear_query_uri;
430             }
431              
432             # ------------------------------------------------------------------------
433             method _listify ($thing) { !defined $thing ? () : ( ( ref $thing eq 'ARRAY' ) ? @{$thing} : $thing ) }
434              
435             # ------------------------------------------------------------------------
436              
437             __PACKAGE__->meta->make_immutable;
438              
439             1;
440              
441             __END__