summaryrefslogtreecommitdiff
path: root/bin/ldap2bind
diff options
context:
space:
mode:
Diffstat (limited to 'bin/ldap2bind')
-rwxr-xr-xbin/ldap2bind344
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