summaryrefslogtreecommitdiff
path: root/build-nagios
blob: 2ace17cf00f6abf5b8aa56bba7f8cf64d041f583 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
#!/usr/bin/ruby

# Copyright (c) 2004, 2005, 2006, 2007, 2008 Peter Palfrader <peter@palfrader.org>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require "yaml"

ORG="relativity"
SHORTORG="rela"
GENERATED_PREFIX="/etc/NOREPLY/generated/nagios/"

nagios_filename = {};
%w(hosts hostgroups services dependencies hostextinfo serviceextinfo servicegroups).each{
	|x| nagios_filename[x] = GENERATED_PREFIX+"auto-#{x}.cfg"
}
nagios_filename['nrpe'] = GENERATED_PREFIX+"nrpe_#{ ORG }.cfg"


MAX_CHECK_ATTEMPTS_DEFAULT=6

NRPE_CHECKNAME="#{ ORG }_check_nrpe"           # check that takes one argument:  service name to be checked
NRPE_CHECKNAME_HOST="#{ ORG }_check_nrpe_host" # check that takes two arguments: relay host on which to run check, service name to be checked

HOST_TEMPLATE_NAME='generic-host'          # host templates that all our host definitions use
SERVICE_TEMPLATE_NAME='generic-service'    # host templates that all our host definitions use
HOST_ALIVE_CHECK='check-host-alive'        # host alive check if server is pingable
NRPE_PROCESS_SERVICE='process - nrpe'      # nrpe checks will depend on this one


def warn (msg)
	STDERR.puts msg
end
def set_if_unset(hash, key, value)
	hash[key] = value unless hash.has_key?(key)
end
def set_complain_if_set(hash, key, value, type, name)
	throw "#{type} definition '#{name}' has '#{key}' already defined" if hash.has_key?(key)
	hash[key] = value
end

# Make an array out of something.  If there is nothing, create an empty array
# if it is just a string, make a list with just that element, if it already is
# an array keep it.
def ensure_array(something)
	if (something == nil)
		result = []
	elsif something.kind_of?(String)
		result = [ something ]
	elsif something.kind_of?(Array)
		result = something
	else
		throw "Do now know how to make an array out of #{something}: " + something.to_yaml
	end
	return result
end


# This class keeps track of the checks done via NRPE and makes sure
# each gets a unique name.
#
# Unforutunately NRPE limits check names to some 30 characters, so
# we need to mangle service names near the end.
class Nrpe
	def initialize
		@checks = {}
	end

	def make_name( name, check )
		name = name.tr_s("^a-zA-Z", "_").gsub("process", "ps")

		result = "#{ SHORTORG }_" + name[0,19]

		hash = ''
		skew = ''
		while (@checks.has_key?(result + hash))
			# hash it, so that we don't lose uniqeness by cutting it off
			hash = (check+skew).crypt("$1$")
			hash = hash[-5,5]  # 5 chars are enough
			hash.tr!("/", "_")
			skew += ' ' # change it a bit so the hash changes
		end
		result += hash
		return result      # max of 32 or so chars
	end

	def add( name, check )
		if @checks.has_value? check
			@checks.each_pair{ |key, value|
				return key if value == check
			}
		end
		key = make_name(name, check)
		@checks[ key ] = check
		return key
	end

	def checks
		return @checks
	end
end
$nrpe = Nrpe.new()


# Prints the keys and values of hash to a file
# This is the function that prints the bodies of most our
# host/service/etc definitions
#
# It skips over such keys as are listed in exclude_keys
# and also skips private keys (those starting with an underscre)
def print_block(fd, kind, hash, exclude_keys)
	fd.puts "define #{kind} {"
	hash.each_pair{ |key, value|
		next if key[0,1] == '_'
		next if exclude_keys.include? key
		fd.puts "	#{key}		#{value}"
	}
	fd.puts "}"
	fd.puts
end

def merge_contacts(host, service)
	%w{contacts contact_groups}.each{ |k|
		contacts = []
		[host, service].each{ |source|
			contacts.push source[k] if source.has_key?(k)
		}
		service[k] = contacts.join(",") unless contacts.empty?
	}
