snmp4em 0.2.1 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ # Use `bundle install` in order to install these gems
2
+ # Use `bundle exec rake` in order to run the specs using the bundle
3
+
4
+ gem "eventmachine", "0.12.10"
5
+ gem "snmp", ">= 1.0.2"
6
+
7
+ gem "rspec", "1.3.0"
data/Manifest CHANGED
@@ -1,16 +1,29 @@
1
- README
1
+ Gemfile
2
+ README.rdoc
2
3
  Rakefile
3
4
  lib/snmp4em.rb
4
- lib/snmp4em/common.rb
5
+ lib/snmp4em/common_requests.rb
6
+ lib/snmp4em/extensions.rb
7
+ lib/snmp4em/extensions/snmp/integer.rb
8
+ lib/snmp4em/extensions/snmp/ip_address.rb
9
+ lib/snmp4em/extensions/snmp/null.rb
10
+ lib/snmp4em/extensions/snmp/object_id.rb
11
+ lib/snmp4em/extensions/snmp/octet_string.rb
12
+ lib/snmp4em/extensions/snmp/response_error.rb
5
13
  lib/snmp4em/handler.rb
14
+ lib/snmp4em/manager.rb
6
15
  lib/snmp4em/requests/snmp_get_request.rb
7
16
  lib/snmp4em/requests/snmp_getbulk_request.rb
8
17
  lib/snmp4em/requests/snmp_getnext_request.rb
9
18
  lib/snmp4em/requests/snmp_set_request.rb
10
19
  lib/snmp4em/requests/snmp_walk_request.rb
11
- lib/snmp4em/snmp_connection.rb
12
20
  lib/snmp4em/snmp_request.rb
13
- lib/snmp4em/snmp_v1.rb
14
- lib/snmp4em/snmp_v2.rb
15
- snmp4em.gemspec
21
+ lib/snmp4em/snmp_v2c_requests.rb
22
+ spec/models/test_message.rb
23
+ spec/models/test_request.rb
24
+ spec/models/test_response.rb
25
+ spec/spec.opts
26
+ spec/spec_helper.rb
27
+ spec/unit/handler_spec.rb
28
+ spec/unit/manager_spec.rb
16
29
  Manifest
@@ -2,12 +2,12 @@
2
2
 
3
3
  == Summary
4
4
 
