#!/usr/bin/ruby # # Copyright (c) 2004 Peter Palfrader # # All rights reserved. # require "ldap" require "getoptlong" require "myldap" require "yaml" require "optparse" # defaults @force_push = false @verbose = false def show_help(parser, code=0, io=STDOUT) io.puts parser exit(code) end ARGV.options do |opts| opts.on_tail("-h", "--help", "Display this help screen") { show_help(opts) } opts.on("-f", "--force" , String, "Force Push to Slaves") { @force_push = true } opts.on("-v", "--verbose" , String, "Show SSH-Transfer more verbose") { @verbose = true } opts.parse! end def fatal(reason) STDERR.puts reason exit 1; end def getOldSerial(name) filename = @baseDir+'/'+name return nil unless FileTest.exists?(filename) File.open(filename).readlines.each{ |line| match = /^; serial: (\d+)/.match(line) next unless match serial = match[1] return serial.to_i } return nil end def makeNewSerial(oldSerial) serial = Time.now.strftime("%Y%m%d00").to_i if oldSerial serial = (oldSerial + 1) if oldSerial >= serial end return serial end def ensureArray(x) if x.kind_of?(Array) return x else return [x] end end def setRecord(domains, type, rrdata, dn) ensureArray(domains).each{ |d| handled = false domainparts = d.split('.') localpart = [] while domainparts.length > 0 if @zones.has_key?(domainparts.join('.')) left = localpart.length > 0 ? localpart.join('.') : '@' right = domainparts.join('.') @zones[ right ]['records'][left] = {} unless @zones[ right ]['records'][left] if @zones[ right ]['records'][left].has_key?(type) throw "There already are #{type} records for #{left} in #{right} (dn=#{dn}" end @zones[ right ]['records'][left][type] = [ rrdata ] handled = true break end localpart << domainparts.shift end # FIXME #STDERR.puts "Warning: Did not find a place to put #{d}'s #{type} records" unless handled } end def setMXs(domains, hosts, dn) throw "Cannot handle mail entry for more than 1 host (dn=#{dn})" if hosts.length != 1 host = hosts[0] rrdata = @rrdata['MX'][host] throw "Do not know MX data for host #{host} (dn=#{dn})" unless rrdata setRecord(domains, 'MX', rrdata, dn); end def setAs(domains, hosts, dn) throw "Cannot handle A entry for more than 1 host (dn=#{dn})" if hosts.length != 1 host = hosts[0] rrdata = @rrdata['A'][host] throw "Do not know A data for host #{host} (dn=#{dn})" unless rrdata setRecord(domains, 'A', rrdata, dn); end def setAsDirect(domains, ipaddress, dn) throw "Cannot handle A entry for more than 1 ipaddress (dn=#{dn})" if ipaddress.length != 1 setRecord(domains, 'A', ipaddress, dn); end @config = YAML::load( File.open( '/etc/noreply/config' ) ) @defaultSoaPerson = @config['module']['bind']['soaPerson'] @defaultTTL = @config['module']['bind']['ttl'] @defaultNameservers = @config['module']['bind']['nameservers'] @baseDir = @config['module']['bind']['baseDir'] @namedconf = @config['module']['bind']['namedconf'] @pushedList = @config['module']['bind']['pushedList'] @reload = @config['module']['bind']['reload'] @rrdata = @config['module']['bind']['rrdata'] @Origin = @config['module']['bind']['thisNameserver'] @nameserverIPmapping = @config['module']['bind']['nameserverIPmapping'] @TSIGslaves = @config['module']['bind']['TSIGslaves'] @pushToServers = @config['module']['bind']['pushToServers'] @SSHidentityFile = @config['module']['bind']['SSHidentityFile'] ldap = MyLDAP.new(@config, "ldap2bind") magicItems = ldap.conn.search2(@config['basedn'], LDAP::LDAP_SCOPE_SUBTREE, '(|(tnMagicDNS=yes)(objectClass=tnDNSrr))') masterDomains = ldap.conn.search2(@config['basedn'], LDAP::LDAP_SCOPE_SUBTREE, 'objectclass=tnDNSsoa') slaveDomains = ldap.conn.search2(@config['basedn'], LDAP::LDAP_SCOPE_SUBTREE, 'objectclass=tnDNSsecondary') @zones = {} @secondaryzonesUnique = {} @secondaryzones = [] masterDomains.each { |d| domain = {} throw "Domain starts with dot: #{d['tnDNSdomainname'][0]}" if ((d['tnDNSdomainname'][0] =~ /^\./) != nil) domain['soaPerson'] = d['tnDNSsoaPerson'] ? d['tnDNSsoaPerson'][0] : @defaultSoaPerson domain['ttl'] = d['tnDNSttl'] ? d['tnDNSttl'][0] : @defaultTTL domain['records'] = {} domain['records']['@'] = {} domain['records']['@']['NS'] = d['tnDNSnameservers'] ? d['tnDNSnameservers'] : @defaultNameservers name = d['tnDNSdomainname'][0] if @zones.has_key?( name ) STDERR.puts "Warning: Domain #{name} has multiple ldap entries in master!" end @zones[ name ] = domain @secondaryzonesUnique[name] = 1 slaveentry = {} slaveentry['name'] = name slaveentry['master'] = [ @nameserverIPmapping[@Origin] ] slaveentry['slaves'] = domain['records']['@']['NS'].reject{|e| e == @Origin} @secondaryzones << slaveentry } slaveDomains.each { |d| name = d['tnDNSdomainname'][0] if @secondaryzonesUnique.has_key?( name ) # STDERR.puts "Warning: Domain #{name} has multiple ldap entries as slaves!" end @secondaryzonesUnique[name] = 1 slaveentry = {} slaveentry['name'] = name slaveentry['master'] = d['tnDNSprimary'] slaveentry['slaves'] = d['tnDNSnameservers'] @secondaryzones << slaveentry d['tnDNSnameservers'].each{ |secondary| if not @nameserverIPmapping.has_key?(secondary) STDERR.puts "Warning: Unknown nameserver #{secondary} for domain #{name}." next end } } magicItems.each { |m| recognized_one = false m['objectClass'].each{ |objectclass| case objectclass when 'top', 'tnMailRemotePerson' when 'tnMailDomain', 'tnMailRelay', 'tnUUCPSystem' setMXs(m['tnMailDomainname'], m['tnHost'], m['dn'][0]) when 'tnWebVHost' setAs(m['tnWebVHostServerName'], m['tnHost'], m['dn'][0]) if m['tnWebVHostServerAlias'] m['tnWebVHostServerAlias'].each{ |vhostalias| setAs(vhostalias, m['tnHost'], m['dn'][0]) } end when 'tnDNSrr' if m['tnDNSaRecord'] setAsDirect(m['tnDNSdomainname'], m['tnDNSaRecord'], m['dn'][0]) else STDERR.puts "Warning: Unhandled tnDNSrr: #{m['dn'][0]}." end else STDERR.puts "Warning: objectClass #{objectclass} in #{m['dn'][0]} not recognized." end } } # clean up Dir.entries( @baseDir ).each{ |e| next if ((e =~ /^\./) != nil) File.unlink( @baseDir + '/' + e ) unless @zones.has_key?( e ) } zonelist = [] zonelist << "// --------------------------------------------------" zonelist << "// -- --" zonelist << "// -- " + "master zones from ldap".center(44)+ " --" zonelist << "// -- --" zonelist << "// -- this file has been automatically created by --" zonelist << "// -- ldap2bind on "+Time.now.strftime("%a, %d %b %Y %H:%M:%S %Z")+" -- IGNORE_LINE" zonelist << "// --------------------------------------------------" zonelist << "" reload_required = false @zones.keys.sort.each{ |name| domain = @zones[name] zonelist << "zone \"#{name}\" {" zonelist << " type master;" zonelist << " file \"#{@baseDir}/#{name}\";" zonelist << " allow-query { any; };" zonelist << " allow-transfer {" have_secondaries = false domain['records']['@']['NS'].reject{|e| e == @Origin}.each{ |secondary| if @TSIGslaves.has_key?(secondary) zonelist << " key #{@TSIGslaves[secondary]}; // #{secondary}" else if not @nameserverIPmapping.has_key?(secondary) STDERR.puts "Warning: Unknown nameserver #{secondary} in zone #{name}: cannot grant zone transfer" next end ip = @nameserverIPmapping[secondary] zonelist << " #{ip}; // #{secondary}" zonelist << " ::ffff:#{ip}/128; // #{secondary}" end have_secondaries = true } zonelist << " \"none\";" unless have_secondaries zonelist << " };" zonelist << "};" zonelist << "" serial = makeNewSerial(getOldSerial(name)) zone = [] zone << "; --------------------------------------------------" zone << "; -- --" zone << "; -- " + name.center(44)+ " --" zone << "; -- --" zone << "; -- this file has been automatically created by --" zone << "; -- ldap2bind on "+Time.now.strftime("%a, %d %b %Y %H:%M:%S %Z")+" -- IGNORE_LINE" zone << "; --------------------------------------------------" zone << "; serial: "+serial.to_s+" IGNORE_LINE" zone << ";" zone << "$TTL #{domain['ttl']}" zone << "@ #{domain['ttl']} IN SOA #{ @Origin } #{ domain['soaPerson'] } (" zone << " #{ serial } ; IGNORE_LINE" zone << " 3H 33m 5W 20m )" # or {domain['ttl']} instead of 20m" # We need to sort here so we get the same order every time, so we can # compare the result with the on disk copy domain['records'].keys.sort.each{ |dom| domain['records'][dom].keys.sort.each{ |type| domain['records'][dom][type].sort.each{ |l| zone << "#{dom} IN #{type} #{l}" } } } zone << "localhost IN A 127.0.0.1" #zone << "localhost. IN A 127.0.0.1" zone << "" zone << "; vim:set syn=dns:" zone.map!{ |line| line + "\n" } if (FileTest.exists?(@baseDir + '/' + name)) on_disk = File.new( @baseDir + '/' + name, "r" ).readlines.reject{ |e| e =~ /IGNORE_LINE/ } next if on_disk == zone.reject{ |e| e =~ /IGNORE_LINE/ } end f = File.new( @baseDir + '/' + name, "w" ) f.write(zone.join('')) f.close reload_required = true } pushingList = [] @secondaryzones.sort{|x,y| x['name'] <=> y['name'] }.each{ |slaveentry| if slaveentry['slaves'].include?(@Origin) zonelist << "zone \"#{slaveentry['name']}\" {" zonelist << " type slave;" zonelist << " notify no;" zonelist << " file \"slave-ldap-#{slaveentry['name']}\";" zonelist << " allow-query { any; };" zonelist << " allow-transfer { \"none\"; };" zonelist << " masters {" slaveentry['master'].each{ |master| zonelist << " #{master};" }; zonelist << " };" zonelist << "};" zonelist << "" end pushingList << slaveentry['name']+" "+slaveentry['master'].join('|')+" "+" "+slaveentry['slaves'].join(" ") } zonelist.map!{ |line| line + "\n" } if (FileTest.exists?(@namedconf)) on_disk = File.new(@namedconf).readlines.reject{ |e| e =~ /IGNORE_LINE/ } reload_required = true unless on_disk == zonelist.reject{ |e| e =~ /IGNORE_LINE/ } else reload_required = true end f = File.new( @namedconf, "w" ) f.write(zonelist.join('')) f.close pushingList.map!{ |line| line + "\n" } # sometimes force push so set it per default to true push_required=@force_push if (FileTest.exists?(@pushedList)) on_disk = File.new(@pushedList).readlines.reject{ |e| e =~ /IGNORE_LINE/ } push_required = true unless on_disk == pushingList.reject{ |e| e =~ /IGNORE_LINE/ } else push_required = true end if push_required f = File.new( @pushedList, "w" ); f.write(pushingList.join('')) f.close if @pushToServers @pushToServers.each{ |server| puts "Pushing to #{server}" if @verbose system("env -i ssh -4 -v -T -l root -i #{@SSHidentityFile} #{server} < #{@pushedList}") or throw "push to #{server} failed\n" else system("env -i ssh -4 -T -l root -i #{@SSHidentityFile} #{server} < #{@pushedList}") or throw "push to #{server} failed\n" end } end end if (reload_required) puts "Reloading bind." system(@reload) or throw "#{@reload} failed\n"; end