File Coverage

blib/lib/Net/Versa/Director.pm
Criterion Covered Total %
statement 17 125 13.6
branch 0 12 0.0
condition 0 3 0.0
subroutine 6 22 27.2
pod 10 10 100.0
total 33 172 19.1


line stmt bran cond sub pod time code
1             package Net::Versa::Director;
2             $Net::Versa::Director::VERSION = '0.004000';
3             # ABSTRACT: Versa Director REST API client library
4              
5 2     2   522720 use v5.36;
  2         8  
6 2     2   1257 use Moo;
  2         18398  
  2         15  
7 2     2   3372 use feature 'signatures';
  2         11  
  2         351  
8 2     2   1632 use Types::Standard qw( Str );
  2         289089  
  2         22  
9 2     2   9422 use Carp qw( croak );
  2         13  
  2         183  
10 2     2   3379 use Net::Versa::Director::Serializer;
  2         8  
  2         3243  
11              
12              
13             has 'user' => (
14             isa => Str,
15             is => 'rw',
16             );
17              
18             has 'passwd' => (
19             isa => Str,
20             is => 'rw',
21             );
22              
23             has '_refresh_token' => (
24             isa => Str,
25             is => 'rw',
26             clearer => 1,
27             );
28              
29             with 'Role::REST::Client';
30              
31             has '+serializer_class' => (
32             default => sub { 'Net::Versa::Director::Serializer' },
33             );
34              
35             with 'Role::REST::Client::Auth::Basic';
36              
37 0     0     sub _is_oauth ($self) {
  0            
  0            
38 0           return $self->server =~ /:9183/;
39             }
40              
41             # has to be after "with 'Role::REST::Client::Auth::Basic';" to be called before
42             # its method modifier
43             before '_call' => sub ($self, $method, $endpoint, $data, $args) {
44             # disable http basic auth if talking to the OAuth port
45             $args->{authentication} = 'none'
46             if $self->_is_oauth;
47             };
48              
49             has '+persistent_headers' => (
50             default => sub {
51             return { Accept => 'application/json' };
52             },
53             );
54              
55 0     0     sub _error_handler ($self, $res) {
  0            
  0            
  0            
56 0 0         if (ref $res->data eq 'HASH') {
57 0 0 0       if (exists $res->data->{error} && ref $res->data->{error} eq 'HASH') {
58 0           croak($res->data->{error});
59             }
60             else {
61 0           croak($res->data);
62             }
63             }
64             # emulate API response
65             else {
66 0           croak({
67             http_status_code => $res->code,
68             message => $res->response->decoded_content,
69             });
70             }
71             }
72              
73 0     0     sub _create ($self, $url, $object_data, $query_params = {}, $expected_code = 201, $args = {}) {
  0            
  0            
  0            
  0            
  0            
  0            
  0            
74 0           my $params = $self->user_agent->www_form_urlencode( $query_params );
75 0           my $res = $self->post("$url?$params", $object_data, $args);
76 0 0         $self->_error_handler($res)
77             unless $res->code == $expected_code;
78              
79 0           return $res->data;
80             }
81              
82 0     0     sub _get ($self, $url, $query_params = {}, $args = {}) {
  0            
  0            
  0            
  0            
  0            
83 0           my $res = $self->get($url, $query_params, $args);
84 0 0         $self->_error_handler($res)
85             unless $res->code == 200;
86              
87 0           return $res->data;
88             }
89              
90 0     0     sub _update ($self, $url, $object, $object_data, $query_params = {}, $args = {}) {
  0            
  0            
  0            
  0            
  0            
  0            
  0            
91 0           my $updated_data = clone($object);
92 0           $updated_data = { %$updated_data, %$object_data };
93 0           my $params = $self->user_agent->www_form_urlencode( $query_params );
94 0           my $res = $self->put("$url?$params", $updated_data, $args);
95 0 0         $self->_error_handler($res)
96             unless $res->code == 200;
97              
98 0           return $res->data;
99             }
100              
101 0     0     sub _delete ($self, $url, $args = {}) {
  0            
  0            
  0            
  0            
102 0           my $res = $self->delete($url, undef, $args);
103 0 0         $self->_error_handler($res)
104             unless $res->code == 200;
105              
106 0           return 1;
107             }
108              
109              
110 0     0 1   sub login ($self, $client_id, $client_secret) {
  0            
  0            
  0            
  0            
111 0           my $login_response = $self->_create('/auth/token', {
112             client_id => $client_id,
113             client_secret => $client_secret,
114             username => $self->user,
115             password => $self->passwd,
116             grant_type => "password",
117             }, {}, 200);
118 0           my $access_token = $login_response->{access_token};
119 0           $self->set_persistent_header('Authorization', "Bearer $access_token");
120 0           $self->_refresh_token($login_response->{refresh_token});
121 0           return $login_response;
122             }
123              
124              
125 0     0 1   sub logout ($self) {
  0            
  0            
126 0           my $res = $self->_create('/auth/revoke', undef, {}, 200);
127              
128 0           $self->_clear_refresh_token;
129 0           $self->clear_persistent_headers;
130              
131 0           return $res;
132             }
133              
134              
135 0     0 1   sub get_director_info ($self) {
  0            
  0            
136             return $self->_get('/api/operational/system/package-info')
137 0           ->{'package-info'}->[0];
138             }
139              
140              
141 0     0 1   sub get_version ($self) {
  0            
  0            
142 0           return $self->get_director_info->{branch};
143             }
144              
145              
146 0     0 1   sub list_appliances ($self) {
  0            
  0            
147             return $self->_get('/vnms/appliance/appliance', { offset => 0, limit => 2048 })
148 0           ->{'versanms.ApplianceStatusResult'}->{appliances};
149             }
150              
151              
152 0     0 1   sub list_device_workflows ($self) {
  0            
  0            
153             return $self->_get('/vnms/sdwan/workflow/devices', { offset => 0, limit => 2048 })
154 0           ->{'versanms.sdwan-device-list'};
155             }
156              
157              
158 0     0 1   sub get_device_workflow ($self, $device_workflow_name ) {
  0            
  0            
  0            
159             return $self->_get("/vnms/sdwan/workflow/devices/device/$device_workflow_name")
160 0           ->{'versanms.sdwan-device-workflow'};
161             }
162              
163              
164 0     0 1   sub list_assets ($self) {
  0            
  0            
165             return $self->_get('/vnms/assets/asset', { offset => 0, limit => 2048 })
166 0           ->{'versanms.AssetsResult'}->{assets};
167             }
168              
169              
170 0     0 1   sub list_device_interfaces ($self, $devicename) {
  0            
  0            
  0            
171             return $self->_get("/api/config/devices/device/$devicename/config/interfaces?deep")
172 0           ->{interfaces};
173             }
174              
175              
176 0     0 1   sub list_device_networks ($self, $devicename) {
  0            
  0            
  0            
177             return $self->_get("/api/config/devices/device/$devicename/config/networks/network")
178 0           ->{network};
179             }
180              
181              
182             1;
183              
184             __END__
185              
186             =pod
187              
188             =encoding UTF-8
189              
190             =head1 NAME
191              
192             Net::Versa::Director - Versa Director REST API client library
193              
194             =head1 VERSION
195              
196             version 0.004000
197              
198             =head1 SYNOPSIS
199              
200             use v5.36;
201             use Net::Versa::Director;
202              
203             # to use the username/password basic authentication
204              
205             my $director = Net::Versa::Director->new(
206             server => 'https://director.example.com:9182',
207             user => 'username',
208             passwd => '$password',
209             clientattrs => {
210             timeout => 10,
211             },
212             );
213              
214             # OR to use the OAuth token based authentication
215              
216             $director = Net::Versa::Director->new(
217             server => 'https://director.example.com:9183',
218             user => 'username',
219             passwd => '$password',
220             clientattrs => {
221             timeout => 10,
222             },
223             );
224              
225             # this is required to fetch the OAuth access and refresh tokens
226             # using the client id and secret passed to user and passwd.
227             $director->login;
228              
229             # at the end of your code, possible in an END block to always execute it
230             # after a successful login to not exceed the maximum number of access
231             # tokens.
232             $director->logout;
233              
234             =head1 DESCRIPTION
235              
236             This module is a client library for the Versa Director REST API using the
237             basic authentication API endpoint on port 9182.
238              
239             Currently it is developed and tested against version 21.2.
240              
241             For more information see
242             L<https://docs.versa-networks.com/Management_and_Orchestration/Versa_Director/Director_REST_APIs/01_Versa_Director_REST_API_Overview>.
243              
244             =head1 METHODS
245              
246             =head2 login
247              
248             Takes a client id and secret.
249              
250             Logs into the Versa Director when using the OAuth token based port 9183.
251              
252             Sets the Authorization header to the Bearer access token.
253              
254             Returns a hashref containing the OAuth access- and refresh-tokens.
255              
256             =head2 logout
257              
258             Revokes the access token if OAuth authentication is used so the maximum number
259             of access tokens of the client isn't exceeded.
260              
261             Returns the response.
262              
263             =head2 get_director_info
264              
265             Returns the Versa Director information as hashref.
266              
267             From /api/operational/system/package-info.
268              
269             =head2 get_version
270              
271             Returns the Versa Director version.
272              
273             From L</get_director_info>->{branch}.
274              
275             =head2 list_appliances
276              
277             Returns an arrayref of Versa appliances.
278              
279             From /vnms/appliance/appliance.
280              
281             =head2 list_device_workflows
282              
283             Returns an arrayref of device workflows.
284              
285             From /vnms/sdwan/workflow/devices.
286              
287             =head2 get_device_workflow
288              
289             Takes a workflow name.
290              
291             Returns a hashref of device workflow data.
292              
293             From /vnms/sdwan/workflow/devices/device/$device_workflow_name.
294              
295             =head2 list_assets
296              
297             Returns an arrayref of Versa appliances.
298              
299             From /vnms/assets/asset.
300              
301             =head2 list_device_interfaces
302              
303             Takes a device name.
304              
305             Returns a hashref of interface types each containing an arrayref of interface
306             hashrefs.
307              
308             From /api/config/devices/device/$devicename/config/interfaces?deep.
309              
310             =head2 list_device_networks
311              
312             Takes a device name.
313              
314             Returns an arrayref of network hashrefs.
315              
316             From /api/config/devices/device/$devicename/config/networks/network?deep=true.
317              
318             =head1 ERROR handling
319              
320             All methods throw an exception on error returning the unmodified data from the API
321             as hashref.
322              
323             Currently the Versa Director has to different API error formats depending on
324             the type of request.
325              
326             =head2 authentication errors
327              
328             The response of an authentication error looks like this:
329              
330             {
331             code => 4001,
332             description => "Invalid user name or password.",
333             http_status_code => 401,
334             message => "Unauthenticated",
335             more_info => "http://nms.versa.com/errors/4001",
336             }
337              
338             =head2 YANG data model errors
339              
340             All API endpoints starting with /api/config or /api/operational return this type of error:
341              
342             =head2 YANG and relational data model errors
343              
344             All API endpoints starting with /vnms return this type of error:
345              
346             {
347             error => "Not Found",
348             exception => "com.versa.vnms.common.exception.VOAEException",
349             http_status_code => 404,
350             message => " device work flow non-existing does not exist ",
351             path => "/vnms/sdwan/workflow/devices/device/non-existing",
352             timestamp => 1696574964569,
353             }
354              
355             =head1 TESTS
356              
357             To run the live API tests the following environment variables need to be set:
358              
359             =over
360              
361             =item NET_VERSA_DIRECTOR_HOSTNAME
362              
363             =item NET_VERSA_DIRECTOR_USERNAME
364              
365             =item NET_VERSA_DIRECTOR_PASSWORD
366              
367             =item NET_VERSA_DIRECTOR_CLIENT_ID
368              
369             =item NET_VERSA_DIRECTOR_CLIENT_SECRET
370              
371             =back
372              
373             If basic authentication tests should be also run set this additional variable to true.
374              
375             =over
376              
377             =item NET_VERSA_DIRECTOR_BASIC_AUTH
378              
379             =back
380              
381             Only read calls are tested so far.
382              
383             =head1 AUTHOR
384              
385             Alexander Hartmaier <abraxxa@cpan.org>
386              
387             =head1 COPYRIGHT AND LICENSE
388              
389             This software is copyright (c) 2025 by Alexander Hartmaier.
390              
391             This is free software; you can redistribute it and/or modify it under
392             the same terms as the Perl 5 programming language system itself.
393              
394             =cut