diff options
Diffstat (limited to 'bin/ldap2bind')
-rwxr-xr-x | bin/ldap2bind | 344 |
1 files changed, 344 insertions, 0 deletions
diff --git a/bin/ldap2bind b/bin/ldap2bind new file mode 100755 index 0000000..139a4b0 --- /dev/null +++ b/bin/ldap2bind @@ -0,0 +1,344 @@ +#!/usr/bin/ruby + +# +# Copyright (c) 2004 Peter Palfrader <peter@palfrader.org> +# +# 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 setRecord(domains, type, rrdata, dn) + 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) + 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 << " file \"slave-ldap-#{slaveentry['name']}\";" + 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) +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) + 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 |