API Ruby class

From MikroTik Wiki
Revision as of 10:19, 24 April 2010 by Astounding (talk | contribs)
Jump to: navigation, search

Ruby GEM

The API Ruby class(es) are now packaged together as a Ruby GEM. The latest GEM is available for download from the author's web site. The current version is 3.1.0 available here:

mtik-3.1.0.gem

RDoc Documentation

The author's site also hosts Ruby RDoc documents for the classes implementing this API. The link is:

http://www.aarongifford.com/computers/mtik/latest/doc/

Examples

Several example scripts are included with the GEM or available for direct download from the author.

  • tikcli.rb - A command-line-like interactive ruby script. You can type MikroTik API commands and arguments directly and have them executed.
  • tikcommand.rb - A non-interactive command-line script to execute a single MikroTik API command and return the results to STDOUT.
  • tikjson.rb - Another non-interactive command-line script that executes a single MikroTik API command and returns the results to STDOUT, however the results are encoded in JSON format. One could easily add CGI handling to this script, install it on a web server, and use it via a web browser. (The author in fact has done something like this for a JavaScript-based management web application that interacts with MikroTik devices via the API.)
  • tikfetch.rb - This non-interactive command-line script lets one instruct a MikroTik device to download one or more files from the provided URL(s).

Here is are several example runs of the tikcli.rb script:

user@bsdhost:~$ ./tikcli.rb 10.20.30.1 admin wrongpassword
<<< '/login' (6)
<<< END-OF-SENTENCE

>>> '!done' (5)
>>> 'ret=bf41fd4286417870c5eb86674a3b8fe4' (36)
>>> '.tag=0' (6)
>>> END-OF SENTENCE

<<< '/login' (6)
<<< '=name=admin' (11)
<<< '=response=0003a042937d84ca4bc4cf7da50aadd507' (44)
<<< END-OF-SENTENCE

>>> '!trap' (5)
>>> 'message=cannot log in' (21)
>>> '.tag=1' (6)
>>> END-OF SENTENCE

>>> '!done' (5)
>>> '.tag=1' (6)
>>> END-OF SENTENCE

=== LOGIN ERROR: Login failed: cannot log in
user@bsdhost:~$ 

That run was deliberately with the wrong password. Here's the login with the correct password:

user@bsdhost:~$ ./tikcli.rb 10.20.30.1 admin correctpassword
<<< '/login' (6)
<<< END-OF-SENTENCE

>>> '!done' (5)
>>> 'ret=857e91c460620a02c3ca72ea7cf6c696' (36)
>>> '.tag=0' (6)
>>> END-OF SENTENCE

<<< '/login' (6)
<<< '=name=admin' (11)
<<< '=response=001a77aec14077ec267c5297969ba1fa24' (44)
<<< END-OF-SENTENCE

>>> '!done' (5)
>>> '.tag=1' (6)
>>> END-OF SENTENCE


Command (/quit to end):

At this point, the interactive client will accept MikroTik API commands in the format /command/name arg1 arg2 arg3 or also 12:/command/name arg1 arg2 arg3 where the 12: is a custom numeric prefix that tells the Ruby interactive client to auto-cancel the command in question after exactly 12 reply sentences are received, since otherwise a command with continuous output would hang the single-threaded interactive client in an endless reply-handling loop (until someone aborted it).

Arguments to API commands in this interactive client must ALREADY be in the API argument form. For example:

Command (/quit to end): /interface/getall ?name=ether1
=== COMMAND: /interface/getall ?name=ether1
<<< '/interface/getall' (17)
<<< '?name=ether1' (12)
<<< END-OF-SENTENCE

>>> '!re' (3)
>>> '.id=*5' (6)
>>> 'name=ether1' (11)
>>> 'type=ether' (10)
>>> 'mtu=1500' (8)
>>> 'l2mtu=1500' (10)
>>> 'bytes=26908361008/15001379552' (29)
>>> 'packets=34880279/26382227' (25)
>>> 'drops=0/0' (9)
>>> 'errors=5/0' (10)
>>> 'dynamic=false' (13)
>>> 'running=true' (12)
>>> 'disabled=false' (14)
>>> 'comment=' (8)
>>> '.tag=4' (6)
>>> END-OF SENTENCE

>>> '!done' (5)
>>> '.tag=4' (6)
>>> END-OF SENTENCE


Command (/quit to end):

Did you see how the user properly prefixed the query parameter name with the query character (question mark ?) and also paired it via = with the query value? With this example CLI, you must manually format all arguments as specified by the MikroTik API.

You may have noticed that this Ruby API implementation automatically adds a unique .tag to every command. That means if you specify a tag value, the Ruby code will ignore it and use its own. It adds a tag so that replies can be correctly matched to the appropriate request.

