API Ruby class
Contents
Class
From Aaron's web site:
#!/usr/local/bin/ruby
########################################################################
# FILE: mtik.rb -- A Ruby MikroTik API utility
# VERSION: 2.0.1
#
# Written by Aaron D. Gifford - http://www.aarongifford.com/
# If you use this, I would enjoy hearing from you:
# http://www.aarongifford.com/leaveanote.html
#
# Copyright (c) 2009, InfoWest, Inc.
# All Rights Reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, the above list of authors and contributors, this list of
# conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of the author(s) or copyright holder(s) nor the
# names of any contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S), AUTHOR(S) AND
# CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
# IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), AUTHOR(S), OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# DCONSEQUENTIAL AMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.
########################################################################
class MTikError < RuntimeError
end
class MTikTrapError < MTikError
end
class MTikFatalError < MTikError
end
## An API reply is just an array of response sentences. Each sentence
## is a key/value Hash object. This is a separate class from the
## built-in Array class just to avoid polluting Array method space
## with the find_sentence method.
class MTikReply < Array
def find_sentence(key)
i = 0
while i < self.length
if self[i].key?(key)
return self[i]
end
i += 1
end
return nil
end
end
## An API request object is an array of words, the first of which is the
## command, the rest of which are optional arguments.
## NOTE:
## * All arguments must ALREADY be encoded in the MikroTik API
## "=key=value" (or ".id=value") style.
## * Any ".tag=tagvalue" arguments WILL BE IGNORED! This software
## creates a unique tag for EACH and EVERY request.
## * A request is incomplete until a '!done' reply sentence has
## been received.
class MTikRequest < Array
@@tagspace = 0 ## For keeping all tags unique...
def initialize(await_completion, command, *args, &callback)
super()
@reply = MTikReply.new
@command = command
@await_completion = await_completion
@complete = false
if block_given?
@callback = callback ## Implicit block callback
else
@callback = args.delete_at(-1) ## Expect explicitly passed Proc callback
end
## Add command to the request sentence list:
self.push(command)
## Add all arguments to the request sentence list:
@arglist = Array.new
self.addargs(args)
## Append a unique tag for the request:
@tag = @@tagspace.to_s
@@tagspace += 1
self.push(".tag=#{@tag}")
end
def addargs(*args)
## Add all additional arguments to the request sentence list:
args.flatten.each do |arg|
## Ignore any argument beginning with ".tag="
next if /^\.tag=/.match(arg)
@arglist.push(arg)
self.push(arg)
end
end
def callback(sentence)
@callback.call(self, sentence)
end
## This is an internal-to-class utility method:
## (The Fixnum class really should have a pack method.)
def self.bytepack(num)
s = String.new
if RUBY_VERSIION >= '1.9.0'
s.force_encoding(Encoding::BINARY)
end
x = self < 0 ? -num : num ## Treat as unsigned
while x > 0
s += (x & 0xff).chr
x >>= 8
end
return s
end
## Another internal utility method:
## Convert a byte string to a MikroTik API "word":
def to_tikword(str)
str = str.dup
if RUBY_VERSION >= '1.9.0'
str.force_encoding(Encoding::BINARY)
end
if str.length < 0x80
return str.length.chr + str
elsif str.length < 0x4000
return bytepack(str.length | 0x8000) + str
elsif str.length < 0x200000
return bytepack(str.length | 0xc00000) + str
elsif str.length < 0x10000000
return bytepack(str.length | 0xe0000000) + str
elsif str.length < 0x0100000000
return 0xf0.chr + bytepack(str.length) + str
else
raise RuntimeError.new(
"String is too long to be encoded for " +
"the MikroTik API using a 4-byte length!"
)
end
end
def request
## Encode the request for sending to the device:
return self.map {|w| to_tikword(w)}.join + 0x00.chr
end
def done!
return @complete = true
end
def done?
return @complete
end
attr_reader :command, :arglist, :tag, :await_completion, :reply
end
class MTik
require 'socket'
require 'digest/md5'
## Defaults:
PORT = 8728
USER = 'admin'
PASS = ''
CONN_TIMEOUT = 60
CMD_TIMEOUT = 60
def initialize(args)
@sock = nil
@requests = Hash.new
@host = args[:host]
@port = args[:port] || MTik::PORT
@user = args[:user] || MTik::USER
@pass = args[:pass] || MTik::PASS
@conn_timeout = args[:conn_timeout] || MTik::CONN_TIMEOUT
@cmd_timeout = args[:cmd_timeout] || MTik::CMD_TIMEOUT
@data = ''
@outstanding = 0
## Initiate connection and immediately login to device:
login
end
attr_reader :requests, :host, :port, :user, :pass, :conn_timeout, :cmd_timeout, :outstanding
## Ruby really needs a String#pack -- If it had one,
## "deadbeef".pack("H*") would work instead of this kludge:
def hexpack(str)
return str.length % 2 == 0 ?
[str].pack('H'+str.length.to_s) :
[str[0,1]].pack('h') + [str[1,str.length-1]].pack('H'+(str.length-1).to_s)
end
def login
connect
return unless connected?
get_reply('/login') do |req, sentence|
## Make sure the reply has the info we expect:
if req.reply.length != 1 || req.reply[0].length != 3 || !req.reply[0].key?('ret')
raise MTikError.new("Login failed: unexpected reply to login attempt.")
end
## Grab the challenge from first (only) sentence in the reply:
challenge = hexpack(req.reply[0]['ret'])
## Generate reply MD5 hash and convert binary hash to hex string:
response = Digest::MD5.hexdigest(0.chr + @pass + challenge)
## Send reply login command:
get_reply('/login', '=name=' + @user, '=response=00' + response) do |req, sentence|
if req.reply[0].key?('!trap')
raise MTikError.new("Login failed: " + (req.reply[0].key?('message') ? req.reply[0]['message'] : 'Unknown error.'))
end
unless req.reply.length == 1 && req.reply[0].length == 2 && req.reply[0].key?('!done')
@sock.close
@sock = nil
raise MTikError.new('Login failed: Unknown response to login.')
end
end
end
end
def connect
return unless @sock.nil?
## TODO: Enclose in begin/rescue/end and catch more errors,
## possibly try again a few times... Implement connection
## timeout stuff (and catch timeout errors)
begin
@sock = TCPSocket::new(@host, @port)
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
@sock = nil
raise e ## Re-raise the exception
end
end
## Wait for and read exactly one sentence, regardless of content:
def get_sentence
## TODO: Implement timeouts, detect disconnection, maybe do auto-reconnect
if @sock.nil?
raise MTikError.new("Cannot retrieve reply sentence--not connected.")
end
sentence = Hash.new
oldlen = -1
while true ## read-data loop
if @data.length == oldlen
sleep(1) ## Wait for some more data
else
while true ## word parsing loop
bytes, word = get_tikword(@data)
@data[0, bytes] = ''
if word.nil?
break
end
if word.length == 0
## Received END-OF-SENTENCE
if sentence.length == 0
raise MTikError.new("Received END-OF-SENTENCE from device with no sentence data.")
end
## Debugging or verbose, show the received sentence:
if $DEBUG || $VERBOSE
sentence.each do |k, v|
if v.nil?
STDERR.print ">>> '#{k}' (#{k.length})\n"
else
STDERR.print ">>> '#{k}=#{v}' (#{k.length+v.length+1})\n"
end
end
STDERR.print ">>> END-OF SENTENCE\n\n"
end
if sentence.key?('!fatal')
## Fatal error (or '/quit'):
close ## Assume disconnection
end
## Finished. Return the sentence:
return sentence
else
## Add word to sentence
if m = /^=?([^=]+)=(.*)$/.match(word)
sentence[m[1]] = m[2]
else
sentence[word] = nil
end
end
end ## word parsing loop
end
## Read some more data:
oldlen = @data.length
@data += @sock.recv(8192)
end ## read-data loop
end
## Read one or more reply sentences:
## TODO: Implement timeouts, detect disconnection, maybe do auto-reconnect
def wait_for_reply
## Sanity check:
if @data.length > 0
raise MTikError.new("An unexpected #{@data.length} bytes were found from a previous reply. API utility may be buggy.\n")
end
if @requests.length < 1 || @outstanding < 1
raise MTikError.new("Cannot retrieve reply--No request was made. (#{@outstanding} outstanding of #{@requests.length} requests)")
end
## SENTENCE READING LOOP:
begin
## Fetch a sentence:
sentence = get_sentence
## Check for '!fatal' before checking for a tag--'!fatal'
## is never(???) tagged:
if sentence.key?('!fatal')
## FATAL ERROR has occured! (Or a '/quit' command was issued...)
if @data.length > 0
raise MTikError.new("Sanity check failed on receipt of '!fatal' message: #{@data.length} more bytes remain to be parsed. API utility may be buggy.")
end
quit = false
## Iterate over all incomplete requests:
@requests.each_value do |r|
unless r.done?
r.done!
@outstanding -= 1
if r.await_completion
## Pass partial reply to callback along with '!fatal' sentence
r.callback(sentence)
end
## Was this a '/quit' command?
if r.command == '/quit'
quit = true
end
end
end
## SANITY CHECK:
if @outstanding != 0
raise MTikError.new("Logic error: sanity check failed with #{@outstanding} outstanding (unfinished) commands awaiting completion.")
end
## Raise fatal error if there wasn't a '/quit' command:
unless quit
raise MTikFatalError.new(sentence.key?('message') ? sentence['message'] : '')
end
## On /quit, just return:
return
end
## We expect ALL sentences thus far to be tagged:
unless sentence.key?('.tag')
## This code tags EVERY request, so NO RESPONSE should be untagged
## except maybe a '!fatal' error...
raise MTikError.new("Unexected untagged response received.")
end
rtag = sentence['.tag']
## Find which request this reply sentence belongs to:
unless @requests.key?(rtag)
raise MTikError.new("Unknown tag '#{rtag}' found in response.")
end
request = @requests[rtag]
## Sanity check: No sentences should arrive for completed requests.
if request.done?
raise MTikError.new("Unexpected new reply sentence received for already-completed request.")
end
## Add the sentence to the request's reply:
request.reply.push(sentence)
## On '!done', flag the request response as complete:
if sentence.key?('!done')
request.done!
@outstanding -= 1
## Pass the data to the callback:
request.callback(sentence)
else
unless request.await_completion && !request.done?
## Pass the data to the callback:
request.callback(sentence)
end
end
## Keep reading sentences as long as there is data to be parsed:
end while @data.length > 0
end
def send_request(await_completion, command, *args, &callback)
args.flatten!
req = nil
if await_completion.is_a?(MTikRequest)
req = await_completion
req.addargs(command)
req.addargs(args)
if block_given?
raise MTikError.new("You cannot call the send_request method with a callback block if you pass an existing MTikRequest object.")
end
else
unless block_given?
callback = args.pop
end
req = MTikRequest.new(await_completion, command, args, callback)
end
@requests[req.tag] = req
@outstanding += 1
if $DEBUG || $VERBOSE
STDERR.print "<<< '#{req.command}' (#{req.command.length})\n" if $DEBUG || $VERBOSE
req.arglist.each do |x|
STDERR.print "<<< '#{x}' (#{x.length})\n"
end
end
STDERR.print "<<< END-OF-SENTENCE\n\n" if $DEBUG || $VERBOSE
@sock.send(req.request, 0)
return req
end
## WARNING: Only use get_reply when all outstanding
## commands will complete, otherwise get_reply will
## hang in the wait_for_reply loop.
def get_reply(command, *args, &callback)
args.flatten!
unless block_given?
callback = args.pop
end
send_request(true, command, args, callback)
while @outstanding > 0
wait_for_reply
end
end
def close
return if @sock.nil?
@sock.close
@sock = nil
end
def connected?
return @sock.nil? ? false : true
end
## Because of differences in the Ruby 1.8.x vs 1.9.x 'String' class,
## add a 'cbyte' utility method that returns the character byte at the
## specified offset of the string that will work with either version
## (treating all strings as 8-bit binary encoded data in Ruby >= 1.9)
if RUBY_VERSION >= '1.9.0'
## Ruby 1.9 supports multi-byte characters, so we
## force the string to be treated as 8-bit binary
## in order to extract just the byte we want. Also
## String#[] in 1.9 returns the character, NOT the
## integer byte value as 1.8 did, hence the #ord
## method call:
def cbyte(str, offset)
return str.encode(Encoding::BINARY)[offset].ord
end
else
## Ruby 1.8 doesn't support multi-byte characters,
## and String#[] works to return the byte of the
## specified character:
def cbyte(str, offset)
return str[offset]
end
end
## Parse binary string data and return the first 'Tik "word"
## found:
def get_tikword(data)
unless data.is_a?(String)
raise ArgumentError.new("bad argument: expected String but got #{data.class}")
end
## Be sure we're working in 8-bit binary (Ruby 1.9+):
if RUBY_VERSION >= '1.9.0'
data.force_encoding(Encoding::BINARY)
end
unless data.length > 0
return 0, nil ## Not enough data to parse
end
## The first byte tells us how the word length is encoded:
len = 0
len_byte = cbyte(data, 0)
if len_byte & 0x80 == 0
len = len_byte & 0x7f
i = 1
elsif len_byte & 0x40 == 0
unless data.length > 0x81
return 0, nil ## Not enough data to parse
end
len = ((len_byte & 0x3f) << 8) | cbyte(data, 1)
i = 2
elsif len_byte & 0x20 == 0
unless data.length > 0x4002
return 0, nil ## Not enough data to parse
end
len = ((len_byte & 0x1f) << 16) | (cbyte(data, 1) << 8) | cbyte(data, 2)
i = 3
elsif len_byte & 0x10 == 0
unless data.length > 0x200003
return 0, nil ## Not enough data to parse
end
len = ((len_byte & 0x0f) << 24) | (cbyte(data, 1) << 16) | (cbyte(data, 2) << 8) | cbyte(data, 3)
i = 4
elsif len_byte == 0xf0
len = (cbyte(data, 1) << 24) | (cbyte(data, 2) << 16) | (cbyte(data, 3) << 8) | cbyte(data, 4)
i = 5
else
## This will also catch reserved control words where the first byte is >= 0xf8
raise ArgumentError.new("bad argument: String length encoding is invalid")
end
if data.length - i < len
return 0, nil ## Not enough data to parse
end
return i + len, data[i, len]
end
## An all-in-one class function to instantiate, connect, send one or
## more commands, retrieve the response(s), close the connection,
## and return the response(s):
def self.command(args)
tk = MTik.new(
:host => args[:host],
:user => args[:user],
:pass => args[:pass],
:port => args[:port],
:conn_timeout => args[:conn_timeout],
:cmd_timeout => args[:cmd_timeout]
)
cmd = args[:command]
replies = Array.new
if cmd.is_a?(String)
## Single command, no arguments
cmd = [ cmd ]
end
if cmd.is_a?(Array)
## Either a single command with arguments
## or multiple commands:
if cmd[0].is_a?(Array)
## Array of arrays means multiple commands:
cmd.each do |c|
tk.send_request(true, c[0], c[1,c.length-1]) do |req, sentence|
replies.push(req.reply)
end
end
else
## Single command
tk.send_request(true, cmd[0], cmd[1,cmd.length-1]) do |req, sentence|
replies.push(req.reply)
end
end
else
raise ArgumentError.new("invalid command argument")
end
while tk.outstanding > 0
tk.wait_for_reply
end
tk.send_request(true, '/quit') {}
tk.wait_for_reply
tk.close
return replies
end
## Act as an interactive client:
def self.interactive_client(*argv)
argv.flatten!
old_verbose = $VERBOSE
$VERBOSE = 1
begin
tk = MTik.new(:host => argv[0], :user => argv[1], :pass => argv[2])
rescue MTikError, Errno::ECONNREFUSED => e
print "=== LOGIN ERROR: #{e.message}\n"
exit
end
while true
print "\nCommand (/quit to end): "
cmd = STDIN.gets.sub(/^\s+/, '').sub(/\s*[\r\n]*$/, '')
maxreply = 0
m = /^(\d+):/.match(cmd)
unless m.nil?
maxreply = m[1].to_i
cmd.sub!(/^\d+:/, '')
end
next if cmd == ''
break if cmd == '/quit'
unless /^\/[a-zA-Z0-9]+/.match(cmd)
print "=== INVALID COMMAND: #{cmd}\n" if $DEBUG || $VERBOSE
break
end
print "=== COMMAND: #{cmd}\n" if $DEBUG || $VERBOSE
args = cmd.split(/\s+/)
cmd = args.shift
trap = false
count = 0
state = 0
begin
tk.send_request(false, cmd, args) do |req, sentence|
if sentence.key?('!trap')
trap = sentence
print "=== TRAP: '" + (trap.key?('message') ? trap['message'] : "UNKNOWN") + "'\n\n"
elsif sentence.key?('!re')
count += 1
if maxreply > 0 && count == maxreply
state = 2
tk.send_request(true, '/cancel', '=tag=' + req.tag) do |req, sentence|
state = 1
end
end
elsif !sentence.key?('!done') && !sentence.key?('!fatal')
raise MTikError.new("Unknown or unexpected reply sentence type.")
end
if state == 0 && req.done?
state = 1
end
end
while state != 1
tk.wait_for_reply
end
rescue MTikError => e
print "=== ERROR: #{e.message}\n"
end
unless tk.connected?
begin
tk.login
rescue MTikError => e
print "=== LOGIN ERROR: #{e.message}\n"
tk.close
exit
end
end
end
tk.get_reply('/quit') do |req, sentence|
print "=== SESSION TERMINATED: '" + (
sentence.keys[1].nil? ?
'Reason unknown.' :
sentence.keys[1] + (
sentence[sentence.keys[1]].nil? ? '' :
sentence[sentence.keys[1]]
)
) + "'\n\n"
end
unless tk.connected?
print "=== Disconnected ===\n\n"
end
tk.close
$VERBOSE = old_verbose
end
end
#### Interactive client example:
if false
MTik::interactive_client(ARGV)
end
#### One-shot command example:
if false
$VERBOSE=1 ## Set how you want
MTik::command(
:host=>ARGV[0],
:user=>ARGV[1],
:pass=>ARGV[2],
:command=>ARGV[3, ARGV.length-1]
)
end
#### One-shot command example, with JSON-style response output:
if false
## Quick-n-dirty JSON-ifyer for this example:
def json_str(str)
newstr = "'"
str.each_byte do |byte|
if byte == 92 ## back-slash
newstr += '\\\\'
elsif byte == 34 ## double-quote
newstr += '\\"'
elseif byte == 39 ## apostrophe/single-quote
newstr += '\\' + "'"
elseif byte == 47 ## forward-slash
newstr += '\\/'
elsif byte == 8 ## backspace
newstr += '\\b'
elsif byte == 12 ## formfeed
newstr += '\\f'
elsif byte == 10 ## linefeed
newstr += '\\n'
elsif byte == 13 ## carriage return
newstr += '\\r'
elsif byte == 9 ## horizontal tab
newstr += '\\t'
elsif byte == 38 ## ampersand
newstr += sprintf('\\u%04X', byte)
elsif byte == 60 ## less-than
newstr += sprintf('\\u%04X', byte)
elsif byte == 62 ## greater-than
newstr += sprintf('\\u%04X', byte)
elsif byte >= 32 && byte <= 126
newstr += byte.chr
else
newstr += sprintf('\\u%04X', byte)
end
end
return newstr + "'"
end
## Quick-n-dirty JSON-ifier for responses:
def json_reply(response)
return '[' +
response.map do |sentence|
'{' +
sentence.map do |key, value|
json_str(key) + ': ' +
if value.nil?
'NULL'
elsif /^-?(?:\d+(\.\d+)?|\d*\.\d+)$/.match(value)
value
else
json_str(value)
end
end.join(',') +
'}'
end.join(',') +
']'
end
print json_reply(
MTik::command(
:host=>ARGV[0],
:user=>ARGV[1],
:pass=>ARGV[2],
:command=>ARGV[3, ARGV.length-1]
)[0]
) + "\n"
end
Examples
The above code includes several example ways to use the Ruby API, including an interactive command-line client. Just change the "if false" line above the section in question to enable any of the three examples.
Here is a sample run of the interactive client (with the appropriate if false changed to if true to enable it):
user@bsdhost:~$ ./mtik.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:~$
Okay, that run was deliberately with the wrong password. Here's the login with the correct password:
user@bsdhost:~$ ./mtik.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 argument ?name=ether1 properly prefixed the query parameter name with the question mark ? and also paired it via = with the query value?
By now, 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: 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):
TO COME: Some non-interactive examples of using the Ruby API.
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.
- Timeouts and auto-reconnecting currently is NOT implemented.
- The above code is single-threaded, but is probably be safe to use within a multi-threaded Ruby application (untested).
- The 2.0 version uses an event/callback style to send commands and receive responses.
- The 2.0 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.
- Version 2.0.1 is a memory hog if you run for very long as it never frees up the request and reply objects created. A future version will expire completed request/reply sets.