File Coverage

blib/lib/OpenStack/Client.pm
Criterion Covered Total %
statement 130 132 98.4
branch 34 36 94.4
condition 9 11 81.8
subroutine 23 23 100.0
pod 11 13 84.6
total 207 215 96.2


line stmt bran cond sub pod time code
1             #
2             # Copyright (c) 2018 cPanel, L.L.C.
3             # All rights reserved.
4             # http://cpanel.net/
5             #
6             # Distributed under the terms of the MIT license. See the LICENSE file for
7             # further details.
8             #
9             package OpenStack::Client;
10              
11 2     2   497 use strict;
  2         10  
  2         56  
12 2     2   10 use warnings;
  2         4  
  2         66  
13              
14 2     2   885 use HTTP::Request ();
  2         47643  
  2         47  
15 2     2   1373 use LWP::UserAgent ();
  2         51066  
  2         55  
16              
17 2     2   652 use JSON ();
  2         9961  
  2         40  
18 2     2   871 use URI::Encode ();
  2         24131  
  2         48  
19              
20 2     2   895 use OpenStack::Client::Response ();
  2         5  
  2         3326  
21              
22             our $VERSION = '1.0005_0003';
23              
24             =encoding utf8
25              
26             =head1 NAME
27              
28             OpenStack::Client - A cute little client to OpenStack services
29              
30             =head1 SYNOPSIS
31              
32             #
33             # First, connect to an API endpoint via the Keystone
34             # authorization service
35             #
36             use OpenStack::Client::Auth ();
37              
38             my $endpoint = 'http://openstack.foo.bar:5000/v2.0';
39              
40             my $auth = OpenStack::Client::Auth->new($endpoint,
41             'tenant' => $ENV{'OS_TENANT_NAME'},
42             'username' => $ENV{'OS_USERNAME'},
43             'password' => $ENV{'OS_PASSWORD'}
44             );
45              
46             my $glance = $auth->service('image',
47             'region' => $ENV{'OS_REGION_NAME'}
48             );
49              
50             my @images = $glance->all('/v2/images', 'images');
51              
52             #
53             # Or, connect directly to an API endpoint by URI
54             #
55             use OpenStack::Client ();
56              
57             my $endpoint = 'http://glance.foo.bar:9292';
58              
59             my $glance = OpenStack::Client->new($endpoint,
60             'token' => {
61             'id' => 'foo'
62             }
63             );
64              
65             my @images = $glance->all('/v2/images', 'images');
66              
67             =head1 DESCRIPTION
68              
69             C is a no-frills OpenStack API client which provides generic
70             access to OpenStack APIs with minimal remote service discovery facilities; with
71             a minimal client, the key understanding of the remote services are primarily
72             predicated on an understanding of the authoritative OpenStack API documentation:
73              
74             http://developer.openstack.org/api-ref.html
75              
76             Authorization, authentication, and access to OpenStack services such as the
77             OpenStack Compute and Networking APIs is made convenient by
78             L. Further, some small handling of response body data
79             such as obtaining the full resultset of a paginated response is handled for
80             convenience.
81              
82             Ordinarily, a client can be obtained conveniently by using the C
83             method on a L object.
84              
85             =head1 INSTANTIATION
86              
87             =over
88              
89             =item Cnew(I<$endpoint>, I<%opts>)>
90              
91             Create a new C object connected to the specified
92             I<$endpoint>. The following values may be specified in I<%opts>:
93              
94             =over
95              
96             =item * B
97              
98             A token obtained from a L object.
99              
100             =back
101              
102             =cut
103              
104             sub new ($%) {
105 60     60 1 19030 my ($class, $endpoint, %opts) = @_;
106              
107 60 100       162 die 'No API endpoint provided' unless $endpoint;
108              
109 59   100     148 $opts{'package_ua'} ||= 'LWP::UserAgent';
110 59   100     135 $opts{'package_request'} ||= 'HTTP::Request';
111 59   100     124 $opts{'package_response'} ||= 'OpenStack::Client::Response';
112              
113 59         199 my $ua = $opts{'package_ua'}->new(
114             'ssl_opts' => {
115             'verify_hostname' => 0
116             }
117             );
118              
119             return bless {
120             'package_ua' => $opts{'package_ua'},
121             'package_request' => $opts{'package_request'},
122             'package_response' => $opts{'package_response'},
123             'ua' => $ua,
124             'endpoint' => $endpoint,
125 59         3573 'token' => $opts{'token'}
126             }, $class;
127             }
128              
129             =back
130              
131             =head1 INSTANCE METHODS
132              
133             These methods are useful for identifying key attributes of an OpenStack service
134             endpoint client.
135              
136             =over
137              
138             =item C<$client-Eendpoint()>
139              
140             Return the absolute HTTP URI to the endpoint this client provides access to.
141              
142             =cut
143              
144             sub endpoint ($) {
145 13     13 1 579 shift->{'endpoint'};
146             }
147              
148             =item C<$client-Etoken()>
149              
150             If a token object was specified when creating C<$client>, then return it.
151              
152             =cut
153              
154             sub token ($) {
155 50     50 1 119 shift->{'token'};
156             }
157              
158             sub uri ($$) {
159 52     52 0 1253 my ($self, $path) = @_;
160              
161             return join '/', map {
162 104         164 my $part = $_;
163              
164 104         242 $part =~ s/^\///;
165 104         327 $part =~ s/\/$//;
166 104         426 $part
167 52         111 } $self->{'endpoint'}, $path;
168             }
169              
170             =back
171              
172             =head1 PERFORMING REMOTE CALLS
173              
174             =over
175              
176             =cut
177              
178             sub request {
179 49     49 0 154 my ($self, %args) = @_;
180              
181 49   100     151 $args{'headers'} ||= {};
182              
183             my $request = $self->{'package_request'}->new(
184 49         149 $args{'method'} => $self->uri($args{'path'})
185             );
186              
187 49         1395 my @headers = $self->_get_headers_list($args{'headers'});
188              
189 49         86 my $count = scalar @headers;
190              
191 49         127 for (my $i=0; $i<$count; $i+=2) {
192 198         1251 my $name = $headers[$i];
193 198         284 my $value = $headers[$i+1];
194              
195 198         382 $request->header($name => $value);
196             }
197              
198 49 100       412 if (defined $args{'body'}) {
199             #
200             # Allow the request body to be supplied by a subroutine reference
201             # which, when called, will supply a chunk of data returned as per the
202             # behavior of LWP::UserAgent. This is useful for uploading arbitrary
203             # amounts of data in a request body.
204             #
205 26 100       86 if (ref($args{'body'}) =~ /CODE/) {
206 1         5 $request->content($args{'body'});
207             } else {
208 25         212 $request->content(JSON::encode_json($args{'body'}));
209             }
210             }
211              
212             return bless $self->{'ua'}->request($request,
213             defined $args{'handler'}? $args{'handler'}: ()),
214 49 100       296 $self->{'package_response'};
215             }
216              
217             =item C<$client-Ecall(I<$args>)>
218              
219             Perform a call to the service endpoint using named arguments in the hash. The
220             following arguments are required:
221              
222             =over
223              
224             =item C - Request method
225              
226             =item C - Resource path
227              
228             =back
229              
230             The following arguments are optional:
231              
232             =over
233              
234             =item C - Request headers
235              
236             Headers are case I; if duplicate header values are declared under
237             different cases, it is undefined which headers shall take precedence. The
238             following headers are sent by default:
239              
240             =over
241              
242             =item Accept
243              
244             Defaults to C.
245              
246             =item Accept-Encoding
247              
248             Defaults to C.
249              
250             =item Content-Type
251              
252             Defaults to C, although some API calls (e.g., a PATCH)
253             expect a different type; the case of an image update, the expected
254             type is C or some version
255             thereof.
256              
257             =back
258              
259             Except for C, any additional token will be added to the request.
260              
261             =item C - Request body
262              
263             This may be a scalar reference to a data structure to be encoded to JSON, or a
264             CODE reference to a subroutine which, when called, will return a chunk of data
265             to be supplied to the API endpoint; the stream is ended when the supplied
266             subroutine returns an empty string or undef.
267              
268             =item C - Response body handler function
269              
270             When specified, this function will be called with two arguments; the first
271             argument is a scalar value containing a chunk of data in the response body, and
272             the second is a scalar reference to a L object representing the
273             current response. This is useful for retrieving very large resources without
274             having to store the entire response body in memory at once for parsing.
275              
276             =back
277              
278             All forms of this method may return the following:
279              
280             =over
281              
282             =item * For B: A decoded JSON object
283              
284             =item * For other response types: The unmodified response body
285              
286             =back
287              
288             In exceptional conditions (such as when the service returns a 4xx or 5xx HTTP
289             response), the client will die with the raw text response from the HTTP
290             service, indicating the nature of the service-side failure to service the
291             current call.
292              
293             =item C<$client-Ecall(I<$method>, I<$path>, I<$body>)>
294              
295             Perform a call to the service endpoint using the HTTP method I<$method>,
296             accessing the resource I<$path> (relative to the absolute endpoint URI),
297             passing an arbitrary value in I<$body>.
298              
299             =item C<$client-Ecall(I<$method>, I<$headers>, I<$path>, I<$body>)>
300              
301             Perform a call to the service endpoint using the HTTP method I<$method>,
302             accessing the resource I<$path> (relative to the absolute endpoint URI),
303             specifying the headers in I<$headers>, passing an arbitrary value in I<$body>.
304              
305             =back
306              
307             =head1 EXAMPLES
308              
309             The following shows how one may update image metadata using the PATCH method
310             supported by version 2 of the Image API. C<@image_updates> is an array of hash
311             references of the structure defined by the PATCH RFC (6902) governing
312             "JavaScript Object Notation (JSON) Patch"; i.e., operations consisting of
313             C, C, or C.
314              
315             my $headers = {
316             'Content-Type' => 'application/openstack-images-v2.1-json-patch'
317             };
318              
319             my $response = $glance->call({
320             'method' => 'PATCH',
321             'headers' => $headers,
322             'path' => qq[/v2/images/$image->{id}],
323             'body' => \@image_updates
324             );
325              
326             =cut
327              
328             sub call {
329 73     73 1 1332 my $self = shift;
330              
331 73 100       229 if (scalar @_ == 4) {
    100          
    100          
    50          
332 3         14 return $self->call({
333             'method' => $_[0],
334             'headers' => $_[1],
335             'path' => $_[2],
336             'body' => $_[3]
337             });
338             } elsif (scalar @_ == 3) {
339 11         52 return $self->call({
340             'method' => $_[0],
341             'headers' => {},
342             'path' => $_[1],
343             'body' => $_[2],
344             });
345             } elsif (scalar @_ == 2) {
346 22         103 return $self->call({
347             'method' => $_[0],
348             'headers' => {},
349             'path' => $_[1],
350             'body' => undef
351             });
352             } elsif (scalar @_ != 1) {
353 0         0 die "Invalid number of arguments: @_";
354             }
355              
356 37         73 my ($args) = @_;
357              
358 37         53 return $self->request(%{$args})->decode_json;
  37         139  
359             }
360              
361             sub _lc_merge {
362 98     98   190 my ($a, $b, %opts) = @_;
363              
364             my %lc_keys_a = map {
365 155         396 lc $_ => $_
366 98         132 } keys %{$a};
  98         180  
367              
368 98         176 foreach my $key_b (keys %{$b}) {
  98         233  
369 196         321 my $key_a = $lc_keys_a{lc $key_b};
370              
371 196 100 33     340 if (!defined($key_a)) {
    50          
372 192         358 $a->{$key_b} = $b->{$key_b};
373             } elsif (exists $a->{$key_a} && $opts{'replace'}) {
374 0         0 $a->{$key_a} = $b->{$key_b};
375             }
376             }
377              
378 98         210 return;
379             }
380              
381             #
382             # Internal method for call() to process headers; returns a list of header name
383             # and value pairs
384             #
385             sub _get_headers_list {
386 49     49   91 my ($self, $headers) = @_;
387              
388 49         158 my %DEFAULTS = (
389             'Accept' => 'application/json, text/plain',
390             'Accept-Encoding' => 'identity, gzip, deflate, compress',
391             'Content-Type' => 'application/json'
392             );
393              
394             #
395             # The client should be not adding X-Auth-Token explicitly, so force it to
396             # the one received during authentication
397             #
398 49         97 my %OVERRIDES = (
399             'X-Auth-Token' => $self->token
400             );
401              
402 49         72 my %new_headers = %{$headers};
  49         96  
403              
404 49         143 _lc_merge(\%new_headers, \%DEFAULTS);
405 49         125 _lc_merge(\%new_headers, \%OVERRIDES, 'replace' => 1);
406              
407 49         210 return %new_headers;
408             }
409              
410             =head1 FETCHING REMOTE RESOURCES
411              
412             =over
413              
414             =item C<$client-Eget(I<$path>, I<%opts>)>
415              
416             Issue an HTTP GET request for resource I<$path>. The keys and values
417             specified in I<%opts> will be URL encoded and appended to I<$path> when forming
418             the request. Response bodies are decoded as per C<$client-Ecall()>.
419              
420             =cut
421              
422             sub get ($$%) {
423 15     15 1 1921 my ($self, $path, %opts) = @_;
424              
425 15         21 my $params;
426              
427 15         74 foreach my $name (sort keys %opts) {
428 9         2458 my $value = $opts{$name};
429              
430 9 100       25 $params .= "&" if defined $params;
431              
432             $params .= sprintf "%s=%s", map {
433 9         16 URI::Encode::uri_encode($_)
  18         11643  
434             } $name, $value;
435             }
436              
437 15 100       9109 if (defined $params) {
438             #
439             # $path might already have request parameters; if so, just append
440             # subsequent values with a & rather than ?.
441             #
442 7 100       28 if ($path =~ /\?/) {
443 1         5 $path .= "&$params";
444             } else {
445 6         16 $path .= "?$params";
446             }
447             }
448              
449 15         41 return $self->call('GET' => $path);
450             }
451              
452             =item C<$client-Eeach(I<$path>, I<$opts>, I<$callback>)>
453              
454             =item C<$client-Eeach(I<$path>, I<$callback>)>
455              
456             Issue an HTTP GET request for the resource I<$path>, while passing each
457             decoded response object to I<$callback> in a single argument. I<$opts> is taken
458             to be a HASH reference containing zero or more key-value pairs to be URL encoded
459             as parameters to each GET request made.
460              
461             =cut
462              
463             sub each ($$@) {
464 4     4 1 1095 my ($self, $path, @args) = @_;
465              
466 4         8 my $opts = {};
467 4         8 my $callback;
468              
469 4 100       14 if (scalar @args == 2) {
    100          
470 1         4 ($opts, $callback) = @args;
471             } elsif (scalar @args == 1) {
472 1         2 ($callback) = @args;
473             } else {
474 2         20 die 'Invalid number of arguments';
475             }
476              
477 2         6 while (defined $path) {
478 2         4 my $result = $self->get($path, %{$opts});
  2         7  
479              
480 2         9 $callback->($result);
481              
482 2         15 $path = $result->{'next'};
483             }
484              
485 2         8 return;
486             }
487              
488             =item C<$client-Eevery(I<$path>, I<$attribute>, I<$opts>, I<$callback>)>
489              
490             =item C<$client-Eevery(I<$path>, I<$attribute>, I<$callback>)>
491              
492             Perform a series of HTTP GET request for the resource I<$path>, decoding the
493             result set and passing each value within each physical JSON response object's
494             attribute named I<$attribute>, to the callback I<$callback> as a single
495             argument. I<$opts> is taken to be a HASH reference containing zero or more
496             key-value pairs to be URL encoded as parameters to each GET request made.
497              
498             =cut
499              
500             sub every ($$$@) {
501 7     7 1 858 my ($self, $path, $attribute, @args) = @_;
502              
503 7         15 my $opts = {};
504 7         11 my $callback;
505              
506 7 100       22 if (scalar @args == 2) {
    100          
507 3         7 ($opts, $callback) = @args;
508             } elsif (scalar @args == 1) {
509 2         4 ($callback) = @args;
510             } else {
511 2         19 die 'Invalid number of arguments';
512             }
513              
514 5         15 while (defined $path) {
515 8         14 my $result = $self->get($path, %{$opts});
  8         23  
516              
517 8 100       28 unless (defined $result->{$attribute}) {
518 1         13 die "Response from $path does not contain attribute '$attribute'";
519             }
520              
521 7         11 foreach my $item (@{$result->{$attribute}}) {
  7         15  
522 12         25 $callback->($item);
523             }
524              
525 7         42 $path = $result->{'next'};
526             }
527              
528 4         13 return;
529             }
530              
531             =item C<$client-Eall(I<$path>, I<$attribute>, I<$opts>)>
532              
533             =item C<$client-Eall(I<$path>, I<$attribute>)>
534              
535             Perform a series of HTTP GET requests for the resource I<$path>, decoding the
536             result set and returning a list of all items found within each response body's
537             attribute named I<$attribute>. I<$opts> is taken to be a HASH reference
538             containing zero or more key-value pairs to be URL encoded as parameters to each
539             GET request made.
540              
541             =cut
542              
543             sub all ($$$@) {
544 2     2 1 98 my ($self, $path, $attribute, $opts) = @_;
545              
546 2         3 my @items;
547              
548             $self->every($path, $attribute, $opts, sub {
549 6     6   12 my ($item) = @_;
550              
551 6         14 push @items, $item;
552 2         11 });
553              
554 2         15 return @items;
555             }
556              
557             =back
558              
559             =head1 CREATING AND UPDATING REMOTE RESOURCES
560              
561             =over
562              
563             =item C<$client-Eput(I<$path>, I<$body>)>
564              
565             Issue an HTTP PUT request to the resource at I<$path>, in the form of a JSON
566             encoding of the contents of I<$body>.
567              
568             =cut
569              
570             sub put ($$$) {
571 1     1 1 50 my ($self, $path, $body) = @_;
572              
573 1         4 return $self->call('PUT' => $path, $body);
574             }
575              
576             =item C<$client-Epost(I<$path>, I<$body>)>
577              
578             Issue an HTTP POST request to the resource at I<$path>, in the form of a
579             JSON encoding of the contents of I<$body>.
580              
581             =cut
582              
583             sub post ($$$) {
584 1     1 1 340 my ($self, $path, $body) = @_;
585              
586 1         9 return $self->call('POST' => $path, $body);
587             }
588              
589             =back
590              
591             =head1 DELETING REMOTE RESOURCES
592              
593             =over
594              
595             =item C<$client-Edelete(I<$path>)>
596              
597             Issue an HTTP DELETE request of the resource at I<$path>.
598              
599             =cut
600              
601             sub delete ($$) {
602 1     1 1 331 my ($self, $path) = @_;
603              
604 1         4 return $self->call('DELETE' => $path);
605             }
606              
607             =back
608              
609             =head1 SEE ALSO
610              
611             =over
612              
613             =item L
614              
615             The OpenStack Keystone authentication and authorization interface
616              
617             =back
618              
619             =head1 AUTHOR
620              
621             Written by Alexandra Hrefna Hilmisdóttir
622              
623             =head1 CONTRIBUTORS
624              
625             =over
626              
627             =item Brett Estrade
628              
629             =back
630              
631             =head1 COPYRIGHT
632              
633             Copyright (c) 2018 cPanel, L.L.C. Released under the terms of the MIT license.
634             See LICENSE for further details.
635              
636             =cut
637              
638             1;