Now here's the same query again, only add another parameter, =interval=1 so that the command will repeatedly send output each second. To avoid the command continuing forever, it will be prefixed with 6: (this CLI script strips the digit(s) and colon before sending the command to the device) to limit the number of response sentences to exactly six before the interactive client will automagically issue an appropriate /cancel =tag=XYZ command to cancel it.

Command (/quit to end): 6:/interface/getall ?name=ether1=interval=1
=== COMMAND: /interface/getall ?name=ether1=interval=1
<<< '/interface/getall' (17)
<<< '?name=ether1' (12)
<<< '=interval=1' (11)
<<< END-OF-SENTENCE

>>> '!re' (3)
>>> '.id=*5' (6)
>>> 'name=ether1' (11)
>>> 'type=ether' (10)
>>> 'mtu=1524' (8)
>>> 'l2mtu=1524' (10)
>>> 'bytes=26909135851/15002882324' (29)
>>> 'packets=34886461/26387909' (25)
>>> 'drops=0/0' (9)
>>> 'errors=5/0' (10)
>>> 'dynamic=false' (13)
>>> 'running=true' (12)
>>> 'disabled=false' (14)
>>> 'comment=' (8)
>>> '.tag=2' (6)
>>> END-OF SENTENCE

>>> '!re' (3)
>>> '.id=*5' (6)
>>> 'name=ether1' (11)
>>> 'type=ether' (10)
>>> 'mtu=1524' (8)
>>> 'l2mtu=1524' (10)
>>> 'bytes=26909140098/15002892177' (29)
>>> 'packets=34886498/26387943' (25)
>>> 'drops=0/0' (9)
>>> 'errors=5/0' (10)
>>> 'dynamic=false' (13)
>>> 'running=true' (12)
>>> 'disabled=false' (14)
>>> 'comment=' (8)
>>> '.tag=2' (6)
>>> END-OF SENTENCE

>>> '!re' (3)
>>> '.id=*5' (6)
>>> 'name=ether1' (11)
>>> 'type=ether' (10)
>>> 'mtu=1524' (8)
>>> 'l2mtu=1524' (10)
>>> 'bytes=26909141508/15002893670' (29)
>>> 'packets=34886508/26387951' (25)
>>> 'drops=0/0' (9)
>>> 'errors=5/0' (10)
>>> 'dynamic=false' (13)
>>> 'running=true' (12)
>>> 'disabled=false' (14)
>>> 'comment=' (8)
>>> '.tag=2' (6)
>>> END-OF SENTENCE

>>> '!re' (3)
>>> '.id=*5' (6)
>>> 'name=ether1' (11)
>>> 'type=ether' (10)
>>> 'mtu=1524' (8)
>>> 'l2mtu=1524' (10)
>>> 'bytes=26909143624/15002895110' (29)
>>> 'packets=34886524/26387963' (25)
>>> 'drops=0/0' (9)
>>> 'errors=5/0' (10)
>>> 'dynamic=false' (13)
>>> 'running=true' (12)
>>> 'disabled=false' (14)
>>> 'comment=' (8)
>>> '.tag=2' (6)
>>> END-OF SENTENCE

>>> '!re' (3)
>>> '.id=*5' (6)
>>> 'name=ether1' (11)
>>> 'type=ether' (10)
>>> 'mtu=1524' (8)
>>> 'l2mtu=1524' (10)
>>> 'bytes=26909144116/15002895406' (29)
>>> 'packets=34886530/26387967' (25)
>>> 'drops=0/0' (9)
>>> 'errors=5/0' (10)
>>> 'dynamic=false' (13)
>>> 'running=true' (12)
>>> 'disabled=false' (14)
>>> 'comment=' (8)
>>> '.tag=2' (6)
>>> END-OF SENTENCE

>>> '!re' (3)
>>> '.id=*5' (6)
>>> 'name=ether1' (11)
>>> 'type=ether' (10)
>>> 'mtu=1524' (8)
>>> 'l2mtu=1524' (10)
>>> 'bytes=26909144824/15002896659' (29)
>>> 'packets=34886535/26387973' (25)
>>> 'drops=0/0' (9)
>>> 'errors=5/0' (10)
>>> 'dynamic=false' (13)
>>> 'running=true' (12)
>>> 'disabled=false' (14)
>>> 'comment=' (8)
>>> '.tag=2' (6)
>>> END-OF SENTENCE

<<< '/cancel' (7)
<<< '=tag=2' (6)
<<< END-OF-SENTENCE

>>> '!trap' (5)
>>> 'category=2' (10)
>>> 'message=interrupted' (19)
>>> '.tag=2' (6)
>>> END-OF SENTENCE

=== TRAP: 'interrupted'

