rubix 0.2.1 → 0.3.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.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.3.0
@@ -62,7 +62,7 @@ rescue Rubix::Error => e
62
62
  end
63
63
 
64
64
  begin
65
- sender = Rubix::Sender.new(Settings)
65
+ sender = Rubix::AutoSender.new(Settings)
66
66
  rescue Rubix::Error => e
67
67
  $stderr.puts e.message
68
68
  exit(1)
@@ -10,6 +10,7 @@ module Rubix
10
10
  autoload :Connection, 'rubix/connection'
11
11
  autoload :Response, 'rubix/response'
12
12
  autoload :Sender, 'rubix/sender'
13
+ autoload :AutoSender, 'rubix/auto_sender'
13
14
 
14
15
  # Set up a <tt>Connection</tt> to a Zabbix API server.
15
16
  #
@@ -0,0 +1,433 @@
1
+ require 'rubix/log'
2
+
3
+ module Rubix
4
+
5
+ # A class used to send data to Zabbix.
6
+ #
7
+ # This sender is used to implement the logic for the +zabbix_pipe+
8
+ # utility. It is initialized with some metadata about a host, its
9
+ # host groups and templates, and applications into which items
10
+ # should be written, and it can then accept data and forward it to a
11
+ # Zabbix server using the +zabbix_sender+ utility that comes with
12
+ # Zabbix.
13
+ #
14
+ # A sender can be given data in either TSV or JSON formats. With
15
+ # the JSON format, it is possible to embed data for hosts, host
16
+ # groups, &c. distinct from that with which this sender was
17
+ # initialized. This is a useful way to send many different kinds of
18
+ # data through the same process.
19
+ #
20
+ # The sender will also auto-vivify any hosts, host gruops,
21
+ # templates, applications, and items it needs in order to be able to
22
+ # write data. This is expensive in terms of time so it can be
23
+ # turned off using the <tt>--fast</tt> option.
24
+ class AutoSender
25
+
26
+ include Logs
27
+
28
+ # @return [Hash] settings
29
+ attr_accessor :settings
30
+
31
+ # @return [Rubix::Host] the host the Sender will send data for
32
+ attr_accessor :host
33
+
34
+ # @return [Array<Rubix::HostGroup>] the hostgroups used to create this host
35
+ attr_accessor :host_groups
36
+
37
+ # @return [Array<Rubix::Template>] the templates used to create this host
38
+ attr_accessor :templates
39
+
40
+ # @return [Array<Rubix::Application>] The applications used to create items
41
+ attr_accessor :applications
42
+
43
+ #
44
+ # == Initialization ==
45
+ #
46
+
47
+ # Create a new sender with the given +settings+.
48
+ #
49
+ # @param [Hash, Configliere::Param] settings
50
+ # @param settings [String] host the name of the Zabbix host to write data for
51
+ # @param settings [String] host_groups comma-separated names of Zabbix host groups the host should belong to
52
+ # @param settings [String] templates comma-separated names of Zabbix templates the host should belong to
53
+ # @param settings [String] applications comma-separated names of applications created items should be scoped under
54
+ # @param settings [String] server URL for the Zabbix server -- *not* the URL for the Zabbix API
55
+ # @param settings [String] configuration_file path to a local Zabbix configuration file as used by the +zabbix_sender+ utility
56
+ # @param settings [true, false] verbose be verbose during execution
57
+ # @param settings [true, false] fast auto-vivify (slow) or not (fast)
58
+ # @param settings [String] pipe path to a named pipe to be read from
59
+ # @param settings [Fixnum] pipe_read_sleep seconds to sleep after an empty read from the a named pipe
60
+ # @param settings [Fixnum] create_item_sleep seconds to sleep after creating a new item
61
+ def initialize settings
62
+ @settings = settings
63
+ confirm_settings
64
+ if fast?
65
+ info("Forwarding for #{settings['host']}...") if settings['verbose']
66
+ else
67
+ initialize_host_groups
68
+ initialize_templates
69
+ initialize_host
70
+ initialize_applications
71
+ info("Forwarding for #{host.name}...") if settings['verbose']
72
+ end
73
+ end
74
+
75
+ # Is this sender running in 'fast' mode? If so, it will *not*
76
+ # auto-vivify any hosts, groups, items, &c.
77
+ #
78
+ # @return [true, false]
79
+ def fast?
80
+ settings['fast']
81
+ end
82
+
83
+ # Will this sender auto-vivify hosts, groups, items, &c.?
84
+ #
85
+ # @return [true, false]
86
+ def auto_vivify?
87
+ !fast?
88
+ end
89
+
90
+ protected
91
+
92
+ # Find or create necessary host groups.
93
+ #
94
+ # @return [Array<Rubix::HostGroup>]
95
+ def initialize_host_groups
96
+ self.host_groups = settings['host_groups'].split(',').flatten.compact.map(&:strip).uniq.map { |group_name | HostGroup.find_or_create(:name => group_name.strip) }
97
+ end
98
+
99
+ # Find necessary templates.
100
+ #
101
+ # @return [Array<Rubix::Template>]
102
+ def initialize_templates
103
+ self.templates = (settings['templates'] || '').split(',').flatten.compact.map(&:strip).uniq.map { |template_name | Template.find(:name => template_name.strip) }.compact
104
+ end
105
+
106
+ # Find or create the host for this data. Host groups and
107
+ # templates will automatically be attached.
108
+ #
109
+ # @return [Rubix::Host]
110
+ def initialize_host
111
+ self.host = (Host.find(:name => settings['host']) || Host.new(:name => settings['host']))
112
+
113
+ current_host_group_names = (host.host_groups || []).map(&:name)
114
+ current_template_names = (host.templates || []).map(&:name)
115
+
116
+ host_groups_to_add, templates_to_add = [], []
117
+
118
+ (self.host_groups || []).each do |hg|
119
+ host_groups_to_add << hg unless current_host_group_names.include?(hg.name)
120
+ end
121
+
122
+ (self.templates || []).each do |t|
123
+ templates_to_add << t unless current_template_names.include?(t.name)
124
+ end
125
+
126
+ host.host_groups = ((host.host_groups || []) + host_groups_to_add).flatten.compact.uniq
127
+ host.templates = ((host.templates || []) + templates_to_add).flatten.compact.uniq
128
+ host.save
129
+ host
130
+ end
131
+
132
+ # Find or create the applications for this data.
133
+ #
134
+ # @return [Array<Rubix::Application>]
135
+ def initialize_applications
136
+ application_names = (settings['applications'] || '').split(',').flatten.compact.map(&:strip).uniq
137
+ self.applications = []
138
+ application_names.each do |app_name|
139
+ app = Application.find(:name => app_name, :host_id => host.id)
140
+ if app
141
+ self.applications << app
142
+ else
143
+ app = Application.new(:name => app_name, :host_id => host.id)
144
+ if app.save
145
+ self.applications << app
146
+ else
147
+ warn("Could not create application '#{app_name}' for host #{host.name}")
148
+ end
149
+ end
150
+ end
151
+ self.applications
152
+ end
153
+
154
+ # Check that all settings are correct in order to be able to
155
+ # successfully write data to Zabbix.
156
+ def confirm_settings
157
+ raise ConnectionError.new("Must specify a Zabbix server to send data to.") unless settings['server']
158
+ raise Error.new("Must specify the path to a local configuraiton file") unless settings['configuration_file'] && File.file?(settings['configuration_file'])
159
+ raise ConnectionError.new("Must specify the name of a host to send data for.") unless settings['host']
160
+ raise ValidationError.new("Must define at least one host group.") if auto_vivify? && (settings['host_groups'].nil? || settings['host_groups'].empty?)
161
+ end
162
+
163
+ public
164
+
165
+ #
166
+ # == Sending Data ==
167
+ #
168
+
169
+ # Run this sender.
170
+ #
171
+ # Will read from the correct source of data and exit the Ruby
172
+ # process once the source is consumed.
173
+ def run
174
+ case
175
+ when settings['pipe']
176
+ process_pipe
177
+ when settings.rest.size > 0
178
+ settings.rest.each do |path|
179
+ process_file(path)
180
+ end
181
+ else
182
+ process_stdin
183
+ end
184
+ exit(0)
185
+ end
186
+
187
+ protected
188
+
189
+ # Process each line of a file.
190
+ #
191
+ # @param [String] path the path to the file to process
192
+ def process_file path
193
+ f = File.new(path)
194
+ process_file_handle(f)
195
+ f.close
196
+ end
197
+
198
+ # Process each line of standard input.
199
+ def process_stdin
200
+ process_file_handle($stdin)
201
+ end
202
+
203
+ # Process each line read from the pipe.
204
+ #
205
+ # The pipe will be opened in a non-blocking read mode. This
206
+ # sender will wait 'pipe_read_sleep' seconds between successive
207
+ # empty reads.
208
+ def process_pipe
209
+ # We want to open this pipe in non-blocking read mode b/c
210
+ # otherwise this process becomes hard to kill.
211
+ f = File.new(settings['pipe'], (File::RDONLY | File::NONBLOCK))
212
+ while true
213
+ process_file_handle(f)
214
+ # In non-blocking mode, an EOFError from f.readline doesn't mean
215
+ # there's no more data to read, just that there's no more data
216
+ # right *now*. If we sleep for a bit there might be more data
217
+ # coming down the pipe.
218
+ sleep settings['pipe_read_sleep']
219
+ end
220
+ f.close
221
+ end
222
+
223
+ # Process each line of a given file handle.
224
+ #
225
+ # @param [File] f the file to process
226
+ def process_file_handle f
227
+ begin
228
+ line = f.readline
229
+ rescue EOFError
230
+ line = nil
231
+ end
232
+ while line
233
+ process_line(line)
234
+ begin
235
+ # FIXME -- this call to File#readline blocks and doesn't let
236
+ # stuff like SIGINT (generated from Ctrl-C on a keyboard,
237
+ # say) take affect.
238
+ line = f.readline
239
+ rescue EOFError
240
+ line = nil
241
+ end
242
+ end
243
+ end
244
+
245
+ public
246
+
247
+ # Process a single line of text.
248
+ #
249
+ # @param [String] line
250
+ def process_line line
251
+ if looks_like_json?(line)
252
+ process_line_of_json_in_new_pipe(line)
253
+ else
254
+ process_line_of_tsv_in_this_pipe(line)
255
+ end
256
+ end
257
+
258
+ protected
259
+
260
+ # Parse and send a single +line+ of TSV input to the Zabbix server.
261
+ # The line will be split at tabs and expects either
262
+ #
263
+ # a) two columns: an item key and a value
264
+ # b) three columns: an item key, a value, and a timestamp
265
+ #
266
+ # Unexpected input will cause an error to be logged.
267
+ #
268
+ # @param [String] line a line of TSV data
269
+ def process_line_of_tsv_in_this_pipe line
270
+ parts = line.strip.split("\t")
271
+ case parts.size
272
+ when 2
273
+ timestamp = Time.now
274
+ key, value = parts
275
+ when 3
276
+ key, value = parts[0..1]
277
+ timestamp = Time.parse(parts.last)
278
+ else
279
+ error("Each line of input must be a tab separated row consisting of 2 columns (key, value) or 3 columns (timestamp, key, value)")
280
+ return
281
+ end
282
+ send_data(key, value, timestamp)
283
+ end
284
+
285
+ # Parse and send a single +line+ of JSON input to the Zabbix server.
286
+ # The JSON must have a key +data+ in order to be processed. The
287
+ # value of 'data' should be an Array of Hashes each with a +key+ and
288
+ # +value+.
289
+ #
290
+ # This ZabbixPipe's settings will be merged with the remainder of
291
+ # the JSON hash. This allows sending values for 'host2' to an
292
+ # instance of ZabbixPipe already set up to receive for 'host1'.
293
+ #
294
+ # This is useful for sending data for keys from multiple hosts
295
+ #
296
+ # Example of expected input:
297
+ #
298
+ # {
299
+ # 'data': [
300
+ # {'key': 'foo.bar.baz', 'value': 10},
301
+ # {'key': 'snap.crackle.pop', 'value': 8 }
302
+ # ]
303
+ # }
304
+ #
305
+ # Or when sending for another host:
306
+ #
307
+ # {
308
+ # 'host': 'shazaam',
309
+ # 'applications': 'silly',
310
+ # 'data': [
311
+ # {'key': 'foo.bar.baz', 'value': 10},
312
+ # {'key': 'snap.crackle.pop', 'value': 8 }
313
+ # ]
314
+ # }
315
+ #
316
+ # @param [String] line a line of JSON data
317
+ def process_line_of_json_in_new_pipe line
318
+ begin
319
+ json = JSON.parse(line)
320
+ rescue JSON::ParserError => e
321
+ error("Malformed JSON")
322
+ return
323
+ end
324
+
325
+ data = json.delete('data')
326
+ unless data && data.is_a?(Array)
327
+ error("A line of JSON input must a have an Array key 'data'")
328
+ return
329
+ end
330
+
331
+ if json.empty?
332
+ # If there are no other settings then the daughter will be the
333
+ # same as the parent -- so just use 'self'.
334
+ daughter_pipe = self
335
+ else
336
+ # We merge the settings from 'self' with whatever else is
337
+ # present in the line.
338
+ begin
339
+ daughter_pipe = self.class.new(settings.stringify_keys.merge(json))
340
+ rescue Error => e
341
+ error(e.message)
342
+ return
343
+ end
344
+ end
345
+
346
+ data.each do |point|
347
+ key = point['key']
348
+ value = point['value']
349
+ unless key && value
350
+ warn("The elements of the 'data' Array must be Hashes with a 'key' and a 'value'")
351
+ next
352
+ end
353
+
354
+ tsv_line = [key, value].map(&:to_s).join("\t")
355
+ daughter_pipe.process_line(tsv_line)
356
+ end
357
+ end
358
+
359
+ # Does the +line+ look like it might be JSON?
360
+ #
361
+ # @param [String] line
362
+ # @return [true, false]
363
+ def looks_like_json? line
364
+ !!(line =~ /^\s*\{/)
365
+ end
366
+
367
+ # Send the +value+ for +key+ at the given +timestamp+ to the Zabbix
368
+ # server.
369
+ #
370
+ # If the +key+ doesn't exist for this local agent's host, it will be
371
+ # added.
372
+ #
373
+ # FIXME passing +timestamp+ has no effect at present...
374
+ #
375
+ # @param [String] key
376
+ # @param [String, Fixnum, Float] value
377
+ # @param [Time] timestamp
378
+ def send_data key, value, timestamp
379
+ ensure_item_exists(key, value) unless fast?
380
+ command = "#{settings['sender']} --config #{settings['configuration_file']} --zabbix-server #{settings['server']} --host #{settings['host']} --key #{key} --value '#{value}'"
381
+ process_zabbix_sender_output(key, `#{command}`)
382
+
383
+ # command = "zabbix_sender --config #{configuration_file} --zabbix-server #{server} --input-file - --with-timestamps"
384
+ # open(command, 'w') do |zabbix_sender|
385
+ # zabbix_sender.write([settings['host'], key, timestamp.to_i, value].map(&:to_s).join("\t"))
386
+ # zabbix_sender.close_write
387
+ # process_zabbix_sender_output(zabbix_sender.read)
388
+ # end
389
+ end
390
+
391
+ # Create an item for the given +key+ if necessary.
392
+ #
393
+ # @param [String] key
394
+ # @param [String, Fixnum, Float] value
395
+ def ensure_item_exists key, value
396
+ item = Item.find(:key => key, :host_id => host.id)
397
+ unless item
398
+ Item.new(:key => key, :host_id => host.id, :applications => applications, :value_type => Item.value_type_from_value(value)).save
399
+
400
+ # There is a time lag of about 15-30 seconds between (successfully)
401
+ # creating an item on the Zabbix server and having the Zabbix accept
402
+ # new data for that item.
403
+ #
404
+ # If it is crucial that *every single* data point be written, dial
405
+ # up this sleep period. The first data point for a new key will put
406
+ # the wrapper to sleep for this period of time, in hopes that the
407
+ # Zabbix server will catch up and be ready to accept new data
408
+ # points.
409
+ #
410
+ # If you don't care that you're going to lose the first few data
411
+ # points you send to Zabbix, then don't worry about it.
412
+ sleep settings['create_item_sleep']
413
+ end
414
+ end
415
+
416
+ # Parse the +text+ output by +zabbix_sender+.
417
+ #
418
+ # @param [String] key
419
+ # @param [String] text the output from +zabbix_sender+
420
+ # @return [Fixnum] the number of data points processed
421
+ def process_zabbix_sender_output key, text
422
+ return unless settings['verbose']
423
+ lines = text.strip.split("\n")
424
+ return if lines.size < 1
425
+ status_line = lines.first
426
+ status_line =~ /Processed +(\d+) +Failed +(\d+) +Total +(\d+)/
427
+ processed, failed, total = $1, $2, $3
428
+ warn("Failed to write #{failed} values to key '#{key}'") if failed.to_i != 0
429
+ processed
430
+ end
431
+
432
+ end
433
+ end
@@ -1,5 +1,4 @@
1
1
  require 'configliere'
2
- require 'open3'
3
2
 
4
3
  module Rubix
5
4
 
@@ -186,22 +185,14 @@ module Rubix
186
185
  end
187
186
 
188
187
  def sender?
189
- if settings[:send] == true
190
- %w[server port host config].each do |var|
191
- raise Rubix::Error.new("Cannot send values to Zabbix: Set value of --#{var}.") if settings[var.to_sym].nil?
192
- end
193
- true
194
- else
195
- false
196
- end
188
+ settings[:send] == true
197
189
  end
198
190
 
199
191
  def output
200
192
  return @output if @output
201
193
  case
202
194
  when sender?
203
- @sender_stdin, @sender_stdout, @sender_stderr, @sender_wait_thr = Open3.popen3("zabbix_sender --zabbix-server #{settings[:server]} --host #{settings[:host]}")
204
- @output = @sender_stdin
195
+ @output = Sender.new(:host => settings[:host], :server => settings[:server], :port => settings[:port], :config => settings[:config])
205
196
  when stdout?
206
197
  @output = $stdout
207
198
  when fifo?
@@ -220,9 +211,6 @@ module Rubix
220
211
  return unless output
221
212
  output.flush
222
213
  case
223
- when sender?
224
- # puts @sender_stdout.read
225
- [@sender_stdin, @sender_stdout, @sender_stderr].each { |fh| fh.close } if sender?
226
214
  when stdout?
227
215
  return
228
216
  else
@@ -1,433 +1,132 @@
1
1
  require 'rubix/log'
2
+ require 'open3'
2
3
 
3
4
  module Rubix
4
5
 
5
6
  # A class used to send data to Zabbix.
6
7
  #
7
- # This sender is used to implement the logic for the +zabbix_pipe+
8
- # utility. It is initialized with some metadata about a host, its
9
- # host groups and templates, and applications into which items
10
- # should be written, and it can then accept data and forward it to a
11
- # Zabbix server using the +zabbix_sender+ utility that comes with
12
- # Zabbix.
13
- #
14
- # A sender can be given data in either TSV or JSON formats. With
15
- # the JSON format, it is possible to embed data for hosts, host
16
- # groups, &c. distinct from that with which this sender was
17
- # initialized. This is a useful way to send many different kinds of
18
- # data through the same process.
19
- #
20
- # The sender will also auto-vivify any hosts, host gruops,
21
- # templates, applications, and items it needs in order to be able to
22
- # write data. This is expensive in terms of time so it can be
23
- # turned off using the <tt>--fast</tt> option.
8
+ # This sender is used to wrap +zabbix_sender+.
24
9
  class Sender
25
10
 
26
11
  include Logs
27
12
 
28
- # @return [Hash] settings
29
- attr_accessor :settings
30
-
31
- # @return [Rubix::Host] the host the Sender will send data for
32
- attr_accessor :host
33
-
34
- # @return [Array<Rubix::HostGroup>] the hostgroups used to create this host
35
- attr_accessor :host_groups
36
-
37
- # @return [Array<Rubix::Template>] the templates used to create this host
38
- attr_accessor :templates
39
-
40
- # @return [Array<Rubix::Application>] The applications used to create items
41
- attr_accessor :applications
42
-
43
13
  #
44
- # == Initialization ==
14
+ # == Properties ==
45
15
  #
46
16
 
47
- # Create a new sender with the given +settings+.
48
- #
49
- # @param [Hash, Configliere::Param] settings
50
- # @param settings [String] host the name of the Zabbix host to write data for
51
- # @param settings [String] host_groups comma-separated names of Zabbix host groups the host should belong to
52
- # @param settings [String] templates comma-separated names of Zabbix templates the host should belong to
53
- # @param settings [String] applications comma-separated names of applications created items should be scoped under
54
- # @param settings [String] server URL for the Zabbix server -- *not* the URL for the Zabbix API
55
- # @param settings [String] configuration_file path to a local Zabbix configuration file as used by the +zabbix_sender+ utility
56
- # @param settings [true, false] verbose be verbose during execution
57
- # @param settings [true, false] fast auto-vivify (slow) or not (fast)
58
- # @param settings [String] pipe path to a named pipe to be read from
59
- # @param settings [Fixnum] pipe_read_sleep seconds to sleep after an empty read from the a named pipe
60
- # @param settings [Fixnum] create_item_sleep seconds to sleep after creating a new item
61
- def initialize settings
62
- @settings = settings
63
- confirm_settings
64
- if fast?
65
- info("Forwarding for #{settings['host']}...") if settings['verbose']
66
- else
67
- initialize_host_groups
68
- initialize_templates
69
- initialize_host
70
- initialize_applications
71
- info("Forwarding for #{host.name}...") if settings['verbose']
72
- end
17
+ # @return [String] The IP of the Zabbix server
18
+ attr_writer :server
19
+ def server
20
+ @server ||= 'localhost'
73
21
  end
74
22
 
75
- # Is this sender running in 'fast' mode? If so, it will *not*
76
- # auto-vivify any hosts, groups, items, &c.
77
- #
78
- # @return [true, false]
79
- def fast?
80
- settings['fast']
23
+ # @return [String, Rubix::Hosts] the Zabbix host name or Rubix::Host the sender will use by default
24
+ attr_reader :host
25
+ def host= nh
26
+ @host = (nh.respond_to?(:name) ? nh.name : nh.to_s)
81
27
  end
82
28
 
83
- # Will this sender auto-vivify hosts, groups, items, &c.?
84
- #
85
- # @return [true, false]
86
- def auto_vivify?
87
- !fast?
29
+ # @return [Fixnum] the port to connect to on the Zabbix server
30
+ attr_writer :port
31
+ def port
32
+ @port ||= 10051
88
33
  end
89
34
 
90
- protected
91
-
92
- # Find or create necessary host groups.
93
- #
94
- # @return [Array<Rubix::HostGroup>]
95
- def initialize_host_groups
96
- self.host_groups = settings['host_groups'].split(',').flatten.compact.map(&:strip).uniq.map { |group_name | HostGroup.find_or_create(:name => group_name.strip) }
35
+ # @return [String] the path to the local Zabbix agent configuration file.
36
+ attr_writer :config
37
+ def config
38
+ @config ||= '/etc/zabbix/zabbix_agentd.conf'
97
39
  end
98
40
 
99
- # Find necessary templates.
100
- #
101
- # @return [Array<Rubix::Template>]
102
- def initialize_templates
103
- self.templates = (settings['templates'] || '').split(',').flatten.compact.map(&:strip).uniq.map { |template_name | Template.find(:name => template_name.strip) }.compact
41
+ # Whether or not to include timestamps with the data.
42
+ attr_writer :timestamps
43
+ def timestamps?
44
+ @timestamps
104
45
  end
105
46
 
106
- # Find or create the host for this data. Host groups and
107
- # templates will automatically be attached.
108
47
  #
109
- # @return [Rubix::Host]
110
- def initialize_host
111
- self.host = (Host.find(:name => settings['host']) || Host.new(:name => settings['host']))
112
-
113
- current_host_group_names = (host.host_groups || []).map(&:name)
114
- current_template_names = (host.templates || []).map(&:name)
115
-
116
- host_groups_to_add, templates_to_add = [], []
117
-
118
- (self.host_groups || []).each do |hg|
119
- host_groups_to_add << hg unless current_host_group_names.include?(hg.name)
120
- end
121
-
122
- (self.templates || []).each do |t|
123
- templates_to_add << t unless current_template_names.include?(t.name)
124
- end
125
-
126
- host.host_groups = ((host.host_groups || []) + host_groups_to_add).flatten.compact.uniq
127
- host.templates = ((host.templates || []) + templates_to_add).flatten.compact.uniq
128
- host.save
129
- host
130
- end
48
+ # == Initialization ==
49
+ #
131
50
 
132
- # Find or create the applications for this data.
51
+ # Create a new sender with the given +settings+.
133
52
  #
134
- # @return [Array<Rubix::Application>]
135
- def initialize_applications
136
- application_names = (settings['applications'] || '').split(',').flatten.compact.map(&:strip).uniq
137
- self.applications = []
138
- application_names.each do |app_name|
139
- app = Application.find(:name => app_name, :host_id => host.id)
140
- if app
141
- self.applications << app
142
- else
143
- app = Application.new(:name => app_name, :host_id => host.id)
144
- if app.save
145
- self.applications << app
146
- else
147
- warn("Could not create application '#{app_name}' for host #{host.name}")
148
- end
149
- end
150
- end
151
- self.applications
53
+ # @param [Hash, Configliere::Param] settings
54
+ # @param settings [String, Rubix::Host] host the name of the Zabbix host to write data for
55
+ # @param settings [String] server the IP of the Zabbix server
56
+ # @param settings [Fixnum] port the port to connect to on the Zabbix server
57
+ # @param settings [String] config the path to the local configuration file
58
+ def initialize settings={}
59
+ @settings = settings
60
+ self.server = settings[:server] if settings[:server]
61
+ self.host = settings[:host] if settings[:host]
62
+ self.port = settings[:port] if settings[:port]
63
+ self.config = settings[:config] if settings[:config]
64
+ self.timestamps = settings[:timestamps]
65
+ confirm_settings
152
66
  end
153
67
 
154
68
  # Check that all settings are correct in order to be able to
155
69
  # successfully write data to Zabbix.
156
70
  def confirm_settings
157
- raise ConnectionError.new("Must specify a Zabbix server to send data to.") unless settings['server']
158
- raise Error.new("Must specify the path to a local configuraiton file") unless settings['configuration_file'] && File.file?(settings['configuration_file'])
159
- raise ConnectionError.new("Must specify the name of a host to send data for.") unless settings['host']
160
- raise ValidationError.new("Must define at least one host group.") if auto_vivify? && (settings['host_groups'].nil? || settings['host_groups'].empty?)
71
+ raise Error.new("Must specify a path to a local configuraiton file") unless config
72
+ raise Error.new("Must specify the IP of a Zabbix server") unless server
73
+ raise Error.new("Must specify the port of a Zabbix server") unless port && port.to_i > 0
74
+ raise Error.new("Must specify a default Zabbix host to write data for") unless host
161
75
  end
162
-
163
- public
164
76
 
165
77
  #
166
78
  # == Sending Data ==
167
79
  #
168
80
 
169
- # Run this sender.
81
+ # The environment for the Zabbix sender invocation.
170
82
  #
171
- # Will read from the correct source of data and exit the Ruby
172
- # process once the source is consumed.
173
- def run
174
- case
175
- when settings['pipe']
176
- process_pipe
177
- when settings.rest.size > 0
178
- settings.rest.each do |path|
179
- process_file(path)
180
- end
181
- else
182
- process_stdin
183
- end
184
- exit(0)
83
+ # @return [Hash]
84
+ def zabbix_sender_env
85
+ {}
185
86
  end
186
87
 
187
- protected
188
-
189
- # Process each line of a file.
88
+ # Construct the command that invokes Zabbix sender.
190
89
  #
191
- # @param [String] path the path to the file to process
192
- def process_file path
193
- f = File.new(path)
194
- process_file_handle(f)
195
- f.close
196
- end
197
-
198
- # Process each line of standard input.
199
- def process_stdin
200
- process_file_handle($stdin)
201
- end
202
-
203
- # Process each line read from the pipe.
204
- #
205
- # The pipe will be opened in a non-blocking read mode. This
206
- # sender will wait 'pipe_read_sleep' seconds between successive
207
- # empty reads.
208
- def process_pipe
209
- # We want to open this pipe in non-blocking read mode b/c
210
- # otherwise this process becomes hard to kill.
211
- f = File.new(settings['pipe'], (File::RDONLY | File::NONBLOCK))
212
- while true
213
- process_file_handle(f)
214
- # In non-blocking mode, an EOFError from f.readline doesn't mean
215
- # there's no more data to read, just that there's no more data
216
- # right *now*. If we sleep for a bit there might be more data
217
- # coming down the pipe.
218
- sleep settings['pipe_read_sleep']
219
- end
220
- f.close
221
- end
222
-
223
- # Process each line of a given file handle.
224
- #
225
- # @param [File] f the file to process
226
- def process_file_handle f
227
- begin
228
- line = f.readline
229
- rescue EOFError
230
- line = nil
231
- end
232
- while line
233
- process_line(line)
234
- begin
235
- # FIXME -- this call to File#readline blocks and doesn't let
236
- # stuff like SIGINT (generated from Ctrl-C on a keyboard,
237
- # say) take affect.
238
- line = f.readline
239
- rescue EOFError
240
- line = nil
241
- end
242
- end
243
- end
244
-
245
- public
246
-
247
- # Process a single line of text.
248
- #
249
- # @param [String] line
250
- def process_line line
251
- if looks_like_json?(line)
252
- process_line_of_json_in_new_pipe(line)
253
- else
254
- process_line_of_tsv_in_this_pipe(line)
255
- end
256
- end
257
-
258
- protected
259
-
260
- # Parse and send a single +line+ of TSV input to the Zabbix server.
261
- # The line will be split at tabs and expects either
262
- #
263
- # a) two columns: an item key and a value
264
- # b) three columns: an item key, a value, and a timestamp
265
- #
266
- # Unexpected input will cause an error to be logged.
267
- #
268
- # @param [String] line a line of TSV data
269
- def process_line_of_tsv_in_this_pipe line
270
- parts = line.strip.split("\t")
271
- case parts.size
272
- when 2
273
- timestamp = Time.now
274
- key, value = parts
275
- when 3
276
- key, value = parts[0..1]
277
- timestamp = Time.parse(parts.last)
278
- else
279
- error("Each line of input must be a tab separated row consisting of 2 columns (key, value) or 3 columns (timestamp, key, value)")
280
- return
90
+ # @return [String]
91
+ def zabbix_sender_command
92
+ "timeout 3 zabbix_sender --zabbix-server #{server} --host #{host} --port #{port} --config #{config} --real-time --input-file - -vv".tap do |c|
93
+ c += " --with-timestamps" if timestamps?
281
94
  end
282
- send_data(key, value, timestamp)
283
95
  end
284
96
 
285
- # Parse and send a single +line+ of JSON input to the Zabbix server.
286
- # The JSON must have a key +data+ in order to be processed. The
287
- # value of 'data' should be an Array of Hashes each with a +key+ and
288
- # +value+.
289
- #
290
- # This ZabbixPipe's settings will be merged with the remainder of
291
- # the JSON hash. This allows sending values for 'host2' to an
292
- # instance of ZabbixPipe already set up to receive for 'host1'.
97
+ # Run a +zabbix_sender+ subprocess in the block.
293
98
  #
294
- # This is useful for sending data for keys from multiple hosts
295
- #
296
- # Example of expected input:
297
- #
298
- # {
299
- # 'data': [
300
- # {'key': 'foo.bar.baz', 'value': 10},
301
- # {'key': 'snap.crackle.pop', 'value': 8 }
302
- # ]
303
- # }
304
- #
305
- # Or when sending for another host:
306
- #
307
- # {
308
- # 'host': 'shazaam',
309
- # 'applications': 'silly',
310
- # 'data': [
311
- # {'key': 'foo.bar.baz', 'value': 10},
312
- # {'key': 'snap.crackle.pop', 'value': 8 }
313
- # ]
314
- # }
315
- #
316
- # @param [String] line a line of JSON data
317
- def process_line_of_json_in_new_pipe line
99
+ # @yield [IO, IO, IO, Thread] Handle the subprocess.
100
+ def with_sender_subprocess &block
318
101
  begin
319
- json = JSON.parse(line)
320
- rescue JSON::ParserError => e
321
- error("Malformed JSON")
322
- return
323
- end
324
-
325
- data = json.delete('data')
326
- unless data && data.is_a?(Array)
327
- error("A line of JSON input must a have an Array key 'data'")
328
- return
329
- end
330
-
331
- if json.empty?
332
- # If there are no other settings then the daughter will be the
333
- # same as the parent -- so just use 'self'.
334
- daughter_pipe = self
335
- else
336
- # We merge the settings from 'self' with whatever else is
337
- # present in the line.
338
- begin
339
- daughter_pipe = self.class.new(settings.stringify_keys.merge(json))
340
- rescue Error => e
341
- error(e.message)
342
- return
343
- end
344
- end
345
-
346
- data.each do |point|
347
- key = point['key']
348
- value = point['value']
349
- unless key && value
350
- warn("The elements of the 'data' Array must be Hashes with a 'key' and a 'value'")
351
- next
352
- end
353
-
354
- tsv_line = [key, value].map(&:to_s).join("\t")
355
- daughter_pipe.process_line(tsv_line)
102
+ Open3.popen3(zabbix_sender_env, zabbix_sender_command, &block)
103
+ rescue Errno::ENOENT, Errno::EACCES => e
104
+ warn(e.message)
356
105
  end
357
106
  end
358
107
 
359
- # Does the +line+ look like it might be JSON?
108
+ # Convenience method for sending a block of text to
109
+ # +zabbix_sender+.
360
110
  #
361
- # @param [String] line
362
- # @return [true, false]
363
- def looks_like_json? line
364
- !!(line =~ /^\s*\{/)
111
+ # @param [String] text
112
+ def puts text
113
+ with_sender_subprocess do |stdin, stdout, stderr, wait_thr|
114
+ stdin.write(text)
115
+ stdin.close
116
+ output = [stdout.read.chomp, stderr.read.chomp].join("\n").strip
117
+ debug(output) if output.size > 0
118
+ end
365
119
  end
366
120
 
367
- # Send the +value+ for +key+ at the given +timestamp+ to the Zabbix
368
- # server.
369
- #
370
- # If the +key+ doesn't exist for this local agent's host, it will be
371
- # added.
372
- #
373
- # FIXME passing +timestamp+ has no effect at present...
374
- #
375
- # @param [String] key
376
- # @param [String, Fixnum, Float] value
377
- # @param [Time] timestamp
378
- def send_data key, value, timestamp
379
- ensure_item_exists(key, value) unless fast?
380
- command = "#{settings['sender']} --config #{settings['configuration_file']} --zabbix-server #{settings['server']} --host #{settings['host']} --key #{key} --value '#{value}'"
381
- process_zabbix_sender_output(key, `#{command}`)
382
-
383
- # command = "zabbix_sender --config #{configuration_file} --zabbix-server #{server} --input-file - --with-timestamps"
384
- # open(command, 'w') do |zabbix_sender|
385
- # zabbix_sender.write([settings['host'], key, timestamp.to_i, value].map(&:to_s).join("\t"))
386
- # zabbix_sender.close_write
387
- # process_zabbix_sender_output(zabbix_sender.read)
388
- # end
121
+ # :nodoc:
122
+ def close
123
+ return
389
124
  end
390
125
 
391
- # Create an item for the given +key+ if necessary.
392
- #
393
- # @param [String] key
394
- # @param [String, Fixnum, Float] value
395
- def ensure_item_exists key, value
396
- item = Item.find(:key => key, :host_id => host.id)
397
- unless item
398
- Item.new(:key => key, :host_id => host.id, :applications => applications, :value_type => Item.value_type_from_value(value)).save
399
-
400
- # There is a time lag of about 15-30 seconds between (successfully)
401
- # creating an item on the Zabbix server and having the Zabbix accept
402
- # new data for that item.
403
- #
404
- # If it is crucial that *every single* data point be written, dial
405
- # up this sleep period. The first data point for a new key will put
406
- # the wrapper to sleep for this period of time, in hopes that the
407
- # Zabbix server will catch up and be ready to accept new data
408
- # points.
409
- #
410
- # If you don't care that you're going to lose the first few data
411
- # points you send to Zabbix, then don't worry about it.
412
- sleep settings['create_item_sleep']
413
- end
126
+ # :nodoc:
127
+ def flush
128
+ return
414
129
  end
415
130
 
416
- # Parse the +text+ output by +zabbix_sender+.
417
- #
418
- # @param [String] key
419
- # @param [String] text the output from +zabbix_sender+
420
- # @return [Fixnum] the number of data points processed
421
- def process_zabbix_sender_output key, text
422
- return unless settings['verbose']
423
- lines = text.strip.split("\n")
424
- return if lines.size < 1
425
- status_line = lines.first
426
- status_line =~ /Processed +(\d+) +Failed +(\d+) +Total +(\d+)/
427
- processed, failed, total = $1, $2, $3
428
- warn("Failed to write #{failed} values to key '#{key}'") if failed.to_i != 0
429
- processed
430
- end
431
-
432
131
  end
433
132
  end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rubix::AutoSender do
4
+
5
+ before do
6
+ @config_file = Tempfile.new('sender', '/tmp')
7
+ end
8
+
9
+ describe "running in --fast mode" do
10
+
11
+ end
12
+
13
+ describe "running in auto-vivify mode" do
14
+
15
+ end
16
+
17
+ end
18
+
@@ -140,5 +140,19 @@ describe Rubix::Monitor do
140
140
  end
141
141
 
142
142
  end
143
+
144
+ describe 'writing to a Sender' do
145
+ before do
146
+ @sender = mock("Rubix::Sender")
147
+ @sender.stub!(:close) ; @sender.stub!(:flush)
148
+ Rubix::Sender.should_receive(:new).with(kind_of(Hash)).and_return(@sender)
149
+ ::ARGV.replace(['--send', '--host=foobar'])
150
+ end
151
+
152
+ it "should write to the sender" do
153
+ @sender.should_receive(:puts).with('- key value')
154
+ @wrapper.run
155
+ end
156
+ end
143
157
 
144
158
  end
@@ -6,18 +6,16 @@ describe Rubix::Sender do
6
6
  @config_file = Tempfile.new('sender', '/tmp')
7
7
  end
8
8
 
9
- describe "running in --fast mode" do
10
-
11
- it "should not attempt to make any calls to the Zabbix API when writing values" do
12
- Rubix.connection.should_not_receive(:request)
13
- @sender = Rubix::Sender.new(mock_settings('configuration_file' => @config_file.path, 'host' => 'foohost', 'server' => 'fooserver', 'fast' => true, 'sender' => 'echo'))
14
- @sender.process_line("foo.bar.baz 123\n")
15
- @sender.process_line({:host => 'newhost', :host_groups => 'foobar,baz', :data => [{:key => 'foo.bar.baz', :value => 123}]}.to_json)
16
- end
9
+ it "has sensible defaults" do
10
+ sender = Rubix::Sender.new(:host => 'foobar')
11
+ sender.server.should == 'localhost'
12
+ sender.port.should == 10051
13
+ sender.config.should == '/etc/zabbix/zabbix_agentd.conf'
17
14
  end
18
15
 
19
- describe "running in auto-vivify mode" do
16
+ it "will raise an error without a host" do
17
+ lambda { Rubix::Sender.new }.should raise_error(Rubix::Error)
20
18
  end
21
-
19
+
22
20
  end
23
21
 
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 2
8
- - 1
9
- version: 0.2.1
7
+ - 3
8
+ - 0
9
+ version: 0.3.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Dhruv Bansal
@@ -100,6 +100,7 @@ files:
100
100
  - lib/rubix/examples/mongo_monitor.rb
101
101
  - lib/rubix/examples/uptime_monitor.rb
102
102
  - lib/rubix/examples/hbase_monitor.rb
103
+ - lib/rubix/auto_sender.rb
103
104
  - lib/rubix/sender.rb
104
105
  - lib/rubix/log.rb
105
106
  - lib/rubix/models.rb
@@ -120,6 +121,7 @@ files:
120
121
  - lib/rubix/connection.rb
121
122
  - lib/rubix/associations.rb
122
123
  - spec/test.yml
124
+ - spec/rubix/auto_sender_spec.rb
123
125
  - spec/rubix/monitors/monitor_spec.rb
124
126
  - spec/rubix/monitors/chef_monitor_spec.rb
125
127
  - spec/rubix/monitors/cluster_monitor_spec.rb