File Coverage

blib/lib/Net/OpenStack/Client/REST.pm
Criterion Covered Total %
statement 125 135 92.5
branch 32 50 64.0
condition 16 26 61.5
subroutine 13 13 100.0
pod 1 1 100.0
total 187 225 83.1


line stmt bran cond sub pod time code
1             package Net::OpenStack::Client::REST;
2             $Net::OpenStack::Client::REST::VERSION = '0.1.4';
3 7     7   3531 use strict;
  7         14  
  7         181  
4 7     7   31 use warnings;
  7         14  
  7         181  
5              
6 7     7   35 use Net::OpenStack::Client::Request qw(@METHODS_REQUIRE_OPTIONS $HDR_X_AUTH_TOKEN);
  7         13  
  7         627  
7 7     7   2370 use Net::OpenStack::Client::Response;
  7         30  
  7         278  
8 7     7   3054 use REST::Client;
  7         303944  
  7         232  
9 7     7   57 use LWP::UserAgent;
  7         37  
  7         116  
10 7     7   33 use JSON::XS;
  7         13  
  7         331  
11              
12 7     7   40 use Readonly;
  7         13  
  7         8672  
13              
14             # Map with HTTP return codes indicating success
15             # if method is missing (only) 200 is conisdered success
16             # if method is present, 200 is not considered success by default
17             Readonly my %SUCCESS => {
18             POST => [201],
19             PUT => [200, 201, 204],
20             DELETE => [204, 201], # yes, 201 when deleting a token
21             };
22              
23              
24             # JSON::XS instance
25             # sort the keys, to create reproducable results
26             my $json = JSON::XS->new()->canonical(1);
27              
28             =head1 methods
29              
30             =over
31              
32             =cut
33              
34             sub _new_client
35             {
36 6     6   17 my ($self) = @_;
37              
38 6         42 my $browser = LWP::UserAgent->new();
39             # Temporary cookie_jar
40 6         15959 $browser->cookie_jar( {} );
41              
42 6         39731 my $rc = REST::Client->new(
43             useragent => $browser,
44             );
45              
46 6         4065 $self->{rc} = $rc;
47             }
48              
49             # Actual REST::Client call
50             # Returns tuple repsonse, repsonse headers and error message.
51             # Processes the repsonse code, including possible JSON decoding
52             # Reports error and returns err (with repsonse undef)
53             sub _call
54             {
55 54     54   130 my ($self, $method, $url, @args) = @_;
56              
57 54         82 my $err;
58 54         93 my $rc = $self->{rc};
59              
60             # make the call
61 54         215 $rc->$method($url, @args);
62              
63 54         431014 my $code = $rc->responseCode();
64 54         269 my $content = $rc->responseContent();
65 54         258 my $rheaders = {map {$_ => $rc->responseHeader($_)} $rc->responseHeaders};
  56         376  
66 54 100       410 my $success = grep {$code == $_} @{$SUCCESS{$method} || [200]};
  82         859  
  54         234  
67              
68 54         122 my $response;
69 54   50     138 my $type = $rheaders->{'Content-Type'} || 'RESPONSE_WO_CONTENT_TYPE_HEADER';
70 54 50       432 if ($type =~ qr{^application/json}i) {
71 54         92 local $@;
72 54         97 eval {
73 54         338 $response = $json->decode($content);
74             };
75 54 50       181 if ($@) {
76 0 0       0 my $report = $success ? 'error' : 'verbose';
77 0 0       0 $self->$report("REST $method with ".($success ? 'success' : 'error').
78             " failure to decode JSON content $content: $@");
79             }
80             } else {
81 0         0 $response = $content;
82             }
83              
84 54 100       139 if ($success) {
85 53         295 $self->verbose("Successful REST $method url $url type $type");
86 53 50       13055 if ($self->{debugapi}) {
87             # might contain sensitive data, eg security token
88 53         187 my $headers_txt = join(',', map {"$_=$rheaders->{$_}"} sort keys %$rheaders);
  55         205  
89 53         220 $self->debug("REST $method full response headers $headers_txt");
90 53         13897 $self->debug("REST $method full response content $content");
91             }
92             } else {
93 1         5 my $errmsg = "$method failed (url $url code $code)";
94 1 50 33     9 if (ref($response) eq 'HASH' &&
95             $response->{error}) {
96 1         3 $err = $response->{error};
97 1         3 $err->{url} = $url;
98 1         2 $err->{method} = $method;
99 1 50       5 $err->{code} = $code if !exists($err->{code});
100             } else {
101 0         0 $err = $errmsg;
102             }
103              
104 1 50       4 $content = '' if ! defined($content);
105 1         4 $errmsg = "$errmsg: $content";
106 1         10 $self->error("REST $errmsg");
107             }
108              
109 54         13900 return $response, $rheaders, $err;
110             }
111              
112             # Handle pagination: https://developer.openstack.org/api-guide/compute/paginated_collections.html
113             # For any decoded reply, walk the tree
114             # look for +_links combos
115             # should be a list (a collection)
116             # the _links part should have a rel=next, href=newurl
117             # follow it, and merge the results with the original key list
118             # lets assume they are in the same relative path
119             # Aaaargh who came up with this crap
120              
121             # Return array of paths that have to processed for paging
122             # Each element is a tuple of path (as arrayref of subpaths)
123             # and url to follow
124             # Assumes that all responses can be retrieved from
125             # and be joined using the same path in JSON
126             # Only pass hashref as data.
127             # TODO: support lookup of links in arrays?
128             sub _page_paths
129             {
130 73     73   787 my ($self, $data) = @_;
131              
132 73         102 my @paths;
133              
134 73         231 foreach my $key (sort keys %$data) {
135 88         173 my $lkey = $key."_links";
136 88         146 my $ref = ref($data->{$key});
137 88 100 66     367 if (exists($data->{$lkey}) &&
    100 66        
138             $ref eq 'ARRAY' &&
139             ref($data->{$lkey}) eq 'ARRAY'
140             ) {
141 6         12 foreach my $link (@{$data->{$lkey}}) {
  6         14  
142 6 50 66     30 if (exists($link->{rel}) &&
      66        
143             $link->{rel} eq 'next' &&
144             exists($link->{href})) {
145             # only first one
146 4         12 push(@paths, [[$key], $link->{href}]);
147 4         11 last;
148             }
149             }
150             } elsif ($ref eq 'HASH') {
151 18         64 foreach my $rpath_tuple ($self->_page_paths($data->{$key})) {
152             # add current key to path element (i.e. the path is relative to $key)
153 6         10 unshift(@{$rpath_tuple->[0]}, $key);
  6         15  
154 6         13 push(@paths, $rpath_tuple);
155             }
156             }
157             }
158              
159 73         182 return @paths;
160             }
161              
162             # only hashrefs
163             sub _page
164             {
165 54     54   1503 my ($self, $method, $response, $headers) = @_;
166              
167 54         83 my $err;
168              
169 54         130 foreach my $path_tuple ($self->_page_paths($response)) {
170             # No body, this is GET only
171             # We only care about the response headers of the first batch
172 2         15 $self->debug("_page method $method url $path_tuple->[1]");
173 2         798 my ($tresponse, $trheaders, $terr) = $self->_call($method, $path_tuple->[1], $headers);
174 2 50       27 if ($terr) {
175             # no temp err here, a failure in the paged GET repsonse is a failure nonetheless
176 0         0 $err = $terr;
177 0         0 last;
178             } else {
179 2         4 my @path = @{$path_tuple->[0]};
  2         6  
180             # extend path of tresponse in path of response
181 2         5 my $rarray = $response;
182 2         5 foreach my $p (@path) {
183 5         8 $rarray = $rarray->{$p};
184 5         11 $tresponse = $tresponse->{$p};
185             }
186 2         9 push(@$rarray, @$tresponse);
187             };
188             }
189              
190 54         117 return $response, $err;
191             }
192              
193             =item rest
194              
195             Given a Request instance C, perform this request.
196             All options are passed to the headers method.
197             The token option is added if the token attribute exists and
198             if not token option was already in the options.
199              
200             =cut
201              
202             sub rest
203             {
204 54     54 1 112 my ($self, $req, %opts) = @_;
205              
206             # methods that require options, must pass said option as body
207             # general call is $rc->$method($url, [body if options], $headers)
208              
209 54         210 my $method = $req->{method};
210 54         109 my $rservice = $req->{service};
211 54         75 my $service;
212 54 50       89 if ($rservice) {
213 54         102 $service = $self->{services}->{$rservice};
214 54 100       103 if (!$service) {
215 3         23 $self->debug("REST $method request endpoint $req->{endpoint} service $rservice has no known service");
216             }
217             } else {
218 0         0 $self->debug("REST $method request endpoint $req->{endpoint} has no service");
219             }
220              
221             # url
222 54         1292 my $url = $req->endpoint($service);
223 54 50       118 if (!$url) {
224 0 0       0 my $msg = "REST $method request endpoint $req->{endpoint} ".
225             ($service ? "service $service" : "no service")." has no endpoint url";
226 0         0 $self->error($msg);
227 0         0 return mkresponse(error => $msg);
228             }
229              
230 54         99 my @args = ($url);
231              
232             # body if needed
233 54         75 my $body;
234 54 100       152 if (grep {$method eq $_} @METHODS_REQUIRE_OPTIONS) {
  162         1016  
235 27         81 my $data = $req->opts_data;
236 27         177 $body = $json->encode($data);
237 27         80 push(@args, $body);
238             }
239              
240             # headers
241 54 100 66     296 $opts{token} = $self->{token} if (exists($self->{token}) && !exists $opts{token});
242 54         163 my $headers = $req->headers(%opts);
243 54         89 push(@args, $headers);
244              
245 54 100       492 $self->debug("REST $method url $url, ".(defined $body ? '' : 'no ')."body, headers ".join(',', sort keys %$headers));
246 54 50       14693 if ($self->{debugapi}) {
247             # might contain sensitive data, eg security token
248 54         218 my $headers_txt = join(',', map {"$_=$headers->{$_}"} sort keys %$headers);
  162         472  
249 54         253 $self->debug("REST $method full headers $headers_txt");
250 54 100       14129 $self->debug("REST $method full body $body") if $body;
251             }
252              
253 54         7222 my ($response, $rheaders, $err) = $self->_call($method, @args);
254             # The err here could be a failure in the paged GET repsonse
255 54 50 66     468 ($response, $err) = $self->_page($method, $response, $headers) if (!$err && $response && ref($response) eq 'HASH');
      66        
256              
257 54         173 my %ropts = (
258             data => $response,
259             headers => $rheaders,
260             error => $err,
261             );
262 54 100       159 $ropts{result_path} = $req->{result} if defined $req->{result};
263 54         174 return mkresponse(%ropts);
264             }
265              
266             =pod
267              
268             =back
269              
270             =cut
271              
272             1;