netsnmp 0.0.0 → 0.0.2
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 +4 -4
- data/lib/netsnmp/oid.rb +65 -6
- data/lib/netsnmp/session.rb +81 -68
- data/lib/netsnmp/version.rb +1 -1
- data/netsnmp.gemspec +1 -0
- data/spec/client_spec.rb +29 -0
- data/spec/support/start_docker.sh +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2a043cc79c90239f031b1873089c8a23f50dd5b8
|
4
|
+
data.tar.gz: 05b0baf81082f36cc1a870c3f4f5a87b40041a60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 63c8a7195237c722b4fb7d0db9f00789ca475c7d080cc8d539f6e1f26adca2482c71d1064925fdd788f95f1f8cac9e89e33c06edc878c22b26be964948af0207
|
7
|
+
data.tar.gz: d30bb03a3a0fc19c34f9afd07512260ea0f4f4bc7f407b5b7c980e9bbb39e166730b34ec5be425b3435270b98e82e002d15b6992ae3834eeb1369bfae805bf19
|
data/lib/netsnmp/oid.rb
CHANGED
@@ -72,23 +72,82 @@ module NETSNMP
|
|
72
72
|
end
|
73
73
|
|
74
74
|
# SNMP v3-relevant OIDs
|
75
|
+
class AuthOID < OID
|
76
|
+
def generate_key(session, user, pass)
|
77
|
+
raise Error, "no given Authorization User" unless user
|
78
|
+
raise Error, "no given Authorization Password" unless pass
|
75
79
|
|
76
|
-
|
80
|
+
session[:securityAuthProto] = pointer
|
81
|
+
session[:securityName] = FFI::MemoryPointer.from_string(user)
|
82
|
+
session[:securityNameLen] = user.length
|
83
|
+
|
84
|
+
auth_len_ptr = FFI::MemoryPointer.new(:size_t)
|
85
|
+
auth_len_ptr.write_int(Core::Constants::USM_AUTH_KU_LEN)
|
86
|
+
|
87
|
+
auth_key_result = Core::LibSNMP.generate_Ku(pointer,
|
88
|
+
session[:securityAuthProtoLen],
|
89
|
+
pass,
|
90
|
+
pass.length,
|
91
|
+
session[:securityAuthKey],
|
92
|
+
auth_len_ptr)
|
93
|
+
unless auth_key_result == Core::Constants::SNMPERR_SUCCESS
|
94
|
+
raise AuthenticationFailed, "failed to authenticate #{auth_user} in #{@host}"
|
95
|
+
end
|
96
|
+
session[:securityAuthKeyLen] = auth_len_ptr.read_int
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class PrivOID < OID
|
101
|
+
|
102
|
+
def generate_key(session, user, pass)
|
103
|
+
raise Error, "no given Priv User" unless user
|
104
|
+
raise Error, "no given Priv Password" unless pass
|
105
|
+
|
106
|
+
session[:securityPrivProto] = pointer
|
107
|
+
|
108
|
+
# other necessary lengths
|
109
|
+
priv_len_ptr = FFI::MemoryPointer.new(:size_t)
|
110
|
+
priv_len_ptr.write_int(Core::Constants::USM_PRIV_KU_LEN)
|
111
|
+
|
112
|
+
# NOTE I know this is handing off the AuthProto, but generates a proper
|
113
|
+
# key for encryption, and using PrivProto does not.
|
114
|
+
priv_key_result = Core::LibSNMP.generate_Ku(session[:securityAuthProto],
|
115
|
+
session[:securityAuthProtoLen],
|
116
|
+
pass,
|
117
|
+
pass.length,
|
118
|
+
session[:securityPrivKey],
|
119
|
+
priv_len_ptr)
|
120
|
+
|
121
|
+
unless priv_key_result == Core::Constants::SNMPERR_SUCCESS
|
122
|
+
raise AuthenticationFailed, "failed to authenticate #{auth_user} in #{@host}"
|
123
|
+
end
|
124
|
+
session[:securityPrivKeyLen] = priv_len_ptr.read_int
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class MD5OID < AuthOID
|
77
130
|
def initialize ; super("1.3.6.1.6.3.10.1.1.2") ; end
|
78
131
|
end
|
79
|
-
class SHA1OID <
|
132
|
+
class SHA1OID < AuthOID
|
80
133
|
def initialize ; super("1.3.6.1.6.3.10.1.1.3") ; end
|
81
134
|
end
|
82
|
-
class NoAuthOID <
|
135
|
+
class NoAuthOID < AuthOID
|
83
136
|
def initialize ; super("1.3.6.1.6.3.10.1.1.1") ; end
|
137
|
+
def generate_key(session, *)
|
138
|
+
session[:securityAuthProto] = pointer
|
139
|
+
end
|
84
140
|
end
|
85
|
-
class AESOID <
|
141
|
+
class AESOID < PrivOID
|
86
142
|
def initialize ; super("1.3.6.1.6.3.10.1.2.4") ; end
|
87
143
|
end
|
88
|
-
class DESOID <
|
144
|
+
class DESOID < PrivOID
|
89
145
|
def initialize ; super("1.3.6.1.6.3.10.1.2.2") ; end
|
90
146
|
end
|
91
|
-
class NoPrivOID <
|
147
|
+
class NoPrivOID < PrivOID
|
92
148
|
def initialize ; super("1.3.6.1.6.3.10.1.2.1") ; end
|
149
|
+
def generate_key(session, *)
|
150
|
+
session[:securityPrivProto] = pointer
|
151
|
+
end
|
93
152
|
end
|
94
153
|
end
|
data/lib/netsnmp/session.rb
CHANGED
@@ -22,12 +22,17 @@ module NETSNMP
|
|
22
22
|
#
|
23
23
|
def initialize(host, opts)
|
24
24
|
@host = host
|
25
|
+
# this is because other evented clients might discover IP first, but hostnames
|
26
|
+
# give you better trackability of errors. Give the opportunity to the users to
|
27
|
+
# pass it, by setting the hostname explicitly. If not, fallback to the host.
|
28
|
+
@hostname = opts.fetch(:hostname, @host)
|
25
29
|
@options = opts
|
30
|
+
@logged_at = nil
|
26
31
|
@request = nil
|
27
32
|
# For now, let's eager load the signature
|
28
33
|
@signature = build_signature(@options)
|
29
34
|
if @signature.null?
|
30
|
-
raise ConnectionFailed, "could not
|
35
|
+
raise ConnectionFailed, "could not build signature for #@hostname"
|
31
36
|
end
|
32
37
|
@requests ||= {}
|
33
38
|
end
|
@@ -44,7 +49,7 @@ module NETSNMP
|
|
44
49
|
transport.close rescue nil
|
45
50
|
end
|
46
51
|
if Core::LibSNMP.snmp_sess_close(@signature) == 0
|
47
|
-
raise Error, "#@
|
52
|
+
raise Error, "#@hostname: Couldn't clean up session properly"
|
48
53
|
end
|
49
54
|
end
|
50
55
|
|
@@ -60,6 +65,26 @@ module NETSNMP
|
|
60
65
|
|
61
66
|
private
|
62
67
|
|
68
|
+
LoggedInTimeout = Class.new(Timeout::Error)
|
69
|
+
def try_login
|
70
|
+
return yield unless @logged_at.nil?
|
71
|
+
|
72
|
+
begin
|
73
|
+
# problem for snmp is, there is no login process.
|
74
|
+
# signature is sent on first packet. As we are not using the synch interface, this will not
|
75
|
+
# be handled by the core library. Which sucks. But even core library would just retry and timeout
|
76
|
+
# at some point.
|
77
|
+
# we do something similar: before any PDU succeeds, after first PDU is async-sent, we wait for reads, but we can't block. If
|
78
|
+
# the socket hasn't anything to read, we can assume it was the wrong USERNAME (?).
|
79
|
+
# TODO: research if this is true.
|
80
|
+
Timeout.timeout(@timeout, LoggedInTimeout) do
|
81
|
+
yield
|
82
|
+
end
|
83
|
+
rescue LoggedInTimeout
|
84
|
+
raise ConnectionFailed, "failed to login to #@hostname"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
63
88
|
def transport
|
64
89
|
@transport ||= fetch_transport
|
65
90
|
end
|
@@ -73,31 +98,40 @@ module NETSNMP
|
|
73
98
|
if ( @reqid = Core::LibSNMP.snmp_sess_async_send(@signature, pdu.pointer, session_callback, nil) ) == 0
|
74
99
|
# it's interesting, pdu's are only fred if the async send is successful... netsnmp 1 - me 0
|
75
100
|
Core::LibSNMP.snmp_free_pdu(pdu.pointer)
|
76
|
-
|
101
|
+
# if it's the first time we're passing here and send fails, we can (?) assume that
|
102
|
+
# AUTH_PASSWORD is wrong
|
103
|
+
if @logged_at.nil?
|
104
|
+
raise ConnectionFailed, "failed to login to #@hostname"
|
105
|
+
else
|
106
|
+
raise SendError, "#@hostname: Failed to send pdu"
|
107
|
+
end
|
77
108
|
end
|
78
109
|
end
|
79
110
|
|
80
111
|
def read
|
81
112
|
receive # trigger callback ahead of time and wait for it
|
113
|
+
# Sounds a bit unreasonable, but only after we arrived here we know for sure that the credentials are proper.
|
114
|
+
# So we can set this variable, so further errors can be safely ignored.
|
115
|
+
@logged_at ||= Time.now
|
82
116
|
handle_response
|
83
117
|
end
|
84
118
|
|
85
119
|
def handle_response
|
86
120
|
operation, response_pdu = @requests.delete(@reqid)
|
87
121
|
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
122
|
when :success
|
93
123
|
response_pdu
|
124
|
+
when :send_failed
|
125
|
+
raise ReceiveError, "#@hostname: Failed to receive pdu"
|
126
|
+
when :timeout
|
127
|
+
raise Timeout::Error, "#@hostname: timed out while waiting for pdu response"
|
94
128
|
else
|
95
|
-
raise Error, "#@
|
129
|
+
raise Error, "#@hostname: unrecognized operation for request #{@reqid}: #{operation} for #{response_pdu}"
|
96
130
|
end
|
97
131
|
end
|
98
132
|
|
99
133
|
def receive
|
100
|
-
readers, _ = wait_readable
|
134
|
+
readers, _ = try_login { wait_readable }
|
101
135
|
case readers.size
|
102
136
|
when 1..Float::INFINITY
|
103
137
|
# triggers callback
|
@@ -105,13 +139,19 @@ module NETSNMP
|
|
105
139
|
when 0
|
106
140
|
Core::LibSNMP.snmp_sess_timeout(@signature)
|
107
141
|
else
|
108
|
-
raise ReceiveError, "#@
|
142
|
+
raise ReceiveError, "#@hostname: error receiving data"
|
109
143
|
end
|
110
144
|
end
|
111
145
|
|
112
146
|
def async_read
|
113
147
|
if Core::LibSNMP.snmp_sess_read(@signature, get_selectable_sockets.pointer) != 0
|
114
|
-
|
148
|
+
# if it's the first time we're passing here and send fails, we can (?) assume that
|
149
|
+
# PRIV_PASSWORD is wrong
|
150
|
+
if @logged_at.nil?
|
151
|
+
raise ConnectionFailed, "failed to login to #@hostname"
|
152
|
+
else
|
153
|
+
raise ReceiveError, "#@hostname: Failed to receive pdu response"
|
154
|
+
end
|
115
155
|
end
|
116
156
|
end
|
117
157
|
|
@@ -164,79 +204,52 @@ module NETSNMP
|
|
164
204
|
return unless session[:version] == Core::Constants::SNMP_VERSION_3
|
165
205
|
|
166
206
|
|
207
|
+
session[:securityAuthProtoLen] = 10
|
208
|
+
session[:securityAuthKeyLen] = Core::Constants::USM_AUTH_KU_LEN
|
209
|
+
session[:securityPrivProtoLen] = 10
|
210
|
+
session[:securityPrivKeyLen] = Core::Constants::USM_PRIV_KU_LEN
|
211
|
+
|
167
212
|
# Security Authorization
|
168
|
-
session[:securityLevel] = options[:security_level]
|
213
|
+
session[:securityLevel] = case options[:security_level]
|
214
|
+
when /noauth/ then Core::Constants::SNMP_SEC_LEVEL_NOAUTH
|
215
|
+
when /auth_?no_?priv/ then Core::Constants::SNMP_SEC_LEVEL_AUTHNOPRIV
|
216
|
+
when /auth_?priv/ then Core::Constants::SNMP_SEC_LEVEL_AUTHPRIV
|
217
|
+
when Integer
|
218
|
+
options[:security_level]
|
219
|
+
else Core::Constants::SNMP_SEC_LEVEL_AUTHPRIV
|
220
|
+
end
|
221
|
+
|
169
222
|
auth_protocol_oid = case options[:auth_protocol]
|
170
223
|
when :md5 then MD5OID.new
|
171
224
|
when :sha1 then SHA1OID.new
|
172
225
|
when nil then NoAuthOID.new
|
173
|
-
else raise Error, "#@
|
226
|
+
else raise Error, "#@hostname: #{options[:auth_protocol]} is an unsupported authorization protocol"
|
174
227
|
end
|
175
228
|
|
176
|
-
|
177
|
-
|
178
|
-
# Priv Protocol
|
229
|
+
# Priv Protocol
|
179
230
|
priv_protocol_oid = case options[:priv_protocol]
|
180
231
|
when :aes then AESOID.new
|
181
232
|
when :des then DESOID.new
|
182
233
|
when nil then NoPrivOID.new
|
183
|
-
else raise Error, "#@
|
234
|
+
else raise Error, "#@hostname: #{options[:priv_protocol]} is an unsupported privacy protocol"
|
184
235
|
end
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
session
|
189
|
-
session[:securityAuthKeyLen] = Core::Constants::USM_AUTH_KU_LEN
|
190
|
-
session[:securityPrivProtoLen] = 10
|
191
|
-
session[:securityPrivKeyLen] = Core::Constants::USM_PRIV_KU_LEN
|
192
|
-
|
236
|
+
|
237
|
+
user, auth_pass, priv_pass = options.values_at(:username, :auth_password, :priv_password)
|
238
|
+
auth_protocol_oid.generate_key(session, user, auth_pass)
|
239
|
+
priv_protocol_oid.generate_key(session, user, priv_pass )
|
193
240
|
|
194
241
|
if options[:context]
|
195
242
|
session[:contextName] = FFI::MemoryPointer.from_string(options[:context])
|
196
243
|
session[:contextNameLen] = options[:context].length
|
197
244
|
end
|
198
245
|
|
199
|
-
|
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
|
246
|
+
|
234
247
|
end
|
235
248
|
|
236
249
|
|
237
250
|
# @param [Hash] options options to open the net-snmp session
|
238
251
|
# @option options [String] :community the snmp community string (defaults to public)
|
239
|
-
# @option options [Integer] :timeout number of
|
252
|
+
# @option options [Integer] :timeout number of sec until first timeout
|
240
253
|
# @option options [Integer] :retries number of retries before timeout
|
241
254
|
# @return [FFI::Pointer] a pointer to the validated session signature, which will therefore be used in all _sess_ methods from libnetsnmp
|
242
255
|
def build_signature(options)
|
@@ -259,9 +272,9 @@ module NETSNMP
|
|
259
272
|
|
260
273
|
session[:peername] = FFI::MemoryPointer.from_string(peername)
|
261
274
|
|
262
|
-
|
263
|
-
session[:
|
264
|
-
|
275
|
+
@timeout = options[:timeout] || 10
|
276
|
+
session[:timeout] = @timeout * 1000000
|
277
|
+
session[:retries] = options[:retries] || 5
|
265
278
|
session_authorization(session, options)
|
266
279
|
Core::LibSNMP.snmp_sess_open(session.pointer)
|
267
280
|
end
|
@@ -289,14 +302,14 @@ module NETSNMP
|
|
289
302
|
|
290
303
|
# TODO: pass exception in case of failure
|
291
304
|
|
305
|
+
response_pdu = ResponsePDU.new(pdu_ptr)
|
306
|
+
@requests[@reqid] = [op, response_pdu]
|
292
307
|
if reqid == @reqid
|
293
|
-
response_pdu = ResponsePDU.new(pdu_ptr)
|
294
308
|
# probably pass the result as a yield from a fiber
|
295
|
-
@requests[@reqid] = [op, response_pdu]
|
296
|
-
|
297
309
|
op.eql?(:unrecognized_operation) ? 0 : 1
|
298
310
|
else
|
299
|
-
|
311
|
+
# this is happening when user is unknown(????)
|
312
|
+
#puts "wow, unexpected #{op}.... #{reqid} different than #{@reqid}"
|
300
313
|
0
|
301
314
|
end
|
302
315
|
end
|
data/lib/netsnmp/version.rb
CHANGED
data/netsnmp.gemspec
CHANGED
data/spec/client_spec.rb
CHANGED
@@ -12,6 +12,35 @@ RSpec.describe NETSNMP::Client do
|
|
12
12
|
|
13
13
|
subject { described_class.new(host, options) }
|
14
14
|
|
15
|
+
describe "establishing session" do
|
16
|
+
let(:oid) { "1.3.6.1.2.1.1.5.0" } # sysName.0
|
17
|
+
context "with a wrong auth password" do
|
18
|
+
let(:options) { host_options.merge( auth_password: "auctoritas2", timeout: 5) }
|
19
|
+
it {
|
20
|
+
expect {
|
21
|
+
subject.get(oid)
|
22
|
+
}.to raise_error(NETSNMP::ConnectionFailed)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
context "with a wrong priv password" do
|
26
|
+
let(:options) { host_options.merge( priv_password: "privatus2", timeout: 5) }
|
27
|
+
it {
|
28
|
+
expect {
|
29
|
+
subject.get(oid)
|
30
|
+
}.to raise_error(NETSNMP::ConnectionFailed)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
context "with an unexisting user" do
|
35
|
+
let(:options) { host_options.merge(username: "simulata", timeout: 5) }
|
36
|
+
it {
|
37
|
+
expect {
|
38
|
+
subject.get(oid)
|
39
|
+
}.to raise_error(NETSNMP::ConnectionFailed)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
15
44
|
describe "#get" do
|
16
45
|
let(:oid) { "1.3.6.1.2.1.1.5.0" } # sysName.0
|
17
46
|
let(:value) { subject.get(oid) }
|
@@ -1,4 +1,5 @@
|
|
1
1
|
sudo docker build --build-arg http_proxy=$http_proxy --build-arg https_proxy=$https_proxy -t snmp-server-emulator -f spec/support/Dockerfile .
|
2
2
|
sudo docker run -d -p :1161/udp --name test-snmp-emulator snmp-server-emulator --agent-udpv4-endpoint=0.0.0.0:1161 --agent-udpv6-endpoint='[::0]:1161'
|
3
|
+
sleep 20 # give some time for the simulator to boot
|
3
4
|
|
4
5
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: netsnmp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tiago Cardoso
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-06
|
11
|
+
date: 2016-09-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ffi
|
@@ -147,7 +147,8 @@ files:
|
|
147
147
|
homepage: ''
|
148
148
|
licenses:
|
149
149
|
- Apache-2.0
|
150
|
-
metadata:
|
150
|
+
metadata:
|
151
|
+
allowed_push_hosts: https://rubygems.org/
|
151
152
|
post_install_message:
|
152
153
|
rdoc_options: []
|
153
154
|
require_paths:
|