>>> '!done' (5)
>>> '.tag=3' (6)
>>> END-OF SENTENCE

>>> '!done' (5)
>>> '.tag=2' (6)
>>> END-OF SENTENCE


Command (/quit to end):

Noninteractive Example

Imagine I have a list of RouterOS devices that all need primary and secondary DNS settings updated. Here's an example Ruby script to do this:

#!/usr/bin/env ruby

require 'rubygems'
require 'mtik'

## List of devices (hostnames/IPs) to contact:
devlist = [
  '10.0.0.4',
  '10.0.0.5',
  '10.0.0.22',
  '10.1.44.22',
  '10.1.44.79'
]

## Example assumes all devices use the same API user/pass:
USERNAME = 'admin'
PASSWORD = 'password'

## Set DNS to these IPs (if these were the name servers in question):
PRIMARYDNS   = '10.20.30.2'
SECONDARYDNS = '192.168.44.2'

## Set to 1 to do each device serially, or greater to fork parallel processes
MAXFORK = 1

children = 0
devlist.each do |host|
  Kernel.fork do
    puts "#{host}: Connecting..."
    mt = nil
    begin
      mt = MTik::Connection.new(
        :host=>host,
        :user=>USERNAME,
        :pass=>PASSWORD
      )
    rescue Errno::ETIMEDOUT, Errno::ENETUNREACH, Errno::EHOSTUNREACH => e
      puts "#{host}: Error connecting: #{e}"
      exit
    end

    ## The MTik::Connection#get_reply() method executes a command, then waits
    ## for it to complete (either with a '!done' or '!trap' response) before
    ## executing the callback code block.  The call will block (execution of
    ## this script halts) and wait for the command to finish.  Don't use this
    ## method if you need to handle simultaneous commands to a single device
    ## over a single API connection.  Use an asynchronous calls send_request()
    ## and wait_for_reply().
    mt.get_reply(
      '/ip/dns/set',
      "=primary-dns=#{PRIMARYDNS}",
      "=secondary-dns=#{SECONDARYDNS}"
    ) do |request, sentence|
      trap = request.reply.find_sentence('!trap')
      if trap.nil?
        puts "#{host}: Update command was sent."
      else
        puts "#{host}: An error occurred while setting DNS servers: #{trap['message']}"
      end
    end

    ## Now let's double-check the settings:
    mt.get_reply('/ip/dns/getall') do |request, sentence|
      trap = request.reply.find_sentence('!trap')
      if trap.nil?
        re = request.reply.find_sentence('!re')
        unless re.nil?
          ## Check DNS settings:
          if re['primary-dns'] == PRIMARYDNS && re['secondary-dns'] == SECONDARYDNS
            puts "#{host}: Successfully updated DNS servers."
          else
            puts "#{host}: WARNING: DNS servers DO NOT MATCH: primary-dns=" +
                 "'#{re['primary-dns']}', secondary-dns='#{re['secondary-dns']}'"
          end
        else
          puts "#{host}: WARNING: '/ip/dns/getall' command did work to retrieve DNS settings!"
        end
      else
        puts "#{host}: An error occurred while setting DNS servers: #{trap['message']}"
      end
    end
    mt.close
  end
  children += 1
  while children >= MAXFORK
    Process.wait
    children -= 1
  end
end

while children > 1
  Process.wait
  children -= 1
end

Output might look a bit like:

user@host:~/$ ./dnsupdate.rb 
10.0.0.4: Connecting...
10.0.0.4: Update command was sent.
10.0.0.4: Successfully updated DNS servers.
10.0.0.5: Connecting...
10.0.0.5: Update command was sent.
10.0.0.5: Successfully updated DNS servers.

... MORE OUTPUT ...

10.1.44.79: Successfully updated DNS servers.
user@host:~/$

The benefit of using Mikrotik's API is you can whip up a script to do something, then feed it a bunch of device IPs, login user IDs and passwords from a database, then execute desired commands on ALL of the devices. Check settings, change settings, monitor stats, etc.

Notes

  • This has only been testing using Ruby 1.9.1 and Ruby 1.8.7 on several FreeBSD hosts, though it should work identically on other Ruby installations.
  • Encoding/decoding longer words has NOT be thoroughly tested.
  • Connection timeouts and auto-reconnections are NOT implemented.
  • The above code is single-threaded, but is probably be safe to use within a multi-threaded Ruby application (untested).
  • The 3.0.0 GEM version uses an event/callback style to send commands and receive responses.
  • The 3.0.0 GEM version DOES support multiple simultaneous commands, and supports commands that send back periodic continual responses, but you have to be careful to implement a main event loop for this functionality to work correctly.

See the author's web site in the external links below for a link to the CHANGELOG.

See also

External links