rubix 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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