zcollective 0.0.9
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/bin/zcollective +659 -0
- data/lib/zcollective/zabbixclient.rb +117 -0
- metadata +60 -0
data/bin/zcollective
ADDED
@@ -0,0 +1,659 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Copyright (c) 2012, 2013, The Scale Factory Ltd.
|
4
|
+
# All rights reserved.
|
5
|
+
#
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
7
|
+
# modification, are permitted provided that the following conditions are met:
|
8
|
+
# * Redistributions of source code must retain the above copyright
|
9
|
+
# notice, this list of conditions and the following disclaimer.
|
10
|
+
# * Redistributions in binary form must reproduce the above copyright
|
11
|
+
# notice, this list of conditions and the following disclaimer in the
|
12
|
+
# documentation and/or other materials provided with the distribution.
|
13
|
+
# * Neither the name of the The Scale Factory Ltd nor the
|
14
|
+
# names of its contributors may be used to endorse or promote products
|
15
|
+
# derived from this software without specific prior written permission.
|
16
|
+
#
|
17
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
20
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE SCALE FACTORY LTD BE LIABLE FOR ANY
|
21
|
+
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
22
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
23
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
24
|
+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
25
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
26
|
+
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
27
|
+
|
28
|
+
require 'rubygems'
|
29
|
+
require 'optparse'
|
30
|
+
require 'logger'
|
31
|
+
require 'json'
|
32
|
+
require 'netaddr'
|
33
|
+
require 'zcollective/zabbixclient'
|
34
|
+
|
35
|
+
begin
|
36
|
+
require 'mcollective'
|
37
|
+
rescue LoadError => e
|
38
|
+
raise unless e.message =~ /mcollective/
|
39
|
+
STDERR.puts "ZCollective requires that you install mcollective. " <<
|
40
|
+
"This is not supplied as a gem - install the OS packages from puppetlabs"
|
41
|
+
exit 2
|
42
|
+
end
|
43
|
+
|
44
|
+
options = {}
|
45
|
+
optparse = OptionParser.new do |opts|
|
46
|
+
|
47
|
+
options[:zabbix_api_url] = 'http://localhost/zabbix/api_jsonrpc.php'
|
48
|
+
opts.on('-z', '--zabbix-api-url url', 'JSON-RPC endpoint for zabbix server') do |u|
|
49
|
+
options[:zabbix_api_url] = u
|
50
|
+
end
|
51
|
+
|
52
|
+
options[:zabbix_user] = 'Admin'
|
53
|
+
opts.on('-u', '--zabbix-user user', 'Zabbix API username') do |u|
|
54
|
+
options[:zabbix_user] = u
|
55
|
+
end
|
56
|
+
|
57
|
+
options[:zabbix_pass] = 'zabbix'
|
58
|
+
opts.on('-p', '--zabbix-pass pass', 'Zabbix API password') do |p|
|
59
|
+
options[:zabbix_pass] = p
|
60
|
+
end
|
61
|
+
|
62
|
+
options[:debug] = false
|
63
|
+
opts.on('-d', '--debug', 'Enable debugging') do
|
64
|
+
options[:debug] = true
|
65
|
+
end
|
66
|
+
|
67
|
+
options[:noop] = false
|
68
|
+
opts.on('-n', '--noop', 'Don\'t make changes') do
|
69
|
+
options[:noop] = true
|
70
|
+
end
|
71
|
+
|
72
|
+
options[:interface_cidr] = '0.0.0.0/0'
|
73
|
+
opts.on('-c', '--interface-cidr CIDR', 'Only consider interfaces matching the given CIDR') do |c|
|
74
|
+
options[:interface_cidr] = c
|
75
|
+
end
|
76
|
+
|
77
|
+
options[:connect_by_ip] = 0
|
78
|
+
opts.on('--connect-by-ip','Configure newly added hosts to connect by IP address instead of DNS') do
|
79
|
+
options[:connect_by_ip] = 1
|
80
|
+
end
|
81
|
+
|
82
|
+
options[:lockfile] = "/tmp/zcollective.lock"
|
83
|
+
opts.on('--lockfile=f', 'Use alternative lock file') do |f|
|
84
|
+
options[:lockfile] = f
|
85
|
+
end
|
86
|
+
|
87
|
+
options[:timeout] = 60
|
88
|
+
opts.on('--timeout=t', 'Time out after number of seconds') do |t|
|
89
|
+
options[:timeout] = t.to_i
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
begin
|
95
|
+
|
96
|
+
optparse.parse!
|
97
|
+
|
98
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
99
|
+
|
100
|
+
$stderr.puts $!.to_s
|
101
|
+
$stderr.puts optparse
|
102
|
+
exit 2
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
log = Logger.new(STDERR)
|
107
|
+
|
108
|
+
if options[:debug]
|
109
|
+
log.level = Logger::DEBUG
|
110
|
+
else
|
111
|
+
log.level = Logger::INFO
|
112
|
+
end
|
113
|
+
|
114
|
+
trap("ALRM") do
|
115
|
+
log.info("Timeout after #{options[:timeout]}s")
|
116
|
+
exit 4
|
117
|
+
end
|
118
|
+
|
119
|
+
Thread.new {
|
120
|
+
sleep options[:timeout]
|
121
|
+
Process.kill( "ALRM", Process.pid )
|
122
|
+
}
|
123
|
+
|
124
|
+
if File.exist?(options[:lockfile])
|
125
|
+
|
126
|
+
other_pid = File.read(options[:lockfile]).to_i
|
127
|
+
|
128
|
+
begin
|
129
|
+
Process.getpgid( other_pid )
|
130
|
+
log.info("Another zcollective process (#{other_pid}) holds the lock")
|
131
|
+
exit 3
|
132
|
+
rescue Errno::ESRCH
|
133
|
+
log.info("Deleting stale lock file")
|
134
|
+
FileUtils.remove_file(options[:lockfile], true)
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
File.open( options[:lockfile], File::CREAT | File::EXCL | File::WRONLY ) do |o|
|
140
|
+
o.write(Process.pid)
|
141
|
+
end
|
142
|
+
|
143
|
+
log.debug( "Connecting to Zabbix RPC service" )
|
144
|
+
|
145
|
+
zabbix_client = ZCollective::ZabbixClient.new(
|
146
|
+
:url => options[:zabbix_api_url],
|
147
|
+
:user => options[:zabbix_user],
|
148
|
+
:password => options[:zabbix_pass],
|
149
|
+
:debug => options[:debug]
|
150
|
+
)
|
151
|
+
|
152
|
+
log.debug( "Connected and authenticated" )
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
############################################################################
|
157
|
+
# Fetch list of zabbix templates
|
158
|
+
|
159
|
+
log.debug( "Fetching list of zabbix templates" )
|
160
|
+
|
161
|
+
zabbix_templates = {}
|
162
|
+
|
163
|
+
zabbix_client.request( 'template.get',
|
164
|
+
'search' => '',
|
165
|
+
'output' => 'extend'
|
166
|
+
).each do |template|
|
167
|
+
|
168
|
+
log.debug( "\tName: #{template['name']} ID: #{template['templateid']}" )
|
169
|
+
zabbix_templates[ template['name'] ] = template['templateid']
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
# We're going to build a big nasty hash of zabbix and mcollective data
|
174
|
+
# because apparently I still think like a Perl engineer. It seems this
|
175
|
+
# dirty bit of Ruby is necessary to allow them to be anonymously built.
|
176
|
+
#
|
177
|
+
nested_hash = lambda {|hash, key| hash[key] = Hash.new(&nested_hash)}
|
178
|
+
hosts = Hash.new(&nested_hash)
|
179
|
+
|
180
|
+
# An array of collectives to create hostgroups of:
|
181
|
+
collectives = Array.new
|
182
|
+
|
183
|
+
|
184
|
+
############################################################################
|
185
|
+
# Fetch list of zabbix groups
|
186
|
+
# Create "ZCollective discovered hosts" group if it doesn't exist
|
187
|
+
|
188
|
+
log.debug( "Fetching list of zabbix hostgroups" )
|
189
|
+
|
190
|
+
zcollective_hostgroup_name = 'ZCollective discovered hosts'
|
191
|
+
zcollective_hostgroup = nil
|
192
|
+
|
193
|
+
zabbix_client.request( 'hostgroup.get',
|
194
|
+
'search' => '',
|
195
|
+
'output' => 'extend'
|
196
|
+
).each do |hostgroup|
|
197
|
+
|
198
|
+
log.debug("\tName: #{hostgroup['name']} ID: #{hostgroup['groupid']}")
|
199
|
+
|
200
|
+
if hostgroup['name'] == zcollective_hostgroup_name
|
201
|
+
zcollective_hostgroup = hostgroup['groupid']
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
if zcollective_hostgroup.nil?
|
207
|
+
|
208
|
+
if options[:noop]
|
209
|
+
|
210
|
+
log.debug("No zcollective hostgroup, but not creating as " <<
|
211
|
+
"we're in --noop mode")
|
212
|
+
|
213
|
+
else
|
214
|
+
|
215
|
+
log.debug("No zcollective hostgroup: creating")
|
216
|
+
|
217
|
+
resp = zabbix_client.request( 'hostgroup.create',
|
218
|
+
'name' => zcollective_hostgroup_name
|
219
|
+
)
|
220
|
+
|
221
|
+
zcollective_hostgroup = resp['groupids'].first
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
log.debug("ZCollective hostgroup: #{zcollective_hostgroup}")
|
228
|
+
|
229
|
+
|
230
|
+
|
231
|
+
############################################################################
|
232
|
+
# Iterate through zabbix hosts
|
233
|
+
|
234
|
+
zabbix_client.request( 'host.get',
|
235
|
+
'search' => '',
|
236
|
+
'output' => 'extend'
|
237
|
+
).each do |host|
|
238
|
+
|
239
|
+
log.debug( "Host: #{host['name']}, ID: #{host['hostid']}" )
|
240
|
+
|
241
|
+
# Iterate through hostinterfaces, looking for zabbix agent type
|
242
|
+
# interfaces.
|
243
|
+
#
|
244
|
+
# I'm not sure how we should handle multiple interfaces here
|
245
|
+
# but it seems a safe assumption that there will only be one
|
246
|
+
# agent type interface per machine.
|
247
|
+
|
248
|
+
zabbix_client.request( 'hostinterface.get',
|
249
|
+
'hostids' => host['hostid'],
|
250
|
+
'output' => 'extend'
|
251
|
+
).each do |interface|
|
252
|
+
|
253
|
+
next unless interface['type'] == "1" # skip non-Zabbix agent interfaces
|
254
|
+
|
255
|
+
log.debug( "\tIP: #{interface['ip']}" )
|
256
|
+
hosts[ host['name'] ][:zabbix][:ip] = interface['ip']
|
257
|
+
|
258
|
+
end
|
259
|
+
|
260
|
+
hosts[ host['name'] ][:zabbix][:hostid] = host['hostid']
|
261
|
+
hosts[ host['name'] ][:zabbix][:templates] = []
|
262
|
+
|
263
|
+
# Iterate through this host's templates
|
264
|
+
|
265
|
+
zabbix_client.request(
|
266
|
+
'template.get',
|
267
|
+
'search' => '',
|
268
|
+
'output' => 'extend',
|
269
|
+
'hostids' => host['hostid']
|
270
|
+
).each do |template|
|
271
|
+
|
272
|
+
log.debug( "\tTemplate: #{template['name']}" )
|
273
|
+
hosts[ host['name'] ][:zabbix][:templates].push( template['name'] )
|
274
|
+
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
278
|
+
|
279
|
+
|
280
|
+
|
281
|
+
############################################################################
|
282
|
+
# Iterate through MCollective hosts
|
283
|
+
|
284
|
+
include MCollective::RPC
|
285
|
+
|
286
|
+
mc = rpcclient("rpcutil", :debug => true)
|
287
|
+
begin
|
288
|
+
zt_mc = rpcclient("zabbix_template", :exit_on_failure => false)
|
289
|
+
rescue Exception
|
290
|
+
log.warn("No zabbix_template mcollective rpc agent found")
|
291
|
+
end
|
292
|
+
if (!zt_mc.nil?)
|
293
|
+
zt_mc.progress = false
|
294
|
+
end
|
295
|
+
mc.progress = false
|
296
|
+
mc.discover.sort.each do |host|
|
297
|
+
|
298
|
+
# MCollective returns FQDN name, and we probably want to use the short
|
299
|
+
# form name in zabbix.
|
300
|
+
|
301
|
+
short_hostname = host.split('.').first
|
302
|
+
|
303
|
+
log.debug("Host: #{short_hostname} (#{host})")
|
304
|
+
|
305
|
+
# Get inventory details for each host
|
306
|
+
inventory = mc.custom_request( "inventory", {}, host,
|
307
|
+
{ "identity" => host }
|
308
|
+
).first
|
309
|
+
|
310
|
+
# Work through network interfaces reported by Facter and find the first
|
311
|
+
# which matches the CIDR passed on the commandline. Use that to talk
|
312
|
+
# zabbix to.
|
313
|
+
|
314
|
+
cidr_to_match = NetAddr::CIDR.create( options[:interface_cidr] )
|
315
|
+
ip = nil
|
316
|
+
|
317
|
+
inventory[:data][:facts].sort.each do |key,value|
|
318
|
+
|
319
|
+
next unless key.match(/^ipaddress_/)
|
320
|
+
|
321
|
+
log.debug("Potential IP interface #{key} with IP #{value}")
|
322
|
+
|
323
|
+
ip_cidr = NetAddr::CIDR.create( value )
|
324
|
+
if ip_cidr.is_contained?( cidr_to_match)
|
325
|
+
|
326
|
+
log.debug("IP matches CIDR #{options[:interface_cidr]}")
|
327
|
+
|
328
|
+
ip = value
|
329
|
+
break
|
330
|
+
|
331
|
+
else
|
332
|
+
log.debug("IP doesn't match CIDR")
|
333
|
+
end
|
334
|
+
|
335
|
+
end
|
336
|
+
|
337
|
+
unless ip
|
338
|
+
raise "Host #{host} has no IP matching the target CIDR #{options[:interface_cidr]}"
|
339
|
+
end
|
340
|
+
|
341
|
+
log.debug("\tIP #{ip}")
|
342
|
+
|
343
|
+
# Find whether we have to use different or any extra templates in Zabbix
|
344
|
+
# for any of the modules on this host. Only do this if we were
|
345
|
+
# successfully able to create the zabbix_template mcollective rpcclient.
|
346
|
+
if (!zt_mc.nil?)
|
347
|
+
host_template_info = zt_mc.custom_request( "templates", {},
|
348
|
+
host,
|
349
|
+
{"identity" => host}
|
350
|
+
).first
|
351
|
+
end
|
352
|
+
|
353
|
+
if (!host_template_info.nil?)
|
354
|
+
hosts[ short_hostname ][:aliases] = host_template_info[:data][:aliases]
|
355
|
+
hosts[ short_hostname ][:extras] = host_template_info[:data][:extras]
|
356
|
+
end
|
357
|
+
|
358
|
+
hosts[ short_hostname ][:mcollective][:ip] = ip
|
359
|
+
hosts[ short_hostname ][:mcollective][:classes] = inventory[:data][:classes]
|
360
|
+
hosts[ short_hostname ][:mcollective][:collectives] = inventory[:data][:collectives]
|
361
|
+
|
362
|
+
collectives << hosts[ short_hostname ][:mcollective][:collectives]
|
363
|
+
|
364
|
+
end
|
365
|
+
|
366
|
+
mc.disconnect
|
367
|
+
|
368
|
+
###########################################################################
|
369
|
+
# Fetch list of zabbix groups & Create hosts groups of discoverred
|
370
|
+
# collectives, to add hosts to later
|
371
|
+
|
372
|
+
log.debug("collectives: #{collectives.flatten.inspect}")
|
373
|
+
log.debug("uniq collectives: #{collectives.flatten.uniq.inspect}")
|
374
|
+
|
375
|
+
collectives_to_hostgroups = collectives.flatten.uniq
|
376
|
+
|
377
|
+
collectives_to_hostgroups.each do | collective |
|
378
|
+
|
379
|
+
log.debug( "Fetching list of zabbix hostgroups" )
|
380
|
+
|
381
|
+
collective_hostgroup_name = "#{collective}"
|
382
|
+
collective_hostgroup_id = nil
|
383
|
+
|
384
|
+
zabbix_client.request( 'hostgroup.get',
|
385
|
+
'search' => '',
|
386
|
+
'output' => 'extend'
|
387
|
+
).each do |hostgroup|
|
388
|
+
|
389
|
+
log.debug("\tName: #{hostgroup['name']} ID: #{hostgroup['groupid']}")
|
390
|
+
|
391
|
+
if hostgroup['name'] == collective_hostgroup_name
|
392
|
+
collective_hostgroup_id = hostgroup['groupid']
|
393
|
+
end
|
394
|
+
|
395
|
+
end
|
396
|
+
|
397
|
+
if collective_hostgroup_id.nil?
|
398
|
+
|
399
|
+
if options[:noop]
|
400
|
+
|
401
|
+
log.debug("No #{collective} hostgroup, but not creating as " <<
|
402
|
+
"we're in --noop mode")
|
403
|
+
else
|
404
|
+
|
405
|
+
log.debug("No #{collective} hostgroup: creating")
|
406
|
+
|
407
|
+
resp = zabbix_client.request( 'hostgroup.create',
|
408
|
+
'name' => collective
|
409
|
+
)
|
410
|
+
|
411
|
+
collective_hostgroup_id = resp['groupids'].first
|
412
|
+
|
413
|
+
end
|
414
|
+
|
415
|
+
end
|
416
|
+
|
417
|
+
log.info("New hostgroup: #{collective}")
|
418
|
+
log.info("New hostgroup's ID: #{collective_hostgroup_id}")
|
419
|
+
|
420
|
+
end
|
421
|
+
|
422
|
+
|
423
|
+
############################################################################
|
424
|
+
# Rationalise the two datasets, iterating over the hosts and carrying out
|
425
|
+
# the appropriate actions
|
426
|
+
|
427
|
+
hosts.each do |host,data|
|
428
|
+
|
429
|
+
|
430
|
+
|
431
|
+
###### Condition 1 #############################################
|
432
|
+
#
|
433
|
+
# Hosts that are found by mcollective but that aren't in zabbix
|
434
|
+
# should be added.
|
435
|
+
|
436
|
+
if data.has_key?(:mcollective) and !data.has_key?(:zabbix)
|
437
|
+
|
438
|
+
log.info( "Host #{host} found by mcollective but not in zabbix" )
|
439
|
+
|
440
|
+
# If mcollective finds a host, but zabbix doesn't list one by
|
441
|
+
# that name, we're going to add it.
|
442
|
+
|
443
|
+
# Iterate through the classes reported by mcollective for this
|
444
|
+
# host. If the class name matches the name of a zabbix template,
|
445
|
+
# get the ID and add an object to the templates_to_add array.
|
446
|
+
# This will be passed to the zabbix API call.
|
447
|
+
|
448
|
+
templates_to_add = []
|
449
|
+
data[:mcollective][:classes].each do |template|
|
450
|
+
# if the class name has a :: in it, replace it with an underscore
|
451
|
+
# for the purposes of finding a template with that name
|
452
|
+
template = template.sub(/::/, '_')
|
453
|
+
classname = template
|
454
|
+
|
455
|
+
# if we have an alias for this template, use that instead
|
456
|
+
if ( ! data[:aliases].nil? and data[:aliases].has_key?( template ) )
|
457
|
+
template = data[:aliases][ template ]
|
458
|
+
log.debug("\tUsing alias #{template} for #{classname}")
|
459
|
+
end
|
460
|
+
|
461
|
+
next unless zabbix_templates.has_key?( template )
|
462
|
+
template_id = zabbix_templates[ template ]
|
463
|
+
log.debug("\tWill be adding template #{template} ID #{template_id}")
|
464
|
+
templates_to_add.push( { 'templateid' => template_id } )
|
465
|
+
|
466
|
+
# if we have any extra zabbix templates to add for this class
|
467
|
+
# then add them to the array of templates to add for this host
|
468
|
+
if ( ! data[:extras].nil? and data[:extras].has_key?( classname ) )
|
469
|
+
|
470
|
+
extra_templates = data[:extras][ classname ].split(/,/)
|
471
|
+
|
472
|
+
extra_templates.each do |extra_template|
|
473
|
+
next unless zabbix_templates.has_key?( extra_template )
|
474
|
+
template_id = zabbix_templates[ extra_template ]
|
475
|
+
log.debug("\tWill be adding template #{extra_template} ID #{template_id} for #{classname}")
|
476
|
+
templates_to_add.push( { 'templateid' => template_id } )
|
477
|
+
end
|
478
|
+
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
### get list of current hostgroup, so we can add to the correct groups by ID
|
483
|
+
groups_by_id = []
|
484
|
+
zabbix_client.request( 'hostgroup.get',
|
485
|
+
'search' => '',
|
486
|
+
'output' => 'extend'
|
487
|
+
).each do |hostgroup|
|
488
|
+
|
489
|
+
host_group_hash = {}
|
490
|
+
|
491
|
+
log.debug("\tName: #{hostgroup['name']} ID: #{hostgroup['groupid']}")
|
492
|
+
|
493
|
+
if data[:mcollective][:collectives].include? hostgroup['name']
|
494
|
+
host_group_hash['groupid'] = hostgroup['groupid']
|
495
|
+
groups_by_id << host_group_hash
|
496
|
+
end
|
497
|
+
|
498
|
+
end
|
499
|
+
|
500
|
+
if options[:noop]
|
501
|
+
|
502
|
+
log.info("--noop passed - not making changes")
|
503
|
+
|
504
|
+
else
|
505
|
+
|
506
|
+
# If we're not in --noop mode, create the host with the
|
507
|
+
# zabbix API. Hosts need at least one interface (for now
|
508
|
+
# we're just adding a Zabbix agent interface), and need
|
509
|
+
# to be in a group.
|
510
|
+
|
511
|
+
resp = zabbix_client.request( 'host.create',
|
512
|
+
'host' => host,
|
513
|
+
'interfaces' => [
|
514
|
+
{
|
515
|
+
'type' => 1,
|
516
|
+
'main' => 1,
|
517
|
+
'useip' => options[:connect_by_ip],
|
518
|
+
'ip' => data[:mcollective][:ip],
|
519
|
+
'dns' => host,
|
520
|
+
'port' => '10050'
|
521
|
+
}
|
522
|
+
],
|
523
|
+
'groups' => groups_by_id,
|
524
|
+
'templates' => templates_to_add
|
525
|
+
)
|
526
|
+
|
527
|
+
# This call returns the created host id
|
528
|
+
|
529
|
+
new_hostid = resp['hostids'].first
|
530
|
+
|
531
|
+
log.info("Host #{host} added as ID #{new_hostid} " <<
|
532
|
+
"with #{templates_to_add.count} templates")
|
533
|
+
|
534
|
+
end
|
535
|
+
|
536
|
+
end
|
537
|
+
|
538
|
+
|
539
|
+
|
540
|
+
###### Condition 2 #############################################
|
541
|
+
# If zabbix contains a host that mcollective knows nothing about
|
542
|
+
# we leave it alone but report it.
|
543
|
+
|
544
|
+
if data.has_key?(:zabbix) and !data.has_key?(:mcollective)
|
545
|
+
|
546
|
+
log.warn( "Host #{host} found in zabbix but not by mcollective" )
|
547
|
+
|
548
|
+
end
|
549
|
+
|
550
|
+
|
551
|
+
|
552
|
+
###### Condition 3 #############################################
|
553
|
+
# Hosts in zabbix and mcollective are checked to ensure that
|
554
|
+
# they are linked with at least the templates they should be
|
555
|
+
|
556
|
+
if data.has_key?(:zabbix) and data.has_key?(:mcollective)
|
557
|
+
|
558
|
+
log.debug( "Host #{host} in both zabbix and mcollective" )
|
559
|
+
|
560
|
+
# Compare interface addresses and warn if mismatched
|
561
|
+
|
562
|
+
if data[:mcollective][:ip] != data[:zabbix][:ip]
|
563
|
+
log.warn("Host #{host} monitored, but IP mismatch " <<
|
564
|
+
"M:#{data[:mcollective][:ip]} " <<
|
565
|
+
"Z:#{data[:zabbix][:ip]}"
|
566
|
+
)
|
567
|
+
end
|
568
|
+
|
569
|
+
templates_need_adding = []
|
570
|
+
|
571
|
+
# Iterate through the classes mcollective lists for the host
|
572
|
+
|
573
|
+
data[:mcollective][:classes].each do |template|
|
574
|
+
|
575
|
+
# templates_should_have is a list of templates that zabbix should have
|
576
|
+
# for this particular host, for this class.
|
577
|
+
templates_should_have = []
|
578
|
+
|
579
|
+
# Ignore any that don't match the name of a zabbix template
|
580
|
+
# Again, replace :: with _ to match template names
|
581
|
+
template = template.sub(/::/, '_')
|
582
|
+
classname = template
|
583
|
+
|
584
|
+
# if we have an alias for this template, use that instead
|
585
|
+
if ( ! data[:aliases].nil? and data[:aliases].has_key?( template ) )
|
586
|
+
template = data[:aliases][ template ]
|
587
|
+
log.debug("\tUsing alias #{template} for #{classname}")
|
588
|
+
end
|
589
|
+
|
590
|
+
next unless zabbix_templates.has_key?( template )
|
591
|
+
templates_should_have.push( template )
|
592
|
+
log.debug("\tHas mcollective class #{classname} matching a zabbix template")
|
593
|
+
|
594
|
+
# if we have any extra zabbix templates to add for this class
|
595
|
+
# then add them to the array of templates to add for this host
|
596
|
+
if ( ! data[:extras].nil? and data[:extras].has_key?( classname ) )
|
597
|
+
|
598
|
+
extra_templates = data[:extras][ classname ].split(/,/)
|
599
|
+
|
600
|
+
extra_templates.each do |extra_template|
|
601
|
+
next unless zabbix_templates.has_key?( extra_template )
|
602
|
+
template_id = zabbix_templates[ extra_template ]
|
603
|
+
log.debug("\tHas extra template #{extra_template} defined for #{classname}")
|
604
|
+
templates_should_have.push( extra_template )
|
605
|
+
end
|
606
|
+
|
607
|
+
end
|
608
|
+
|
609
|
+
# iterate over the templates that we should have, and if they're
|
610
|
+
# not already linked, add them to the list of templates which
|
611
|
+
# need adding for this host.
|
612
|
+
templates_should_have.each do |tpl_should_have|
|
613
|
+
|
614
|
+
if data[:zabbix][:templates].index( tpl_should_have )
|
615
|
+
|
616
|
+
# The host in zabbix is already linked to this template
|
617
|
+
log.debug("\tZabbix host already linked to template #{tpl_should_have}")
|
618
|
+
|
619
|
+
else
|
620
|
+
|
621
|
+
# Zabbix shows that although it knows about this template
|
622
|
+
# the host in question isn't linked to it. We add this
|
623
|
+
# template to a list of those that are missing in zabbix.
|
624
|
+
log.info("\tZabbix #{host} not linked to template #{tpl_should_have}")
|
625
|
+
templates_need_adding.push( { 'templateid' => zabbix_templates[ tpl_should_have ] } )
|
626
|
+
|
627
|
+
end
|
628
|
+
|
629
|
+
end
|
630
|
+
|
631
|
+
end
|
632
|
+
|
633
|
+
if templates_need_adding.count > 0
|
634
|
+
|
635
|
+
if options[:noop]
|
636
|
+
|
637
|
+
log.info("--noop passed - not making changes")
|
638
|
+
|
639
|
+
else
|
640
|
+
|
641
|
+
# If we're not running --noop and we found missing templates,
|
642
|
+
# link the zabbix host with these.
|
643
|
+
|
644
|
+
zabbix_client.request( 'template.massadd',
|
645
|
+
'templates' => templates_need_adding,
|
646
|
+
'hosts' => { 'hostid' => data[:zabbix][:hostid] }
|
647
|
+
)
|
648
|
+
|
649
|
+
log.info("Added missing templates to #{host}")
|
650
|
+
|
651
|
+
end
|
652
|
+
|
653
|
+
end
|
654
|
+
|
655
|
+
end
|
656
|
+
|
657
|
+
end
|
658
|
+
|
659
|
+
FileUtils.remove_file(options[:lockfile], true)
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# Copyright (c) 2012, 2013, The Scale Factory Ltd.
|
2
|
+
# All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
6
|
+
# * Redistributions of source code must retain the above copyright
|
7
|
+
# notice, this list of conditions and the following disclaimer.
|
8
|
+
# * Redistributions in binary form must reproduce the above copyright
|
9
|
+
# notice, this list of conditions and the following disclaimer in the
|
10
|
+
# documentation and/or other materials provided with the distribution.
|
11
|
+
# * Neither the name of the The Scale Factory Ltd nor the
|
12
|
+
# names of its contributors may be used to endorse or promote products
|
13
|
+
# derived from this software without specific prior written permission.
|
14
|
+
#
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
16
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
17
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
18
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE SCALE FACTORY LTD BE LIABLE FOR ANY
|
19
|
+
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
20
|
+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
21
|
+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
22
|
+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
23
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
24
|
+
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
25
|
+
|
26
|
+
require 'json'
|
27
|
+
require 'net/http'
|
28
|
+
require 'logger'
|
29
|
+
require 'ostruct'
|
30
|
+
|
31
|
+
module ZCollective
|
32
|
+
|
33
|
+
class ZabbixClient
|
34
|
+
|
35
|
+
@restclient_options = { :content_type => 'application/json' }
|
36
|
+
|
37
|
+
@url
|
38
|
+
@auth_hash
|
39
|
+
@options
|
40
|
+
@log
|
41
|
+
|
42
|
+
def initialize ( options = {} )
|
43
|
+
@options = options
|
44
|
+
|
45
|
+
@log = Logger.new(STDERR)
|
46
|
+
if( @options[:debug] )
|
47
|
+
@log.level = Logger::DEBUG
|
48
|
+
else
|
49
|
+
@log.level = Logger::WARN
|
50
|
+
end
|
51
|
+
|
52
|
+
@auth_hash = authenticate
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
def authenticate ( )
|
57
|
+
|
58
|
+
response = request( 'user.authenticate',
|
59
|
+
:user => @options[:user],
|
60
|
+
:password => @options[:password]
|
61
|
+
)
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
def request_json( method, *args )
|
66
|
+
|
67
|
+
req = {
|
68
|
+
:jsonrpc => '2.0',
|
69
|
+
:method => method,
|
70
|
+
:params => Hash[*args.flatten],
|
71
|
+
:id => rand( 100000 )
|
72
|
+
}
|
73
|
+
|
74
|
+
if @auth_hash
|
75
|
+
req[:auth] = @auth_hash
|
76
|
+
end
|
77
|
+
|
78
|
+
JSON.generate( req )
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
def request( method, *args )
|
83
|
+
|
84
|
+
json = request_json( method, *args )
|
85
|
+
|
86
|
+
uri = URI.parse( @options[:url] )
|
87
|
+
proxy = ENV['http_proxy'] ? URI.parse(ENV['http_proxy']) : OpenStruct.new
|
88
|
+
http = Net::HTTP::Proxy(proxy.host, proxy.port).new( uri.host, uri.port )
|
89
|
+
|
90
|
+
request = Net::HTTP::Post.new( uri.request_uri )
|
91
|
+
request.add_field( 'Content-Type', 'application/json-rpc' )
|
92
|
+
request.body = json
|
93
|
+
|
94
|
+
@log.debug( "HTTP Request: #{uri} #{json}" )
|
95
|
+
|
96
|
+
response = http.request( request )
|
97
|
+
|
98
|
+
unless response.code == "200"
|
99
|
+
raise "HTTP Error: #{response.code}"
|
100
|
+
end
|
101
|
+
|
102
|
+
@log.debug( "HTTP Response: #{response.body}" )
|
103
|
+
|
104
|
+
result = JSON.parse( response.body )
|
105
|
+
|
106
|
+
if result['error']
|
107
|
+
raise "JSON-RPC error: #{result['error']['message']} (#{result['error']['data']})"
|
108
|
+
end
|
109
|
+
|
110
|
+
result['result']
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zcollective
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.9
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jon Topper
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-12-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: netaddr
|
16
|
+
requirement: &70263403228180 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.5.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70263403228180
|
25
|
+
description: ZCollective is a tool used to configure Zabbix using data discovered
|
26
|
+
using MCollective.
|
27
|
+
email: jon@scalefactory.com
|
28
|
+
executables:
|
29
|
+
- zcollective
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/zcollective/zabbixclient.rb
|
34
|
+
- bin/zcollective
|
35
|
+
homepage: http://github.com/scalefactory/zcollective
|
36
|
+
licenses: []
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
requirements: []
|
54
|
+
rubyforge_project:
|
55
|
+
rubygems_version: 1.8.11
|
56
|
+
signing_key:
|
57
|
+
specification_version: 3
|
58
|
+
summary: Zabbix/MCollective integration
|
59
|
+
test_files: []
|
60
|
+
has_rdoc:
|