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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +20 -0
  6. data/README.md +28 -0
  7. data/Rakefile +24 -0
  8. data/TODO.md +6 -0
  9. data/bin/mib2rb +129 -0
  10. data/bin/net-snmp2 +64 -0
  11. data/examples/agent.rb +104 -0
  12. data/examples/manager.rb +11 -0
  13. data/examples/trap_handler.rb +46 -0
  14. data/examples/v1_trap_session.rb +24 -0
  15. data/examples/v2_trap_session.rb +21 -0
  16. data/lib/net-snmp2.rb +85 -0
  17. data/lib/net/snmp.rb +27 -0
  18. data/lib/net/snmp/agent/agent.rb +48 -0
  19. data/lib/net/snmp/agent/provider.rb +51 -0
  20. data/lib/net/snmp/agent/provider_dsl.rb +124 -0
  21. data/lib/net/snmp/agent/request_dispatcher.rb +38 -0
  22. data/lib/net/snmp/constants.rb +287 -0
  23. data/lib/net/snmp/debug.rb +54 -0
  24. data/lib/net/snmp/dispatcher.rb +108 -0
  25. data/lib/net/snmp/error.rb +29 -0
  26. data/lib/net/snmp/listener.rb +76 -0
  27. data/lib/net/snmp/message.rb +142 -0
  28. data/lib/net/snmp/mib/mib.rb +67 -0
  29. data/lib/net/snmp/mib/module.rb +39 -0
  30. data/lib/net/snmp/mib/node.rb +122 -0
  31. data/lib/net/snmp/mib/templates.rb +48 -0
  32. data/lib/net/snmp/oid.rb +134 -0
  33. data/lib/net/snmp/pdu.rb +235 -0
  34. data/lib/net/snmp/repl/manager_repl.rb +243 -0
  35. data/lib/net/snmp/session.rb +560 -0
  36. data/lib/net/snmp/trap_handler/trap_handler.rb +42 -0
  37. data/lib/net/snmp/trap_handler/v1_trap_dsl.rb +44 -0
  38. data/lib/net/snmp/trap_handler/v2_trap_dsl.rb +38 -0
  39. data/lib/net/snmp/trap_session.rb +92 -0
  40. data/lib/net/snmp/utility.rb +10 -0
  41. data/lib/net/snmp/varbind.rb +57 -0
  42. data/lib/net/snmp/version.rb +5 -0
  43. data/lib/net/snmp/wrapper.rb +450 -0
  44. data/net-snmp2.gemspec +30 -0
  45. data/spec/README.md +105 -0
  46. data/spec/async_spec.rb +123 -0
  47. data/spec/em_spec.rb +23 -0
  48. data/spec/error_spec.rb +34 -0
  49. data/spec/fiber_spec.rb +41 -0
  50. data/spec/mib_spec.rb +68 -0
  51. data/spec/net-snmp_spec.rb +18 -0
  52. data/spec/oid_spec.rb +21 -0
  53. data/spec/spec_helper.rb +10 -0
  54. data/spec/sync_spec.rb +132 -0
  55. data/spec/thread_spec.rb +19 -0
  56. data/spec/trap_spec.rb +45 -0
  57. data/spec/utility_spec.rb +10 -0
  58. data/spec/wrapper_spec.rb +69 -0
  59. 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