logstash-input-relp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MTA3ZmU0ZDgzYzVlOWIxZTRhMzFlODU1NTFlNDgyZTQ5MTU4ZGY0OA==
5
+ data.tar.gz: !binary |-
6
+ MDAxNDczYmU0ZTU0OTE5N2U5OGE2OWY0ZTdjMDBmNzYwM2Q5YzU3Mg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZTdjMmI2ZjRhYWNiZDhjZTBjYWQyNzhjMTRhMWRiODBmNWMxNjc1ODJhZmVk
10
+ OWM5OTZlZWQ4ZWNhYTM4Yzg3MGM3MTFjNGRmMTllMDFlZjQzZjEyYTExZDEz
11
+ ZTQyMjk5ZTBlMDExOGM0YjA1MzAzM2ZkNWZlN2M3MTkxZTMxNzY=
12
+ data.tar.gz: !binary |-
13
+ MjM0NzFlNjNlZWQ5MTU0ODNkZTAxODJlZmMzNzEwZDgyOGQ0MGQ3YzVhOTI5
14
+ MDk5Y2ZhYmU5ZTVkODUyMDE0OTk0YzJkODY4MGE0OTFkZjc5NWI0ZmYxZTli
15
+ MGIwYmNkNWZiNzBjNWQ1OTM1NzhjYzMyZWZjZWZiOGQzN2NkZTI=
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .bundle
4
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+ gem 'rake'
3
+ gem 'gem_publisher'
4
+ gem 'archive-tar-minitar'
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2012-2014 Elasticsearch <http://www.elasticsearch.org>
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ @files=[]
2
+
3
+ task :default do
4
+ system("rake -T")
5
+ end
6
+
@@ -0,0 +1,107 @@
1
+ # encoding: utf-8
2
+ require "logstash/inputs/base"
3
+ require "logstash/namespace"
4
+ require "logstash/util/relp"
5
+ require "logstash/util/socket_peer"
6
+
7
+
8
+
9
+ # Read RELP events over a TCP socket.
10
+ #
11
+ # For more information about RELP, see
12
+ # <http://www.rsyslog.com/doc/imrelp.html>
13
+ #
14
+ # This protocol implements application-level acknowledgements to help protect
15
+ # against message loss.
16
+ #
17
+ # Message acks only function as far as messages being put into the queue for
18
+ # filters; anything lost after that point will not be retransmitted
19
+ class LogStash::Inputs::Relp < LogStash::Inputs::Base
20
+ class Interrupted < StandardError; end
21
+
22
+ config_name "relp"
23
+ milestone 1
24
+
25
+ default :codec, "plain"
26
+
27
+ # The address to listen on.
28
+ config :host, :validate => :string, :default => "0.0.0.0"
29
+
30
+ # The port to listen on.
31
+ config :port, :validate => :number, :required => true
32
+
33
+ def initialize(*args)
34
+ super(*args)
35
+ end # def initialize
36
+
37
+ public
38
+ def register
39
+ @logger.info("Starting relp input listener", :address => "#{@host}:#{@port}")
40
+ @relp_server = RelpServer.new(@host, @port,['syslog'])
41
+ end # def register
42
+
43
+ private
44
+ def relp_stream(relpserver,socket,output_queue,client_address)
45
+ loop do
46
+ frame = relpserver.syslog_read(socket)
47
+ @codec.decode(frame["message"]) do |event|
48
+ decorate(event)
49
+ event["host"] = client_address
50
+ output_queue << event
51
+ end
52
+
53
+ #To get this far, the message must have made it into the queue for
54
+ #filtering. I don't think it's possible to wait for output before ack
55
+ #without fundamentally breaking the plugin architecture
56
+ relpserver.ack(socket, frame['txnr'])
57
+ end
58
+ end
59
+
60
+ public
61
+ def run(output_queue)
62
+ @thread = Thread.current
63
+ loop do
64
+ begin
65
+ # Start a new thread for each connection.
66
+ Thread.start(@relp_server.accept) do |client|
67
+ rs = client[0]
68
+ socket = client[1]
69
+ # monkeypatch a 'peer' method onto the socket.
70
+ socket.instance_eval { class << self; include ::LogStash::Util::SocketPeer end }
71
+ peer = socket.peer
72
+ @logger.debug("Relp Connection to #{peer} created")
73
+ begin
74
+ relp_stream(rs,socket, output_queue, peer)
75
+ rescue Relp::ConnectionClosed => e
76
+ @logger.debug("Relp Connection to #{peer} Closed")
77
+ rescue Relp::RelpError => e
78
+ @logger.warn('Relp error: '+e.class.to_s+' '+e.message)
79
+ #TODO: Still not happy with this, are they all warn level?
80
+ #Will this catch everything I want it to?
81
+ #Relp spec says to close connection on error, ensure this is the case
82
+ end
83
+ end # Thread.start
84
+ rescue Relp::InvalidCommand,Relp::InappropriateCommand => e
85
+ @logger.warn('Relp client trying to open connection with something other than open:'+e.message)
86
+ rescue Relp::InsufficientCommands
87
+ @logger.warn('Relp client incapable of syslog')
88
+ rescue IOError, Interrupted
89
+ if @interrupted
90
+ # Intended shutdown, get out of the loop
91
+ @relp_server.shutdown
92
+ break
93
+ else
94
+ # Else it was a genuine IOError caused by something else, so propagate it up..
95
+ raise
96
+ end
97
+ end
98
+ end # loop
99
+ end # def run
100
+
101
+ def teardown
102
+ @interrupted = true
103
+ @thread.raise(Interrupted.new)
104
+ end
105
+ end # class LogStash::Inputs::Relp
106
+
107
+ #TODO: structured error logging
@@ -0,0 +1,326 @@
1
+ # encoding: utf-8
2
+ require "socket"
3
+
4
+ class Relp#This isn't much use on its own, but gives RelpServer and RelpClient things
5
+
6
+ RelpVersion = '0'#TODO: spec says this is experimental, but rsyslog still seems to exclusively use it
7
+ RelpSoftware = 'logstash,1.1.1,http://logstash.net'
8
+
9
+ class RelpError < StandardError; end
10
+ class InvalidCommand < RelpError; end
11
+ class InappropriateCommand < RelpError; end
12
+ class ConnectionClosed < RelpError; end
13
+ class InsufficientCommands < RelpError; end
14
+
15
+ def valid_command?(command)
16
+ valid_commands = Array.new
17
+
18
+ #Allow anything in the basic protocol for both directions
19
+ valid_commands << 'open'
20
+ valid_commands << 'close'
21
+
22
+ #These are things that are part of the basic protocol, but only valid in one direction (rsp, close etc.) TODO: would they be invalid or just innapropriate?
23
+ valid_commands += @basic_relp_commands
24
+
25
+ #These are extra commands that we require, otherwise refuse the connection TODO: some of these are only valid on one direction
26
+ valid_commands += @required_relp_commands
27
+
28
+ #TODO: optional_relp_commands
29
+
30
+ #TODO: vague mentions of abort and starttls commands in spec need looking into
31
+ return valid_commands.include?(command)
32
+ end
33
+
34
+ def frame_write(socket, frame)
35
+ unless self.server? #I think we have to trust a server to be using the correct txnr
36
+ #Only allow txnr to be 0 or be determined automatically
37
+ frame['txnr'] = self.nexttxnr() unless frame['txnr']==0
38
+ end
39
+ frame['txnr'] = frame['txnr'].to_s
40
+ frame['message'] = '' if frame['message'].nil?
41
+ frame['datalen'] = frame['message'].length.to_s
42
+ wiredata=[
43
+ frame['txnr'],
44
+ frame['command'],
45
+ frame['datalen'],
46
+ frame['message']
47
+ ].join(' ').strip
48
+ begin
49
+ @logger.debug? and @logger.debug("Writing to socket", :data => wiredata)
50
+ socket.write(wiredata)
51
+ #Ending each frame with a newline is required in the specifications
52
+ #Doing it a separately is useful (but a bit of a bodge) because
53
+ #for some reason it seems to take 2 writes after the server closes the
54
+ #connection before we get an exception
55
+ socket.write("\n")
56
+ rescue Errno::EPIPE,IOError,Errno::ECONNRESET#TODO: is this sufficient to catch all broken connections?
57
+ raise ConnectionClosed
58
+ end
59
+ return frame['txnr'].to_i
60
+ end
61
+
62
+ def frame_read(socket)
63
+ begin
64
+ frame = Hash.new
65
+ frame['txnr'] = socket.readline(' ').strip.to_i
66
+ frame['command'] = socket.readline(' ').strip
67
+
68
+ #Things get a little tricky here because if the length is 0 it is not followed by a space.
69
+ leading_digit=socket.read(1)
70
+ if leading_digit=='0' then
71
+ frame['datalen'] = 0
72
+ frame['message'] = ''
73
+ else
74
+ frame['datalen'] = (leading_digit + socket.readline(' ')).strip.to_i
75
+ frame['message'] = socket.read(frame['datalen'])
76
+ end
77
+ @logger.debug? and @logger.debug("Read frame", :frame => frame)
78
+ rescue EOFError,Errno::ECONNRESET,IOError
79
+ raise ConnectionClosed
80
+ end
81
+ if ! self.valid_command?(frame['command'])#TODO: is this enough to catch framing errors?
82
+ if self.server?
83
+ self.serverclose(socket)
84
+ else
85
+ self.close
86
+ end
87
+ raise InvalidCommand,frame['command']
88
+ end
89
+ return frame
90
+ end
91
+
92
+ def server?
93
+ @server
94
+ end
95
+
96
+ end
97
+
98
+ class RelpServer < Relp
99
+
100
+ def initialize(host,port,required_commands=[])
101
+ @logger = Cabin::Channel.get(LogStash)
102
+
103
+ @server=true
104
+
105
+ #These are things that are part of the basic protocol, but only valid in one direction (rsp, close etc.)
106
+ @basic_relp_commands = ['close']#TODO: check for others
107
+
108
+ #These are extra commands that we require, otherwise refuse the connection
109
+ @required_relp_commands = required_commands
110
+
111
+ begin
112
+ @server = TCPServer.new(host, port)
113
+ rescue Errno::EADDRINUSE
114
+ @logger.error("Could not start RELP server: Address in use",
115
+ :host => host, :port => port)
116
+ raise
117
+ end
118
+ @logger.info? and @logger.info("Started RELP Server", :host => host, :port => port)
119
+ end
120
+
121
+ def accept
122
+ socket = @server.accept
123
+ frame=self.frame_read(socket)
124
+ if frame['command'] == 'open'
125
+ offer=Hash[*frame['message'].scan(/^(.*)=(.*)$/).flatten]
126
+ if offer['relp_version'].nil?
127
+ @logger.warn("No relp version specified")
128
+ #if no version specified, relp spec says we must close connection
129
+ self.serverclose(socket)
130
+ raise RelpError, 'No relp_version specified'
131
+ #subtracting one array from the other checks to see if all elements in @required_relp_commands are present in the offer
132
+ elsif ! (@required_relp_commands - offer['commands'].split(',')).empty?
133
+ @logger.warn("Not all required commands are available", :required => @required_relp_commands, :offer => offer['commands'])
134
+ #Tell them why we're closing the connection:
135
+ response_frame = Hash.new
136
+ response_frame['txnr'] = frame['txnr']
137
+ response_frame['command'] = 'rsp'
138
+ response_frame['message'] = '500 Required command(s) '
139
+ + (@required_relp_commands - offer['commands'].split(',')).join(',')
140
+ + ' not offered'
141
+ self.frame_write(socket,response_frame)
142
+ self.serverclose(socket)
143
+ raise InsufficientCommands, offer['commands']
144
+ + ' offered, require ' + @required_relp_commands.join(',')
145
+ else
146
+ #attempt to set up connection
147
+ response_frame = Hash.new
148
+ response_frame['txnr'] = frame['txnr']
149
+ response_frame['command'] = 'rsp'
150
+
151
+ response_frame['message'] = '200 OK '
152
+ response_frame['message'] += 'relp_version=' + RelpVersion + "\n"
153
+ response_frame['message'] += 'relp_software=' + RelpSoftware + "\n"
154
+ response_frame['message'] += 'commands=' + @required_relp_commands.join(',')#TODO: optional ones
155
+ self.frame_write(socket, response_frame)
156
+ return self, socket
157
+ end
158
+ else
159
+ self.serverclose(socket)
160
+ raise InappropriateCommand, frame['command'] + ' expecting open'
161
+ end
162
+ end
163
+
164
+ #This does not ack the frame, just reads it
165
+ def syslog_read(socket)
166
+ frame = self.frame_read(socket)
167
+ if frame['command'] == 'syslog'
168
+ return frame
169
+ elsif frame['command'] == 'close'
170
+ #the client is closing the connection, acknowledge the close and act on it
171
+ response_frame = Hash.new
172
+ response_frame['txnr'] = frame['txnr']
173
+ response_frame['command'] = 'rsp'
174
+ self.frame_write(socket,response_frame)
175
+ self.serverclose(socket)
176
+ raise ConnectionClosed
177
+ else
178
+ #the client is trying to do something unexpected
179
+ self.serverclose(socket)
180
+ raise InappropriateCommand, frame['command'] + ' expecting syslog'
181
+ end
182
+ end
183
+
184
+ def serverclose(socket)
185
+ frame = Hash.new
186
+ frame['txnr'] = 0
187
+ frame['command'] = 'serverclose'
188
+ begin
189
+ self.frame_write(socket,frame)
190
+ socket.close
191
+ rescue ConnectionClosed
192
+ end
193
+ end
194
+
195
+ def shutdown
196
+ @server.close
197
+ rescue Exception#@server might already be down
198
+ end
199
+
200
+ def ack(socket, txnr)
201
+ frame = Hash.new
202
+ frame['txnr'] = txnr
203
+ frame['command'] = 'rsp'
204
+ frame['message'] = '200 OK'
205
+ self.frame_write(socket, frame)
206
+ end
207
+
208
+ end
209
+
210
+ #This is only used by the tests; any problems here are not as important as elsewhere
211
+ class RelpClient < Relp
212
+
213
+ def initialize(host,port,required_commands = [],buffer_size = 128,
214
+ retransmission_timeout=10)
215
+ @logger = Cabin::Channel.get(LogStash)
216
+ @logger.info? and @logger.info("Starting RELP client", :host => host, :port => port)
217
+ @server = false
218
+ @buffer = Hash.new
219
+
220
+ @buffer_size = buffer_size
221
+ @retransmission_timeout = retransmission_timeout
222
+
223
+ #These are things that are part of the basic protocol, but only valid in one direction (rsp, close etc.)
224
+ @basic_relp_commands = ['serverclose','rsp']#TODO: check for others
225
+
226
+ #These are extra commands that we require, otherwise refuse the connection
227
+ @required_relp_commands = required_commands
228
+
229
+ @socket=TCPSocket.new(host,port)
230
+
231
+ #This'll start the automatic frame numbering
232
+ @lasttxnr = 0
233
+
234
+ offer=Hash.new
235
+ offer['command'] = 'open'
236
+ offer['message'] = 'relp_version=' + RelpVersion + "\n"
237
+ offer['message'] += 'relp_software=' + RelpSoftware + "\n"
238
+ offer['message'] += 'commands=' + @required_relp_commands.join(',')#TODO: add optional ones
239
+ self.frame_write(@socket, offer)
240
+ response_frame = self.frame_read(@socket)
241
+ if response_frame['message'][0,3] != '200'
242
+ raise RelpError,response_frame['message']
243
+ end
244
+
245
+ response=Hash[*response_frame['message'][7..-1].scan(/^(.*)=(.*)$/).flatten]
246
+ if response['relp_version'].nil?
247
+ #if no version specified, relp spec says we must close connection
248
+ self.close()
249
+ raise RelpError, 'No relp_version specified; offer: '
250
+ + response_frame['message'][6..-1].scan(/^(.*)=(.*)$/).flatten
251
+
252
+ #subtracting one array from the other checks to see if all elements in @required_relp_commands are present in the offer
253
+ elsif ! (@required_relp_commands - response['commands'].split(',')).empty?
254
+ #if it can't receive syslog it's useless to us; close the connection
255
+ self.close()
256
+ raise InsufficientCommands, response['commands'] + ' offered, require '
257
+ + @required_relp_commands.join(',')
258
+ end
259
+ #If we've got this far with no problems, we're good to go
260
+ @logger.info? and @logger.info("Connection establish with server")
261
+
262
+ #This thread deals with responses that come back
263
+ reader = Thread.start do
264
+ loop do
265
+ f = self.frame_read(@socket)
266
+ if f['command'] == 'rsp' && f['message'] == '200 OK'
267
+ @buffer.delete(f['txnr'])
268
+ elsif f['command'] == 'rsp' && f['message'][0,1] == '5'
269
+ #TODO: What if we get an error for something we're already retransmitted due to timeout?
270
+ new_txnr = self.frame_write(@socket, @buffer[f['txnr']])
271
+ @buffer[new_txnr] = @buffer[f['txnr']]
272
+ @buffer.delete(f['txnr'])
273
+ elsif f['command'] == 'serverclose' || f['txnr'] == @close_txnr
274
+ break
275
+ else
276
+ #Don't know what's going on if we get here, but it can't be good
277
+ raise RelpError#TODO: raising errors like this makes no sense
278
+ end
279
+ end
280
+ end
281
+
282
+ #While this one deals with frames for which we get no reply
283
+ Thread.start do
284
+ old_buffer = Hash.new
285
+ loop do
286
+ #This returns old txnrs that are still present
287
+ (@buffer.keys & old_buffer.keys).each do |txnr|
288
+ new_txnr = self.frame_write(@socket, @buffer[txnr])
289
+ @buffer[new_txnr] = @buffer[txnr]
290
+ @buffer.delete(txnr)
291
+ end
292
+ old_buffer = @buffer
293
+ sleep @retransmission_timeout
294
+ end
295
+ end
296
+ end
297
+
298
+ #TODO: have a way to get back unacked messages on close
299
+ def close
300
+ frame = Hash.new
301
+ frame['command'] = 'close'
302
+ @close_txnr=self.frame_write(@socket, frame)
303
+ #TODO: ought to properly wait for a reply etc. The serverclose will make it work though
304
+ sleep @retransmission_timeout
305
+ @socket.close#TODO: shutdown?
306
+ return @buffer
307
+ end
308
+
309
+ def syslog_write(logline)
310
+
311
+ #If the buffer is already full, wait until a gap opens up
312
+ sleep 0.1 until @buffer.length<@buffer_size
313
+
314
+ frame = Hash.new
315
+ frame['command'] = 'syslog'
316
+ frame['message'] = logline
317
+
318
+ txnr = self.frame_write(@socket, frame)
319
+ @buffer[txnr] = frame
320
+ end
321
+
322
+ def nexttxnr
323
+ @lasttxnr += 1
324
+ end
325
+
326
+ end
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-input-relp'
4
+ s.version = '0.1.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "Read RELP events over a TCP socket."
7
+ s.description = "Read RELP events over a TCP socket."
8
+ s.authors = ["Elasticsearch"]
9
+ s.email = 'richard.pijnenburg@elasticsearch.com'
10
+ s.homepage = "http://logstash.net/"
11
+ s.require_paths = ["lib"]
12
+
13
+ # Files
14
+ s.files = `git ls-files`.split($\)+::Dir.glob('vendor/*')
15
+
16
+ # Tests
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ # Special flag to let us know this is actually a logstash plugin
20
+ s.metadata = { "logstash_plugin" => "true", "group" => "input" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency 'logstash', '>= 1.4.0', '< 2.0.0'
24
+
25
+ s.add_runtime_dependency 'logstash-codec-plain'
26
+ end
27
+
@@ -0,0 +1,9 @@
1
+ require "gem_publisher"
2
+
3
+ desc "Publish gem to RubyGems.org"
4
+ task :publish_gem do |t|
5
+ gem_file = Dir.glob(File.expand_path('../*.gemspec',File.dirname(__FILE__))).first
6
+ gem = GemPublisher.publish_if_updated(gem_file, :rubygems)
7
+ puts "Published #{gem}" if gem
8
+ end
9
+
@@ -0,0 +1,169 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "digest/sha1"
4
+
5
+ def vendor(*args)
6
+ return File.join("vendor", *args)
7
+ end
8
+
9
+ directory "vendor/" => ["vendor"] do |task, args|
10
+ mkdir task.name
11
+ end
12
+
13
+ def fetch(url, sha1, output)
14
+
15
+ puts "Downloading #{url}"
16
+ actual_sha1 = download(url, output)
17
+
18
+ if actual_sha1 != sha1
19
+ fail "SHA1 does not match (expected '#{sha1}' but got '#{actual_sha1}')"
20
+ end
21
+ end # def fetch
22
+
23
+ def file_fetch(url, sha1)
24
+ filename = File.basename( URI(url).path )
25
+ output = "vendor/#{filename}"
26
+ task output => [ "vendor/" ] do
27
+ begin
28
+ actual_sha1 = file_sha1(output)
29
+ if actual_sha1 != sha1
30
+ fetch(url, sha1, output)
31
+ end
32
+ rescue Errno::ENOENT
33
+ fetch(url, sha1, output)
34
+ end
35
+ end.invoke
36
+
37
+ return output
38
+ end
39
+
40
+ def file_sha1(path)
41
+ digest = Digest::SHA1.new
42
+ fd = File.new(path, "r")
43
+ while true
44
+ begin
45
+ digest << fd.sysread(16384)
46
+ rescue EOFError
47
+ break
48
+ end
49
+ end
50
+ return digest.hexdigest
51
+ ensure
52
+ fd.close if fd
53
+ end
54
+
55
+ def download(url, output)
56
+ uri = URI(url)
57
+ digest = Digest::SHA1.new
58
+ tmp = "#{output}.tmp"
59
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => (uri.scheme == "https")) do |http|
60
+ request = Net::HTTP::Get.new(uri.path)
61
+ http.request(request) do |response|
62
+ fail "HTTP fetch failed for #{url}. #{response}" if [200, 301].include?(response.code)
63
+ size = (response["content-length"].to_i || -1).to_f
64
+ count = 0
65
+ File.open(tmp, "w") do |fd|
66
+ response.read_body do |chunk|
67
+ fd.write(chunk)
68
+ digest << chunk
69
+ if size > 0 && $stdout.tty?
70
+ count += chunk.bytesize
71
+ $stdout.write(sprintf("\r%0.2f%%", count/size * 100))
72
+ end
73
+ end
74
+ end
75
+ $stdout.write("\r \r") if $stdout.tty?
76
+ end
77
+ end
78
+
79
+ File.rename(tmp, output)
80
+
81
+ return digest.hexdigest
82
+ rescue SocketError => e
83
+ puts "Failure while downloading #{url}: #{e}"
84
+ raise
85
+ ensure
86
+ File.unlink(tmp) if File.exist?(tmp)
87
+ end # def download
88
+
89
+ def untar(tarball, &block)
90
+ require "archive/tar/minitar"
91
+ tgz = Zlib::GzipReader.new(File.open(tarball))
92
+ # Pull out typesdb
93
+ tar = Archive::Tar::Minitar::Input.open(tgz)
94
+ tar.each do |entry|
95
+ path = block.call(entry)
96
+ next if path.nil?
97
+ parent = File.dirname(path)
98
+
99
+ mkdir_p parent unless File.directory?(parent)
100
+
101
+ # Skip this file if the output file is the same size
102
+ if entry.directory?
103
+ mkdir path unless File.directory?(path)
104
+ else
105
+ entry_mode = entry.instance_eval { @mode } & 0777
106
+ if File.exists?(path)
107
+ stat = File.stat(path)
108
+ # TODO(sissel): Submit a patch to archive-tar-minitar upstream to
109
+ # expose headers in the entry.
110
+ entry_size = entry.instance_eval { @size }
111
+ # If file sizes are same, skip writing.
112
+ next if stat.size == entry_size && (stat.mode & 0777) == entry_mode
113
+ end
114
+ puts "Extracting #{entry.full_name} from #{tarball} #{entry_mode.to_s(8)}"
115
+ File.open(path, "w") do |fd|
116
+ # eof? check lets us skip empty files. Necessary because the API provided by
117
+ # Archive::Tar::Minitar::Reader::EntryStream only mostly acts like an
118
+ # IO object. Something about empty files in this EntryStream causes
119
+ # IO.copy_stream to throw "can't convert nil into String" on JRuby
120
+ # TODO(sissel): File a bug about this.
121
+ while !entry.eof?
122
+ chunk = entry.read(16384)
123
+ fd.write(chunk)
124
+ end
125
+ #IO.copy_stream(entry, fd)
126
+ end
127
+ File.chmod(entry_mode, path)
128
+ end
129
+ end
130
+ tar.close
131
+ File.unlink(tarball) if File.file?(tarball)
132
+ end # def untar
133
+
134
+ def ungz(file)
135
+
136
+ outpath = file.gsub('.gz', '')
137
+ tgz = Zlib::GzipReader.new(File.open(file))
138
+ begin
139
+ File.open(outpath, "w") do |out|
140
+ IO::copy_stream(tgz, out)
141
+ end
142
+ File.unlink(file)
143
+ rescue
144
+ File.unlink(outpath) if File.file?(outpath)
145
+ raise
146
+ end
147
+ tgz.close
148
+ end
149
+
150
+ desc "Process any vendor files required for this plugin"
151
+ task "vendor" do |task, args|
152
+
153
+ @files.each do |file|
154
+ download = file_fetch(file['url'], file['sha1'])
155
+ if download =~ /.tar.gz/
156
+ prefix = download.gsub('.tar.gz', '').gsub('vendor/', '')
157
+ untar(download) do |entry|
158
+ if !file['files'].nil?
159
+ next unless file['files'].include?(entry.full_name.gsub(prefix, ''))
160
+ out = entry.full_name.split("/").last
161
+ end
162
+ File.join('vendor', out)
163
+ end
164
+ elsif download =~ /.gz/
165
+ ungz(download)
166
+ end
167
+ end
168
+
169
+ end
@@ -0,0 +1,69 @@
1
+ # coding: utf-8
2
+ require "spec_helper"
3
+ require "socket"
4
+ require "logstash/util/relp"
5
+
6
+ describe "inputs/relp", :socket => true do
7
+
8
+ describe "Single client connection" do
9
+ event_count = 10
10
+ port = 5511
11
+ config <<-CONFIG
12
+ input {
13
+ relp {
14
+ type => "blah"
15
+ port => #{port}
16
+ }
17
+ }
18
+ CONFIG
19
+
20
+ input do |pipeline, queue|
21
+ th = Thread.new { pipeline.run }
22
+ sleep 0.1 while !pipeline.ready?
23
+
24
+ #Send events from clients
25
+ client = RelpClient.new("0.0.0.0", port, ["syslog"])
26
+ event_count.times do |value|
27
+ client.syslog_write("Hello #{value}")
28
+ end
29
+
30
+ events = event_count.times.collect { queue.pop }
31
+ event_count.times do |i|
32
+ insist { events[i]["message"] } == "Hello #{i}"
33
+ end
34
+
35
+ pipeline.shutdown
36
+ th.join
37
+ end # input
38
+ end
39
+ describe "Two client connection" do
40
+ event_count = 100
41
+ port = 5512
42
+ config <<-CONFIG
43
+ input {
44
+ relp {
45
+ type => "blah"
46
+ port => #{port}
47
+ }
48
+ }
49
+ CONFIG
50
+
51
+ input do |pipeline, queue|
52
+ Thread.new { pipeline.run }
53
+ sleep 0.1 while !pipeline.ready?
54
+
55
+ #Send events from clients sockets
56
+ client = RelpClient.new("0.0.0.0", port, ["syslog"])
57
+ client2 = RelpClient.new("0.0.0.0", port, ["syslog"])
58
+
59
+ event_count.times do |value|
60
+ client.syslog_write("Hello from client")
61
+ client2.syslog_write("Hello from client 2")
62
+ end
63
+
64
+ events = (event_count*2).times.collect { queue.pop }
65
+ insist { events.select{|event| event["message"]=="Hello from client" }.size } == event_count
66
+ insist { events.select{|event| event["message"]=="Hello from client 2" }.size } == event_count
67
+ end # input
68
+ end
69
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-input-relp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Elasticsearch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logstash
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.4.0
20
+ - - <
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.4.0
30
+ - - <
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: logstash-codec-plain
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ description: Read RELP events over a TCP socket.
48
+ email: richard.pijnenburg@elasticsearch.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE
56
+ - Rakefile
57
+ - lib/logstash/inputs/relp.rb
58
+ - lib/logstash/util/relp.rb
59
+ - logstash-input-relp.gemspec
60
+ - rakelib/publish.rake
61
+ - rakelib/vendor.rake
62
+ - spec/inputs/relp_spec.rb
63
+ homepage: http://logstash.net/
64
+ licenses:
65
+ - Apache License (2.0)
66
+ metadata:
67
+ logstash_plugin: 'true'
68
+ group: input
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.4.1
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Read RELP events over a TCP socket.
89
+ test_files:
90
+ - spec/inputs/relp_spec.rb