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