net-snmp2 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +24 -0
- data/TODO.md +6 -0
- data/bin/mib2rb +129 -0
- data/bin/net-snmp2 +64 -0
- data/examples/agent.rb +104 -0
- data/examples/manager.rb +11 -0
- data/examples/trap_handler.rb +46 -0
- data/examples/v1_trap_session.rb +24 -0
- data/examples/v2_trap_session.rb +21 -0
- data/lib/net-snmp2.rb +85 -0
- data/lib/net/snmp.rb +27 -0
- data/lib/net/snmp/agent/agent.rb +48 -0
- data/lib/net/snmp/agent/provider.rb +51 -0
- data/lib/net/snmp/agent/provider_dsl.rb +124 -0
- data/lib/net/snmp/agent/request_dispatcher.rb +38 -0
- data/lib/net/snmp/constants.rb +287 -0
- data/lib/net/snmp/debug.rb +54 -0
- data/lib/net/snmp/dispatcher.rb +108 -0
- data/lib/net/snmp/error.rb +29 -0
- data/lib/net/snmp/listener.rb +76 -0
- data/lib/net/snmp/message.rb +142 -0
- data/lib/net/snmp/mib/mib.rb +67 -0
- data/lib/net/snmp/mib/module.rb +39 -0
- data/lib/net/snmp/mib/node.rb +122 -0
- data/lib/net/snmp/mib/templates.rb +48 -0
- data/lib/net/snmp/oid.rb +134 -0
- data/lib/net/snmp/pdu.rb +235 -0
- data/lib/net/snmp/repl/manager_repl.rb +243 -0
- data/lib/net/snmp/session.rb +560 -0
- data/lib/net/snmp/trap_handler/trap_handler.rb +42 -0
- data/lib/net/snmp/trap_handler/v1_trap_dsl.rb +44 -0
- data/lib/net/snmp/trap_handler/v2_trap_dsl.rb +38 -0
- data/lib/net/snmp/trap_session.rb +92 -0
- data/lib/net/snmp/utility.rb +10 -0
- data/lib/net/snmp/varbind.rb +57 -0
- data/lib/net/snmp/version.rb +5 -0
- data/lib/net/snmp/wrapper.rb +450 -0
- data/net-snmp2.gemspec +30 -0
- data/spec/README.md +105 -0
- data/spec/async_spec.rb +123 -0
- data/spec/em_spec.rb +23 -0
- data/spec/error_spec.rb +34 -0
- data/spec/fiber_spec.rb +41 -0
- data/spec/mib_spec.rb +68 -0
- data/spec/net-snmp_spec.rb +18 -0
- data/spec/oid_spec.rb +21 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/sync_spec.rb +132 -0
- data/spec/thread_spec.rb +19 -0
- data/spec/trap_spec.rb +45 -0
- data/spec/utility_spec.rb +10 -0
- data/spec/wrapper_spec.rb +69 -0
- metadata +166 -0
@@ -0,0 +1,243 @@
|
|
1
|
+
module Net::SNMP
|
2
|
+
class ManagerRepl
|
3
|
+
include Net::SNMP
|
4
|
+
attr_accessor :sessions, :pdu
|
5
|
+
alias response pdu
|
6
|
+
|
7
|
+
NO_SESSION_PROMPT = <<EOF
|
8
|
+
|
9
|
+
No manager sessions active
|
10
|
+
--------------------------
|
11
|
+
|
12
|
+
- Use `manage(options)` to start a session.
|
13
|
+
- run `? manage` for more details
|
14
|
+
|
15
|
+
EOF
|
16
|
+
|
17
|
+
def self.start(session=nil)
|
18
|
+
pry_context = self.new(session)
|
19
|
+
Pry.config.prompt_name = "snmp"
|
20
|
+
pry_context.pry
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(session=nil)
|
24
|
+
@sessions = []
|
25
|
+
if session
|
26
|
+
@sessions << session
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Adds a session for managing options[:peername] to the sessions list.
|
31
|
+
# - Note that all requests are sent to all active sessions, and their
|
32
|
+
# responses are all displayed under their peername.
|
33
|
+
# - You may close a session by calling `close(peername)`
|
34
|
+
#
|
35
|
+
# Arguments
|
36
|
+
# - options: A Hash or String object
|
37
|
+
# + As a Hash, accepts the following keys
|
38
|
+
# - peername: ADDRESS_OF_AGENT
|
39
|
+
# - port: PORT (defaults to 161)
|
40
|
+
# - version: '1'|'2c'|'3' (defaults to '2c')
|
41
|
+
# + As a string, implies the peername, with the version defaulting to '2c'
|
42
|
+
#
|
43
|
+
# Examples:
|
44
|
+
# manage('192.168.1.5')
|
45
|
+
# manage('192.168.1.5:162')
|
46
|
+
# manage(peername: '192.168.1.5', port: 161, version '2c')
|
47
|
+
# manage(peername: '192.168.1.5:161', version '2c')
|
48
|
+
def manage(options)
|
49
|
+
options = {:peername => options} if options.kind_of?(String)
|
50
|
+
sessions << Session.open(options)
|
51
|
+
"Opened session to manage peer: #{options[:peername]}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Close the session with the given `peername`
|
55
|
+
#
|
56
|
+
# Arguments
|
57
|
+
# - peername: May be a string, matching the peername, or an index.
|
58
|
+
# Run the `peer` command to see indexes & peernames for
|
59
|
+
# all active sessions
|
60
|
+
#
|
61
|
+
# Example:
|
62
|
+
# [7] net-snmp2> peers
|
63
|
+
#
|
64
|
+
# Currently Managing
|
65
|
+
# ------------------
|
66
|
+
#
|
67
|
+
# [0] localhost:161
|
68
|
+
# [1] localhost:162
|
69
|
+
# [2] localhost:163
|
70
|
+
#
|
71
|
+
# => nil
|
72
|
+
# [8] net-snmp2> close 'localhost:163'
|
73
|
+
# => "Closed session for peer: localhost:163"
|
74
|
+
# [9] net-snmp2> close 1
|
75
|
+
# => "Closed session for peer: localhost:162"
|
76
|
+
def close(peername)
|
77
|
+
case peername
|
78
|
+
when Numeric
|
79
|
+
index = peername
|
80
|
+
if index < (sessions.length)
|
81
|
+
session = sessions[index]
|
82
|
+
session.close
|
83
|
+
sessions.delete(session)
|
84
|
+
"Closed session for peer: #{session.peername}"
|
85
|
+
else
|
86
|
+
"Invalid session index #{index}. Use `peers` to list active sessions."
|
87
|
+
end
|
88
|
+
when String
|
89
|
+
session = sessions.find { |sess| sess.peername.to_s == peername.to_s }
|
90
|
+
if (session)
|
91
|
+
session.close
|
92
|
+
sessions.delete(session)
|
93
|
+
"Closed session for peer: #{session.peername}"
|
94
|
+
else
|
95
|
+
"No session active for '#{peername}'. Use `peers` to list active sessions."
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# List the peers currently being managed
|
101
|
+
def peers
|
102
|
+
if sessions.count > 0
|
103
|
+
puts
|
104
|
+
puts "Currently Managing"
|
105
|
+
puts "------------------"
|
106
|
+
puts
|
107
|
+
sessions.each_with_index do |session, i|
|
108
|
+
puts "[#{i}] #{session.peername}"
|
109
|
+
end
|
110
|
+
puts
|
111
|
+
else
|
112
|
+
puts "No active sessions"
|
113
|
+
end
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
# Translates a numerical oid to it's MIB name, or a name to numerical oid
|
118
|
+
#
|
119
|
+
# Arguments
|
120
|
+
# - oid: Either a string, as the numerical OID or MIB variable name,
|
121
|
+
# or an OID object
|
122
|
+
def translate(oid)
|
123
|
+
MIB.translate(oid)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Prints a description of a MIB variable
|
127
|
+
#
|
128
|
+
# Arguments
|
129
|
+
# - oid: May be either a numeric OID, or MIB variable name
|
130
|
+
def describe(oid)
|
131
|
+
nodes = [MIB[oid]]
|
132
|
+
puts ERB.new(Net::SNMP::MIB::Templates::DESCRIBE, nil, "-").result(binding)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Prints a description of a the MIB subtree starting at root `oid`
|
136
|
+
#
|
137
|
+
# Arguments
|
138
|
+
# - oid: May be either a numeric OID, or MIB variable name
|
139
|
+
def describe_tree(oid)
|
140
|
+
root = MIB[oid]
|
141
|
+
nodes = [root] + root.descendants.to_a
|
142
|
+
puts ERB.new(Net::SNMP::MIB::Templates::DESCRIBE, nil, "-").result(binding)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Issue an SNMP GET Request to all active peers
|
146
|
+
#
|
147
|
+
# Arguments
|
148
|
+
# - oids: A single oid, or an array of oids
|
149
|
+
def get(oids, options = {})
|
150
|
+
each_session do |session|
|
151
|
+
@pdu = session.get(oids)
|
152
|
+
@pdu.print
|
153
|
+
puts "ERROR" if @pdu.error?
|
154
|
+
end
|
155
|
+
"GET"
|
156
|
+
end
|
157
|
+
|
158
|
+
# Issue an SNMP GETNEXT Request to all active peers
|
159
|
+
#
|
160
|
+
# Arguments
|
161
|
+
# - oids: A single oid, or an array of oids
|
162
|
+
def get_next(oids, options = {})
|
163
|
+
each_session do |session|
|
164
|
+
@pdu = session.get_next(oids)
|
165
|
+
@pdu.print
|
166
|
+
puts "ERROR" if @pdu.error?
|
167
|
+
end
|
168
|
+
"GETNEXT"
|
169
|
+
end
|
170
|
+
|
171
|
+
# Issue an SNMP GETBULK Request to all active peers
|
172
|
+
#
|
173
|
+
# Arguments
|
174
|
+
# - oids: A single oid, or an array of oids
|
175
|
+
# - options: A Hash accepting the typical options keys for a request, plus
|
176
|
+
# + non_repeaters: The number of non-repeated oids in the request
|
177
|
+
# + max_repititions: The maximum repititions to return for all repeaters
|
178
|
+
# Note that the non-repeating varbinds must be added first.
|
179
|
+
def get_bulk(oids, options = {})
|
180
|
+
each_session do |session|
|
181
|
+
@pdu = session.get_bulk(oids)
|
182
|
+
@pdu.print
|
183
|
+
puts "ERROR" if @pdu.error?
|
184
|
+
end
|
185
|
+
"GETBULK"
|
186
|
+
end
|
187
|
+
|
188
|
+
# Performs a walk on all active peers for each oid provided
|
189
|
+
#
|
190
|
+
# Arguments
|
191
|
+
# - oids: A single oid, or an array of oids
|
192
|
+
def walk(oids, options = {})
|
193
|
+
each_session do |session|
|
194
|
+
session.walk(oids).each { |oid, value|
|
195
|
+
puts "#{MIB.translate(oid)}(#{oid}) = #{value}"
|
196
|
+
}
|
197
|
+
end
|
198
|
+
"WALK"
|
199
|
+
end
|
200
|
+
|
201
|
+
# Issue an SNMP Set Request to all active peers
|
202
|
+
#
|
203
|
+
# Arguments
|
204
|
+
# - varbinds: An single varbind, or an array of varbinds, each of which may be
|
205
|
+
# + An Array of length 3 `[oid, type, value]`
|
206
|
+
# + An Array of length 2 `[oid, value]`
|
207
|
+
# + Or a Hash `{oid: oid, type: type, value: value}`
|
208
|
+
# * Hash syntax is the same as supported by PDU.add_varbind
|
209
|
+
# * If type is not supplied, it is infered by the value
|
210
|
+
def set(varbinds, options = {})
|
211
|
+
each_session do |session|
|
212
|
+
@pdu = session.set(varbinds)
|
213
|
+
@pdu.print
|
214
|
+
puts "ERROR" if @pdu.error?
|
215
|
+
end
|
216
|
+
"SET"
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
def each_session(&block)
|
222
|
+
unless sessions.count > 0
|
223
|
+
puts NO_SESSION_PROMPT
|
224
|
+
raise 'No active session'
|
225
|
+
end
|
226
|
+
|
227
|
+
sessions.each_with_index do |session, i|
|
228
|
+
name = "#{session.peername}"
|
229
|
+
hrule = (['-'] * name.length).join ''
|
230
|
+
# Add a blank line before the first result
|
231
|
+
puts if i == 0
|
232
|
+
puts hrule
|
233
|
+
puts name
|
234
|
+
puts hrule
|
235
|
+
block[session]
|
236
|
+
# Add a blank line after each result
|
237
|
+
puts
|
238
|
+
end
|
239
|
+
|
240
|
+
nil
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,560 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'pp'
|
4
|
+
module Net
|
5
|
+
module SNMP
|
6
|
+
# Provides an API for managing SNMP agents
|
7
|
+
class Session
|
8
|
+
extend Forwardable
|
9
|
+
include Net::SNMP::Debug
|
10
|
+
attr_accessor :struct, :callback, :requests, :peername, :port, :community
|
11
|
+
attr_reader :version
|
12
|
+
def_delegator :@struct, :pointer
|
13
|
+
@lock = Mutex.new
|
14
|
+
@sessions = {}
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_accessor :sessions, :lock
|
18
|
+
|
19
|
+
# Open a new session. Accepts a block which yields the session.
|
20
|
+
#
|
21
|
+
# Net::SNMP::Session.open(:peername => 'test.net-snmp.org', :community => 'public') do |sess|
|
22
|
+
# pdu = sess.get(["sysDescr.0"])
|
23
|
+
# pdu.print
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# Arguments
|
27
|
+
# - options: A Hash or String object
|
28
|
+
# + As a Hash, supports the following keys
|
29
|
+
# - peername: hostname
|
30
|
+
# - community: snmp community string. Default is public
|
31
|
+
# - version: snmp version. Possible values include 1, '2c', and 3. Default is 1.
|
32
|
+
# - timeout: snmp timeout in seconds
|
33
|
+
# - retries: snmp retries. default = 5
|
34
|
+
# - security_level: SNMPv3 only. default = Net::SNMP::Constants::SNMP_SEC_LEVEL_NOAUTH
|
35
|
+
# - auth_protocol: SNMPv3 only. default is nil (usmNoAuthProtocol). Possible values include :md5, :sha1, and nil
|
36
|
+
# - priv_protocol: SNMPv3 only. default is nil (usmNoPrivProtocol). Possible values include :des, :aes, and nil
|
37
|
+
# - context: SNMPv3 only.
|
38
|
+
# - username: SNMPv3 only.
|
39
|
+
# - auth_password: SNMPv3 only.
|
40
|
+
# - priv_password: SNMPv3 only.
|
41
|
+
#
|
42
|
+
# Returns a new Net::SNMP::Session
|
43
|
+
def open(options = {})
|
44
|
+
session = new(options)
|
45
|
+
if Net::SNMP::thread_safe
|
46
|
+
Net::SNMP::Session.lock.synchronize {
|
47
|
+
Net::SNMP::Session.sessions[session.sessid] = session
|
48
|
+
}
|
49
|
+
else
|
50
|
+
Net::SNMP::Session.sessions[session.sessid] = session
|
51
|
+
end
|
52
|
+
if block_given?
|
53
|
+
yield session
|
54
|
+
end
|
55
|
+
session
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(options = {})
|
60
|
+
options = {:peername => options} if options.kind_of?(String)
|
61
|
+
@timeout = options[:timeout] || 1
|
62
|
+
@retries = options[:retries] || 5
|
63
|
+
@requests = {}
|
64
|
+
@peername = options[:peername] || 'localhost'
|
65
|
+
# If the port is supplied in the peername, don't
|
66
|
+
# worry about the port option (avoids appending two port numbers)
|
67
|
+
unless @peername[':']
|
68
|
+
options[:port] ||= 161
|
69
|
+
@port = options[:port]
|
70
|
+
@peername = "#{@peername}:#{options[:port]}"
|
71
|
+
end
|
72
|
+
@community = options[:community] || "public"
|
73
|
+
options[:community_len] = @community.length
|
74
|
+
options[:version] ||= Constants::SNMP_VERSION_2c
|
75
|
+
@version = options[:version]
|
76
|
+
@sess = Wrapper::SnmpSession.new(nil)
|
77
|
+
Wrapper.snmp_sess_init(@sess.pointer)
|
78
|
+
@sess.community = FFI::MemoryPointer.from_string(@community)
|
79
|
+
@sess.community_len = @community.length
|
80
|
+
@sess.peername = FFI::MemoryPointer.from_string(@peername)
|
81
|
+
@sess.version = case @version.to_s
|
82
|
+
when '1'
|
83
|
+
Constants::SNMP_VERSION_1
|
84
|
+
when '2', '2c'
|
85
|
+
Constants::SNMP_VERSION_2c
|
86
|
+
when '3'
|
87
|
+
Constants::SNMP_VERSION_3
|
88
|
+
else
|
89
|
+
Constants::SNMP_VERSION_1
|
90
|
+
end
|
91
|
+
debug "setting timeout = #{@timeout} retries = #{@retries}"
|
92
|
+
@sess.timeout = @timeout * 1000000
|
93
|
+
@sess.retries = @retries
|
94
|
+
|
95
|
+
if @sess.version == Constants::SNMP_VERSION_3
|
96
|
+
@sess.securityLevel = options[:security_level] || Constants::SNMP_SEC_LEVEL_NOAUTH
|
97
|
+
@sess.securityAuthProto = case options[:auth_protocol]
|
98
|
+
when :sha1
|
99
|
+
OID.new("1.3.6.1.6.3.10.1.1.3").pointer
|
100
|
+
when :md5
|
101
|
+
OID.new("1.3.6.1.6.3.10.1.1.2").pointer
|
102
|
+
when nil
|
103
|
+
OID.new("1.3.6.1.6.3.10.1.1.1").pointer
|
104
|
+
end
|
105
|
+
@sess.securityPrivProto = case options[:priv_protocol]
|
106
|
+
when :aes
|
107
|
+
OID.new("1.3.6.1.6.3.10.1.2.4").pointer
|
108
|
+
when :des
|
109
|
+
OID.new("1.3.6.1.6.3.10.1.2.2").pointer
|
110
|
+
when nil
|
111
|
+
OID.new("1.3.6.1.6.3.10.1.2.1").pointer
|
112
|
+
end
|
113
|
+
|
114
|
+
@sess.securityAuthProtoLen = 10
|
115
|
+
@sess.securityAuthKeyLen = Constants::USM_AUTH_KU_LEN
|
116
|
+
|
117
|
+
@sess.securityPrivProtoLen = 10
|
118
|
+
@sess.securityPrivKeyLen = Constants::USM_PRIV_KU_LEN
|
119
|
+
|
120
|
+
|
121
|
+
if options[:context]
|
122
|
+
@sess.contextName = FFI::MemoryPointer.from_string(options[:context])
|
123
|
+
@sess.contextNameLen = options[:context].length
|
124
|
+
end
|
125
|
+
|
126
|
+
# Do not generate_Ku, unless we're Auth or AuthPriv
|
127
|
+
unless @sess.securityLevel == Constants::SNMP_SEC_LEVEL_NOAUTH
|
128
|
+
options[:auth_password] ||= options[:password] # backward compatability
|
129
|
+
if options[:username].nil? or options[:auth_password].nil?
|
130
|
+
raise Net::SNMP::Error.new "SecurityLevel requires username and password"
|
131
|
+
end
|
132
|
+
if options[:username]
|
133
|
+
@sess.securityName = FFI::MemoryPointer.from_string(options[:username])
|
134
|
+
@sess.securityNameLen = options[:username].length
|
135
|
+
end
|
136
|
+
|
137
|
+
auth_len_ptr = FFI::MemoryPointer.new(:size_t)
|
138
|
+
auth_len_ptr.write_int(Constants::USM_AUTH_KU_LEN)
|
139
|
+
auth_key_result = Wrapper.generate_Ku(@sess.securityAuthProto,
|
140
|
+
@sess.securityAuthProtoLen,
|
141
|
+
options[:auth_password],
|
142
|
+
options[:auth_password].length,
|
143
|
+
@sess.securityAuthKey,
|
144
|
+
auth_len_ptr)
|
145
|
+
@sess.securityAuthKeyLen = auth_len_ptr.read_int
|
146
|
+
|
147
|
+
if @sess.securityLevel == Constants::SNMP_SEC_LEVEL_AUTHPRIV
|
148
|
+
priv_len_ptr = FFI::MemoryPointer.new(:size_t)
|
149
|
+
priv_len_ptr.write_int(Constants::USM_PRIV_KU_LEN)
|
150
|
+
|
151
|
+
# NOTE I know this is handing off the AuthProto, but generates a proper
|
152
|
+
# key for encryption, and using PrivProto does not.
|
153
|
+
priv_key_result = Wrapper.generate_Ku(@sess.securityAuthProto,
|
154
|
+
@sess.securityAuthProtoLen,
|
155
|
+
options[:priv_password],
|
156
|
+
options[:priv_password].length,
|
157
|
+
@sess.securityPrivKey,
|
158
|
+
priv_len_ptr)
|
159
|
+
@sess.securityPrivKeyLen = priv_len_ptr.read_int
|
160
|
+
end
|
161
|
+
|
162
|
+
unless auth_key_result == Constants::SNMPERR_SUCCESS and priv_key_result == Constants::SNMPERR_SUCCESS
|
163
|
+
Wrapper.snmp_perror("netsnmp")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
# General callback just takes the pdu, calls the session callback if any, then the request specific callback.
|
168
|
+
|
169
|
+
@struct = Wrapper.snmp_sess_open(@sess.pointer)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Close the snmp session and free associated resources.
|
173
|
+
def close
|
174
|
+
if Net::SNMP.thread_safe
|
175
|
+
self.class.lock.synchronize {
|
176
|
+
Wrapper.snmp_sess_close(@struct)
|
177
|
+
self.class.sessions.delete(self.sessid)
|
178
|
+
}
|
179
|
+
else
|
180
|
+
Wrapper.snmp_sess_close(@struct)
|
181
|
+
self.class.sessions.delete(self.sessid)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Issue an SNMP GET Request.
|
186
|
+
# See #send_pdu
|
187
|
+
def get(oidlist, options = {}, &block)
|
188
|
+
pdu = PDU.new(Constants::SNMP_MSG_GET)
|
189
|
+
oidlist = [oidlist] unless oidlist.kind_of?(Array)
|
190
|
+
oidlist.each do |oid|
|
191
|
+
pdu.add_varbind(:oid => oid)
|
192
|
+
end
|
193
|
+
send_pdu(pdu, options, &block)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Issue an SNMP GETNEXT Request
|
197
|
+
# See #send_pdu
|
198
|
+
def get_next(oidlist, options = {}, &block)
|
199
|
+
pdu = PDU.new(Constants::SNMP_MSG_GETNEXT)
|
200
|
+
oidlist = [oidlist] unless oidlist.kind_of?(Array)
|
201
|
+
oidlist.each do |oid|
|
202
|
+
pdu.add_varbind(:oid => oid)
|
203
|
+
end
|
204
|
+
send_pdu(pdu, options, &block)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Issue an SNMP GETBULK Request
|
208
|
+
# Supports typical options, plus
|
209
|
+
# - `:non_repeaters` The number of non-repeated oids in the request
|
210
|
+
# - `:max_repititions` The maximum repititions to return for all repeaters
|
211
|
+
# Note that the non-repeating varbinds must be added first.
|
212
|
+
# See #send_pdu
|
213
|
+
def get_bulk(oidlist, options = {}, &block)
|
214
|
+
pdu = PDU.new(Constants::SNMP_MSG_GETBULK)
|
215
|
+
oidlist = [oidlist] unless oidlist.kind_of?(Array)
|
216
|
+
oidlist.each do |oid|
|
217
|
+
pdu.add_varbind(:oid => oid)
|
218
|
+
end
|
219
|
+
pdu.non_repeaters = options[:non_repeaters] || 0
|
220
|
+
pdu.max_repetitions = options[:max_repetitions] || 10
|
221
|
+
send_pdu(pdu, options, &block)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Issue an SNMP Set Request.
|
225
|
+
# - vb_list: An single varbind, or an array of varbinds, each of which may be
|
226
|
+
# + An Array of length 3 `[oid, type, value]`
|
227
|
+
# + An Array of length 2 `[oid, value]`
|
228
|
+
# + Or a Hash `{oid: oid, type: type, value: value}`
|
229
|
+
# * Hash syntax is the same as supported by PDU.add_varbind
|
230
|
+
# * If type is not supplied, it is infered by the value
|
231
|
+
# See #send_pdu
|
232
|
+
def set(vb_list, options = {}, &block)
|
233
|
+
pdu = PDU.new(Constants::SNMP_MSG_SET)
|
234
|
+
|
235
|
+
# Normalize input to an array if a single varbind is supplied
|
236
|
+
if vb_list.kind_of?(Hash) || (vb_list.kind_of?(Enumerable) && !vb_list.first.kind_of?(Enumerable))
|
237
|
+
vb_list = [vb_list]
|
238
|
+
end
|
239
|
+
|
240
|
+
vb_list.each do |vb|
|
241
|
+
if vb.kind_of?(Hash)
|
242
|
+
pdu.add_varbind(vb)
|
243
|
+
elsif vb.kind_of?(Enumerable) && vb.length == 3
|
244
|
+
pdu.add_varbind(:oid => vb[0], :type => vb[1], :value => vb[2])
|
245
|
+
elsif vb.kind_of?(Enumerable) && vb.length == 2
|
246
|
+
pdu.add_varbind(:oid => vb[0], :value => vb[1])
|
247
|
+
else
|
248
|
+
raise "Invalid varbind: #{vb}"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
send_pdu(pdu, options, &block)
|
252
|
+
end
|
253
|
+
|
254
|
+
# Proxy getters to the C struct representing the session
|
255
|
+
def method_missing(m, *args)
|
256
|
+
if @struct.respond_to?(m)
|
257
|
+
@struct.send(m, *args)
|
258
|
+
else
|
259
|
+
super
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def default_max_repeaters
|
264
|
+
# We could do something based on transport here. 25 seems safe
|
265
|
+
25
|
266
|
+
end
|
267
|
+
|
268
|
+
# Raise a NET::SNMP::Error with the session attached
|
269
|
+
def error(msg, options = {})
|
270
|
+
#Wrapper.snmp_sess_perror(msg, @sess.pointer)
|
271
|
+
err = Error.new({:session => self}.merge(options))
|
272
|
+
raise err, msg
|
273
|
+
end
|
274
|
+
|
275
|
+
|
276
|
+
# Check the session for SNMP responses from asynchronous SNMP requests
|
277
|
+
# This method will check for new responses and call the associated
|
278
|
+
# response callbacks.
|
279
|
+
# +timeout+ A timeout of nil indicates a poll and will return immediately.
|
280
|
+
# A value of false will block until data is available. Otherwise, pass
|
281
|
+
# the number of seconds to block.
|
282
|
+
# Returns the number of file descriptors handled.
|
283
|
+
def select(timeout = nil)
|
284
|
+
if @fdset
|
285
|
+
# Re-use the same fd set buffer to avoid
|
286
|
+
# multiple allocation overhead.
|
287
|
+
@fdset.clear
|
288
|
+
else
|
289
|
+
# 8K should be plenty of space
|
290
|
+
@fdset = FFI::MemoryPointer.new(1024 * 8)
|
291
|
+
end
|
292
|
+
|
293
|
+
num_fds = FFI::MemoryPointer.new(:int)
|
294
|
+
tv_sec = timeout ? timeout.round : 0
|
295
|
+
tv_usec = timeout ? (timeout - timeout.round) * 1000000 : 0
|
296
|
+
tval = Wrapper::TimeVal.new(:tv_sec => tv_sec, :tv_usec => tv_usec)
|
297
|
+
block = FFI::MemoryPointer.new(:int)
|
298
|
+
if timeout.nil?
|
299
|
+
block.write_int(0)
|
300
|
+
else
|
301
|
+
block.write_int(1)
|
302
|
+
end
|
303
|
+
|
304
|
+
Wrapper.snmp_sess_select_info(@struct, num_fds, @fdset, tval.pointer, block )
|
305
|
+
tv = (timeout == false ? nil : tval)
|
306
|
+
#debug "Calling select #{Time.now}"
|
307
|
+
num_ready = FFI::LibC.select(num_fds.read_int, @fdset, nil, nil, tv)
|
308
|
+
#debug "Done select #{Time.now}"
|
309
|
+
if num_ready > 0
|
310
|
+
Wrapper.snmp_sess_read(@struct, @fdset)
|
311
|
+
elsif num_ready == 0
|
312
|
+
Wrapper.snmp_sess_timeout(@struct)
|
313
|
+
elsif num_ready == -1
|
314
|
+
# error. check snmp_error?
|
315
|
+
error("select")
|
316
|
+
else
|
317
|
+
error("wtf is wrong with select?")
|
318
|
+
end
|
319
|
+
num_ready
|
320
|
+
end
|
321
|
+
|
322
|
+
alias :poll :select
|
323
|
+
|
324
|
+
# Issue repeated getnext requests on each oid passed in until
|
325
|
+
# the result is no longer a child. Returns a hash with the numeric
|
326
|
+
# oid strings as keys.
|
327
|
+
# XXX work in progress. only works synchronously (except with EM + fibers).
|
328
|
+
# Need to do better error checking and use getbulk when avaiable.
|
329
|
+
def walk(oidlist, options = {})
|
330
|
+
oidlist = [oidlist] unless oidlist.kind_of?(Array)
|
331
|
+
oidlist = oidlist.map {|o| o.kind_of?(OID) ? o : OID.new(o)}
|
332
|
+
all_results = {}
|
333
|
+
base_list = oidlist
|
334
|
+
while(!oidlist.empty? && pdu = get_next(oidlist, options))
|
335
|
+
debug "================ Walk: Get Next ====================="
|
336
|
+
debug "base_list: \n#{base_list.map { |o| " - #{o.to_s}" }.join("\n")}"
|
337
|
+
prev_base = base_list.dup
|
338
|
+
oidlist = []
|
339
|
+
pdu.varbinds.each_with_index do |vb, i|
|
340
|
+
if prev_base[i].parent_of?(vb.oid) && vb.object_type != Constants::SNMP_ENDOFMIBVIEW
|
341
|
+
# Still in subtree. Store results and add next oid to list
|
342
|
+
debug "adding #{vb.oid} to oidlist"
|
343
|
+
all_results[vb.oid.to_s] = vb.value
|
344
|
+
oidlist << vb.oid
|
345
|
+
else
|
346
|
+
# End of subtree. Don't add to list or results
|
347
|
+
debug "End of subtree"
|
348
|
+
base_list.delete_at(i)
|
349
|
+
debug "not adding #{vb.oid}"
|
350
|
+
end
|
351
|
+
# If get a pdu error, we can only tell the first failing varbind,
|
352
|
+
# So we remove it and resend all the rest
|
353
|
+
if pdu.error? && pdu.errindex == i + 1
|
354
|
+
oidlist.pop # remove the bad oid
|
355
|
+
debug "caught error"
|
356
|
+
if pdu.varbinds.size > i+1
|
357
|
+
# recram rest of oids on list
|
358
|
+
((i+1)..pdu.varbinds.size).each do |j|
|
359
|
+
debug "j = #{j}"
|
360
|
+
debug "adding #{j} = #{prev_list[j]}"
|
361
|
+
oidlist << prev_list[j]
|
362
|
+
end
|
363
|
+
# delete failing oid from base_list
|
364
|
+
base_list.delete_at(i)
|
365
|
+
end
|
366
|
+
break
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
if block_given?
|
371
|
+
yield all_results
|
372
|
+
end
|
373
|
+
all_results
|
374
|
+
end
|
375
|
+
|
376
|
+
|
377
|
+
# Given a list of columns (e.g ['ifIndex', 'ifDescr'], will return a hash with
|
378
|
+
# the indexes as keys and hashes as values.
|
379
|
+
# puts sess.get_columns(['ifIndex', 'ifDescr']).inspect
|
380
|
+
# {'1' => {'ifIndex' => '1', 'ifDescr' => 'lo0'}, '2' => {'ifIndex' => '2', 'ifDescr' => 'en0'}}
|
381
|
+
def columns(columns, options = {})
|
382
|
+
columns = columns.map {|c| c.kind_of?(OID) ? c : OID.new(c)}
|
383
|
+
walk_hash = walk(columns, options)
|
384
|
+
results = {}
|
385
|
+
walk_hash.each do |k, v|
|
386
|
+
oid = OID.new(k)
|
387
|
+
results[oid.index] ||= {}
|
388
|
+
results[oid.index][oid.node.label] = v
|
389
|
+
end
|
390
|
+
if block_given?
|
391
|
+
yield results
|
392
|
+
end
|
393
|
+
results
|
394
|
+
end
|
395
|
+
|
396
|
+
# table('ifEntry'). You must pass the direct parent entry. Calls columns with all
|
397
|
+
# columns in +table_name+
|
398
|
+
def table(table_name, &blk)
|
399
|
+
column_names = MIB::Node.get_node(table_name).children.collect {|c| c.oid }
|
400
|
+
results = columns(column_names)
|
401
|
+
if block_given?
|
402
|
+
yield results
|
403
|
+
end
|
404
|
+
results
|
405
|
+
end
|
406
|
+
|
407
|
+
# Send a PDU
|
408
|
+
# +pdu+ The Net::SNMP::PDU object to send. Usually created by Session.get, Session.getnext, etc.
|
409
|
+
# +callback+ An optional callback. It should take two parameters, status and response_pdu.
|
410
|
+
# If no +callback+ is given, the call will block until the response is available and will return
|
411
|
+
# the response pdu. If an error occurs, a Net::SNMP::Error will be thrown.
|
412
|
+
# If +callback+ is passed, the PDU will be sent and +send_pdu+ will return immediately. You must
|
413
|
+
# then call Session.select to invoke the callback. This is usually done in some sort of event loop.
|
414
|
+
# See Net::SNMP::Dispatcher.
|
415
|
+
#
|
416
|
+
# If you're running inside eventmachine and have fibers (ruby 1.9, jruby, etc), sychronous calls will
|
417
|
+
# actually run asynchronously behind the scenes. Just run Net::SNMP::Dispatcher.fiber_loop in your
|
418
|
+
# reactor.
|
419
|
+
#
|
420
|
+
# pdu = Net::SNMP::PDU.new(Constants::SNMP_MSG_GET)
|
421
|
+
# pdu.add_varbind(:oid => 'sysDescr.0')
|
422
|
+
# session.send_pdu(pdu) do |status, pdu|
|
423
|
+
# if status == :success
|
424
|
+
# pdu.print
|
425
|
+
# elsif status == :timeout
|
426
|
+
# puts "Timed Out"
|
427
|
+
# else
|
428
|
+
# puts "A problem occurred"
|
429
|
+
# end
|
430
|
+
# end
|
431
|
+
# session.select(false) #block until data is ready. Callback will be called.
|
432
|
+
# begin
|
433
|
+
# result = session.send_pdu(pdu)
|
434
|
+
# puts result.inspect
|
435
|
+
# rescue Net::SNMP::Error => e
|
436
|
+
# puts e.message
|
437
|
+
# end
|
438
|
+
def send_pdu(pdu, options = {}, &callback)
|
439
|
+
if options[:blocking]
|
440
|
+
return send_pdu_blocking(pdu)
|
441
|
+
end
|
442
|
+
if block_given?
|
443
|
+
@requests[pdu.reqid] = callback
|
444
|
+
debug "calling async_send"
|
445
|
+
if Wrapper.snmp_sess_async_send(@struct, pdu.pointer, sess_callback, nil) == 0
|
446
|
+
error("snmp_get async failed")
|
447
|
+
end
|
448
|
+
nil
|
449
|
+
else
|
450
|
+
if defined?(EM) && EM.reactor_running? && defined?(Fiber)
|
451
|
+
f = Fiber.current
|
452
|
+
send_pdu pdu do | op, response_pdu |
|
453
|
+
f.resume([op, response_pdu])
|
454
|
+
end
|
455
|
+
op, result = Fiber.yield
|
456
|
+
case op
|
457
|
+
when :timeout
|
458
|
+
raise TimeoutError.new, "timeout"
|
459
|
+
when :send_failed
|
460
|
+
error "send failed"
|
461
|
+
when :success
|
462
|
+
result
|
463
|
+
when :connect, :disconnect
|
464
|
+
nil #does this ever happen?
|
465
|
+
else
|
466
|
+
error "unknown operation #{op}"
|
467
|
+
end
|
468
|
+
else
|
469
|
+
send_pdu_blocking(pdu)
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
def send_pdu_blocking(pdu)
|
475
|
+
response_ptr = FFI::MemoryPointer.new(:pointer)
|
476
|
+
if [Constants::SNMP_MSG_TRAP, Constants::SNMP_MSG_TRAP2, Constants::SNMP_MSG_RESPONSE].include?(pdu.command)
|
477
|
+
# Since we don't expect a response, the native net-snmp lib is going to free this
|
478
|
+
# pdu for us. Polite, though this may be, it causes intermittent segfaults when freeing
|
479
|
+
# memory malloc'ed by ruby. So, clone the pdu into a new memory buffer,
|
480
|
+
# and pass that along.
|
481
|
+
clone = Wrapper.snmp_clone_pdu(pdu.struct)
|
482
|
+
status = Wrapper.snmp_sess_send(@struct, clone)
|
483
|
+
if status == 0
|
484
|
+
error("snmp_sess_send")
|
485
|
+
end
|
486
|
+
:success
|
487
|
+
else
|
488
|
+
status = Wrapper.snmp_sess_synch_response(@struct, pdu.pointer, response_ptr)
|
489
|
+
unless status == Constants::STAT_SUCCESS
|
490
|
+
error("snmp_sess_synch_response", :status => status)
|
491
|
+
end
|
492
|
+
PDU.new(response_ptr.read_pointer)
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
def errno
|
497
|
+
get_error
|
498
|
+
@errno
|
499
|
+
end
|
500
|
+
|
501
|
+
# The SNMP Session error code
|
502
|
+
def snmp_err
|
503
|
+
get_error
|
504
|
+
@snmp_err
|
505
|
+
end
|
506
|
+
|
507
|
+
# The SNMP Session error message
|
508
|
+
def error_message
|
509
|
+
get_error
|
510
|
+
@snmp_msg
|
511
|
+
end
|
512
|
+
|
513
|
+
def print_errors
|
514
|
+
puts "errno: #{errno}, snmp_err: #{@snmp_err}, message: #{@snmp_msg}"
|
515
|
+
end
|
516
|
+
|
517
|
+
private
|
518
|
+
|
519
|
+
def sess_callback
|
520
|
+
@sess_callback ||= FFI::Function.new(:int, [:int, :pointer, :int, :pointer, :pointer]) do |operation, session, reqid, pdu_ptr, magic|
|
521
|
+
debug "in callback #{operation.inspect} #{session.inspect}"
|
522
|
+
op = case operation
|
523
|
+
when Constants::NETSNMP_CALLBACK_OP_RECEIVED_MESSAGE
|
524
|
+
:success
|
525
|
+
when Constants::NETSNMP_CALLBACK_OP_TIMED_OUT
|
526
|
+
:timeout
|
527
|
+
when Constants::NETSNMP_CALLBACK_OP_SEND_FAILED
|
528
|
+
:send_failed
|
529
|
+
when Constants::NETSNMP_CALLBACK_OP_CONNECT
|
530
|
+
:connect
|
531
|
+
when Constants::NETSNMP_CALLBACK_OP_DISCONNECT
|
532
|
+
:disconnect
|
533
|
+
else
|
534
|
+
error "Invalid PDU Operation"
|
535
|
+
end
|
536
|
+
|
537
|
+
if @requests[reqid]
|
538
|
+
pdu = PDU.new(pdu_ptr)
|
539
|
+
callback_return = @requests[reqid].call(op, pdu)
|
540
|
+
@requests.delete(reqid)
|
541
|
+
callback_return == false ? 0 : 1 #if callback returns false (failure), pass it on. otherwise return 1 (success)
|
542
|
+
else
|
543
|
+
0 # Do what here? Can this happen? Maybe request timed out and was deleted?
|
544
|
+
end
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
def get_error
|
549
|
+
errno_ptr = FFI::MemoryPointer.new(:int)
|
550
|
+
snmp_err_ptr = FFI::MemoryPointer.new(:int)
|
551
|
+
msg_ptr = FFI::MemoryPointer.new(:pointer)
|
552
|
+
Wrapper.snmp_sess_error(@struct.pointer, errno_ptr, snmp_err_ptr, msg_ptr)
|
553
|
+
@errno = errno_ptr.null? ? nil : errno_ptr.read_int
|
554
|
+
@snmp_err = snmp_err_ptr.null? ? nil : snmp_err_ptr.read_int
|
555
|
+
@snmp_msg = (msg_ptr.null? || msg_ptr.read_pointer.null?) ? nil : msg_ptr.read_pointer.read_string
|
556
|
+
end
|
557
|
+
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|