end

# Add the service definition service to hosts
# f is the file for service definitions, deps the file for dependencies
def addService(hosts, service, files, servers)

	set_if_unset        service, 'use'               , SERVICE_TEMPLATE_NAME
	set_if_unset        service, 'max_check_attempts', MAX_CHECK_ATTEMPTS_DEFAULT

	service['max_check_attempts'] = MAX_CHECK_ATTEMPTS_DEFAULT + service['max_check_attempts'] if service['max_check_attempts'] < 0

	if service['nrpe']
		throw "We already have a check_command (#{service['check_command']}) but we are in the NRPE block (nrpe: #{service['nrpe']})."+
			"  This should have been caught much earlier" if service.has_key?('check_command');

		check = $nrpe.add(service['service_description'], service['nrpe'])
		service['check_command'] = "#{ NRPE_CHECKNAME }!#{ check }"

		service['depends'] = ensure_array( service['depends'] )
		service['depends'] << NRPE_PROCESS_SERVICE unless service['service_description'] == NRPE_PROCESS_SERVICE  # Depend on NRPE unless we are it
	end

	hosts.each{ |host|
		s = service.clone
		set_complain_if_set s, 'host_name', host, 'Service', s['service_description']
		merge_contacts(servers[host], s)

		print_block files['services'], 'service', s, %w(nrpe runfrom remotecheck
		                                                depends
		                                                hosts hostgroups excludehosts excludehostgroups)
	}

	if service['depends']
		service['depends'].each{ |prerequisite|
			hosts.each{ |host|
				prerequisite_host = host
				pre = prerequisite
				# split off a hostname if there's one
				bananasplit = prerequisite.split(':')
				if bananasplit.size == 2
					prerequisite_host = bananasplit[0]
					pre = bananasplit[1]
				elsif bananasplit.size > 2
					throw "Cannot prase prerequisite #{prerequisite} for service #{service['service_description']} into host:service"
				end
				dependency = {
					'host_name'                     => prerequisite_host,
					'service_description'           => pre,
					'dependent_host_name'           => host,
					'dependent_service_description' => service['service_description'],
					'execution_failure_criteria'    => 'n',
					'notification_failure_criteria' => 'w,u,c'
				};
				print_block files['dependencies'], 'servicedependency', dependency, %w()
			}
		}
	end


	set_complain_if_set service['_extinfo'], 'service_description' , service['service_description'], 'serviceextinfo', service['service_description']
	set_complain_if_set service['_extinfo'], 'host_name'           , hosts.join(',')               , 'serviceextinfo', service['service_description']

	print_block files['serviceextinfo'], 'serviceextinfo', service['_extinfo'], %w()
end

# hostlists in services can be given as both, single hosts and hostgroups
# This functinn merges hostgroups and a simple list of hosts
#
# it also takes a prefix so that it can be used for excludelists as well
def merge_hosts_and_hostgroups(service, servers, hostgroups, prefix)
	hosts = []
	hosts = service[prefix+'hosts'].split(/,/).map{ |x| x.strip } if service[prefix+'hosts']
	hosts.each{ |host|
		throw "host #{host} does not exist - used in service #{service['service_description']}" unless servers[host]
	};
	if service[prefix+'hostgroups']
		service[prefix+'hostgroups'].split(/,/).map{ |x| x.strip }.each{ |hg|
			throw "hostgroup #{hg} does not exist - used in service #{service['service_description']}" unless hostgroups[hg]
			hosts = hosts.concat hostgroups[hg]['_memberlist']
		}
	end

	return hosts
end

# Figure out the hosts a given service applies to
#
# For a given service find the list of hosts minus excluded hosts that this service runs on
def find_hosts(service, servers, hostgroups)
	hosts        = merge_hosts_and_hostgroups service, servers, hostgroups, ''
	excludehosts = merge_hosts_and_hostgroups service, servers, hostgroups, 'exclude'

	excludehosts.each{ |host|
		if hosts.delete(host) == nil
			throw "Cannot remove host #{host} from service #{service['service_description']}: it's not included anyway or excluded twice."
		end
	}

	return hosts
