netsnmp 0.0.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.
@@ -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