logstash-input-snmp 0.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d344864bfc29473393a8d06527a52b5a606efcb8384ce300751898be6daec7ca
4
+ data.tar.gz: 3fdf527c93de3d59a6f7c3f20e07e3bfc02d893706b649d2ee7c5e310097e7d0
5
+ SHA512:
6
+ metadata.gz: 8d2dc6418f15151c5ade6a900fb28f352020c78e9b95b579b9d8a594f84f539e6bb65570c074f41d9b7ed4c5ba96c6ffac5a745512094af74cb2f3860e0cc277
7
+ data.tar.gz: 827ed778277f612c1e99982bd8a039d99fcc87b9221f7b26e400c1a672d1b2cf12c21e4a83f4a32ca208a619db9ae31a8fac468d249a3b8e2ee47c6f76bc5a76
@@ -0,0 +1,3 @@
1
+ ## 0.1.0.beta1
2
+ - First beta version
3
+
@@ -0,0 +1,10 @@
1
+ The following is a list of people who have contributed ideas, code, bug
2
+ reports, or in general have helped logstash along its way.
3
+
4
+ Contributors:
5
+ * Colin Surprenant - colin.surprenant@gmail.com
6
+
7
+ Note: If you've sent us patches, bug reports, or otherwise contributed to
8
+ Logstash, and you aren't on the list above and want to be, please let us know
9
+ and we'll make sure you're here. Contributions from folks like you are what make
10
+ open source awesome.
@@ -0,0 +1,2 @@
1
+ # logstash-input-snmp
2
+ Example input plugin. This should help bootstrap your effort to write your own input plugin!
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ logstash_path = ENV["LOGSTASH_PATH"] || "../../logstash"
6
+ use_logstash_source = ENV["LOGSTASH_SOURCE"] && ENV["LOGSTASH_SOURCE"].to_s == "1"
7
+
8
+ if Dir.exist?(logstash_path) && use_logstash_source
9
+ gem 'logstash-core', :path => "#{logstash_path}/logstash-core"
10
+ gem 'logstash-core-plugin-api', :path => "#{logstash_path}/logstash-core-plugin-api"
11
+ end
data/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Licensed under the Apache License, Version 2.0 (the "License");
2
+ you may not use this file except in compliance with the License.
3
+ You may obtain a copy of the License at
4
+
5
+ http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
@@ -0,0 +1,86 @@
1
+ # Logstash Plugin
2
+
3
+ This is a plugin for [Logstash](https://github.com/elastic/logstash).
4
+
5
+ It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
6
+
7
+ ## Documentation
8
+
9
+ Logstash provides infrastructure to automatically generate documentation for this plugin. We use the asciidoc format to write documentation so any comments in the source code will be first converted into asciidoc and then into html. All plugin documentation are placed under one [central location](http://www.elastic.co/guide/en/logstash/current/).
10
+
11
+ - For formatting code or config example, you can use the asciidoc `[source,ruby]` directive
12
+ - For more asciidoc formatting tips, see the excellent reference here https://github.com/elastic/docs#asciidoc-guide
13
+
14
+ ## Need Help?
15
+
16
+ Need help? Try #logstash on freenode IRC or the https://discuss.elastic.co/c/logstash discussion forum.
17
+
18
+ ## Developing
19
+
20
+ ### 1. Plugin Developement and Testing
21
+
22
+ #### Code
23
+ - To get started, you'll need JRuby with the Bundler gem installed.
24
+
25
+ - Create a new plugin or clone and existing from the GitHub [logstash-plugins](https://github.com/logstash-plugins) organization. We also provide [example plugins](https://github.com/logstash-plugins?query=example).
26
+
27
+ - Install dependencies
28
+ ```sh
29
+ bundle install
30
+ ```
31
+
32
+ #### Test
33
+
34
+ - Update your dependencies
35
+
36
+ ```sh
37
+ bundle install
38
+ ```
39
+
40
+ - Run tests
41
+
42
+ ```sh
43
+ bundle exec rspec
44
+ ```
45
+
46
+ ### 2. Running your unpublished Plugin in Logstash
47
+
48
+ #### 2.1 Run in a local Logstash clone
49
+
50
+ - Edit Logstash `Gemfile` and add the local plugin path, for example:
51
+ ```ruby
52
+ gem "logstash-filter-awesome", :path => "/your/local/logstash-filter-awesome"
53
+ ```
54
+ - Install plugin
55
+ ```sh
56
+ bin/logstash-plugin install --no-verify
57
+ ```
58
+ - Run Logstash with your plugin
59
+ ```sh
60
+ bin/logstash -e 'filter {awesome {}}'
61
+ ```
62
+ At this point any modifications to the plugin code will be applied to this local Logstash setup. After modifying the plugin, simply rerun Logstash.
63
+
64
+ #### 2.2 Run in an installed Logstash
65
+
66
+ You can use the same **2.1** method to run your plugin in an installed Logstash by editing its `Gemfile` and pointing the `:path` to your local plugin development directory or you can build the gem and install it using:
67
+
68
+ - Build your plugin gem
69
+ ```sh
70
+ gem build logstash-filter-awesome.gemspec
71
+ ```
72
+ - Install the plugin from the Logstash home
73
+ ```sh
74
+ bin/logstash-plugin install /your/local/plugin/logstash-filter-awesome.gem
75
+ ```
76
+ - Start Logstash and proceed to test the plugin
77
+
78
+ ## Contributing
79
+
80
+ All contributions are welcome: ideas, patches, documentation, bug reports, complaints, and even something you drew up on a napkin.
81
+
82
+ Programming is not a required skill. Whatever you've seen about open source and maintainers or community members saying "send patches or die" - you will not see that here.
83
+
84
+ It is more important to the community that you are able to contribute.
85
+
86
+ For more information about contributing, see the [CONTRIBUTING](https://github.com/elastic/logstash/blob/master/CONTRIBUTING.md) file.
@@ -0,0 +1,4 @@
1
+ # AUTOGENERATED BY THE GRADLE SCRIPT. DO NOT EDIT.
2
+
3
+ require 'jar_dependencies'
4
+ require_jar('org.snmp4j', 'snmp4j', '2.5.11')
@@ -0,0 +1,158 @@
1
+ # encoding: utf-8
2
+ require "logstash/inputs/base"
3
+ require "logstash/namespace"
4
+ require "stud/interval"
5
+ require "socket" # for Socket.gethostname
6
+ require_relative "snmp/client"
7
+ require_relative "snmp/mib"
8
+
9
+ # Generate a repeating message.
10
+ #
11
+ # This plugin is intented only as an example.
12
+
13
+ class LogStash::Inputs::Snmp < LogStash::Inputs::Base
14
+ config_name "snmp"
15
+
16
+ # List of OIDs for which we want to retrieve the scalar value
17
+ config :get,:validate => :array # ["1.3.6.1.2.1.1.1.0"]
18
+
19
+ # List of OIDs for which we want to retrieve the subtree of information
20
+ config :walk,:validate => :array # ["1.3.6.1.2.1.1.1.0"]
21
+
22
+ # List of hosts to query the configured `get` and `walk` options.
23
+ #
24
+ # Each host definition is a hash and must define the `host` key and value.
25
+ # `host` must use the format {tcp|udp}:{ip address}/{port}
26
+ # for example `host => "udp:127.0.0.1/161"`
27
+ # Each host definition can optionally include the following keys and values:
28
+ # `community` with a default value of `public`
29
+ # `version` with a default value of `2c`
30
+ # `retries` with a detault value of `2`
31
+ # `timeout` in milliseconds with a default value of `1000`
32
+ config :hosts, :validate => :array #[ {"host" => "udp:127.0.0.1/161", "community" => "public"} ]
33
+
34
+ # List of paths of MIB .dic files of dirs. If a dir path is specified, all files with .dic extension will be loaded.
35
+ #
36
+ # ATTENTION: a MIB .dic file must be generated using the libsmi library `smidump` command line utility
37
+ # like this for example. Here the `RFC1213-MIB.txt` file is an ASN.1 MIB file.
38
+ #
39
+ # `$ smidump -k -f python RFC1213-MIB.txt > RFC1213-MIB.dic`
40
+ #
41
+ # The OSS libsmi library https://www.ibr.cs.tu-bs.de/projects/libsmi/ is available & installable
42
+ # on most OS.
43
+ config :mib_paths, :validate => :array # ["path/to/mib.dic", "path/to/mib/dir"]
44
+
45
+ # number of OID root digits to ignore in event field name. For example, in a numeric OID
46
+ # like 1.3.6.1.2.1.1.1.0" the first 5 digits could be ignored by setting oid_root_skip => 5
47
+ # which would result in a field name "1.1.1.0". Similarly when a MIB is used an OID such
48
+ # as "1.3.6.1.2.mib-2.system.sysDescr.0" would become "mib-2.system.sysDescr.0"
49
+ config :oid_root_skip, :validate => :number, :default => 0
50
+
51
+ # Set polling interval in seconds
52
+ #
53
+ # The default, `30`, means poll each host every 30second.
54
+ config :interval, :validate => :number, :default => 30
55
+
56
+ def register
57
+ validate_oids!
58
+ validate_hosts!
59
+
60
+ mib = LogStash::SnmpMib.new
61
+ Array(@mib_paths).each do |path|
62
+ # TODO handle errors
63
+ mib.add_mib_path(path)
64
+ end
65
+
66
+ @client_definitions = []
67
+ @hosts.each do |host|
68
+ host_name = host["host"]
69
+ community = host["community"] || "public"
70
+ version = host["version"] || "2c"
71
+ retries = host["retries"] || 2
72
+ timeout = host["timeout"] || 1000
73
+
74
+ definition = {
75
+ :client => LogStash::SnmpClient.new(host_name, community, version, retries, timeout, mib),
76
+ :get => Array(get),
77
+ :walk => Array(walk),
78
+ }
79
+ @client_definitions << definition
80
+ end
81
+ end
82
+
83
+ def run(queue)
84
+ # for now a naive single threaded poller which sleeps for the given interval between
85
+ # each run. each run polls all the defined hosts for the get and walk options.
86
+ while !stop?
87
+ @client_definitions.each do |definition|
88
+ result = {}
89
+ if !definition[:get].empty?
90
+ begin
91
+ result = result.merge(definition[:client].get(definition[:get], @oid_root_skip))
92
+ rescue => e
93
+ logger.error("error invoking get operation on OIDs: #{definition[:get]}, ignoring", :exception => e, :backtrace => e.backtrace)
94
+ end
95
+ end
96
+ if !definition[:walk].empty?
97
+ definition[:walk].each do |oid|
98
+ begin
99
+ result = result.merge(definition[:client].walk(oid, @oid_root_skip))
100
+ rescue => e
101
+ logger.error("error invoking walk operation on OID: #{oid}, ignoring", :exception => e, :backtrace => e.backtrace)
102
+ end
103
+ end
104
+ end
105
+
106
+ unless result.empty?
107
+ event = LogStash::Event.new(result)
108
+ decorate(event)
109
+ queue << event
110
+ end
111
+ end
112
+
113
+ Stud.stoppable_sleep(@interval) { stop? }
114
+ end
115
+ end
116
+
117
+ def stop
118
+ end
119
+
120
+ private
121
+
122
+ OID_REGEX = /^\.?([0-9\.]+)$/
123
+ HOST_REGEX = /^(udp|tcp):.+\/\d+$/i
124
+
125
+ def validate_oids!
126
+ @get = Array(@get).map do |oid|
127
+ # verify oids for valid pattern and get rid or any leading dot if present
128
+ unless oid =~ OID_REGEX
129
+ raise(LogStash::ConfigurationError, "The get option oid '#{oid}' has an invalid format")
130
+ end
131
+ $1
132
+ end
133
+
134
+ @walk = Array(@walk).map do |oid|
135
+ # verify oids for valid pattern and get rid or any leading dot if present
136
+ unless oid =~ OID_REGEX
137
+ raise(LogStash::ConfigurationError, "The walk option oid '#{oid}' has an invalid format")
138
+ end
139
+ $1
140
+ end
141
+
142
+ raise(LogStash::ConfigurationError, "at least one get OID or one walk OID is required") if @get.empty? && @walk.empty?
143
+ end
144
+
145
+ def validate_hosts!
146
+ # TODO: for new we only validate the host part, not the other optional options
147
+
148
+ raise(LogStash::ConfigurationError, "at least one host definition is required") if Array(@hosts).empty?
149
+
150
+ @hosts.each do |host|
151
+ raise(LogStash::ConfigurationError, "each host definition must have a \"host\" option") if !host.is_a?(Hash) || host["host"].nil?
152
+ unless host["host"] =~ HOST_REGEX
153
+ raise(LogStash::ConfigurationError, "invalid host option format")
154
+ end
155
+ raise(LogStash::ConfigurationError, "tcp is not yet supported, only udp") if $1 =~ /tcp/i
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,135 @@
1
+ require "java"
2
+ require "logstash-input-snmp_jars.rb"
3
+
4
+ java_import "org.snmp4j.CommunityTarget"
5
+ java_import "org.snmp4j.PDU"
6
+ java_import "org.snmp4j.Snmp"
7
+ java_import "org.snmp4j.Target"
8
+ java_import "org.snmp4j.TransportMapping"
9
+ java_import "org.snmp4j.event.ResponseEvent"
10
+ java_import "org.snmp4j.mp.SnmpConstants"
11
+ java_import "org.snmp4j.smi.Address"
12
+ java_import "org.snmp4j.smi.GenericAddress"
13
+ java_import "org.snmp4j.smi.OID"
14
+ java_import "org.snmp4j.smi.OctetString"
15
+ java_import "org.snmp4j.smi.VariableBinding"
16
+ java_import "org.snmp4j.transport.DefaultUdpTransportMapping"
17
+ java_import "org.snmp4j.util.TreeUtils"
18
+ java_import "org.snmp4j.util.DefaultPDUFactory"
19
+ java_import "org.snmp4j.asn1.BER"
20
+
21
+ module LogStash
22
+ class SnmpClientError < StandardError
23
+ end
24
+
25
+ class SnmpClient
26
+
27
+ def initialize(address, community, version, retries, timeout, mib)
28
+ @target = build_target(address, community, version, retries, timeout)
29
+ @mib = mib
30
+
31
+ # for now hardwired udp transport
32
+ transport = DefaultUdpTransportMapping.new
33
+ @snmp = Snmp.new(transport)
34
+ transport.listen()
35
+ end
36
+
37
+ def get(oids, strip_root = 0)
38
+ pdu = PDU.new
39
+ Array(oids).each { |oid| pdu.add(VariableBinding.new(OID.new(oid))) }
40
+ pdu.setType(PDU::GET)
41
+
42
+ response_event = @snmp.send(pdu, @target, nil)
43
+ return nil if response_event.nil?
44
+
45
+ e = response_event.getError
46
+ raise(SnmpClientError, "error sending snmp get request: #{e.inspect}, #{e.getMessage}") if e
47
+
48
+ result = {}
49
+ response_pdu = response_event.getResponse
50
+ raise(SnmpClientError, "timeout sending snmp get request") if response_pdu.nil?
51
+
52
+ size = response_pdu.size
53
+ (0..size - 1).each do |i|
54
+ variable_binding = response_pdu.get(i)
55
+ oid = variable_binding.getOid.toString
56
+ variable = variable_binding.getVariable
57
+ value = coerce(variable)
58
+
59
+ result[@mib.map_oid(oid, strip_root)] = value
60
+ end
61
+
62
+ result
63
+ end
64
+
65
+
66
+ def walk(oid, strip_root = 0)
67
+ result = {}
68
+ treeUtils = TreeUtils.new(@snmp, DefaultPDUFactory.new)
69
+ events = treeUtils.getSubtree(@target, OID.new(oid))
70
+ return nil if events.nil? || events.size == 0
71
+
72
+ events.each do |event|
73
+ next if event.nil?
74
+
75
+ if event.isError
76
+ # TODO: see if we can salvage non errored event here
77
+ raise(SnmpClientError, "error sending snmp walk request: #{event.getErrorMessage}")
78
+ end
79
+
80
+ var_bindings = event.getVariableBindings
81
+ next if var_bindings.nil? || var_bindings.size == 0
82
+
83
+ var_bindings.each do |var_binding|
84
+ next if var_binding.nil?
85
+
86
+ oid = var_binding.getOid.toString
87
+ variable = var_binding.getVariable
88
+ value = coerce(variable)
89
+
90
+ result[@mib.map_oid(oid, strip_root)] = value
91
+ end
92
+ end
93
+
94
+ result
95
+ end
96
+
97
+ private
98
+
99
+ def coerce(variable)
100
+ variable_syntax = variable.getSyntax
101
+ # puts("variable.getSyntaxString=#{variable.getSyntaxString}")
102
+ case variable_syntax
103
+ when BER::OCTETSTRING
104
+ variable.toString
105
+ when BER::TIMETICKS, BER::COUNTER, BER::COUNTER32
106
+ variable.toLong
107
+ when BER::INTEGER, BER::INTEGER32, BER::GAUGE, BER::GAUGE32
108
+ variable.toInt
109
+ when BER::IPADDRESS
110
+ variable.toString
111
+ when BER::OID
112
+ variable.toString
113
+ when BER::NOSUCHOBJECT
114
+ "Error: No Such Instance currently exists at this OID"
115
+ else
116
+ raise(SnmpClientError, "unknown variable syntax #{variable_syntax}, #{variable.getSyntaxString}")
117
+ end
118
+ end
119
+
120
+ def build_target(address, community, version, retries, timeout)
121
+ target = CommunityTarget.new
122
+ target.setCommunity(OctetString.new(community))
123
+ target.setAddress(GenericAddress.parse(address))
124
+ target.setRetries(retries)
125
+ target.setTimeout(timeout)
126
+ target.setVersion(parse_version(version))
127
+ target
128
+ end
129
+
130
+ def parse_version(version)
131
+ # TODO implement
132
+ SnmpConstants.version2c
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,159 @@
1
+ # encoding: utf-8
2
+
3
+ module LogStash
4
+ class SnmpMibError < StandardError
5
+ end
6
+
7
+ class SnmpMib
8
+ attr_reader :tree
9
+
10
+ class Oid
11
+ def self.parse(oid)
12
+ oid.split(".").map(&:to_i)
13
+ end
14
+ end
15
+
16
+ class BaseNode
17
+ attr_reader :name, :childs
18
+
19
+ def initialize(name)
20
+ @name = name
21
+ @childs = []
22
+ end
23
+ end
24
+
25
+ class Node < BaseNode
26
+ attr_reader :node_type, :module_name, :oid, :oid_path
27
+
28
+ def initialize(node_type, name, module_name, oid)
29
+ super(name)
30
+ @node_type = node_type
31
+ @module_name = module_name
32
+ @oid = oid
33
+ @oid_path = Oid.parse(oid)
34
+ end
35
+ end
36
+
37
+ class Tree
38
+ def initialize
39
+ @root = BaseNode.new("root")
40
+ end
41
+
42
+ def add_node(node)
43
+ warnings = []
44
+ current = @root
45
+ path = node.oid_path.dup
46
+
47
+ # follow the OID path up until but not including the last node
48
+ # and add intermediate missing nodes if needed
49
+ last_node = path.pop
50
+ path.each do |i|
51
+ if current.childs[i].nil?
52
+ current.childs[i] = BaseNode.new(i.to_s)
53
+ end
54
+ current = current.childs[i]
55
+ end
56
+
57
+ if current.childs[last_node] && current.childs[last_node].name != node.name
58
+ warnings << "warning: overwriting MIB OID '#{node.oid}' and name '#{current.childs[last_node].name}' with new name '#{node.name}' from module '#{node.module_name}'"
59
+ end
60
+ current.childs[last_node] = node
61
+
62
+ warnings
63
+ end
64
+
65
+ def map_oid(oid, strip_root = 0)
66
+ path = Oid.parse(oid)
67
+
68
+ result = []
69
+ node = @root
70
+
71
+ loop do
72
+ break if path.empty?
73
+ i = path.shift
74
+
75
+ node = node.childs[i]
76
+
77
+ if node.nil?
78
+ result += path.unshift(i)
79
+ break
80
+ end
81
+ result << node.name
82
+ end
83
+
84
+ result.drop(strip_root).join(".")
85
+ end
86
+ end
87
+
88
+ def initialize
89
+ @tree = Tree.new
90
+ end
91
+
92
+ # add a specific mib dic file or all mib dic files of the given directory to the current mib database
93
+ # @param path [String] a file or directory path to mib dic file(s)
94
+ # @return [Array] array of warning strings if any OID or name has been overwritten or the empty array when no warning
95
+ def add_mib_path(path)
96
+ dic_files = if ::File.directory?(path)
97
+ Dir[::File.join(path, "*.dic")]
98
+ elsif ::File.file?(path)
99
+ [path]
100
+ else
101
+ raise(SnmpMibError, "file or directory path expected: #{path.to_s}")
102
+ end
103
+
104
+ warnings = []
105
+ dic_files.each do |f|
106
+ module_name, nodes = read_mib_dic(f)
107
+
108
+ nodes.each do |k, v|
109
+ warnings += @tree.add_node(Node.new(v["nodetype"], k, v["moduleName"], v["oid"]))
110
+ end
111
+ end
112
+
113
+ warnings
114
+ end
115
+
116
+ # read and parse a mib dic file
117
+ #
118
+ # @param filename [String] file path of a mib dic file
119
+ # @return [[String, Hash, Hash, Hash]] the 2-tuple of the mib module name and the complete nodes
120
+ def read_mib_dic(filename)
121
+ mib = eval_mib_dic(filename)
122
+ raise(SnmpMibError, "invalid mib dic format for file #{filename}") unless mib
123
+ module_name = mib["moduleName"]
124
+ raise(SnmpMibError, "invalid mib dic format for file #{filename}") unless module_name
125
+ nodes = mib["nodes"]
126
+ raise(SnmpMibError, "no nodes defined in mib dic file #{filename}") unless nodes
127
+
128
+ # name_hash is { mib-name => oid }
129
+ # name_hash = {}
130
+ # nodes.each { |k, v| name_hash[k] = v["oid"] }
131
+ # if mib["notifications"]
132
+ # mib["notifications"].each { |k, v| name_hash[k] = v["oid"] }
133
+ # end
134
+
135
+ [module_name, nodes]
136
+ end
137
+
138
+ def map_oid(oid, strip_root = 0)
139
+ @tree.map_oid(oid, strip_root)
140
+ end
141
+
142
+ private
143
+
144
+ def eval_mib_dic(filename)
145
+ mib_dic = IO.read(filename)
146
+ mib_hash = mib_dic.
147
+ gsub(':', '=>'). # fix hash syntax
148
+ gsub('(', '[').gsub(')', ']'). # fix tuple syntax
149
+ sub('FILENAME =', 'filename ='). # get rid of constants
150
+ sub('MIB =', 'mib =')
151
+
152
+ mib = nil
153
+ eval(mib_hash)
154
+ mib
155
+ rescue => e
156
+ raise(SnmpMibError, "error parsing mib dic file: #{filename}, error: #{e.message}")
157
+ end
158
+ end
159
+ end