File Coverage

blib/lib/API/MikroTik.pm
Criterion Covered Total %
statement 148 151 98.0
branch 34 44 77.2
condition 19 28 67.8
subroutine 35 35 100.0
pod 2 2 100.0
total 238 260 91.5


line stmt bran cond sub pod time code
1             package API::MikroTik;
2 3     3   144505 use Mojo::Base '-base';
  3         578633  
  3         30  
3              
4 3     3   2492 use API::MikroTik::Response;
  3         9  
  3         29  
5 3     3   119 use API::MikroTik::Sentence qw(encode_sentence);
  3         6  
  3         131  
6 3     3   18 use Carp ();
  3         7  
  3         51  
7 3     3   1407 use Mojo::Collection;
  3         10135  
  3         132  
8 3     3   1435 use Mojo::IOLoop;
  3         332250  
  3         37  
9 3     3   169 use Mojo::Util qw(md5_sum deprecated);
  3         8  
  3         161  
10 3     3   20 use Scalar::Util 'weaken';
  3         6  
  3         192  
11              
12             deprecated "API::MikroTik is deprecated in favour of MikroTik::Client\n";
13              
14 3     3   18 use constant CONN_TIMEOUT => $ENV{API_MIKROTIK_CONNTIMEOUT};
  3         6  
  3         197  
15 3   50 3   17 use constant DEBUG => $ENV{API_MIKROTIK_DEBUG} || 0;
  3         6  
  3         209  
16 3     3   18 use constant PROMISES => !!(eval { require Mojo::Promise; 1 });
  3         7  
  3         4  
  3         43  
  3         7493  
