[project @ peter@palfrader.org-20090128202601-op1dddmxu20gcze3]
[mirror/dsa-nagios.git] / build-nagios
1 #!/usr/bin/ruby
2
3 # Copyright (c) 2004, 2005, 2006, 2007, 2008 Peter Palfrader <peter@palfrader.org>
4
5 require "yaml"
6
7 ORG="dsa"
8 SHORTORG="dsa"
9 GENERATED_PREFIX="./generated/"
10
11 nagios_filename = {};
12 %w(hosts hostgroups services dependencies hostextinfo serviceextinfo servicegroups).each{
13         |x| nagios_filename[x] = GENERATED_PREFIX+"auto-#{x}.cfg"
14 }
15 nagios_filename['nrpe'] = GENERATED_PREFIX+"nrpe_#{ ORG }.cfg"
16
17
18 MAX_CHECK_ATTEMPTS_DEFAULT=6
19
20 NRPE_CHECKNAME="#{ ORG }_check_nrpe"           # check that takes one argument:  service name to be checked
21 NRPE_CHECKNAME_HOST="#{ ORG }_check_nrpe_host" # check that takes two arguments: relay host on which to run check, service name to be checked
22
23 HOST_TEMPLATE_NAME='generic-host'          # host templates that all our host definitions use
24 SERVICE_TEMPLATE_NAME='generic-service'    # host templates that all our host definitions use
25 HOST_ALIVE_CHECK='check-host-alive'        # host alive check if server is pingable
26 NRPE_PROCESS_SERVICE='process - nrpe'      # nrpe checks will depend on this one
27
28
29 def warn (msg)
30         STDERR.puts msg
31 end
32 def set_if_unset(hash, key, value)
33         hash[key] = value unless hash.has_key?(key)
34 end
35 def set_complain_if_set(hash, key, value, type, name)
36         throw "#{type} definition '#{name}' has '#{key}' already defined" if hash.has_key?(key)
37         hash[key] = value
38 end
39
40 # Make an array out of something.  If there is nothing, create an empty array
41 # if it is just a string, make a list with just that element, if it already is
42 # an array keep it.
43 def ensure_array(something)
44         if (something == nil)
45                 result = []
46         elsif something.kind_of?(String)
47                 result = [ something ]
48         elsif something.kind_of?(Array)
49                 result = something
50         else
51                 throw "Do now know how to make an array out of #{something}: " + something.to_yaml
52         end
53         return result
54 end
55
56
57 # This class keeps track of the checks done via NRPE and makes sure
58 # each gets a unique name.
59 #
60 # Unforutunately NRPE limits check names to some 30 characters, so
61 # we need to mangle service names near the end.
62 class Nrpe
63         def initialize
64                 @checks = {}
65         end
66
67         def make_name( name, check )
68                 name = name.tr_s("^a-zA-Z", "_").gsub("process", "ps")
69
70                 result = "#{ SHORTORG }_" + name[0,19]
71
72                 hash = ''
73                 skew = ''
74                 while (@checks.has_key?(result + hash))
75                         # hash it, so that we don't lose uniqeness by cutting it off
76                         hash = (check+skew).crypt("$1$")
77                         hash = hash[-5,5]  # 5 chars are enough
78                         hash.tr!("/", "_")
79                         skew += ' ' # change it a bit so the hash changes
80                 end
81                 result += hash
82                 return result      # max of 32 or so chars
83         end
84
85         def add( name, check )
86                 if @checks.has_value? check
87                         @checks.each_pair{ |key, value|
88                                 return key if value == check
89                         }
90                 end
91                 key = make_name(name, check)
92                 @checks[ key ] = check
93                 return key
94         end
95
96         def checks
97                 return @checks
98         end
99 end
100 $nrpe = Nrpe.new()
101
102
103 # Prints the keys and values of hash to a file
104 # This is the function that prints the bodies of most our
105 # host/service/etc definitions
106 #
107 # It skips over such keys as are listed in exclude_keys
108 # and also skips private keys (those starting with an underscre)
109 def print_block(fd, kind, hash, exclude_keys)
110         fd.puts "define #{kind} {"
111         hash.each_pair{ |key, value|
112                 next if key[0,1] == '_'
113                 next if exclude_keys.include? key
114                 fd.puts "       #{key}          #{value}"
115         }
116         fd.puts "}"
117         fd.puts
118 end
119
120 def merge_contacts(host, service)
121         %w{contacts contact_groups}.each{ |k|
122                 contacts = []
123                 [host, service].each{ |source|
124                         contacts.push source[k] if source.has_key?(k)
125                 }
126                 service[k] = contacts.join(",") unless contacts.empty?
127         }
128 end
129
130 # Add the service definition service to hosts
131 # f is the file for service definitions, deps the file for dependencies
132 def addService(hosts, service, files, servers)
133
134         set_if_unset        service, 'use'               , SERVICE_TEMPLATE_NAME
135         set_if_unset        service, 'max_check_attempts', MAX_CHECK_ATTEMPTS_DEFAULT
136
137         service['max_check_attempts'] = MAX_CHECK_ATTEMPTS_DEFAULT + service['max_check_attempts'] if service['max_check_attempts'] < 0
138
139         if service['nrpe']
140                 throw "We already have a check_command (#{service['check_command']}) but we are in the NRPE block (nrpe: #{service['nrpe']})."+
141                         "  This should have been caught much earlier" if service.has_key?('check_command');
142
143                 check = $nrpe.add(service['service_description'], service['nrpe'])
144                 service['check_command'] = "#{ NRPE_CHECKNAME }!#{ check }"
145
146                 service['depends'] = ensure_array( service['depends'] )
147                 service['depends'] << NRPE_PROCESS_SERVICE unless service['service_description'] == NRPE_PROCESS_SERVICE  # Depend on NRPE unless we are it
148         end
149
150         hosts.each{ |host|
151                 s = service.clone
152                 set_complain_if_set s, 'host_name', host, 'Service', s['service_description']
153                 merge_contacts(servers[host], s)
154
155                 print_block files['services'], 'service', s, %w(nrpe runfrom remotecheck
156                                                                 depends
157                                                                 hosts hostgroups excludehosts excludehostgroups)
158         }
159
160         if service['depends']
161                 service['depends'].each{ |prerequisite|
162                         hosts.each{ |host|
163                                 prerequisite_host = host
164                                 pre = prerequisite
165                                 # split off a hostname if there's one
166                                 bananasplit = prerequisite.split(':')
167                                 if bananasplit.size == 2
168                                         prerequisite_host = bananasplit[0]
169                                         pre = bananasplit[1]
170                                 elsif bananasplit.size > 2
171                                         throw "Cannot prase prerequisite #{prerequisite} for service #{service['service_description']} into host:service"
172                                 end
173                                 dependency = {
174                                         'host_name'                     => prerequisite_host,
175                                         'service_description'           => pre,
176                                         'dependent_host_name'           => host,
177                                         'dependent_service_description' => service['service_description'],
178                                         'execution_failure_criteria'    => 'n',
179                                         'notification_failure_criteria' => 'w,u,c'
180                                 };
181                                 print_block files['dependencies'], 'servicedependency', dependency, %w()
182                         }
183                 }
184         end
185
186
187         set_complain_if_set service['_extinfo'], 'service_description' , service['service_description'], 'serviceextinfo', service['service_description']
188         set_complain_if_set service['_extinfo'], 'host_name'           , hosts.join(',')               , 'serviceextinfo', service['service_description']
189
190         print_block files['serviceextinfo'], 'serviceextinfo', service['_extinfo'], %w()
191 end
192
193 # hostlists in services can be given as both, single hosts and hostgroups
194 # This functinn merges hostgroups and a simple list of hosts
195 #
196 # it also takes a prefix so that it can be used for excludelists as well
197 def merge_hosts_and_hostgroups(service, servers, hostgroups, prefix)
198         hosts = []
199         hosts = service[prefix+'hosts'].split(/,/).map{ |x| x.strip } if service[prefix+'hosts']
200         hosts.each{ |host|
201                 throw "host #{host} does not exist - used in service #{service['service_description']}" unless servers[host]
202         };
203         if service[prefix+'hostgroups']
204                 service[prefix+'hostgroups'].split(/,/).map{ |x| x.strip }.each{ |hg|
205                         throw "hostgroup #{hg} does not exist - used in service #{service['service_description']}" unless hostgroups[hg]
206                         hosts = hosts.concat hostgroups[hg]['_memberlist']
207                 }
208         end
209
210         return hosts
211 end
212
213 # Figure out the hosts a given service applies to
214 #
215 # For a given service find the list of hosts minus excluded hosts that this service runs on
216 def find_hosts(service, servers, hostgroups)
217         hosts        = merge_hosts_and_hostgroups service, servers, hostgroups, ''
218         excludehosts = merge_hosts_and_hostgroups service, servers, hostgroups, 'exclude'
219
220         excludehosts.each{ |host|
221                 if hosts.delete(host) == nil
222                         throw "Cannot remove host #{host} from service #{service['service_description']}: it's not included anyway or excluded twice."
223                 end
224         }
225
226         return hosts
227 end
228
229 # Move all elements that have a key that starts with "extinfo-"
230 # into the _extinfo subhash
231 def split_away_extinfo(hash)
232         hash['_extinfo'] = {}
233         hash.keys.each{ |key|
234                 if key[0, 8] == 'extinfo-'
235                         hash['_extinfo'][ key[8, key.length-8] ] = hash[key]
236                         hash.delete(key);
237                 end
238         }
239 end
240
241
242 #############################################################################################
243 #############################################################################################
244 #############################################################################################
245
246 # Load the config
247 config = YAML::load( File.open( 'nagios-master.cfg' ) )
248
249 files = {}
250 # Remove old created files
251 nagios_filename.each_pair{ |name, filename|
252         files[name] = File.new(filename, "w")
253 }
254
255 #################################
256 # create a few hostgroups
257 #################################
258 # create the "all" and "pingable" hostgroups
259 config['hostgroups']['all'] = {}
260 config['hostgroups']['all']['alias'] = "all servers"
261 config['hostgroups']['all']['private'] = true
262 config['hostgroups']['pingable'] = {}
263 config['hostgroups']['pingable']['alias'] = "pingable servers"
264 config['hostgroups']['pingable']['private'] = true
265
266 config['hostgroups'].each_pair{ |name, hg|
267         throw "Empty hostgroup or hostgroup #{name} not a hash" unless hg.kind_of?(Hash)
268         split_away_extinfo hg
269
270         hg['_memberlist'] = []
271 }
272
273 config['servers'].each_pair{ |name, server|
274         throw "Empty server or server #{name} not a hash" unless server.kind_of?(Hash)
275
276         split_away_extinfo server
277
278         throw "No hostgroups defined for #{name}" unless server['hostgroups']
279         server['_hostgroups'] = server['hostgroups'].split(/,/).map{ |x| x.strip };
280         server['_hostgroups'] << 'all'
281         server['_hostgroups'] << 'pingable' unless server['pingable'] == false
282
283         server['_hostgroups'].each{ |hg|
284                 throw "Hostgroup #{hg} is not defined" unless config['hostgroups'].has_key?(hg)
285                 config['hostgroups'][hg]['_memberlist'] << name
286         };
287 }
288
289 config['servicegroups'] = {} unless config.has_key? 'servicegroups'
290
291 ##############
292 # HOSTS
293 ##############
294 config['servers'].each_pair{ |name, server|
295         # Formerly we used 'ip' instead of 'address' in our source file
296         # Handle this change but warn                                   XXX
297         if server.has_key?('ip')
298                 STDERR.puts("Host definition for #{name} has an 'ip' field.  Please use 'address' instead");
299                 server['address'] = server['ip'];
300                 server.delete('ip');
301         end
302
303         set_complain_if_set server, 'host_name'    , name, 'Host', name
304         set_if_unset        server, 'alias'        , name
305         set_if_unset        server, 'use'          , HOST_TEMPLATE_NAME
306         set_if_unset        server, 'check_command', HOST_ALIVE_CHECK    unless server['pingable'] == false
307
308         print_block files['hosts']      , 'host'       , server            , %w(hostgroups pingable)
309
310
311
312         # Handle hostextinfo
313         #config['hostgroups'][  server['_hostgroups'].first  ]['_extinfo'].each_pair{ |k, v|
314         # find the first hostgroup that has extinfo
315         extinfo = server['_hostgroups'].collect{ |hgname | config['hostgroups'][hgname]['_extinfo'] }.delete_if{ |ei| ei.size == 0 }.first
316         if extinfo then
317                 extinfo.each_pair do |k, v|
318                         # substitute hostname into the notes_url
319                         v = sprintf(v,name) if k == 'notes_url'
320
321                         set_if_unset server['_extinfo'], k ,v
322                 end
323         end
324
325         set_complain_if_set server['_extinfo'], 'host_name'       , name, 'hostextinfo', name
326         set_if_unset        server['_extinfo'], 'vrml_image'      , server['_extinfo']['icon_image'] if server['_extinfo'].has_key?('icon_image')
327         set_if_unset        server['_extinfo'], 'statusmap_image' , server['_extinfo']['icon_image'] if server['_extinfo'].has_key?('icon_image')
328
329         print_block files['hostextinfo'], 'hostextinfo', server['_extinfo'], %w()
330 }
331
332
333
334 ##############
335 # HOSTGROUPS
336 ##############
337 config['hostgroups'].each_pair{ |name, hg|
338         next if hg['private']
339
340         set_complain_if_set hg, 'hostgroup_name', name                       , 'Hostgroup', name
341         set_complain_if_set hg, 'members'       , hg['_memberlist'].join(","), 'Hostgroup', name
342
343         print_block files['hostgroups'], 'hostgroup', hg, %w()
344 }
345
346
347 ##############
348 # SERVICES and DEPENDENCIES
349 ##############
350 config['services'].each{ |service|
351         throw "Empty service or service not a hash" unless service.kind_of?(Hash)
352
353         split_away_extinfo service
354
355
356         # Both 'name' and 'service_description' are valid for a service's name
357         # Internally we only use service_description as that's nagios' official term
358         if service.has_key?('name')
359                 throw "Service definition has both a name (#{service['name']})" +
360                       "and a service_description (#{service['service_description']})" if service.has_key?('service_description')
361                 #STDERR.puts("Service definition #{service['name']} has a 'name' field.  Please use 'service_description' instead");
362                 service['service_description'] = service['name'];
363                 service.delete('name');
364         end
365         # Both 'check' and 'check_command' are valid for a service's check command
366         # Internally we only use check_command as that's nagios' official term
367         if service.has_key?('check')
368                 throw "Service definition has both a check (#{service['check']})" +
369                       "and a check_command (#{service['check_command']})" if service.has_key?('check_command')
370                 #STDERR.puts("Service definition #{service['service_description']} has a 'check' field.  Please use 'check_command' instead");
371                 service['check_command'] = service['check'];
372                 service.delete('check');
373         end
374
375
376         hosts = find_hosts service, config['servers'], config['hostgroups']
377         throw "no hosts for service #{service['service_description']}" if hosts.empty?
378
379         throw "nrpe, check, and remotecheck are mutually exclusive in service #{service['service_description']}" if 
380                 (service['nrpe'] ? 1 : 0) +
381                 (service['check_command'] ? 1 : 0) +
382                 (service['remotecheck'] ? 1 : 0)  >= 2
383
384         if service['runfrom'] && service['remotecheck']
385                 # If the service check is to be run from a remote monitor server ("relay")
386                 # add that as an NRPE check to be run on the relay and make this
387                 # service also depend on NRPE on the relay
388                 relay = service['runfrom']
389
390                 hosts.each{ |host|
391                         # how to recursively copy this thing?
392                         hostservice = YAML::load( service.to_yaml )
393                         host_ip = config['servers'][host]['address']
394                         throw "For some reason I do not have an address for #{host}.  This shouldn't be." unless host_ip
395
396                         check = $nrpe.add("#{host}_#{hostservice['service_description']}", hostservice['remotecheck'].gsub(/\$HOSTADDRESS\$/, host_ip))
397                         hostservice['check_command'] = "#{NRPE_CHECKNAME_HOST}!#{ config['servers'][ relay ]['address'] }!#{ check }"
398
399                         # Make sure dependencies are an array.  If there are none, create an empty array
400                         # if depends is just a string, make a list with just that element
401                         hostservice['depends'] = ensure_array( hostservice['depends'] )
402                         # And append this new dependency
403                         hostservice['depends'] << "#{ relay }:#{ NRPE_PROCESS_SERVICE }";
404
405                         addService( [ host ], hostservice, files, config['servers'])
406                 }
407         elsif service['runfrom'] || service['remotecheck']
408                 throw "runfrom and remotecheck must either appear both or not at all in service #{service['service_description']}"
409                 throw "must not remotecheck without runfrom" if service['remotecheck']
410         else
411                 addService(hosts, service, files, config['servers'])
412         end
413 }
414
415
416
417 ##############
418 # SERVICEGROUPS
419 ##############
420 config['servicegroups'].each_pair{ |name, sg|
421         set_complain_if_set sg, 'servicegroup_name', name                       , 'Servicegroup', name
422
423         print_block files['servicegroups'], 'servicegroup', sg, %w()
424 }
425
426
427 ##############
428 # NRPE config file
429 ##############
430 $nrpe.checks.each_pair{ |name, check|
431         files['nrpe'].puts "command[#{ name }]=#{ check }"
432 }
433
434