File Coverage

blib/lib/OpenStack/Client.pm
Criterion Covered Total %
statement 131 133 98.5
branch 34 36 94.4
condition 9 11 81.8
subroutine 23 23 100.0
pod 11 13 84.6
total 208 216 96.3


line stmt bran cond sub pod time code
1             #
2             # Copyright (c) 2019 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   543 use strict;
  2         10  
  2         57  
12 2     2   10 use warnings;
  2         4  
  2         61  
13              
14 2     2   914 use HTTP::Request ();
  2         48345  
  2         46  
15 2     2   1405 use LWP::UserAgent ();
  2         51811  
  2         61  
16              
17 2     2   716 use JSON ();
  2         10341  
  2         41  
18 2     2   984 use URI::Encode ();
  2         24739  
  2         69  
19              
20 2     2   941 use OpenStack::Client::Response ();
  2         7  
  2         3266  
21              
22             our $VERSION = '1.0006_0001';
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 19544 my ($class, $endpoint, %opts) = @_;
106              
107 60 100       157 die 'No API endpoint provided' unless $endpoint;
108              
109 59   100     175 $opts{'package_ua'} ||= 'LWP::UserAgent';
110 59   100     143 $opts{'package_request'} ||= 'HTTP::Request';
111 59   100     130 $opts{'package_response'} ||= 'OpenStack::Client::Response';
112              
113 59         201 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         3644 '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 576 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 112 shift->{'token'};
156             }
157              
158             sub uri ($$) {
159 52     52 0 1271 my ($self, $path) = @_;
160              
161             return join '/', map {
162 104         157 my $part = $_;
163              
164 104         246 $part =~ s/^\///;
165 104         331 $part =~ s/\/$//;
166 104         437 $part
167 52         105 } $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 147 my ($self, %args) = @_;
180              
181 49   100     151 $args{'headers'} ||= {};
182              
183             my $request = $self->{'package_request'}->new(
184 49         135 $args{'method'} => $self->uri($args{'path'})
185             );
186              
187 49         1363 my @headers = $self->_get_headers_list($args{'headers'});
188              
189 49         82 my $count = scalar @headers;
190              
191 49         130 for (my $i=0; $i<$count; $i+=2) {
192 198         1210 my $name = $headers[$i];
193 198         287 my $value = $headers[$i+1];
194              
195 198         398 $request->header($name => $value);
196             }
197              
198 49 100       405 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       79 if (ref($args{'body'}) =~ /CODE/) {
206 1         5 $request->content($args{'body'});
207             } else {
208 25         215 $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       284 $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 1343 my $self = shift;
330              
331 73 100       269 if (scalar @_ == 4) {
    100          
    100          
    50          
332 3         16 return $self->call({
333             'method' => $_[0],
334             'headers' => $_[1],
335             'path' => $_[2],
336             'body' => $_[3]
337             });
338             } elsif (scalar @_ == 3) {
339 11         47 return $self->call({
340             'method' => $_[0],
341             'headers' => {},
342             'path' => $_[1],
343             'body' => $_[2],
344             });
345             } elsif (scalar @_ == 2) {
346 22         115 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         65 my ($args) = @_;
357              
358 37         57 return $self->request(%{$args})->decode_json;
  37         134  
359             }
360              
361             sub _lc_merge {
362 98     98   197 my ($a, $b, %opts) = @_;
363              
364             my %lc_keys_a = map {
365 155         376 lc $_ => $_
366 98         142 } keys %{$a};
  98         184  
367              
368 98         172 foreach my $key_b (keys %{$b}) {
  98         207  
369 196         333 my $key_a = $lc_keys_a{lc $key_b};
370              
371 196 100 33     330 if (!defined($key_a)) {
    50          
372 192         373 $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         212 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   94 my ($self, $headers) = @_;
387              
388 49         145 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         102 my %OVERRIDES = (
399             'X-Auth-Token' => $self->token
400             );
401              
402 49         74 my %new_headers = %{$headers};
  49         101  
403              
404 49         158 _lc_merge(\%new_headers, \%DEFAULTS);
405 49         118 _lc_merge(\%new_headers, \%OVERRIDES, 'replace' => 1);
406              
407 49         221 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 1789 my ($self, $path, %opts) = @_;
424              
425 15         25 my $params;
426              
427 15         42 foreach my $name (sort keys %opts) {
428 9         2337 my $value = $opts{$name};
429              
430 9 100       23 $params .= "&" if defined $params;
431              
432             $params .= sprintf "%s=%s", map {
433 9         19 URI::Encode::uri_encode($_)
  18         11198  
434             } $name, $value;
435             }
436              
437 15 100       8324 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       43 if ($path =~ /\?/) {
443 1         4 $path .= "&$params";
444             } else {
445 6         18 $path .= "?$params";
446             }
447             }
448              
449 15         39 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 1077 my ($self, $path, @args) = @_;
465              
466 4         9 my $opts = {};
467 4         7 my $callback;
468              
469 4 100       16 if (scalar @args == 2) {
    100          
470 1         3 ($opts, $callback) = @args;
471             } elsif (scalar @args == 1) {
472 1         3 ($callback) = @args;
473             } else {
474 2         18 die 'Invalid number of arguments';
475             }
476              
477 2         6 while (defined $path) {
478 2         5 my $result = $self->get($path, %{$opts});
  2         6  
479              
480 2         8 $callback->($result);
481              
482 2         16 $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 835 my ($self, $path, $attribute, @args) = @_;
502              
503 7         15 my $opts = {};
504 7         10 my $callback;
505              
506 7 100       24 if (scalar @args == 2) {
    100          
507 3         7 ($opts, $callback) = @args;
508             } elsif (scalar @args == 1) {
509 2         5 ($callback) = @args;
510             } else {
511 2         18 die 'Invalid number of arguments';
512             }
513              
514 5         13 while (defined $path) {
515 8         15 my $result = $self->get($path, %{$opts});
  8         22  
516              
517 8 100       26 unless (defined $result->{$attribute}) {
518 1         6 my $keys = join( ', ', sort keys %$result );
519 1         14 die "Response from $path does not contain attribute '$attribute', possible options are " . $keys;
520             }
521              
522 7         12 foreach my $item (@{$result->{$attribute}}) {
  7         28  
523 12         27 $callback->($item);
524             }
525              
526 7         38 $path = $result->{'next'};
527             }
528              
529 4         12 return;
530             }
531              
532             =item C<$client-Eall(I<$path>, I<$attribute>, I<$opts>)>
533              
534             =item C<$client-Eall(I<$path>, I<$attribute>)>
535              
536             Perform a series of HTTP GET requests for the resource I<$path>, decoding the
537             result set and returning a list of all items found within each response body's
538             attribute named I<$attribute>. I<$opts> is taken to be a HASH reference
539             containing zero or more key-value pairs to be URL encoded as parameters to each
540             GET request made.
541              
542             =cut
543              
544             sub all ($$$@) {
545 2     2 1 90 my ($self, $path, $attribute, $opts) = @_;
546              
547 2         3 my @items;
548              
549             $self->every($path, $attribute, $opts, sub {
550 6     6   9 my ($item) = @_;
551              
552 6         15 push @items, $item;
553 2         11 });
554              
555 2         17 return @items;
556             }
557              
558             =back
559              
560             =head1 CREATING AND UPDATING REMOTE RESOURCES
561              
562             =over
563              
564             =item C<$client-Eput(I<$path>, I<$body>)>
565              
566             Issue an HTTP PUT request to the resource at I<$path>, in the form of a JSON
567             encoding of the contents of I<$body>.
568              
569             =cut
570              
571             sub put ($$$) {
572 1     1 1 50 my ($self, $path, $body) = @_;
573              
574 1         4 return $self->call('PUT' => $path, $body);
575             }
576              
577             =item C<$client-Epost(I<$path>, I<$body>)>
578              
579             Issue an HTTP POST request to the resource at I<$path>, in the form of a
580             JSON encoding of the contents of I<$body>.
581              
582             =cut
583              
584             sub post ($$$) {
585 1     1 1 344 my ($self, $path, $body) = @_;
586              
587 1         10 return $self->call('POST' => $path, $body);
588             }
589              
590             =back
591              
592             =head1 DELETING REMOTE RESOURCES
593              
594             =over
595              
596             =item C<$client-Edelete(I<$path>)>
597              
598             Issue an HTTP DELETE request of the resource at I<$path>.
599              
600             =cut
601              
602             sub delete ($$) {
603 1     1 1 335 my ($self, $path) = @_;
604              
605 1         5 return $self->call('DELETE' => $path);
606             }
607              
608             =back
609              
610             =head1 SEE ALSO
611              
612             =over
613              
614             =item L
615              
616             The OpenStack Keystone authentication and authorization interface
617              
618             =back
619              
620             =head1 AUTHOR
621              
622             Written by Alexandra Hrefna Maheu
623              
624             =head1 CONTRIBUTORS
625              
626             =over
627              
628             =item Brett Estrade
629              
630             =back
631              
632             =head1 COPYRIGHT
633              
634             Copyright (c) 2019 cPanel, L.L.C. Released under the terms of the MIT license.
635             See LICENSE for further details.
636              
637             =cut
638              
639             1;