end

# Move all elements that have a key that starts with "extinfo-"
# into the _extinfo subhash
def split_away_extinfo(hash)
	hash['_extinfo'] = {}
	hash.keys.each{ |key|
		if key[0, 8] == 'extinfo-'
			hash['_extinfo'][ key[8, key.length-8] ] = hash[key]
			hash.delete(key);
		end
	}
end


#############################################################################################
#############################################################################################
#############################################################################################

# Load the config
config = YAML::load( File.open( 'nagios-master.cfg' ) )

files = {}
# Remove old created files
nagios_filename.each_pair{ |name, filename|
	files[name] = File.new(filename, "w")
}

#################################
# create a few hostgroups
#################################
# create the "all" and "pingable" hostgroups
config['hostgroups']['all'] = {}
config['hostgroups']['all']['alias'] = "all servers"
config['hostgroups']['all']['private'] = true
config['hostgroups']['pingable'] = {}
config['hostgroups']['pingable']['alias'] = "pingable servers"
config['hostgroups']['pingable']['private'] = true

config['hostgroups'].each_pair{ |name, hg|
	throw "Empty hostgroup or hostgroup #{name} not a hash" unless hg.kind_of?(Hash)
	split_away_extinfo hg

	hg['_memberlist'] = []
}

config['servers'].each_pair{ |name, server|
	throw "Empty server or server #{name} not a hash" unless server.kind_of?(Hash)

	split_away_extinfo server

	throw "No hostgroups defined for #{name}" unless server['hostgroups']
	server['_hostgroups'] = server['hostgroups'].split(/,/).map{ |x| x.strip };
	server['_hostgroups'] << 'all'
	server['_hostgroups'] << 'pingable' unless server['pingable'] == false

	server['_hostgroups'].each{ |hg|
		throw "Hostgroup #{hg} is not defined" unless config['hostgroups'].has_key?(hg)
		config['hostgroups'][hg]['_memberlist'] << name
	};
}

config['servicegroups'] = {} unless config.has_key? 'servicegroups'

##############
# HOSTS
##############
config['servers'].each_pair{ |name, server|
	# Formerly we used 'ip' instead of 'address' in our source file
	# Handle this change but warn					XXX
	if server.has_key?('ip')
		STDERR.puts("Host definition for #{name} has an 'ip' field.  Please use 'address' instead");
		server['address'] = server['ip'];
		server.delete('ip');
	end

	set_complain_if_set server, 'host_name'    , name, 'Host', name
	set_if_unset        server, 'alias'        , name
	set_if_unset        server, 'use'          , HOST_TEMPLATE_NAME
	set_if_unset        server, 'check_command', HOST_ALIVE_CHECK    unless server['pingable'] == false

	print_block files['hosts']      , 'host'       , server            , %w(hostgroups pingable)



	# Handle hostextinfo
	#config['hostgroups'][  server['_hostgroups'].first  ]['_extinfo'].each_pair{ |k, v|
	# find the first hostgroup that has extinfo
	extinfo = server['_hostgroups'].collect{ |hgname | config['hostgroups'][hgname]['_extinfo'] }.delete_if{ |ei| ei.size == 0 }.first
	if extinfo then
		extinfo.each_pair do |k, v|
			# substitute hostname into the notes_url
			v = sprintf(v,name) if k == 'notes_url'

			set_if_unset server['_extinfo'], k ,v
		end
	end

	set_complain_if_set server['_extinfo'], 'host_name'       , name, 'hostextinfo', name
	set_if_unset        server['_extinfo'], 'vrml_image'      , server['_extinfo']['icon_image'] if server['_extinfo'].has_key?('icon_image')
	set_if_unset        server['_extinfo'], 'statusmap_image' , server['_extinfo']['icon_image'] if server['_extinfo'].has_key?('icon_image')

	print_block files['hostextinfo'], 'hostextinfo', server['_extinfo'], %w()
}