17              
18             our $VERSION = 'v0.242';
19              
20             has error => '';
21             has host => '192.168.88.1';
22             has ioloop => sub { Mojo::IOLoop->new() };
23             has password => '';
24             has port => 0;
25             has timeout => 10;
26             has tls => 1;
27             has user => 'admin';
28             has _tag => 0;
29              
30             # Aliases
31             Mojo::Util::monkey_patch(__PACKAGE__, 'cmd', \&command);
32             Mojo::Util::monkey_patch(__PACKAGE__, 'cmd_p', \&command_p);
33             Mojo::Util::monkey_patch(__PACKAGE__, '_fail', \&_finish);
34              
35 1 50   1   2109 sub DESTROY { Mojo::Util::_global_destruction() or shift->_cleanup() }
36              
37             sub cancel {
38 2 50   2 1 24 my $cb = ref $_[-1] eq 'CODE' ? pop : sub { };
        2      
39 2         32 return shift->_command(Mojo::IOLoop->singleton, '/cancel', {'tag' => shift},
40             undef, $cb);
41             }
42              
43             sub command {
44 12 100   12   11392 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
45 12         36 my ($self, $cmd, $attr, $query) = @_;
46              
47             # non-blocking
48 12 100       43 return $self->_command(Mojo::IOLoop->singleton, $cmd, $attr, $query, $cb)
49             if $cb;
50              
51             # blocking
52 9         13 my $res;
53             $self->_command($self->ioloop, $cmd, $attr, $query,
54 9     9   32 sub { $_[0]->ioloop->stop(); $res = $_[2]; });
  9         35  
  9         164  
55 9         28 $self->ioloop->start();
56              
57 9         905 return $res;
58             }
59              
60             sub command_p {
61 3     3   5820 Carp::croak 'Mojolicious v7.54+ is required for using promises.'
62             unless PROMISES;
63 3         11 my ($self, $cmd, $attr, $query) = @_;
64              
65 3         23 my $p = Mojo::Promise->new();
66             $self->_command(
67             Mojo::IOLoop->singleton,
68             $cmd, $attr, $query,
69             sub {
70 3 100   3   17 return $p->reject($_[1], $_[2]) if $_[1];
71 1         5 $p->resolve($_[2]);
72             }
73 3         91 );
74              
75 3         19 return $p;
76             }
77              
78             sub subscribe {
79 1 50   1 1 16 do { $_[0]->{error} = 'can\'t subscribe in blocking mode'; return; }
  0         0  
  0         0  
80             unless ref $_[-1] eq 'CODE';
81 1         3 my $cb = pop;
82 1         3 my ($self, $cmd, $attr, $query) = @_;
83 1         3 $attr->{'.subscription'} = 1;
84 1         4 return $self->_command(Mojo::IOLoop->singleton, $cmd, $attr, $query, $cb);
85             }
86              
87             sub _cleanup {
88 1     1   12 my $self = shift;
89             $_->{timeout} && $_->{loop}->remove($_->{timeout})
90 1   0     2 for values %{$self->{requests}};
  1         6  
91 1   33     2 $_ && $_->unsubscribe('close')->close() for values %{$self->{handles}};
  1         15  
92 1         284 delete $self->{handles};
93             }
94              
95             sub _close {
96 2     2   9 my ($self, $loop) = @_;
97 2         8 $self->_fail_all($loop, 'closed prematurely');
98 2         8 delete $self->{handles}{$loop};
99 2         12 delete $self->{responses}{$loop};
100             }
101              
102             sub _command {
103 28     28   310 my ($self, $loop, $cmd, $attr, $query, $cb) = @_;
104              
105 28         62 my $tag = ++$self->{_tag};
106 28         123 my $r = $self->{requests}{$tag} = {tag => $tag, loop => $loop, cb => $cb};
107 28         64 $r->{subscription} = delete $attr->{'.subscription'};
108              
109 28         38 warn "-- got request for command '$cmd' (tag: $tag)\n" if DEBUG;
110              
111 28         107 $r->{sentence} = encode_sentence($cmd, $attr, $query, $tag);
112 28         80 return $self->_send_request($r);
113             }
114              
115             sub _connect {
116 7     7   17 my ($self, $r) = @_;
117              
118 7         10 warn "-- creating new connection\n" if DEBUG;
119              
120 7         24 my $queue = $self->{queues}{$r->{loop}} = [$r];
121              
122 7         30 my $tls = $self->tls;
123 7 0       47 my $port = $self->port ? $self->{port} : $tls ? 8729 : 8728;
    50          
124              
125             $r->{loop}->client(
126             {
127             address => $self->host,
128             port => $port,
129             timeout => CONN_TIMEOUT,
130             tls => $tls,
131             tls_ciphers => 'HIGH'
132             } => sub {
133 7     7   13141 my ($loop, $err, $stream) = @_;
134              
135 7         27 delete $self->{queues}{$loop};
136              
137 7 100       31 if ($err) { $self->_fail($_, $err) for @$queue; return }
  2         14  
  2         129  
138              
139 5         10 warn "-- connection established\n" if DEBUG;
140              
141 5         13 $self->{handles}{$loop} = $stream;
142              
143 5         19 weaken $self;
144 5         27 $stream->on(read => sub { $self->_read($loop, $_[1]) });
  21         8148  
145             $stream->on(
146 5 0       48 error => sub { $self and $self->_fail_all($loop, $_[1]) });
  0         0  
147 5 50       43 $stream->on(close => sub { $self && $self->_close($loop) });
  2         501266  
148              
149             $self->_login(
150             $loop,
151             sub {
152 5 100       18 if ($_[1]) {
153 1         7 $_[0]->_fail($_, $_[1]) for @$queue;
154 1         6 $stream->close();
155 1         57 return;
156             }
157 4         18 $self->_write_sentence($stream, $_) for @$queue;
158             }
159 5         44 );
160             }
161 7         54 );
162              
163 7         1691 return $r->{tag};
164             }
165              
166             sub _enqueue {
167 10     10   24 my ($self, $r) = @_;
168 10 100       45 return $self->_connect($r) unless my $queue = $self->{queues}{$r->{loop}};
169 3         7 push @$queue, $r;
170 3         11 return $r->{tag};
171             }
172              
173             sub _fail_all {
174             $_[0]->_fail($_, $_[2])
175 2     2   5 for grep { $_->{loop} eq $_[1] } values %{$_[0]->{requests}};
  1         10  
  2         10  
176             }
177              
178             sub _finish {
179 28     28   64 my ($self, $r, $err) = @_;
180 28         124 delete $self->{requests}{$r->{tag}};
181 28 100       85 if (my $timer = $r->{timeout}) { $r->{loop}->remove($timer) }
  24         83  
182 28   100     1112 $r->{cb}->($self, ($self->{error} = $err // ''), $r->{data});
183             }
184              
185             sub _login {
186 5     5   27 my ($self, $loop, $cb) = @_;
187 5         11 warn "-- trying to log in\n" if DEBUG;
188              
189             $loop->delay(
190             sub {
191 5     5   1953 $self->_command($loop, '/login', {}, undef, $_[0]->begin());
192             },
193             sub {
194 5     5   156 my ($delay, $err, $res) = @_;
195 5 50       18 return $self->$cb($err) if $err;
196             my $secret
197 5         20 = md5_sum("\x00", $self->password, pack 'H*', $res->[0]{ret});
198 5         83 $self->_command($loop, '/login',
199             {name => $self->user, response => "00$secret"},
200             undef, $delay->begin());
201             },
202             sub {
203 5     5   145 $self->$cb($_[1]);
204             },
205 5         74 );
206             }
207              
208             sub _read {
209 21     21   54 my ($self, $loop, $bytes) = @_;
210              
211 21         36 warn "-- read bytes from socket: " . (length $bytes) . "\n" if DEBUG;
212              
213 21   66     120 my $response = $self->{responses}{$loop} ||= API::MikroTik::Response->new();
214 21         119 my $data = $response->parse(\$bytes);
215              
216 21         55 for (@$data) {
217 34 100       1060 next unless my $r = $self->{requests}{delete $_->{'.tag'}};
218 33         64 my $type = delete $_->{'.type'};
219 21   66     115 push @{$r->{data} ||= Mojo::Collection->new()}, $_
220 33 100 100     128 if %$_ && !$r->{subscription};
221              
222 33 100 100     298 if ($type eq '!re' && $r->{subscription}) {
    100 100        
    100          
223 1         8 $r->{cb}->($self, '', $_);
224              
225             }
226             elsif ($type eq '!done') {
227 15   66     67 $r->{data} ||= Mojo::Collection->new();
228 15         68 $self->_finish($r);
229              
230             }
231             elsif ($type eq '!trap' || $type eq '!fatal') {
232 7         23 $self->_fail($r, $_->{message});
233             }
234             }
235             }
236              
237             sub _send_request {
238 28     28   54 my ($self, $r) = @_;
239 28 100       131 return $self->_enqueue($r) unless my $stream = $self->{handles}{$r->{loop}};
240 18         55 return $self->_write_sentence($stream, $r);
241             }
242              
243             sub _write_sentence {
244 25     25   53 my ($self, $stream, $r) = @_;
245 25         39 warn "-- writing sentence for tag: $r->{tag}\n" if DEBUG;
246              
247 25         107 $stream->write($r->{sentence});
248              
249 25 100       728 return $r->{tag} if $r->{subscription};
250              
251 24         75 weaken $self;
252             $r->{timeout} = $r->{loop}
253 24     2   69 ->timer($self->timeout => sub { $self->_fail($r, 'response timeout') });
  2         1502059  
254              
255 24         1635 return $r->{tag};
256             }
257              
258             1;
259              
260              
261             =encoding utf8
262              
263             =head1 NAME
264              
265             API::MikroTik - Non-blocking interface to MikroTik API
266              
267             =head1 SYNOPSIS
268              
269             my $api = API::MikroTik->new();
270              
271             # Blocking
272             my $list = $api->command(
273             '/interface/print',
274             {'.proplist' => '.id,name,type'},
275             {type => ['ipip-tunnel', 'gre-tunnel'], running => 'true'}
276             );
277             if (my $err = $api->error) { die "$err\n" }
278             printf "%s: %s\n", $_->{name}, $_->{type} for @$list;
279              
280              
281             # Non-blocking
282             my $tag = $api->command(
283             '/system/resource/print',
284             {'.proplist' => 'board-name,version,uptime'} => sub {
285             my ($api, $err, $list) = @_;
286             ...;
287             }
288             );
289             Mojo::IOLoop->start();
290              
291             # Subscribe
292             $tag = $api->subscribe(
293             '/interface/listen' => sub {
294             my ($api, $err, $el) = @_;
295             ...;
296             }
297             );
298             Mojo::IOLoop->timer(3 => sub { $api->cancel($tag) });
299             Mojo::IOLoop->start();
300              
301             # Errors handling
302             $api->command(
303             '/random/command' => sub {
304             my ($api, $err, $list) = @_;
305              
306             if ($err) {
307             warn "Error: $err, category: " . $list->[0]{category};
308             return;
309             }
310              
311             ...;
312             }
313             );
314             Mojo::IOLoop->start();
315              
316             # Promises
317             $api->cmd_p('/interface/print')
318             ->then(sub { my $res = shift }, sub { my ($err, $attr) = @_ })
319             ->finally(sub { Mojo::IOLoop->stop() });
320             Mojo::IOLoop->start();
321              
322             =head1 DESCRIPTION
323              
324             B LB<.>
325              
326             Both blocking and non-blocking interface to a MikroTik API service. With queries,
327             command subscriptions and Promises/A+ (courtesy of an I/O loop). Based on
328             L and would work alongside L.
329              
330             =head1 ATTRIBUTES
331              
332             L implements the following attributes.
333              
334             =head2 error
335              
336             my $last_error = $api->error;
337              
338             Keeps an error from last L call. Empty string on successful commands.
339              
340             =head2 host
341              
342             my $host = $api->host;
343             $api = $api->host('border-gw.local');
344              
345             Host name or IP address to connect to. Defaults to C<192.168.88.1>.
346              
347             =head2 ioloop
348              
349             my $loop = $api->ioloop;
350             $api = $api->loop(Mojo::IOLoop->new());
351              
352             Event loop object to use for blocking operations, defaults to L
353             object.
354              
355             =head2 password
356              
357             my $pass = $api->password;
358             $api = $api->password('secret');
359              
360             Password for authentication. Empty string by default.
361              
362             =head2 port
363              
364             my $port = $api->port;
365             $api = $api->port(8000);
366              
367             API service port for connection. Defaults to C<8729> and C<8728> for TLS and
368             clear text connections respectively.
369              
370             =head2 timeout
371              
372             my $timeout = $api->timeout;
373             $api = $api->timeout(15);
374              
375             Timeout in seconds for sending request and receiving response before command
376             will be canceled. Default is C<10> seconds.
377              
378             =head2 tls
379              
380             my $tls = $api->tls;
381             $api = $api->tls(1);
382              
383             Use TLS for connection. Enabled by default.
384              
385             =head2 user
386              
387             my $user = $api->user;
388             $api = $api->user('admin');
389              
390             User name for authentication purposes. Defaults to C.
391              
392             =head1 METHODS
393              
394             =head2 cancel
395              
396             # subscribe to a command output
397             my $tag = $api->subscribe('/ping', {address => '127.0.0.1'} => sub {...});
398              
399             # cancel command after 10 seconds
400             Mojo::IOLoop->timer(10 => sub { $api->cancel($tag) });
401              
402             # or with callback
403             $api->cancel($tag => sub {...});
404              
405             Cancels background commands. Can accept a callback as last argument.
406              
407             =head2 cmd
408              
409             my $list = $api->cmd('/interface/print');
410              
411             An alias for L.
412              
413             =head2 cmd_p
414              
415             my $promise = $api->cmd_p('/interface/print');
416              
417             An alias for L.
418              
419             =head2 command
420              
421             my $command = '/interface/print';
422             my $attr = {'.proplist' => '.id,name,type'};
423             my $query = {type => ['ipip-tunnel', 'gre-tunnel'], running => 'true'};
424              
425             my $list = $api->command($command, $attr, $query);
426             die $api->error if $api->error;
427             for (@$list) {...}
428              
429             $api->command('/user/set', {'.id' => 'admin', comment => 'System admin'});
430              
431             # Non-blocking
432             $api->command('/ip/address/print' => sub {
433             my ($api, $err, $list) = @_;
434              
435             return if $err;
436              
437             for (@$list) {...}
438             });
439              
440             # Omit attributes
441             $api->command('/user/print', undef, {name => 'admin'} => sub {...});
442              
443             # Errors handling
444             $list = $api->command('/random/command');
445             if (my $err = $api->error) {
446             die "Error: $err, category: " . $list->[0]{category};
447             }
448              
449             Executes a command on a remote host and returns L with hashrefs
450             containing elements returned by a host. You can append a callback for non-blocking
451             calls.
452              
453             In a case of error it may return extra attributes to C or C API
454             replies in addition to error messages in an L attribute or an C<$err>
455             argument. You should never rely on defines of the result to catch errors.
456              
457             For a query syntax refer to L.
458              
459             =head2 command_p
460              
461             my $promise = $api->command_p('/interface/print');
462              
463             $promise->then(
464             sub {
465             my $res = shift;
466             ...
467             })->catch(sub {
468             my ($err, $attr) = @_;
469             });
470              
471             Same as L, but always performs requests non-blocking and returns a
472             L object instead of accepting a callback. L v7.54+ is
473             required for promises functionality.
474              
475             =head2 subscribe
476              
477             my $tag = $api->subscribe('/ping',
478             {address => '127.0.0.1'} => sub {
479             my ($api, $err, $res) = @_;
480             });
481              
482             Mojo::IOLoop->timer(
483             3 => sub { $api->cancel($tag) }
484             );
485              
486             Subscribe to an output of commands with continuous responses such as C or
487             C. Should be terminated with L.
488              
489             =head1 DEBUGGING
490              
491             You can set the API_MIKROTIK_DEBUG environment variable to get some debug output
492             printed to stderr.
493              
494             Also, you can change connection timeout with the API_MIKROTIK_CONNTIMEOUT variable.
495              
496             =head1 COPYRIGHT AND LICENSE
497              
498             Andre Parker, 2017-2018.
499              
500             This program is free software, you can redistribute it and/or modify it under
501             the terms of the Artistic License version 2.0.
502              
503             =head1 SEE ALSO
504              
505             L, L
506              
507             =cut
508