5
- This library extends the Ruby-SNMP[http://snmplib.rubyforge.org] to use the asynchronous EventMachine[http://rubyeventmachine.com] library for added performance and scalability. This allows code to scale monitoring applications to access a very high number of devices without the need for complex asynchronous I/O handling.
5
+ This gem extends Ruby-SNMP[http://snmplib.rubyforge.org] to use the asynchronous EventMachine[http://rubyeventmachine.com] library for added performance and scalability. This allows code to scale monitoring applications to access a very high number of devices without the need for complex asynchronous I/O handling.
6
6
 
7
7
 
8
8
  == Features
9
9
 
10
- The initial version 0.1.0 of this software supports:
10
+ Version 0.2.1 supports:
11
11
 
12
12
  * SNMP v1 and v2 only
13
13
  * SNMP Get, GetNext, GetBulk (v2 only), Set, and Walk requests.
@@ -38,27 +38,27 @@ A few definitions:
38
38
 
39
39
  A simple SNMP-GET:
40
40
 
41
- EM::run do
42
- snmp = SNMP4EM::SNMPv1.new(:host => "192.168.1.1")
41
+ EM.run {
42
+ snmp = SNMP4EM::Manager.new(:host => "192.168.1.1", :version => :SNMPv1)
43
43
 
44
- request = snmp.get([OID_SYSNAME, OID_SYSLOCATION])
44
+ request = snmp.get([OID_SYSNAME, OID_SYSLOCATION])
45
45
 
46
- request.callback do |response|
47
- puts "System name = #{response[OID_SYSNAME]}"
48
- puts "System location = #{response[OID_SYSLOCATION]}"
49
- end
46
+ request.callback do |response|
47
+ puts "System name = #{response[OID_SYSNAME]}"
48
+ puts "System location = #{response[OID_SYSLOCATION]}"
49
+ end
50
50
 
51
- request.errback do |error|
52
- puts "GET got error #{error}"
53
- end
54
- end
51
+ request.errback do |error|
52
+ puts "GET got error #{error}"
53
+ end
54
+ }
55
55
 
56
56
  A simple SNMP-GETNEXT:
57
57
 
58
- EM::run do
59
- snmp = SNMP4EM::SNMPv1.new(:host => "192.168.1.1")
58
+ EM.run {
59
+ snmp = SNMP4EM::Manager.new(:host => "192.168.1.1")
60
60
 
61
- request = snmp.getnext([OID_SYSNAME])
61
+ request = snmp.getnext(OID_SYSNAME)
62
62
 
63
63
  request.callback do |response|
64
64
  r = response[OID_SYSNAME]
@@ -66,14 +66,14 @@ A simple SNMP-GETNEXT:
66
66
  end
67
67
 
68
68
  request.errback do |error|
69
- puts "GET got error #{error}"
69
+ puts "GETNEXT got error #{error}"
70
70
  end
71
- end
71
+ }
72
72
 
73
73
  A simple SNMP-SET:
74
74
 
75
- EM::run do
76
- snmp = SNMP4EM::SNMPv1.new(:host => "192.168.1.1")
75
+ EM.run {
76
+ snmp = SNMP4EM::Manager.new(:host => "192.168.1.1")
77
77
 
78
78
  request = snmp.set({OID_SYSNAME => "My System Name", OID_SYSLOCATION => "My System Location"})
79
79
 
@@ -92,16 +92,16 @@ A simple SNMP-SET:
92
92
  end
93
93
 
94
94
  request.errback do |error|
95
- puts "GET got error #{error}"
95
+ puts "SET got error #{error}"
96
96
  end
97
- end
97
+ }
98
98
 
99
99
  A simple SNMP-WALK:
100
100
 
101
- EM::run do
102
- snmp = SNMP4EM::SNMPv1.new(:host => "192.168.1.1")
101
+ EM.run {
102
+ snmp = SNMP4EM::Manager.new(:host => "192.168.1.1")
103
103
 
104
- request = snmp.walk([OID_SYSTEM])
104
+ request = snmp.walk(OID_SYSTEM)
105
105
 
106
106
  request.callback do |response|
107
107
  if (response[OID_SYSTEM].is_a? Array)
@@ -114,16 +114,16 @@ A simple SNMP-WALK:
114
114
  end
115
115
 
116
116
  request.errback do |error|
117
- puts "GET got error #{error}"
117
+ puts "WALK got error #{error}"
118
118
  end
119
- end
119
+ }
120
120
 
121
121
  A simple SNMP-GET-BULK:
122
122
 
123
- EM::run do
124
- snmp = SNMP4EM::SNMPv2.new(:host => "192.168.1.1")
123
+ EM.run {
124
+ snmp = SNMP4EM::Manager.new(:host => "192.168.1.1")
125
125
 
126
- request = snmp.getbulk([OID_SYSTEM])
126
+ request = snmp.getbulk(OID_SYSTEM)
127
127
 
128
128
  request.callback do |response|
129
129
  if (response[OID_SYSTEM].is_a? Array)
@@ -136,21 +136,24 @@ A simple SNMP-GET-BULK:
136
136
  end
137
137
 
138
138
  request.errback do |error|
139
- puts "GET got error #{error}"
139
+ puts "GET-BULK got error #{error}"
140
140
  end
141
- end
141
+ }
142
142
 
143
143
  == Change Log
144
144
 
145
- Version 0.1.0:
145
+ Version 0.2.1:
146
146
 
147
- * Initial deployment, ability to run get/getnext/set/walk requests in parallel
147
+ * Code cleanups, speed boosts
148
148
 
149
149
  Version 0.2.0:
150
150
 
151
151
  * Added support for SNMPv2, including GET-BULK operations
152
152
 
153
- == Developer Contact Information
153
+ Version 0.1.0:
154
+
155
+ * Initial deployment, ability to run get/getnext/set/walk requests in parallel
156
+
157
+ == Credits
154
158
 
155
- Norman Elton
156
- normelton@gmail.com
159
+ Author: Norman Elton normelton@gmail.com
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require "rubygems"
2
+ require "rake"
3
+ require "echoe"
4
+
5
+ gem "rspec", "1.3.0"
6
+
7
+ require "spec/rake/spectask"
8
+
9
+ Echoe.new("snmp4em", "0.2.1") do |p|
10
+ p.description = "A high-performance SNMP engine built on EventMachine and Ruby-SNMP"
11
+ p.url = "http://github.com/normelton/snmp4em"
12
+ p.author = "Norman Elton"
13
+ p.email = "normelton@gmail.com"
14
+ p.development_dependencies = []
15
+ p.rdoc_pattern = ["lib", "README.rdoc"]
16
+ p.dependencies = ["snmp"]
17
+ end
@@ -1,25 +1,7 @@
1
1
  # The SNMP4EM library
2
2
 
3
3
  module SNMP4EM
4
- class SNMPv1 < SnmpConnection
5
- attr_reader :community_ro, :community_rw
6
-
7
- # Creates a new object to communicate with SNMPv1 agents. Optionally pass in the following parameters:
8
- # * _host_ - IP/hostname of remote agent (default: 127.0.0.1)
9
- # * _port_ - UDP port on remote agent (default: 161)
10
- # * _community_ - Community string to use (default: public)
11
- # * _community_ro_ - Read-only community string to use for get/getnext/walk operations (default: public)
12
- # * _community_rw_ - Read-write community string to use for set operations (default: public)
13
- # * _timeout_ - Number of seconds to wait before a request times out (default: 1)
14
- # * _retries_ - Number of retries before failing (default: 3)
15
-
16
- def initialize(args = {})
17
- super(args)
18
-
19
- @community_ro = args[:community_ro] || args[:community] || "public"
20
- @community_rw = args[:community_rw] || args[:community] || "public"
21
- end
22
-
4
+ module CommonRequests
23
5
  # Sends an SNMP-GET request to the remote agent for all OIDs specified in the _oids_ array. Returns a SnmpGetRequest object,
24
6
  # which implements EM::Deferrable. From there, implement a callback/errback to fetch the result. On success, the result will be
25
7
  # a hash, mapping requested OID values to results.
@@ -28,9 +10,7 @@ module SNMP4EM
28
10
  # * _return_raw_ - Return objects and errors as their raw SNMP types, such as SNMP::Integer instead of native Ruby integers, SNMP::OctetString instead of native Ruby strings, etc. (default: false)
29
11
 
30
12
  def get(oids, args = {})
31
- request = SnmpGetRequest.new(self, oids, args.merge(:version => :SNMPv1))
32
- SnmpConnection.pending_requests << request
33
- return request
13
+ SnmpGetRequest.new(self, oids, args.merge(:version => @version))
34
14
  end
35
15
 
36
16
  # Sends an SNMP-GETNEXT request to the remote agent for all OIDs specified in the _oids_ array. Returns a SnmpGetRequest object,
@@ -42,9 +22,7 @@ module SNMP4EM
42
22
  # * _return_raw_ - Return objects and errors as their raw SNMP types, such as SNMP::Integer instead of native Ruby integers, SNMP::OctetString instead of native Ruby strings, etc. (default: false)
43
23
 
44
24
  def getnext(oids, args = {})
45
- request = SnmpGetNextRequest.new(self, oids, args.merge(:version => :SNMPv1))
46
- SnmpConnection.pending_requests << request
47
- return request
25
+ SnmpGetNextRequest.new(self, oids, args.merge(:version => @version))
48
26
  end
49
27
 
50
28
  # Sends an SNMP-SET request to the remote agent for all OIDs specified in the _oids_ hash. The hash must map OID values to requested
@@ -57,9 +35,7 @@ module SNMP4EM
57
35
  # * _return_raw_ - Return error objects as SNMP::ResponseError instead of a symbol
58
36
 
59
37
  def set(oids, args = {})
60
- request = SnmpSetRequest.new(self, oids, args.merge(:version => :SNMPv1))
61
- SnmpConnection.pending_requests << request
62
- return request
38
+ SnmpSetRequest.new(self, oids, args.merge(:version => @version))
63
39
  end
64
40
 
65
41
  # Sends a series of SNMP-GETNEXT requests to simulate an SNMP "walk" operation. Given an OID prefix, the library will keep requesting the
@@ -74,9 +50,7 @@ module SNMP4EM
74
50
  # * _max_results_ - Maximum number of results to be returned for any single OID prefix (default: nil = unlimited)
75
51
 
76
52
  def walk(oids, args = {})
77
- request = SnmpWalkRequest.new(self, oids, args.merge(:version => :SNMPv1))
78
- SnmpConnection.pending_requests << request
79
- return request
53
+ SnmpWalkRequest.new(self, oids, args.merge(:version => @version))
80
54
  end
81
55
  end
82
- end
56
+ end
@@ -0,0 +1,7 @@
1
+ module SNMP #:nodoc:
2
+
3
+ class Integer #:nodoc:
4
+ alias :rubify :to_i
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ module SNMP #:nodoc:
2
+
3
+ class IpAddress #:nodoc:
4
+ alias :rubify :to_s
5
+ end
6
+
7
+ end
@@ -0,0 +1,11 @@
1
+ module SNMP #:nodoc:
2
+
3
+ class Null #:nodoc:
4
+ class << self
5
+ def rubify
6
+ nil
7
+ end
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,7 @@
1
+ module SNMP #:nodoc:
2
+
3
+ class ObjectId #:nodoc:
4
+ alias :rubify :to_s
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ module SNMP #:nodoc:
2
+
3
+ class OctetString #:nodoc:
4
+ alias :rubify :to_s
5
+ end
6
+
7
+ end
@@ -0,0 +1,17 @@
1
+ module SNMP #:nodoc:
2
+
3
+ class ResponseError
4
+ attr_reader :error_status
5
+ alias :rubify :error_status #:nodoc:
6
+
7
+ def initialize(error_status) #:nodoc:
8
+ @error_status = error_status
9
+ end
10
+
11
+ # String representation of this error
12
+ def to_s
13
+ @error_status.to_s
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,6 @@
1
+ require "snmp4em/extensions/snmp/integer.rb"
2
+ require "snmp4em/extensions/snmp/ip_address.rb"
3
+ require "snmp4em/extensions/snmp/null.rb"
4
+ require "snmp4em/extensions/snmp/object_id.rb"
5
+ require "snmp4em/extensions/snmp/octet_string.rb"
6
+ require "snmp4em/extensions/snmp/response_error.rb"
@@ -1,18 +1,27 @@
1
1
  module SNMP4EM
2
- module Handler #:nodoc:
2
+ class Handler < EventMachine::Connection #:nodoc:
3
3
  def receive_data(data)
4
4
  begin
5
5
  message = SNMP::Message.decode(data)
6
6
  rescue Exception => err
7
+ # the request that this malformed response corresponds to
8
+ # will timeout and retry
7
9
  return
8
10
  end
9
11
 
10
12
  response = message.pdu
11
- request_id = response.request_id
12
-
13
- if (request = SnmpConnection.pending_requests.select{|r| r.snmp_id == request_id}.first)
13
+ request = Manager.pending_requests[response.request_id]
14
+
15
+ #
16
+ # in the event of a timeout retry, the request will have been
17
+ # pruned from the Manager, so the response is to an expired
18
+ # request, ignore it.
19
+ #
20
+
21
+ if request
22
+ request.timeout_timer.cancel
14
23
  request.handle_response(response)
15
24
  end
16
25
  end
17
26
  end
18
- end
27
+ end
@@ -0,0 +1,69 @@
1
+ # The SNMP4EM library
2
+
3
+ module SNMP4EM
4
+ class Manager
5
+ include SNMP4EM::CommonRequests
6
+
7
+ #
8
+ # @pending_requests maps a request's id to its SnmpRequest
9
+ #
10
+ @pending_requests = {}
11
+ @socket = nil
12
+
13
+ class << self
14
+ attr_reader :pending_requests
15
+ attr_reader :socket
16
+
17
+ def init_socket #:nodoc:
18
+ # When the socket is in error state, close the socket and re-open a new one.
19
+ if !@socket.nil? && @socket.error?
20
+ @socket.close_connection
21
+ @socket = nil
22
+ end
23
+
24
+ @socket ||= EM::open_datagram_socket("0.0.0.0", 0, Handler)
25
+ end
26
+
27
+ def track_request(request)
28
+ @pending_requests.delete(request.snmp_id)
29
+
30
+ begin
31
+ request.snmp_id = rand(2**31) # Largest SNMP Signed INTEGER
32
+ end while @pending_requests[request.snmp_id]
33
+
34
+ @pending_requests[request.snmp_id] = request
35
+ end
36
+ end
37
+
38
+ attr_reader :host, :port, :timeout, :retries, :version, :community_ro, :community_rw
39
+
40
+ # Creates a new object to communicate with SNMPv1 agents. Optionally pass in the following parameters:
41
+ # * _host_ - IP/hostname of remote agent (default: 127.0.0.1)
42
+ # * _port_ - UDP port on remote agent (default: 161)
43
+ # * _community_ - Community string to use (default: public)
44
+ # * _community_ro_ - Read-only community string to use for get/getnext/walk operations (default: public)
45
+ # * _community_rw_ - Read-write community string to use for set operations (default: public)
46
+ # * _timeout_ - Number of seconds to wait before a request times out (default: 1)
47
+ # * _retries_ - Number of retries before failing (default: 3)
48
+
49
+ def initialize(args = {})
50
+ @host = args[:host] || "127.0.0.1"
51
+ @port = args[:port] || 161
52
+ @timeout = args[:timeout] || 1
53
+ @retries = args[:retries] || 3
54
+ @version = args[:version] || :SNMPv2c
55
+
56
+ self.extend SNMPv2cRequests if @version == :SNMPv2c
57
+
58
+ @community_ro = args[:community_ro] || args[:community] || "public"
59
+ @community_rw = args[:community_rw] || args[:community] || "public"
60
+
61
+ self.class.init_socket
62
+ end
63
+
64
+ def send(message) #:nodoc:
65
+ self.class.socket.send_datagram message.encode, @host, @port
66
+ end
67
+
68
+ end
69
+ end
@@ -1,38 +1,22 @@
1
1
  module SNMP4EM
2
2
 
3
- # Returned from SNMP4EM::SNMPv1.get(). This implements EM::Deferrable, so you can hang a callback()
4
- # or errback() to retrieve the results.
3
+ # This implements EM::Deferrable, so you can hang a callback() or errback() to retrieve the results.
5
4
 
6
5
  class SnmpGetRequest < SnmpRequest
7
- attr_reader :snmp_id
6
+ attr_accessor :snmp_id
8
7
 
9
8
  # For an SNMP-GET request, @pending_oids will be a ruby array of SNMP::ObjectNames that need to be fetched. As
10
9
  # responses come back from the agent, this array will be pruned of any error-producing OIDs. Once no errors
11
10
  # are returned, the @responses hash will be populated and returned.
12
11
 
13
- def initialize(sender, oids, args = {}) #:nodoc:
14
- @sender = sender
15
-
16
- @timeout_timer = nil
17
- @timeout_retries = @sender.retries
18
- @error_retries = oids.size
19
-
20
- @version = args[:version] || :SNMPv1
21
- @return_raw = args[:return_raw] || false
22
-
23
- @responses = Hash.new
24
- @pending_oids = SNMP::VarBindList.new(oids).collect{|r| r.name}
25
-
26
- init_callbacks
27
- send
28
- end
29
-
30
12
  def handle_response(response) #:nodoc:
31
- if (response.error_status == :noError)
13
+ if response.error_status == :noError
32
14
  # No errors, populate the @responses object so it can be returned
33
15
  response.each_varbind do |vb|
34
16
  request_oid = @pending_oids.shift
35
- @responses[request_oid.to_s] = vb.value
17
+ value = @return_raw || !vb.value.respond_to?(:rubify) ? vb.value : vb.value.rubify
18
+
19
+ @responses[request_oid.to_s] = value
36
20
  end
37
21
 
38
22
  else
@@ -40,17 +24,10 @@ module SNMP4EM
40
24
  error_oid = @pending_oids.delete_at(response.error_index - 1)
41
25
  @responses[error_oid.to_s] = SNMP::ResponseError.new(response.error_status)
42
26
  end
43
-
44
- if (@pending_oids.empty? || @error_retries.zero?)
45
- until @pending_oids.empty?
46
- error_oid = @pending_oids.shift
47
- @responses[error_oid.to_s] = SNMP::ResponseError.new(:genErr)
48
- end
49
-
50
- @responses.each_pair do |oid, value|
51
- @responses[oid] = value.rubify if (!@return_raw && value.respond_to?(:rubify))
52
- end
53
-
27
+
28
+ if @error_retries < 0
29
+ fail "exhausted all retries"
30
+ elsif @pending_oids.empty?
54
31
  # Send the @responses back to the requester, we're done!
55
32
  succeed @responses
56
33
  else
@@ -62,13 +39,13 @@ module SNMP4EM
62
39
  private
63
40
 
64
41
  def send
65
- # Send the contents of @pending_oids
42
+ Manager.track_request(self)
66
43
 
67
- @snmp_id = generate_snmp_id
44
+ # Send the contents of @pending_oids
68
45
 
69
46
  vb_list = SNMP::VarBindList.new(@pending_oids)
70
47
  request = SNMP::GetRequest.new(@snmp_id, vb_list)
71
- message = SNMP::Message.new(@version, @sender.community_ro, request)
48
+ message = SNMP::Message.new(@sender.version, @sender.community_ro, request)
72
49
 
73
50
  super(message)
74
51
  end
@@ -1,37 +1,23 @@
1
1
  module SNMP4EM
2
2
 
3
- # Returned from SNMP4EM::SNMPv1.get(). This implements EM::Deferrable, so you can hang a callback()
4
- # or errback() to retrieve the results.
3
+ # This implements EM::Deferrable, so you can hang a callback() or errback() to retrieve the results.
5
4
 
6
5
  class SnmpGetBulkRequest < SnmpRequest
7
- attr_reader :snmp_id
6
+ attr_accessor :snmp_id
8
7
 
9
8
  # For an SNMP-GETBULK request, @pending_oids will be a ruby array of SNMP::ObjectNames that need to be fetched. As
10
9
  # responses come back from the agent, this array will be pruned of any error-producing OIDs. Once no errors
11
10
  # are returned, the @responses hash will be populated and returned.
12
11
 
13
12
  def initialize(sender, oids, args = {}) #:nodoc:
14
- @sender = sender
15
-
16
- @timeout_timer = nil
17
- @timeout_retries = @sender.retries
18
- @error_retries = oids.size
19
-
20
- @version = args[:version] || :SNMPv2c
21
- @return_raw = args[:return_raw] || false
22
-
23
13
  @nonrepeaters = args[:nonrepeaters] || 0
24
14
  @maxrepetitions = args[:maxrepetitions] || 10
25
15
 
26
- @responses = Hash.new
27
- @pending_oids = SNMP::VarBindList.new(oids).collect{|r| r.name}
28
-
29
- init_callbacks
30
- send
16
+ super
31
17
  end
32
18
 
33
19
  def handle_response(response) #:nodoc:
34
- if (response.error_status == :noError)
20
+ if response.error_status == :noError
35
21
  # No errors, populate the @responses object so it can be returned
36
22
 
37
23
  @nonrepeaters.times do |i|
@@ -85,9 +71,9 @@ module SNMP4EM
85
71
  private
86
72
 
87
73
  def send
88
- # Send the contents of @pending_oids
74
+ Manager.track_request(self)
89
75
 
90
- @snmp_id = generate_snmp_id
76
+ # Send the contents of @pending_oids
91
77
 
92
78
  vb_list = SNMP::VarBindList.new(@pending_oids)
93
79
  request = SNMP::GetBulkRequest.new(@snmp_id, vb_list)
@@ -95,7 +81,7 @@ module SNMP4EM
95
81
  request.max_repetitions = @maxrepetitions
96
82
  request.non_repeaters = @nonrepeaters
97
83
 
98
- message = SNMP::Message.new(@version, @sender.community_ro, request)
84
+ message = SNMP::Message.new(@sender.version, @sender.community_ro, request)
99
85
 
100
86
  super(message)
101
87
  end