##############
# HOSTGROUPS
##############
config['hostgroups'].each_pair{ |name, hg|
	next if hg['private']

	set_complain_if_set hg, 'hostgroup_name', name                       , 'Hostgroup', name
	set_complain_if_set hg, 'members'       , hg['_memberlist'].join(","), 'Hostgroup', name

	print_block files['hostgroups'], 'hostgroup', hg, %w()
}


##############
# SERVICES and DEPENDENCIES
##############
config['services'].each{ |service|
	throw "Empty service or service not a hash" unless service.kind_of?(Hash)

	split_away_extinfo service


	# Both 'name' and 'service_description' are valid for a service's name
	# Internally we only use service_description as that's nagios' official term
	if service.has_key?('name')
		throw "Service definition has both a name (#{service['name']})" +
		      "and a service_description (#{service['service_description']})" if service.has_key?('service_description')
		#STDERR.puts("Service definition #{service['name']} has a 'name' field.  Please use 'service_description' instead");
		service['service_description'] = service['name'];
		service.delete('name');
	end
	# Both 'check' and 'check_command' are valid for a service's check command
	# Internally we only use check_command as that's nagios' official term
	if service.has_key?('check')
		throw "Service definition has both a check (#{service['check']})" +
		      "and a check_command (#{service['check_command']})" if service.has_key?('check_command')
		#STDERR.puts("Service definition #{service['service_description']} has a 'check' field.  Please use 'check_command' instead");
		service['check_command'] = service['check'];
		service.delete('check');
	end


	hosts = find_hosts service, config['servers'], config['hostgroups']
	throw "no hosts for service #{service['service_description']}" if hosts.empty?

	throw "nrpe, check, and remotecheck are mutually exclusive in service #{service['service_description']}" if 
		(service['nrpe'] ? 1 : 0) +
		(service['check_command'] ? 1 : 0) +
		(service['remotecheck'] ? 1 : 0)  >= 2

	if service['runfrom'] && service['remotecheck']
		# If the service check is to be run from a remote monitor server ("relay")
		# add that as an NRPE check to be run on the relay and make this
		# service also depend on NRPE on the relay
		relay = service['runfrom']

		hosts.each{ |host|
			# how to recursively copy this thing?
			hostservice = YAML::load( service.to_yaml )
			host_ip = config['servers'][host]['address']
			throw "For some reason I do not have an address for #{host}.  This shouldn't be." unless host_ip

			remotecheck = hostservice['remotecheck']
			remotecheck.gsub!(/\$HOSTADDRESS\$/, host_ip)
			remotecheck.gsub!(/\$HOSTNAME\$/, host)
			check = $nrpe.add("#{host}_#{hostservice['service_description']}", remotecheck)
			hostservice['check_command'] = "#{NRPE_CHECKNAME_HOST}!#{ config['servers'][ relay ]['address'] }!#{ check }"

			# Make sure dependencies are an array.  If there are none, create an empty array
			# if depends is just a string, make a list with just that element
			hostservice['depends'] = ensure_array( hostservice['depends'] )
			# And append this new dependency
			hostservice['depends'] << "#{ relay }:#{ NRPE_PROCESS_SERVICE }";

			addService( [ host ], hostservice, files, config['servers'])
		}
	elsif service['runfrom'] || service['remotecheck']
		throw "runfrom and remotecheck must either appear both or not at all in service #{service['service_description']}"
		throw "must not remotecheck without runfrom" if service['remotecheck']
	else
		addService(hosts, service, files, config['servers'])
	end
}



##############
# SERVICEGROUPS
##############
config['servicegroups'].each_pair{ |name, sg|
	set_complain_if_set sg, 'servicegroup_name', name                       , 'Servicegroup', name

	print_block files['servicegroups'], 'servicegroup', sg, %w()
}


##############
# NRPE config file
##############
$nrpe.checks.each_pair{ |name, check|
	files['nrpe'].puts "command[#{ name }]=#{ check }"
}