mikrotik 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +9 -0
- data/.yardopts +1 -0
- data/Gemfile +11 -0
- data/Guardfile +8 -0
- data/README.markdown +4 -0
- data/Rakefile +47 -0
- data/examples/bandwidth_monitor.rb +34 -0
- data/examples/dhcp.rb +8 -0
- data/examples/uptime.rb +8 -0
- data/lib/extensions/events.rb +41 -0
- data/lib/extensions/string.rb +27 -0
- data/lib/mikrotik.rb +42 -0
- data/lib/mikrotik/client.rb +124 -0
- data/lib/mikrotik/command.rb +81 -0
- data/lib/mikrotik/connection.rb +84 -0
- data/lib/mikrotik/errors.rb +11 -0
- data/lib/mikrotik/protocol/parser.rb +126 -0
- data/lib/mikrotik/protocol/sentence.rb +70 -0
- data/lib/mikrotik/utilities.rb +22 -0
- data/lib/mikrotik/version.rb +3 -0
- data/mikrotik.gemspec +17 -0
- data/spec/helper.rb +1 -0
- data/spec/parser.rb +52 -0
- metadata +70 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--no-private -e ./doc/setup.rb
|
data/Gemfile
ADDED
data/Guardfile
ADDED
data/README.markdown
ADDED
data/Rakefile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'bundler'
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
Bundler.setup(:default, :development)
|
|
6
|
+
rescue Bundler::BundlerError => e
|
|
7
|
+
$stderr.puts e.message
|
|
8
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
|
9
|
+
exit e.status_code
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require 'rubygems'
|
|
13
|
+
require 'rake'
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
require 'jeweler'
|
|
17
|
+
Jeweler::Tasks.new do |gem|
|
|
18
|
+
gem.name = "mikrotik"
|
|
19
|
+
gem.summary = %Q{Mikrotik API protocol client in Ruby}
|
|
20
|
+
gem.description = %Q{An implementation of the Mikrotik API protocol in Ruby/EventMachine}
|
|
21
|
+
gem.email = "iain@ominiom.com"
|
|
22
|
+
gem.homepage = "http://github.com/ominiom/mikrotik"
|
|
23
|
+
gem.authors = ["Iain Wilson"]
|
|
24
|
+
gem.files = FileList["lib/*.rb", "lib/*/*.rb", "lib/*/*/*.rb"]
|
|
25
|
+
end
|
|
26
|
+
Jeweler::GemcutterTasks.new
|
|
27
|
+
rescue LoadError
|
|
28
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
require 'rake/testtask'
|
|
32
|
+
Rake::TestTask.new(:test) do |test|
|
|
33
|
+
test.libs << 'lib' << 'test'
|
|
34
|
+
test.pattern = 'test/**/test_*.rb'
|
|
35
|
+
test.verbose = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
task :default => :test
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
require 'yard'
|
|
42
|
+
YARD::Rake::YardocTask.new
|
|
43
|
+
rescue LoadError
|
|
44
|
+
task :yardoc do
|
|
45
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Add lib to load path
|
|
2
|
+
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
3
|
+
|
|
4
|
+
require 'mikrotik'
|
|
5
|
+
|
|
6
|
+
EventMachine.run do
|
|
7
|
+
client = Mikrotik.connect :host => "192.168.1.254", :port => 8728, :username => "admin", :password => "debUwu8A"
|
|
8
|
+
|
|
9
|
+
puts "Connecting..."
|
|
10
|
+
|
|
11
|
+
client.on_login_success do |client|
|
|
12
|
+
puts "Login successful"
|
|
13
|
+
|
|
14
|
+
# Run a command every 5 seconds
|
|
15
|
+
EM.add_timer(EM::PeriodicTimer.new(5) do
|
|
16
|
+
client.execute(client.command(:interface, :print).returning(:name, :bytes).where(:name => :ether1).on_reply do |reply|
|
|
17
|
+
# Sum up the in and out counts
|
|
18
|
+
bytes = reply.result(:bytes).split('/').map(&:to_i).inject(0) { |i, n| i = i + n }
|
|
19
|
+
# Format nicely
|
|
20
|
+
total = sprintf "%.2f", (bytes / 1024.0 / 1024.0)
|
|
21
|
+
# Output
|
|
22
|
+
puts "Interface '#{reply.result :name}' has transferred #{total} MB"
|
|
23
|
+
end)
|
|
24
|
+
end)
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
trap 'SIGINT' do
|
|
29
|
+
client.disconnect
|
|
30
|
+
puts "Disconnected from #{client.options[:host]}"
|
|
31
|
+
exit
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
data/examples/dhcp.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
client.execute(client.command(:ip, :dhcp_server, :lease, :getall).on_done do |replies|
|
|
2
|
+
puts replies.collect { |reply| "IP '#{reply.result :address}' assigned to MAC #{reply.result('mac-address')}" }
|
|
3
|
+
end)
|
|
4
|
+
|
|
5
|
+
client.on_pending_complete do |client|
|
|
6
|
+
puts "All commands completed, closing session..."
|
|
7
|
+
client.disconnect
|
|
8
|
+
end
|
data/examples/uptime.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
client.execute(client.command(:system, :resource, :print).returning(:uptime).on_reply do |reply|
|
|
2
|
+
puts "System uptime is #{reply.result :uptime}"
|
|
3
|
+
end)
|
|
4
|
+
|
|
5
|
+
client.on_pending_complete do |client|
|
|
6
|
+
puts "All commands completed, closing session..."
|
|
7
|
+
client.disconnect
|
|
8
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @private
|
|
2
|
+
module Events
|
|
3
|
+
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.extend(ClassMethods)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# @return [Boolean] If the event has a handler registered
|
|
9
|
+
def has_event_handler?(event)
|
|
10
|
+
@events ||= {}
|
|
11
|
+
!@events["on_#{event}".to_sym].nil?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
|
|
16
|
+
# @param [Array<String, Symbol>] events Array of event names
|
|
17
|
+
# @example
|
|
18
|
+
# has_events :delete, :create
|
|
19
|
+
def has_events(*events)
|
|
20
|
+
events.map { |event| event.to_s }.each do |event|
|
|
21
|
+
self.class_eval <<-"END"
|
|
22
|
+
def on_#{event}(*args, &block)
|
|
23
|
+
@events ||= {}
|
|
24
|
+
if block_given? then
|
|
25
|
+
@events[:on_#{event}] = block.to_proc
|
|
26
|
+
else
|
|
27
|
+
@events[:on_#{event}].call(*args) unless @events[:on_#{event}].nil?
|
|
28
|
+
end
|
|
29
|
+
return self
|
|
30
|
+
end
|
|
31
|
+
END
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def has_event(event)
|
|
36
|
+
has_events(*[event])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @private
|
|
2
|
+
class String
|
|
3
|
+
|
|
4
|
+
def to_mikrotik_word
|
|
5
|
+
str = self.dup
|
|
6
|
+
if RUBY_VERSION >= '1.9.0'
|
|
7
|
+
str.force_encoding(Encoding::BINARY)
|
|
8
|
+
end
|
|
9
|
+
if str.length < 0x80
|
|
10
|
+
return str.length.chr + str
|
|
11
|
+
elsif str.length < 0x4000
|
|
12
|
+
return Microtik::Utilities.bytepack(str.length | 0x8000, 2) + str
|
|
13
|
+
elsif str.length < 0x200000
|
|
14
|
+
return Microtik::Utilities.bytepack(str.length | 0xc00000, 3) + str
|
|
15
|
+
elsif str.length < 0x10000000
|
|
16
|
+
return Microtik::Utilities.bytepack(str.length | 0xe0000000, 4) + str
|
|
17
|
+
elsif str.length < 0x0100000000
|
|
18
|
+
return 0xf0.chr + Microtik::Utilities.bytepack(str.length, 5) + str
|
|
19
|
+
else
|
|
20
|
+
raise RuntimeError.new(
|
|
21
|
+
"String is too long to be encoded for " +
|
|
22
|
+
"the MikroTik API using a 4-byte length!"
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
data/lib/mikrotik.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'eventmachine'
|
|
2
|
+
require 'digest/md5'
|
|
3
|
+
|
|
4
|
+
# TODO : Allow underscores to be used instead of dashes in command paths and properties - e.g. in dhcp-server and mac-address
|
|
5
|
+
|
|
6
|
+
module Mikrotik
|
|
7
|
+
|
|
8
|
+
module Protocol; end;
|
|
9
|
+
|
|
10
|
+
# @return [Boolean] Whether debugging prints are enabled or not
|
|
11
|
+
def self.debugging
|
|
12
|
+
false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Prints message if debugging is enabled
|
|
16
|
+
# @param [Array<String>] message Parts of the message
|
|
17
|
+
def self.debug(message)
|
|
18
|
+
puts message.join(' ') if debugging
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Shorthand for Mikrotik::Client.connect
|
|
22
|
+
# @return [Mikrotik::Client]
|
|
23
|
+
def self.connect(*args)
|
|
24
|
+
Client.connect(*args)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Add ourselves to the load path
|
|
30
|
+
$: << File.dirname(__FILE__)
|
|
31
|
+
|
|
32
|
+
# Load up the code
|
|
33
|
+
require 'extensions/string'
|
|
34
|
+
require 'extensions/events'
|
|
35
|
+
require 'mikrotik/version'
|
|
36
|
+
require 'mikrotik/errors'
|
|
37
|
+
require 'mikrotik/client'
|
|
38
|
+
require 'mikrotik/connection'
|
|
39
|
+
require 'mikrotik/command'
|
|
40
|
+
require 'mikrotik/utilities'
|
|
41
|
+
require 'mikrotik/protocol/parser'
|
|
42
|
+
require 'mikrotik/protocol/sentence'
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Contains all methods for interacting with a device
|
|
2
|
+
# such as logging in and executing commands
|
|
3
|
+
#
|
|
4
|
+
# @see Mikrotik::Client.connect
|
|
5
|
+
module Mikrotik::Client
|
|
6
|
+
include Events
|
|
7
|
+
|
|
8
|
+
# Fired when the API login command has returned successfully.
|
|
9
|
+
# After this event has fired it is safe to start sending commands
|
|
10
|
+
has_event :login_success
|
|
11
|
+
|
|
12
|
+
# Fired when the API login command returns an error -
|
|
13
|
+
# for example in cases where the credentials were wrong
|
|
14
|
+
has_event :login_failure
|
|
15
|
+
|
|
16
|
+
# Fired when the queue of commands awaiting completion
|
|
17
|
+
# becomes empty
|
|
18
|
+
has_event :pending_complete
|
|
19
|
+
|
|
20
|
+
# @return [Hash] Options used for the connection
|
|
21
|
+
# @see Client#connect
|
|
22
|
+
attr_reader :options
|
|
23
|
+
|
|
24
|
+
# Connects to a RouterOS device. Required options are +:host+
|
|
25
|
+
# and +:password+ - +:username+ and +:port+ default
|
|
26
|
+
# to +admin+ and +8728+ respectively
|
|
27
|
+
#
|
|
28
|
+
# @param [Hash] options +:test+ Hash of connection options
|
|
29
|
+
# @option options [String] :host Device's IP address
|
|
30
|
+
# @option options [Integer] :port (8728) Port of the API service on the device
|
|
31
|
+
# @option options [String] :username ('admin') API username to authenticate with
|
|
32
|
+
# @option options [String] :password Password for the given username
|
|
33
|
+
# @return [Client]
|
|
34
|
+
# @raise [ArgumentError] If not all required options are given
|
|
35
|
+
# @example Connection with minimum options set
|
|
36
|
+
# Mikrotik::Client.connect({
|
|
37
|
+
# :host => '192.168.1.1',
|
|
38
|
+
# :password => 'topsecret'
|
|
39
|
+
# })
|
|
40
|
+
# @example Connection with all options set to override defaults
|
|
41
|
+
# Mikrotik::Client.connect({
|
|
42
|
+
# :host => '10.200.0.21',
|
|
43
|
+
# :port => 3450,
|
|
44
|
+
# :username => 'apiuser',
|
|
45
|
+
# :password => 'topsecret'
|
|
46
|
+
# })
|
|
47
|
+
#
|
|
48
|
+
def self.connect(options = { :username => 'admin', :port => 8728 })
|
|
49
|
+
defaults = { :username => 'admin', :port => 8728 }
|
|
50
|
+
options = defaults.merge(options)
|
|
51
|
+
# Check required options have been given
|
|
52
|
+
all_required_options = !([:host, :port, :username, :password].map { |option|
|
|
53
|
+
options.key?(option) && !options[option].nil?
|
|
54
|
+
}.include?(false))
|
|
55
|
+
raise ArgumentError, "Options for host and password must be given" unless all_required_options
|
|
56
|
+
|
|
57
|
+
c = EM.connect options[:host], options[:port], self, options
|
|
58
|
+
c.pending_connect_timeout = 10.0
|
|
59
|
+
c
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Logs in to the device. This is called automatically when
|
|
63
|
+
# the TCP connection succeeds. To be sure the device is ready
|
|
64
|
+
# to +execute+ commands before sending any register an event handler
|
|
65
|
+
# on +on_login_success+ and send commands from there
|
|
66
|
+
def login
|
|
67
|
+
execute command(:login).on_reply { |reply|
|
|
68
|
+
|
|
69
|
+
challenge = Mikrotik::Utilities.hex_to_bin(reply.result :ret)
|
|
70
|
+
response = "00" + Digest::MD5.hexdigest(0.chr + @options[:password] + challenge)
|
|
71
|
+
|
|
72
|
+
execute command(:login).with(
|
|
73
|
+
:name => @options[:username],
|
|
74
|
+
:response => response
|
|
75
|
+
).on_done { |replies|
|
|
76
|
+
Mikrotik.debug [:client, :on_login_success]
|
|
77
|
+
@logged_in = true
|
|
78
|
+
on_login_success(self)
|
|
79
|
+
}.on_trap { |trap|
|
|
80
|
+
Mikrotik.debug [:client, :on_login_failure]
|
|
81
|
+
if has_event_handler? :on_login_failure then
|
|
82
|
+
on_login_failure(self)
|
|
83
|
+
else
|
|
84
|
+
raise Mikrotik::Errors::UnhandledTrap, 'Login failed'
|
|
85
|
+
end
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Sends a command to the device
|
|
92
|
+
# @param command [Mikrotik::Command] The command to be executed
|
|
93
|
+
# @return [Mikrotik::Command] The command executed
|
|
94
|
+
def execute(command)
|
|
95
|
+
send command
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @return [Boolean] Status of the session login
|
|
99
|
+
def logged_in?
|
|
100
|
+
@logged_in
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Shortcut for creating a Mikrotik::Command
|
|
104
|
+
# @return Mikrotik::Command
|
|
105
|
+
# @see Mikrotik::Command
|
|
106
|
+
def command(*path)
|
|
107
|
+
Mikrotik::Command.new(*path)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inspect
|
|
111
|
+
"#<#{self.class.name}:0x#{self.object_id} host=#{@options[:host]}>"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def initialize(opts = {})
|
|
115
|
+
@closing = false
|
|
116
|
+
@logged_in = false
|
|
117
|
+
@options = opts
|
|
118
|
+
@events = {}
|
|
119
|
+
@buffer = ""
|
|
120
|
+
@next_tag = 1
|
|
121
|
+
extend Mikrotik::Connection
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Class encapsulating a API command to be executed on the device
|
|
2
|
+
class Mikrotik::Command
|
|
3
|
+
include Events
|
|
4
|
+
|
|
5
|
+
# Fired when the command completes
|
|
6
|
+
# @yield [replies]
|
|
7
|
+
# @yieldparam [false, Array<Mikrotik::Protocol::Sentence>] replies All replies received from the device for this command
|
|
8
|
+
has_events :done
|
|
9
|
+
|
|
10
|
+
# Fired on each reply received from the command
|
|
11
|
+
# @yield [reply]
|
|
12
|
+
# @yieldparam [false, Mikrotik::Protocol::Sentence] reply The sentence received in reply
|
|
13
|
+
has_event :reply
|
|
14
|
+
|
|
15
|
+
# Fired when an error response is received from the command
|
|
16
|
+
# @yield [error_message]
|
|
17
|
+
# @yieldparam [false, String] error_message Error description
|
|
18
|
+
has_event :trap
|
|
19
|
+
|
|
20
|
+
# @return [Array<Mikrotik::Protocol::Sentence>] All sentences sent in response to this command so far
|
|
21
|
+
attr_reader :replies
|
|
22
|
+
|
|
23
|
+
# @return [Hash] All option name/value pairs set on the command
|
|
24
|
+
attr_reader :options
|
|
25
|
+
|
|
26
|
+
# @return [String] Path to the command
|
|
27
|
+
attr_reader :command
|
|
28
|
+
|
|
29
|
+
# Creates a new command object
|
|
30
|
+
# @param [Array<String, Symbol>] path Path of the API command
|
|
31
|
+
def initialize(*command_path)
|
|
32
|
+
@command = command_path.collect { |part| "#{part}".gsub('_', '-') }.join('/')
|
|
33
|
+
@command = "/#{@command}" unless @command.start_with?('/')
|
|
34
|
+
@options = {}
|
|
35
|
+
@replies = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Adds property names and values to the command
|
|
39
|
+
# @param [Hash] options
|
|
40
|
+
def with(options = {})
|
|
41
|
+
options.each_pair do |option, value|
|
|
42
|
+
@options["=#{option}"] = value
|
|
43
|
+
end
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Adds querying conditions to the command
|
|
48
|
+
# @param [Hash] conditions
|
|
49
|
+
def where(conditions = {})
|
|
50
|
+
conditions.each_pair do |property, value|
|
|
51
|
+
@options["?#{property}"] = value
|
|
52
|
+
end
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Specifies what properties should be returned by the device in response to this command
|
|
57
|
+
#
|
|
58
|
+
# @param [Array<String, Symbol>] properties List of the names of properties to request in response
|
|
59
|
+
# @return [self]
|
|
60
|
+
def returning(*properties)
|
|
61
|
+
@options['=.proplist'] ||= []
|
|
62
|
+
properties.each do |property|
|
|
63
|
+
@options['=.proplist'] << "#{property}"
|
|
64
|
+
end
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Encodes the command for transmission
|
|
69
|
+
# @return [String] Command encoded as an API sentence in binary string format
|
|
70
|
+
def encoded
|
|
71
|
+
@command.to_mikrotik_word + @options.collect { |key, value|
|
|
72
|
+
case value.class.name
|
|
73
|
+
when 'Array'
|
|
74
|
+
[key, value.collect { |item| "#{item}" }.join(',')].join '='
|
|
75
|
+
else
|
|
76
|
+
"#{key}=#{value}"
|
|
77
|
+
end
|
|
78
|
+
}.map { |option| option.to_mikrotik_word }.join + 0x00.chr
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Contains logic for sending and receiving data to and from the device
|
|
2
|
+
module Mikrotik::Connection
|
|
3
|
+
|
|
4
|
+
# @private
|
|
5
|
+
def connection_completed
|
|
6
|
+
@commands = {}
|
|
7
|
+
@sentences = []
|
|
8
|
+
@parser = Mikrotik::Protocol::Parser.new
|
|
9
|
+
@parser.on_sentence do |sentence|
|
|
10
|
+
handle_reply(sentence)
|
|
11
|
+
end
|
|
12
|
+
login
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Disconnects from the device
|
|
16
|
+
def disconnect
|
|
17
|
+
Mikrotik.debug [:connection, :disconnect]
|
|
18
|
+
@closing = true
|
|
19
|
+
close_connection(true)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @private
|
|
23
|
+
def send(command)
|
|
24
|
+
command.options['.tag'] = @next_tag
|
|
25
|
+
Mikrotik.debug [:connection, :send, command.encoded]
|
|
26
|
+
@commands[@next_tag] = command
|
|
27
|
+
send_data(command.encoded)
|
|
28
|
+
@next_tag = @next_tag + 1
|
|
29
|
+
command
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @private
|
|
33
|
+
def receive_data(data)
|
|
34
|
+
Mikrotik.debug [:connection, :receive, data.size, data]
|
|
35
|
+
@parser << data
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @private
|
|
39
|
+
def unbind
|
|
40
|
+
raise Mikrotik::Errors::ConnectionDropped unless @closing
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# @private
|
|
46
|
+
def handle_reply(reply)
|
|
47
|
+
unless reply.tagged? then
|
|
48
|
+
Mikrotik.debug [:connection, :handle_reply, :untagged]
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
Mikrotik.debug [:connection, :handle_reply, :tagged, reply.tag]
|
|
52
|
+
# Retreive the command this reply is intended for by its tag
|
|
53
|
+
command = @commands[reply.tag]
|
|
54
|
+
command.replies << reply unless reply.completely_done?
|
|
55
|
+
# Catch those nasty errors
|
|
56
|
+
if reply.trap? then
|
|
57
|
+
Mikrotik.debug [:command, :on_trap]
|
|
58
|
+
# If there's an error handler, fire it...
|
|
59
|
+
if command.has_event_handler?(:on_trap) then
|
|
60
|
+
command.on_trap(reply.param(:message))
|
|
61
|
+
else
|
|
62
|
+
# ...if not, raise an error
|
|
63
|
+
raise Mikrotik::Errors::UnhandledTrap, reply.result(:message) || "Unknown error"
|
|
64
|
+
end
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
# Still fire on_reply events for done messages that contain more data
|
|
68
|
+
unless reply.completely_done? then
|
|
69
|
+
Mikrotik.debug [:command, :on_reply]
|
|
70
|
+
command.on_reply(reply)
|
|
71
|
+
end
|
|
72
|
+
# If this was the last reply, fire the on_done event handler
|
|
73
|
+
if reply.done? then
|
|
74
|
+
Mikrotik.debug [:command, :on_done, reply.tag]
|
|
75
|
+
command.on_done(command.replies)
|
|
76
|
+
# As the command has completed remove it from the list of active commands
|
|
77
|
+
@commands.delete(reply.tag)
|
|
78
|
+
Mikrotik.debug [:client, :commands, :size, @commands.size]
|
|
79
|
+
end
|
|
80
|
+
# If there are no more active commands fire the event
|
|
81
|
+
on_pending_complete(self) if @commands.empty?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Mikrotik::Errors
|
|
2
|
+
|
|
3
|
+
# Raised when an error response is received to a
|
|
4
|
+
# command with no on_trap event handler
|
|
5
|
+
class UnhandledTrap < RuntimeError; end;
|
|
6
|
+
|
|
7
|
+
# Raised when the connection to the device
|
|
8
|
+
# disconnects unexpectedly
|
|
9
|
+
class ConnectionDropped < RuntimeError; end;
|
|
10
|
+
|
|
11
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Responsible for extracting replies from API sentence data
|
|
2
|
+
class Mikrotik::Protocol::Parser
|
|
3
|
+
include Events
|
|
4
|
+
|
|
5
|
+
# Fired when the parser extracts a complete API sentence
|
|
6
|
+
# @yield [Mikrotik::Protocol::Sentence] sentence
|
|
7
|
+
has_event :sentence
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@data = ''
|
|
11
|
+
@data.force_encoding(Encoding::BINARY) if RUBY_VERSION > '1.9'
|
|
12
|
+
reset!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Discards the sentence currently being parsed
|
|
16
|
+
def reset!
|
|
17
|
+
@sentence = Mikrotik::Protocol::Sentence.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Adds data to the parser buffer and runs the parser
|
|
21
|
+
def <<(data)
|
|
22
|
+
@data << data
|
|
23
|
+
success = true
|
|
24
|
+
success = get_sentence while success
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
PAIR = /^(.+)=(.+)$/
|
|
28
|
+
|
|
29
|
+
# Indentifies the size of the length field in bits
|
|
30
|
+
# @return boolean, integer
|
|
31
|
+
def get_length_size
|
|
32
|
+
return false if @data.empty?
|
|
33
|
+
|
|
34
|
+
# High bits used for length type
|
|
35
|
+
# Low bits used as first bits of length
|
|
36
|
+
|
|
37
|
+
# 0xxxxxxx = 7 bit length
|
|
38
|
+
# 10xxxxxx = 14 bit length
|
|
39
|
+
# 110xxxxx = 21 bit length
|
|
40
|
+
# 1110xxxx = 28 bit length
|
|
41
|
+
# 11110000 = 32 bit length follows
|
|
42
|
+
|
|
43
|
+
t = @data.unpack('C').first
|
|
44
|
+
|
|
45
|
+
return 7 if t & 0b10000000 == 0
|
|
46
|
+
return 14 if t & 0b01000000 == 0
|
|
47
|
+
return 21 if t & 0b00100000 == 0
|
|
48
|
+
return 28 if t & 0b00010000 == 0
|
|
49
|
+
return 32 if t == 0b11110000
|
|
50
|
+
|
|
51
|
+
raise ArgumentError, "Invalid length type encoding"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def get_length
|
|
55
|
+
size = get_length_size
|
|
56
|
+
|
|
57
|
+
return false unless size
|
|
58
|
+
|
|
59
|
+
need = bytes(size + 1)
|
|
60
|
+
return false unless @data.size >= need
|
|
61
|
+
|
|
62
|
+
length = nil
|
|
63
|
+
field = @data[0, need].unpack('C*')
|
|
64
|
+
|
|
65
|
+
case size
|
|
66
|
+
when 7
|
|
67
|
+
length = field[0] & 0x7f
|
|
68
|
+
when 14
|
|
69
|
+
length = ((field[0] & 0x3f) << 8) | field[1]
|
|
70
|
+
when 21
|
|
71
|
+
length = ((field[0] & 0x1f) << 16) | field[1] << 8 | field[2]
|
|
72
|
+
when 28
|
|
73
|
+
length = ((field[0] & 0x0f) << 24) | field[1] << 16 | field[2] << 8 | field[3]
|
|
74
|
+
when 32
|
|
75
|
+
length = field[1..4].pack('C4').unpack('N').first
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if length
|
|
79
|
+
return length
|
|
80
|
+
else
|
|
81
|
+
raise ArgumentError, "Invalid word length"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def get_word
|
|
86
|
+
return false if @data.empty?
|
|
87
|
+
length_size = bytes(get_length_size + 1)
|
|
88
|
+
length = get_length
|
|
89
|
+
|
|
90
|
+
total_size = length_size + length
|
|
91
|
+
|
|
92
|
+
if length && @data.size >= total_size
|
|
93
|
+
@data.slice!(0, length_size)
|
|
94
|
+
word = @data.slice!(0, length)
|
|
95
|
+
Mikrotik.debug [:parser, :got_word, word]
|
|
96
|
+
return word
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
return false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def get_sentence
|
|
103
|
+
while word = get_word
|
|
104
|
+
if word.empty?
|
|
105
|
+
Mikrotik.debug [:parser, :got_sentence, @sentence]
|
|
106
|
+
on_sentence(@sentence)
|
|
107
|
+
reset!
|
|
108
|
+
return true
|
|
109
|
+
else
|
|
110
|
+
if word =~ PAIR
|
|
111
|
+
key, value = word.scan(PAIR).flatten
|
|
112
|
+
@sentence[key] = value
|
|
113
|
+
else
|
|
114
|
+
@sentence[word] = nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
return false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def bytes(bits)
|
|
123
|
+
(bits / 8.0).ceil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
class Mikrotik::Protocol::Sentence < Hash
|
|
2
|
+
|
|
3
|
+
# Sentence only contains a zero length word
|
|
4
|
+
# @return [Boolean]
|
|
5
|
+
def empty?
|
|
6
|
+
super || false
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Is this the last reply for a command?
|
|
10
|
+
# @return [Boolean]
|
|
11
|
+
def done?
|
|
12
|
+
key?('!done')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Is this reply only informing that a command is done, and contains no other data?
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def completely_done?
|
|
18
|
+
size == 2 and done? and tagged?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Is this informing of an error that should be trapped?
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
def trap?
|
|
24
|
+
key?('!trap')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Does the reply have a command tag associated with it?
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
def tagged?
|
|
30
|
+
key?('.tag')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Retreives the reply's command tag
|
|
34
|
+
#
|
|
35
|
+
# @return [nil] If the command is not tagged
|
|
36
|
+
# @return [Integer] tag If the command is tagged
|
|
37
|
+
def tag
|
|
38
|
+
tagged? ? self['.tag'] : nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Fetches a returned property
|
|
42
|
+
# @return value
|
|
43
|
+
def result(key)
|
|
44
|
+
self["=#{key}"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Fetches any returned value in this reply, converting it to a ruby type first
|
|
48
|
+
def [](key)
|
|
49
|
+
return nil unless key?(key)
|
|
50
|
+
case key
|
|
51
|
+
when '.tag'
|
|
52
|
+
fetch(key).to_i
|
|
53
|
+
else
|
|
54
|
+
value = fetch(key)
|
|
55
|
+
#case value
|
|
56
|
+
#when /\d+/
|
|
57
|
+
# value.to_i
|
|
58
|
+
#when /\d+.\d+/
|
|
59
|
+
# value.to_f
|
|
60
|
+
#when 'true'
|
|
61
|
+
# true
|
|
62
|
+
#when 'false'
|
|
63
|
+
# false
|
|
64
|
+
#else
|
|
65
|
+
# value
|
|
66
|
+
#end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @private
|
|
2
|
+
class Mikrotik::Utilities
|
|
3
|
+
|
|
4
|
+
def self.hex_to_bin(input)
|
|
5
|
+
padding = input.size.even? ? '' : '0'
|
|
6
|
+
[padding + input].pack('H*')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.bytepack(num, size)
|
|
10
|
+
s = String.new
|
|
11
|
+
s.force_encoding(Encoding::BINARY) if RUBY_VERSION >= '1.9.0'
|
|
12
|
+
x = num < 0 ? -num : num # Treat as unsigned
|
|
13
|
+
while size > 0
|
|
14
|
+
size -= 1
|
|
15
|
+
s = (x & 0xff).chr + s
|
|
16
|
+
x >>= 8
|
|
17
|
+
end
|
|
18
|
+
raise RuntimeError, "Number #{num} is too large to fit in #{size} bytes." if x > 0
|
|
19
|
+
return s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
data/mikrotik.gemspec
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "mikrotik/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "mikrotik"
|
|
7
|
+
s.version = Mikrotik::VERSION
|
|
8
|
+
s.authors = ["Iain Wilson"]
|
|
9
|
+
s.homepage = "http://github.com/ominiom/mikrotik"
|
|
10
|
+
s.summary = %q{A Mikrotik RouterOS API client using EventMachine}
|
|
11
|
+
s.description = %q{Provides an interface to run commands and collect results from a RouterOS device}
|
|
12
|
+
|
|
13
|
+
s.files = `git ls-files`.split("\n")
|
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
16
|
+
s.require_paths = ["lib"]
|
|
17
|
+
end
|
data/spec/helper.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require './lib/mikrotik.rb'
|
data/spec/parser.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'helper'
|
|
2
|
+
|
|
3
|
+
describe Mikrotik::Protocol::Parser do
|
|
4
|
+
|
|
5
|
+
before :each do
|
|
6
|
+
@parser = Mikrotik::Protocol::Parser.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe '#get_length_size' do
|
|
10
|
+
|
|
11
|
+
it "returns 7 for 1 byte lengths" do
|
|
12
|
+
@parser.instance_variable_set(:@data, 0b01111111.chr)
|
|
13
|
+
@parser.send(:get_length_size).should eq(7)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "returns 14 for 2 byte lengths" do
|
|
17
|
+
@parser.instance_variable_set(:@data, 0b10111111.chr)
|
|
18
|
+
@parser.send(:get_length_size).should eq(14)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "returns 21 for 3 byte lengths" do
|
|
22
|
+
@parser.instance_variable_set(:@data, 0b11011111.chr)
|
|
23
|
+
@parser.send(:get_length_size).should eq(21)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "returns 28 for 28 bit lengths" do
|
|
27
|
+
@parser.instance_variable_set(:@data, 0b11101111.chr)
|
|
28
|
+
@parser.send(:get_length_size).should eq(28)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "returns 32 for 32 bit lengths" do
|
|
32
|
+
@parser.instance_variable_set(:@data, 0b11110000.chr)
|
|
33
|
+
@parser.send(:get_length_size).should eq(32)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#get_length' do
|
|
39
|
+
|
|
40
|
+
it "handles 7 bit lengths" do
|
|
41
|
+
@parser.instance_variable_set(:@data, 0b01111101.chr)
|
|
42
|
+
@parser.send(:get_length).should eq(0b1111101)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "handles 14 bit lengths" do
|
|
46
|
+
@parser.instance_variable_set(:@data, 0b10111111.chr + 0b11111111.chr)
|
|
47
|
+
@parser.send(:get_length).should eq(63 * 256 + 255)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mikrotik
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Iain Wilson
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-03-18 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description: Provides an interface to run commands and collect results from a RouterOS
|
|
15
|
+
device
|
|
16
|
+
email:
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- .gitignore
|
|
22
|
+
- .yardopts
|
|
23
|
+
- Gemfile
|
|
24
|
+
- Guardfile
|
|
25
|
+
- README.markdown
|
|
26
|
+
- Rakefile
|
|
27
|
+
- doc/setup.rb
|
|
28
|
+
- examples/bandwidth_monitor.rb
|
|
29
|
+
- examples/dhcp.rb
|
|
30
|
+
- examples/uptime.rb
|
|
31
|
+
- lib/extensions/events.rb
|
|
32
|
+
- lib/extensions/string.rb
|
|
33
|
+
- lib/mikrotik.rb
|
|
34
|
+
- lib/mikrotik/client.rb
|
|
35
|
+
- lib/mikrotik/command.rb
|
|
36
|
+
- lib/mikrotik/connection.rb
|
|
37
|
+
- lib/mikrotik/errors.rb
|
|
38
|
+
- lib/mikrotik/protocol/parser.rb
|
|
39
|
+
- lib/mikrotik/protocol/sentence.rb
|
|
40
|
+
- lib/mikrotik/utilities.rb
|
|
41
|
+
- lib/mikrotik/version.rb
|
|
42
|
+
- mikrotik.gemspec
|
|
43
|
+
- spec/helper.rb
|
|
44
|
+
- spec/parser.rb
|
|
45
|
+
homepage: http://github.com/ominiom/mikrotik
|
|
46
|
+
licenses: []
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
none: false
|
|
53
|
+
requirements:
|
|
54
|
+
- - ! '>='
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '0'
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
none: false
|
|
59
|
+
requirements:
|
|
60
|
+
- - ! '>='
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
requirements: []
|
|
64
|
+
rubyforge_project:
|
|
65
|
+
rubygems_version: 1.8.15
|
|
66
|
+
signing_key:
|
|
67
|
+
specification_version: 3
|
|
68
|
+
summary: A Mikrotik RouterOS API client using EventMachine
|
|
69
|
+
test_files: []
|
|
70
|
+
has_rdoc:
|