netsnmp 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,306 @@
1
+ module NETSNMP
2
+ # The Entity abstracts the C net-snmp session, and the lifecycle steps.
3
+ #
4
+ # For example, a session must be initialized (memory allocated) and opened
5
+ # (authentication, encryption, signature)
6
+ #
7
+ # The session uses the signature to send and receive PDUs. They are built somewhere else.
8
+ #
9
+ # After the session is established, a socket handle is read from the structure. This will
10
+ # be later used for non-blocking behaviour. It's important to notice, there is no
11
+ # usage of the C net-snmp sync API, we always do async send/response, even if the
12
+ # ruby API "feels" blocking. This was done so that the GIL can be released between
13
+ # sends and receives, and the load can be shared through different threads possibly.
14
+ # As we use the session abstraction, this means we ONLY use the thread-safe API.
15
+ #
16
+ class Session
17
+
18
+ attr_reader :host, :signature
19
+
20
+ # @param [String] host the host IP/hostname
21
+ # @param [Hash] opts the options set
22
+ #
23
+ def initialize(host, opts)
24
+ @host = host
25
+ @options = opts
26
+ @request = nil
27
+ # For now, let's eager load the signature
28
+ @signature = build_signature(@options)
29
+ if @signature.null?
30
+ raise ConnectionFailed, "could not connect to #{host}"
31
+ end
32
+ @requests ||= {}
33
+ end
34
+
35
+ # TODO: do we need this?
36
+ def reachable?
37
+ !!transport
38
+ end
39
+
40
+ # Closes the session
41
+ def close
42
+ return unless @signature
43
+ if @transport
44
+ transport.close rescue nil
45
+ end
46
+ if Core::LibSNMP.snmp_sess_close(@signature) == 0
47
+ raise Error, "#@host: Couldn't clean up session properly"
48
+ end
49
+ end
50
+
51
+ # sends a request PDU and waits for the response
52
+ #
53
+ # @param [RequestPDU] pdu a request pdu
54
+ # @param [Hash] opts additional options
55
+ # @option opts [true, false] :async if true, it doesn't wait for response (defaults to false)
56
+ def send(pdu, **opts)
57
+ write(pdu)
58
+ read
59
+ end
60
+
61
+ private
62
+
63
+ def transport
64
+ @transport ||= fetch_transport
65
+ end
66
+
67
+ def write(pdu)
68
+ wait_writable
69
+ async_send(pdu)
70
+ end
71
+
72
+ def async_send(pdu)
73
+ if ( @reqid = Core::LibSNMP.snmp_sess_async_send(@signature, pdu.pointer, session_callback, nil) ) == 0
74
+ # it's interesting, pdu's are only fred if the async send is successful... netsnmp 1 - me 0
75
+ Core::LibSNMP.snmp_free_pdu(pdu.pointer)
76
+ raise SendError, "#@host: Failed to send pdu"
77
+ end
78
+ end
79
+
80
+ def read
81
+ receive # trigger callback ahead of time and wait for it
82
+ handle_response
83
+ end
84
+
85
+ def handle_response
86
+ operation, response_pdu = @requests.delete(@reqid)
87
+ case operation
88
+ when :send_failed
89
+ raise ReceiveError, "#@host: Failed to receive pdu"
90
+ when :timeout
91
+ raise Timeout::Error, "#@host: timed out while waiting for pdu response"
92
+ when :success
93
+ response_pdu
94
+ else
95
+ raise Error, "#@host: unrecognized operation for request #{@reqid}: #{operation} for #{response_pdu}"
96
+ end
97
+ end
98
+
99
+ def receive
100
+ readers, _ = wait_readable
101
+ case readers.size
102
+ when 1..Float::INFINITY
103
+ # triggers callback
104
+ async_read
105
+ when 0
106
+ Core::LibSNMP.snmp_sess_timeout(@signature)
107
+ else
108
+ raise ReceiveError, "#@host: error receiving data"
109
+ end
110
+ end
111
+
112
+ def async_read
113
+ if Core::LibSNMP.snmp_sess_read(@signature, get_selectable_sockets.pointer) != 0
114
+ raise ReceiveError, "#@host: Failed to receive pdu response"
115
+ end
116
+ end
117
+
118
+ def timeout
119
+ Core::LibSNMP.snmp_sess_timeout(@signature)
120
+ end
121
+
122
+ def wait_writable
123
+ IO.select([],[transport])
124
+ end
125
+
126
+ def wait_readable
127
+ IO.select([transport])
128
+ end
129
+
130
+ def get_selectable_sockets
131
+ fdset = Core::C::FDSet.new
132
+ fdset.clear
133
+ num_fds = FFI::MemoryPointer.new(:int)
134
+ tv_sec = 0
135
+ tv_usec = 0
136
+ tval = Core::C::Timeval.new
137
+ tval[:tv_sec] = tv_sec
138
+ tval[:tv_usec] = tv_usec
139
+ block = FFI::MemoryPointer.new(:int)
140
+ block.write_int(0)
141
+ Core::LibSNMP.snmp_sess_select_info(@signature, num_fds, fdset.pointer, tval.pointer, block )
142
+ fdset
143
+ end
144
+
145
+
146
+ # @param [Core::Structures::Session] session the snmp session structure
147
+ # @param [Hash] options session options with authorization parameters
148
+ # @option options [String] :version the snmp protocol version (if < 3, forget the rest)
149
+ # @option options [Integer, nil] :security_level the SNMP security level (defaults to authPriv)
150
+ # @option options [Symbol, nil] :auth_protocol the authorization protocol (ex: :md5, :sha1)
151
+ # @option options [Symbol, nil] :priv_protocol the privacy protocol (ex: :aes, :des)
152
+ # @option options [String, nil] :context the authoritative context
153
+ # @option options [String] :version the snmp protocol version (defaults to 3, if not 3, you actually don't need the rest)
154
+ # @option options [String] :username the username to login with
155
+ # @option options [String] :auth_password the authorization password
156
+ # @option options [String] :priv_password the privacy password
157
+ def session_authorization(session, options)
158
+ # we support version 3 by default
159
+ session[:version] = case options[:version]
160
+ when /v?1/ then Core::Constants::SNMP_VERSION_1
161
+ when /v?2c?/ then Core::Constants::SNMP_VERSION_2c
162
+ when /v?3/, nil then Core::Constants::SNMP_VERSION_3
163
+ end
164
+ return unless session[:version] == Core::Constants::SNMP_VERSION_3
165
+
166
+
167
+ # Security Authorization
168
+ session[:securityLevel] = options[:security_level] || Core::Constants::SNMP_SEC_LEVEL_AUTHPRIV
169
+ auth_protocol_oid = case options[:auth_protocol]
170
+ when :md5 then MD5OID.new
171
+ when :sha1 then SHA1OID.new
172
+ when nil then NoAuthOID.new
173
+ else raise Error, "#@host: #{options[:auth_protocol]} is an unsupported authorization protocol"
174
+ end
175
+
176
+ session[:securityAuthProto] = auth_protocol_oid.pointer
177
+
178
+ # Priv Protocol
179
+ priv_protocol_oid = case options[:priv_protocol]
180
+ when :aes then AESOID.new
181
+ when :des then DESOID.new
182
+ when nil then NoPrivOID.new
183
+ else raise Error, "#@host: #{options[:priv_protocol]} is an unsupported privacy protocol"
184
+ end
185
+ session[:securityPrivProto] = priv_protocol_oid.pointer
186
+
187
+ # other necessary lengths
188
+ session[:securityAuthProtoLen] = 10
189
+ session[:securityAuthKeyLen] = Core::Constants::USM_AUTH_KU_LEN
190
+ session[:securityPrivProtoLen] = 10
191
+ session[:securityPrivKeyLen] = Core::Constants::USM_PRIV_KU_LEN
192
+
193
+
194
+ if options[:context]
195
+ session[:contextName] = FFI::MemoryPointer.from_string(options[:context])
196
+ session[:contextNameLen] = options[:context].length
197
+ end
198
+
199
+ # Authentication
200
+ # Do not generate_Ku, unless we're Auth or AuthPriv
201
+ auth_user, auth_pass = options.values_at(:username, :auth_password)
202
+ raise Error, "#@host: no given Authorization User" unless auth_user
203
+ session[:securityName] = FFI::MemoryPointer.from_string(auth_user)
204
+ session[:securityNameLen] = auth_user.length
205
+
206
+ auth_len_ptr = FFI::MemoryPointer.new(:size_t)
207
+ auth_len_ptr.write_int(Core::Constants::USM_AUTH_KU_LEN)
208
+ auth_key_result = Core::LibSNMP.generate_Ku(session[:securityAuthProto],
209
+ session[:securityAuthProtoLen],
210
+ auth_pass,
211
+ auth_pass.length,
212
+ session[:securityAuthKey],
213
+ auth_len_ptr)
214
+ session[:securityAuthKeyLen] = auth_len_ptr.read_int
215
+
216
+ priv_len_ptr = FFI::MemoryPointer.new(:size_t)
217
+ priv_len_ptr.write_int(Core::Constants::USM_PRIV_KU_LEN)
218
+
219
+ priv_pass = options[:priv_password]
220
+ # NOTE I know this is handing off the AuthProto, but generates a proper
221
+ # key for encryption, and using PrivProto does not.
222
+ priv_key_result = Core::LibSNMP.generate_Ku(session[:securityAuthProto],
223
+ session[:securityAuthProtoLen],
224
+ priv_pass,
225
+ priv_pass.length,
226
+ session[:securityPrivKey],
227
+ priv_len_ptr)
228
+ session[:securityPrivKeyLen] = priv_len_ptr.read_int
229
+
230
+ unless auth_key_result == Core::Constants::SNMPERR_SUCCESS and
231
+ priv_key_result == Core::Constants::SNMPERR_SUCCESS
232
+ raise AuthenticationFailed, "failed to authenticate #{auth_user} in #{@host}"
233
+ end
234
+ end
235
+
236
+
237
+ # @param [Hash] options options to open the net-snmp session
238
+ # @option options [String] :community the snmp community string (defaults to public)
239
+ # @option options [Integer] :timeout number of millisecs until first timeout
240
+ # @option options [Integer] :retries number of retries before timeout
241
+ # @return [FFI::Pointer] a pointer to the validated session signature, which will therefore be used in all _sess_ methods from libnetsnmp
242
+ def build_signature(options)
243
+ # allocate new session
244
+ session = Core::Structures::Session.new(nil)
245
+ Core::LibSNMP.snmp_sess_init(session.pointer)
246
+
247
+ # initialize session
248
+ if options[:community]
249
+ community = options[:community]
250
+ session[:community] = FFI::MemoryPointer.from_string(community)
251
+ session[:community_len] = community.length
252
+ end
253
+
254
+ peername = host
255
+ unless peername[':']
256
+ port = options[:port] || '161'.freeze
257
+ peername = "#{peername}:#{port}"
258
+ end
259
+
260
+ session[:peername] = FFI::MemoryPointer.from_string(peername)
261
+
262
+ session[:timeout] = options[:timeout] if options.has_key?(:timeout)
263
+ session[:retries] = options[:retries] if options.has_key?(:retries)
264
+
265
+ session_authorization(session, options)
266
+ Core::LibSNMP.snmp_sess_open(session.pointer)
267
+ end
268
+
269
+ def fetch_transport
270
+ return unless @signature
271
+ list = Core::Structures::SessionList.new @signature
272
+ return if not list or list.pointer.null?
273
+ t = Core::Structures::Transport.new list[:transport]
274
+ IO.new(t[:sock])
275
+ end
276
+
277
+ # @param [Core::Structures::Session] session the snmp session structure
278
+ def session_callback
279
+ @callback ||= FFI::Function.new(:int, [:int, :pointer, :int, :pointer, :pointer]) do |operation, session, reqid, pdu_ptr, magic|
280
+ op = case operation
281
+ when Core::Constants::NETSNMP_CALLBACK_OP_RECEIVED_MESSAGE then :success
282
+ when Core::Constants::NETSNMP_CALLBACK_OP_TIMED_OUT then :timeout
283
+ when Core::Constants::NETSNMP_CALLBACK_OP_SEND_FAILED then :send_failed
284
+ when Core::Constants::NETSNMP_CALLBACK_OP_CONNECT then :connect
285
+ when Core::Constants::NETSNMP_CALLBACK_OP_DISCONNECT then :disconnect
286
+ else :unrecognized_operation
287
+ end
288
+
289
+
290
+ # TODO: pass exception in case of failure
291
+
292
+ if reqid == @reqid
293
+ response_pdu = ResponsePDU.new(pdu_ptr)
294
+ # probably pass the result as a yield from a fiber
295
+ @requests[@reqid] = [op, response_pdu]
296
+
297
+ op.eql?(:unrecognized_operation) ? 0 : 1
298
+ else
299
+ puts "wow, unexpected #{op}.... #{reqid} different than #{@reqid}"
300
+ 0
301
+ end
302
+ end
303
+
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,181 @@
1
+ module NETSNMP
2
+ # Abstracts the PDU variable structure into a ruby object
3
+ #
4
+ class Varbind
5
+ Error = Class.new(Error)
6
+
7
+ attr_reader :struct
8
+
9
+ # @param [FFI::Pointer] pointer to the variable list
10
+ def initialize(pointer)
11
+ @struct = Core::Structures::VariableList.new(pointer)
12
+ end
13
+ end
14
+
15
+
16
+ # Abstracts the Varbind used for the PDU Request
17
+ class RequestVarbind < Varbind
18
+
19
+ # @param [RequestPDU] pdu the request pdu for this varbind
20
+ # @param [OID] oid the oid for this varbind
21
+ # @param [Object] value the value for the oid
22
+ # @param [Hash] options additional options
23
+ # @option options [Symbol, Integer, nil] :type C net-snmp type flag,
24
+ # type-label for value (see #convert_type), if not set it's inferred from the value
25
+ #
26
+ def initialize(pdu, oid, value, options={})
27
+ type = case options[:type]
28
+ when Integer then options[:type] # assume that the code is properly passed
29
+ when Symbol then convert_type(options[:type]) # DSL-specific API
30
+ when nil then infer_from_value(value)
31
+ else
32
+ raise Error, "#{options[:type]} is an unsupported type"
33
+ end
34
+
35
+ value_length = case type
36
+ when Core::Constants::ASN_NULL,
37
+ Core::Constants::SNMP_NOSUCHOBJECT,
38
+ Core::Constants::SNMP_NOSUCHINSTANCE,
39
+ Core::Constants::SNMP_ENDOFMIBVIEW
40
+ 0
41
+ else value ? value.size : 0
42
+ end
43
+ value = convert_value(value, type)
44
+
45
+ pointer = Core::LibSNMP.snmp_pdu_add_variable(pdu.pointer, oid.pointer, oid.length, type, value, value_length)
46
+ super(pointer)
47
+ end
48
+
49
+
50
+ private
51
+
52
+ # @param [Object] value value to infer the type from
53
+ # @return [Integer] the C net-snmp flag indicating the type
54
+ # @raise [Error] when the value is from an unexpected type
55
+ #
56
+ def infer_from_value(value)
57
+ case value
58
+ when String then Core::Constants::ASN_OCTET_STR
59
+ when Fixnum then Core::Constants::ASN_INTEGER
60
+ when OID then Core::Constants::ASN_OBJECT_ID
61
+ when nil then Core::Constants::ASN_NULL
62
+ else raise Error, "#{value} is from an unsupported type"
63
+ end
64
+ end
65
+
66
+ # @param [Symbol] symbol_type symbol representing the type
67
+ # @return [Integer] the C net-snmp flag indicating the type
68
+ # @raise [Error] when the symbol is unsupported
69
+ #
70
+ def convert_type(symbol_type)
71
+ case symbol_type
72
+ when :integer then Core::Constants::ASN_INTEGER
73
+ when :gauge then Core::Constants::ASN_GAUGE
74
+ when :counter then Core::Constants::ASN_COUNTER
75
+ when :timeticks then Core::Constants::ASN_TIMETICKS
76
+ when :unsigned then Core::Constants::ASN_UNSIGNED
77
+ when :boolean then Core::Constants::ASN_BOOLEAN
78
+ when :string then Core::Constants::ASN_OCTET_STR
79
+ when :binary then Core::Constants::ASN_BIT_STR
80
+ when :ip_address then Core::Constants::ASN_IPADDRESS
81
+ else
82
+ raise Error, "#{symbol_type} cannot be converted"
83
+ end
84
+ end
85
+
86
+ # @param [Object] value the value to convert
87
+ # @param [Integer] type the C net-snmp level object type flakg
88
+ #
89
+ # @return [FFI::Pointer] pointer to the memory location where the value is stored
90
+ #
91
+ def convert_value(value, type)
92
+ case type
93
+ when Core::Constants::ASN_INTEGER,
94
+ Core::Constants::ASN_GAUGE,
95
+ Core::Constants::ASN_COUNTER,
96
+ Core::Constants::ASN_TIMETICKS,
97
+ Core::Constants::ASN_UNSIGNED
98
+ new_val = FFI::MemoryPointer.new(:long)
99
+ new_val.write_long(value)
100
+ new_val
101
+ when Core::Constants::ASN_OCTET_STR,
102
+ Core::Constants::ASN_BIT_STR,
103
+ Core::Constants::ASN_OPAQUE
104
+ value
105
+ when Core::Constants::ASN_IPADDRESS
106
+ # TODO
107
+ when Core::Constants::ASN_OBJECT_ID
108
+ value.pointer
109
+ when Core::Constants::ASN_NULL,
110
+ Core::Constants::SNMP_NOSUCHOBJECT,
111
+ Core::Constants::SNMP_NOSUCHINSTANCE,
112
+ Core::Constants::SNMP_ENDOFMIBVIEW
113
+ nil
114
+ else
115
+ raise Error, "Unknown variable type: #{type}"
116
+ end
117
+ end
118
+ end
119
+
120
+ # Abstracts the Varbind used for the PDU Response
121
+ #
122
+ class ResponseVarbind < Varbind
123
+
124
+ attr_reader :value, :oid_code
125
+
126
+ # @param [FFI::Pointer] pointer pointer to the response varbind structure
127
+ #
128
+ # @note it loads the value and oid code on initialization
129
+ #
130
+ def initialize(pointer)
131
+ super
132
+ @value = load_varbind_value
133
+ @oid_code = load_oid_code
134
+ end
135
+
136
+ private
137
+
138
+ # @return [String] the oid code from the varbind
139
+ def load_oid_code
140
+ OID.read_pointer(@struct[:name], @struct[:name_length])
141
+ end
142
+
143
+ # @return [Object] the value for the varbind (a ruby type, a string, an integer, a symbol etc...)
144
+ #
145
+ def load_varbind_value
146
+ object_type = @struct[:type]
147
+ case object_type
148
+ when Core::Constants::ASN_OCTET_STR,
149
+ Core::Constants::ASN_OPAQUE
150
+ @struct[:val][:string].read_string(@struct[:val_len])
151
+ when Core::Constants::ASN_INTEGER
152
+ @struct[:val][:integer].read_long
153
+ when Core::Constants::ASN_UINTEGER,
154
+ Core::Constants::ASN_TIMETICKS,
155
+ Core::Constants::ASN_COUNTER,
156
+ Core::Constants::ASN_GAUGE
157
+ @struct[:val][:integer].read_ulong
158
+ when Core::Constants::ASN_IPADDRESS
159
+ @struct[:val][:objid].read_string(@struct[:val_len]).unpack('CCCC').join(".")
160
+ when Core::Constants::ASN_NULL
161
+ nil
162
+ when Core::Constants::ASN_OBJECT_ID
163
+ OID.from_pointer(@struct[:val][:objid], @struct[:val_len] / OID.default_size)
164
+ when Core::Constants::ASN_COUNTER64
165
+ counter = Core::Structures::Counter64.new(@struct[:val][:counter64])
166
+ counter[:high] * 2^32 + counter[:low]
167
+ when Core::Constants::ASN_BIT_STR
168
+ # XXX not sure what to do here. Is this obsolete?
169
+ when Core::Constants::SNMP_ENDOFMIBVIEW
170
+ :endofmibview
171
+ when Core::Constants::SNMP_NOSUCHOBJECT
172
+ :nosuchobject
173
+ when Core::Constants::SNMP_NOSUCHINSTANCE
174
+ :nosuchinstance
175
+ else
176
+ raise Error, "#{object_type} is an invalid type"
177
+ end
178
+ end
179
+
180
+ end
181
+ end