net-snmp2 0.3.0
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.
- 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
|