File Coverage

blib/lib/VMware/vCloudDirector/API.pm
Criterion Covered Total %
statement 12 14 85.7
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 17 19 89.4


line stmt bran cond sub pod time code
1             package VMware::vCloudDirector::API;
2              
3             # ABSTRACT: Module to do stuff!
4              
5 1     1   747 use strict;
  1         3  
  1         37  
6 1     1   8 use warnings;
  1         3  
  1         38  
7 1     1   15 use v5.10; # needed for state variable
  1         5  
8              
9             our $VERSION = '0.006'; # VERSION
10             our $AUTHORITY = 'cpan:NIGELM'; # AUTHORITY
11              
12 1     1   673 use Moose;
  1         463130  
  1         8  
13 1     1   6646 use Method::Signatures;
  0            
  0            
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";
186             try { $message .= ' - ' . $response->decoded_content->{Error}{'-message'}; }
187             catch { $message .= ' - Unknown'; }
188             VMware::vCloudDirector::Error->throw(
189             { message => $message,
190             uri => $uri,
191             request => $request,
192             response => $response
193             }
194             );
195             }
196              
197             return $response;
198             }
199              
200             # ------------------------------------------------------------------------
201              
202              
203             has api_version => (
204             is => 'ro',
205             isa => 'Str',
206             lazy => 1,
207             clearer => '_clear_api_version',
208             builder => '_build_api_version'
209             );
210             has _url_login => (
211             is => 'rw',
212             isa => Uri,
213             lazy => 1,
214             clearer => '_clear_url_login',
215             builder => '_build_url_login'
216             );
217             has _raw_version => (
218             is => 'rw',
219             isa => 'HashRef',
220             lazy => 1,
221             clearer => '_clear_raw_version',
222             builder => '_build_raw_version'
223             );
224             has _raw_version_full => (
225             is => 'rw',
226             isa => 'HashRef',
227             lazy => 1,
228             clearer => '_clear_raw_version_full',
229             builder => '_build_raw_version_full'
230             );
231              
232             method _build_api_version () { return $self->_raw_version->{Version}; }
233             method _build_url_login () { return URI->new( $self->_raw_version->{LoginUrl} ); }
234              
235             method _build_raw_version () {
236             my $hash = $self->_raw_version_full;
237             my $version = 0;
238             my $version_block;
239             for my $verblock ( @{ $hash->{SupportedVersions}{VersionInfo} } ) {
240             next if ( ( $verblock->{-deprecated} || '' ) eq 'true' );
241             if ( $verblock->{Version} > $version ) {
242             $version_block = $verblock;
243             $version = $verblock->{Version};
244             }
245             }
246              
247             $self->_debug("API: version used: $version") if ( $self->debug );
248             die "No valid version block seen" unless ($version_block);
249              
250             return $version_block;
251             }
252              
253             method _build_raw_version_full () {
254             my $response = $self->_request( 'GET', '/api/versions', undef, { Accept => 'text/xml' } );
255             return $self->_decode_xml_response($response);
256             }
257              
258             # ------------------------ ------------------------------------------------
259              
260              
261             has authorization_token => (
262             is => 'ro',
263             isa => 'Str',
264             writer => '_set_authorization_token',
265             clearer => '_clear_authorization_token',
266             predicate => 'has_authorization_token'
267             );
268              
269             has current_session => (
270             is => 'ro',
271             isa => 'VMware::vCloudDirector::Object',
272             clearer => '_clear_current_session',
273             predicate => 'has_current_session',
274             lazy => 1,
275             builder => '_build_current_session'
276             );
277              
278             method _build_current_session () {
279             my $login_id = join( '@', $self->username, $self->orgname );
280             my $encoded_auth = 'Basic ' . MIME::Base64::encode( join( ':', $login_id, $self->password ) );
281             $self->_debug("API: attempting login as: $login_id") if ( $self->debug );
282             my $response =
283             $self->_request( 'POST', $self->_url_login, undef, { Authorization => $encoded_auth } );
284              
285             # if we got here then it succeeded, since we throw on failure
286             my $token = $response->header('x-vcloud-authorization');
287             $self->_set_authorization_token($token);
288             $self->_debug("API: authentication token: $token") if ( $self->debug );
289              
290             # we also reset the base url to match the login URL
291             ## $self->_set_base_url( $self->_url_login->clone->path('') );
292              
293             my ($session) = $self->_build_returned_objects($response);
294             return $session;
295             }
296              
297             method login () { return $self->current_session; }
298              
299             method logout () {
300             if ( $self->has_current_session ) {
301              
302             # just do this - it might fail, but little you can do now
303             try { $self->DELETE( $self->_url_login ); }
304             catch { warn "DELETE of session failed: ", @_; }
305             }
306             $self->_clear_api_data;
307             }
308              
309             # ------------------------------------------------------------------------
310             method _build_returned_objects ($response) {
311              
312             if ( $response->is_success ) {
313             $self->_debug("API: building objects") if ( $self->debug );
314              
315             my $hash = $self->_decode_xml_response($response);
316             unless ( defined($hash) ) {
317             $self->_debug("API: returned null object") if ( $self->debug );
318             return;
319             }
320              
321             # See if this is a list of things, in which case root element will
322             # be ThingList and it will have a set of Thing in it
323             my @top_keys = keys %{$hash};
324             my $top_key = $top_keys[0];
325             my $thing_type = substr( $top_key, 0, -4 );
326             if ( ( scalar(@top_keys) == 1 )
327             and ( substr( $top_key, -4, 4 ) eq 'List' )
328             and is_plain_hashref( $hash->{$top_key} )
329             and ( exists( $hash->{$top_key}{$thing_type} ) ) ) {
330             my @thing_objects;
331             $self->_debug("API: building a set of [$thing_type] objects") if ( $self->debug );
332             foreach my $thing ( $self->_listify( $hash->{$top_key}{$thing_type} ) ) {
333             my $object = VMware::vCloudDirector::Object->new(
334             hash => { $thing_type => $thing },
335             api => $self,
336             _partial_object => 1
337             );
338             push @thing_objects, $object;
339             }
340             return @thing_objects;
341             }
342              
343             # was not a list of things, so just objectify the one thing here
344             else {
345             $self->_debug("API: building a single [$top_key] object") if ( $self->debug );
346             return VMware::vCloudDirector::Object->new( hash => $hash, api => $self );
347             }
348             }
349              
350             # there was an error here - so bomb out
351             else {
352             VMware::vCloudDirector::Error->throw(
353             { message => 'Error reponse passed to object builder', response => $response } );
354             }
355             }
356              
357             # ------------------------------------------------------------------------
358              
359              
360             method GET ($url) {
361             $self->current_session; # ensure/force valid session in place
362             my $response = $self->_request( 'GET', $url );
363             return $self->_build_returned_objects($response);
364             }
365              
366             method GET_hash ($url) {
367             $self->current_session; # ensure/force valid session in place
368             my $response = $self->_request( 'GET', $url );
369             return $self->_decode_xml_response($response);
370             }
371              
372             method PUT ($url, $xml_hash) {
373             $self->current_session; # ensure/force valid session in place
374             my $content = is_plain_hashref($xml_hash) ? $self->_encode_xml_content($xml_hash) : $xml_hash;
375             my $response = $self->_request( 'PUT', $url );
376             return $self->_build_returned_objects($response);
377             }
378              
379             method POST ($url, $xml_hash) {
380             $self->current_session; # ensure/force valid session in place
381             my $content = is_plain_hashref($xml_hash) ? $self->_encode_xml_content($xml_hash) : $xml_hash;
382             my $response = $self->_request( 'POST', $url );
383             return $self->_build_returned_objects($response);
384             }
385              
386             method DELETE ($url) {
387             $self->current_session; # ensure/force valid session in place
388             my $response = $self->_request( 'DELETE', $url );
389             return $self->_build_returned_objects($response);
390             }
391              
392             # ------------------------------------------------------------------------
393              
394              
395             has query_uri => (
396             is => 'ro',
397             isa => Uri,
398             lazy => 1,
399             builder => '_build_query_uri',
400             clearer => '_clear_query_uri',
401             );
402              
403             method _build_query_uri () {
404             my @links = $self->current_session->find_links( rel => 'down', type => 'queryList' );
405             VMware::vCloudDirector::Error->throw('Cannot find single query URL')
406             unless ( scalar(@links) == 1 );
407             return $links[0]->href;
408             }
409              
410             # ------------------------------------------------------------------------
411              
412              
413             method _clear_api_data () {
414             $self->_clear_default_accept_header;
415             $self->_clear_base_url;
416             $self->_clear_ua;
417             $self->_clear_api_version;
418             $self->_clear_url_login;
419             $self->_clear_raw_version;
420             $self->_clear_raw_version_full;
421             $self->_clear_authorization_token;
422             $self->_clear_current_session;
423             $self->_clear_query_uri;
424             }
425              
426             # ------------------------------------------------------------------------
427             method _listify ($thing) { !defined $thing ? () : ( ( ref $thing eq 'ARRAY' ) ? @{$thing} : $thing ) }
428              
429             # ------------------------------------------------------------------------
430              
431             __PACKAGE__->meta->make_immutable;
432              
433             1;
434              
435             __END__
436              
437             =pod
438              
439             =encoding UTF-8
440              
441             =head1 NAME
442              
443             VMware::vCloudDirector::API - Module to do stuff!
444              
445             =head1 VERSION
446              
447             version 0.006
448              
449             =head2 Attributes
450              
451             =head3 hostname
452              
453             Hostname of the vCloud server. Must have a vCloud instance listening for https
454             on port 443.
455              
456             =head3 username
457              
458             Username to use to login to vCloud server.
459              
460             =head3 password
461              
462             Password to use to login to vCloud server.
463              
464             =head3 orgname
465              
466             Org name to use to login to vCloud server - this defaults to C<System>.
467              
468             =head3 timeout
469              
470             Command timeout in seconds. Defaults to 120.
471              
472             =head3 default_accept_header
473              
474             The default MIME types to accept. This is automatically set based on the
475             information received back from the API versions.
476              
477             =head3 ssl_verify
478              
479             Whether to do standard SSL certificate verification. Defaults to set.
480              
481             =head3 ssl_ca_file
482              
483             The SSL CA set to trust packaged in a file. This defaults to those set in the
484             L<Mozilla::CA>
485              
486             =head2 debug
487              
488             Set debug level. The higher the debug level, the more chatter is exposed.
489              
490             Defaults to 0 (no output) unless the environment variable C<VCLOUD_API_DEBUG>
491             is set to something that is non-zero. Picked up at create time in C<BUILD()>.
492              
493             =head2 API SHORTHAND METHODS
494              
495             =head3 api_version
496              
497             The C<api_version> holds the version number of the highest discovered non-
498             deprecated API, it is initialised by connecting to the C</api/versions>
499             endpoint, and is called implicitly during the login setup. Once filled the
500             values are cached.
501              
502             =head3 authorization_token
503              
504             The C<authorization_token> holds the vCloud authentication token that has been
505             handed out. It is set by L<login>, and can be tested for by using the
506             predicate C<has_authorization_token>.
507              
508             =head3 current_session
509              
510             The current session object for this login. Attempting to access this forces a
511             login and creation of a current session.
512              
513             =head3 login
514              
515             Returns the L<current_session> which co-incidently forces a login.
516              
517             =head3 logout
518              
519             If there is a current session, DELETEs it, and clears the current session state
520             data.
521              
522             =head3 GET ($url)
523              
524             Forces a session establishment, and does a GET operation on the given URL,
525             returning the objects that were built.
526              
527             =head3 GET_hash ($url)
528              
529             Forces a session establishment, and does a GET operation on the given URL,
530             returning the XML equivalent hash that was built.
531              
532             =head3 PUT ($url, $xml_hash)
533              
534             Forces a session establishment, and does a PUT operation on the given URL,
535             passing the XML string or encoded hash, returning the objects that were built.
536              
537             =head3 POST ($url, $xml_hash)
538              
539             Forces a session establishment, and does a POST operation on the given URL,
540             passing the XML string or encoded hash, returning the objects that were built.
541              
542             =head3 DELETE ($url)
543              
544             Forces a session establishment, and does a DELETE operation on the given URL,
545             returning the objects that were built.
546              
547             =head3 query_uri
548              
549             Returns the URI for query operations, as taken from the initial session object.
550              
551             =head2 _clear_api_data
552              
553             Clears out all the API state data, including the current login state. This is
554             not intended to be used from outside the module, and will completely trash the
555             current state requiring a new login. The basic information passed at object
556             construction time is not deleted, so a new session could be created.
557              
558             =head1 AUTHOR
559              
560             Nigel Metheringham <nigelm@cpan.org>
561              
562             =head1 COPYRIGHT AND LICENSE
563              
564             This software is copyright (c) 2017 by Nigel Metheringham.
565              
566             This is free software; you can redistribute it and/or modify it under
567             the same terms as the Perl 5 programming language system itself.
568              
569             =cut