ud-mailgate: remove exception for münchen.debian.net
[mirror/userdir-ldap.git] / ud-generate
index f4f8b58..a787dfa 100755 (executable)
@@ -6,7 +6,7 @@
 #   Copyright (c) 2003-2004  James Troup <troup@debian.org>
 #   Copyright (c) 2004-2005,7  Joey Schulze <joey@infodrom.org>
 #   Copyright (c) 2001-2007  Ryan Murray <rmurray@debian.org>
 #   Copyright (c) 2003-2004  James Troup <troup@debian.org>
 #   Copyright (c) 2004-2005,7  Joey Schulze <joey@infodrom.org>
 #   Copyright (c) 2001-2007  Ryan Murray <rmurray@debian.org>
-#   Copyright (c) 2008,2009,2010 Peter Palfrader <peter@palfrader.org>
+#   Copyright (c) 2008,2009,2010,2011 Peter Palfrader <peter@palfrader.org>
 #   Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
 #   Copyright (c) 2008 Mark Hymers <mhy@debian.org>
 #   Copyright (c) 2008 Luk Claes <luk@debian.org>
 #   Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
 #   Copyright (c) 2008 Mark Hymers <mhy@debian.org>
 #   Copyright (c) 2008 Luk Claes <luk@debian.org>
 #   along with this program; if not, write to the Free Software
 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
 #   along with this program; if not, write to the Free Software
 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 
-import string, re, time, ldap, getopt, sys, os, pwd, posix, socket, base64, sha, shutil, errno, tarfile, grp
+from dsa_mq.connection import Connection
+from dsa_mq.config import Config
+
+import string, re, time, ldap, optparse, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp, fcntl, dbm
 from userdir_ldap import *
 from userdir_exceptions import *
 import UDLdap
 from userdir_ldap import *
 from userdir_exceptions import *
 import UDLdap
+from xml.etree.ElementTree import Element, SubElement, Comment
+from xml.etree import ElementTree
+from xml.dom import minidom
 try:
    from cStringIO import StringIO
 except ImportError:
    from StringIO import StringIO
 try:
    from cStringIO import StringIO
 except ImportError:
    from StringIO import StringIO
-
-global Allowed
-global CurrentHost
+try:
+   import simplejson as json
+except ImportError:
+   import json
+   if not '__author__' in json.__dict__:
+      sys.stderr.write("Warning: This is probably the wrong json module.  We want python 2.6's json\n")
+      sys.stderr.write("module, or simplejson on pytyon 2.5.  Let's see if/how stuff blows up.\n")
 
 if os.getuid() == 0:
    sys.stderr.write("You should probably not run ud-generate as root.\n")
    sys.exit(1)
 
 
 if os.getuid() == 0:
    sys.stderr.write("You should probably not run ud-generate as root.\n")
    sys.exit(1)
 
-PasswdAttrs = None
-DebianUsers = None
-DisabledUsers = []
-GroupIDMap = {}
-SubGroupMap = {}
-Allowed = None
-CurrentHost = ""
+
+#
+# GLOBAL STATE
+#
+GroupIDMap = None
+SubGroupMap = None
+
+
 
 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
 
 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
+MAX_UD_AGE = 3600*24
 
 
-EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
+EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)(,\s*([^ <>@]+@[^ ,<>@]+))*$")
 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
-IsV6Addr = re.compile("^[a-fA-F0-9:]+$")
 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
 isSSHFP = re.compile("^\s*IN\s+SSHFP")
 DNSZone = ".debian.net"
 Keyrings = ConfModule.sync_keyrings.split(":")
 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
 isSSHFP = re.compile("^\s*IN\s+SSHFP")
 DNSZone = ".debian.net"
 Keyrings = ConfModule.sync_keyrings.split(":")
+GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
+GitoliteSSHCommand = getattr(ConfModule, "gitolitesshcommand", None)
+GitoliteExportHosts = re.compile(getattr(ConfModule, "gitoliteexporthosts", "."))
+MX_remap = json.loads(ConfModule.MX_remap)
+use_mq = getattr(ConfModule, "use_mq", True)
+
+rtc_realm = getattr(ConfModule, "rtc_realm", None)
+rtc_append = getattr(ConfModule, "rtc_append", None)
+
+def prettify(elem):
+   """Return a pretty-printed XML string for the Element.
+   """
+   rough_string = ElementTree.tostring(elem, 'utf-8')
+   reparsed = minidom.parseString(rough_string)
+   return reparsed.toprettyxml(indent="  ")
 
 def safe_makedirs(dir):
    try:
 
 def safe_makedirs(dir):
    try:
@@ -81,6 +107,25 @@ def safe_rmtree(dir):
       else:
          raise e
 
       else:
          raise e
 
+def get_lock(fn, wait=5*60):
+   f = open(fn, "w")
+   sl = 0.1
+   ends = time.time() + wait
+
+   while True:
+      success = False
+      try:
+         fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
+         return f
+      except IOError:
+         pass
+      if time.time() >= ends:
+         return None
+      sl = min(sl*2, 10, ends - time.time())
+      time.sleep(sl)
+   return None
+
+
 def Sanitize(Str):
    return Str.translate(string.maketrans("\n\r\t", "$$$"))
 
 def Sanitize(Str):
    return Str.translate(string.maketrans("\n\r\t", "$$$"))
 
@@ -91,19 +136,17 @@ def DoLink(From, To, File):
       pass
    posix.link(From + File, To + File)
 
       pass
    posix.link(From + File, To + File)
 
-def IsRetired(DnRecord):
+def IsRetired(account):
    """
    Looks for accountStatus in the LDAP record and tries to
    match it against one of the known retired statuses
    """
 
    """
    Looks for accountStatus in the LDAP record and tries to
    match it against one of the known retired statuses
    """
 
-   status = GetAttr(DnRecord, "accountStatus", None)
-   if status is None:
-      return False
+   status = account['accountStatus']
 
    line = status.split()
    status = line[0]
 
    line = status.split()
    status = line[0]
-   
+
    if status == "inactive":
       return True
 
    if status == "inactive":
       return True
 
@@ -122,34 +165,21 @@ def IsRetired(DnRecord):
 
    return False
 
 
    return False
 
-def IsGidDebian(x):
-   try:
-      return int(GetAttr(x, "gidNumber", 0)) == 800
-   except ValueError:
-      return False
-
 # See if this user is in the group list
 # See if this user is in the group list
-def IsInGroup(DnRecord):
-  if Allowed is None:
-     return True
-
+def IsInGroup(account, allowed, current_host):
   # See if the primary group is in the list
   # See if the primary group is in the list
-  if Allowed.has_key(GetAttr(DnRecord, "gidNumber")) != 0:
-     return True
+  if str(account['gidNumber']) in allowed: return True
 
   # Check the host based ACL
 
   # Check the host based ACL
-  if DnRecord[1].has_key("allowedHost") != 0:
-     if CurrentHost in DnRecord[1]["allowedHost"]:
-        return True
+  if account.is_allowed_by_hostacl(current_host): return True
 
   # See if there are supplementary groups
 
   # See if there are supplementary groups
-  if DnRecord[1].has_key("supplementaryGid") == 0:
-     return False
+  if not 'supplementaryGid' in account: return False
 
   supgroups=[]
 
   supgroups=[]
-  addGroups(supgroups, DnRecord[1]["supplementaryGid"], GetAttr(DnRecord, "uid"))
+  addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
   for g in supgroups:
   for g in supgroups:
-     if Allowed.has_key(g):
+     if g in allowed:
         return True
   return False
 
         return True
   return False
 
@@ -176,37 +206,32 @@ def Done(File, F, Fdb):
       os.rename(File + ".tdb.tmp", File + ".tdb")
 
 # Generate the password list
       os.rename(File + ".tdb.tmp", File + ".tdb")
 
 # Generate the password list
-def GenPasswd(File, HomePrefix, PwdMarker):
+def GenPasswd(accounts, File, HomePrefix, PwdMarker):
    F = None
    try:
       F = open(File + ".tdb.tmp", "w")
    F = None
    try:
       F = open(File + ".tdb.tmp", "w")
-     
+
       userlist = {}
       userlist = {}
-      # Fetch all the users
-      global PasswdAttrs
-     
-      I = 0
-      for x in PasswdAttrs:
-         if x[1].has_key("uidNumber") == 0 or not IsInGroup(x):
-            continue
-     
+      i = 0
+      for a in accounts:
          # Do not let people try to buffer overflow some busted passwd parser.
          # Do not let people try to buffer overflow some busted passwd parser.
-         if len(GetAttr(x, "gecos")) > 100 or len(GetAttr(x, "loginShell")) > 50:
-            continue
-     
-         userlist[GetAttr(x, "uid")] = int(GetAttr(x, "gidNumber"))
-         Line = "%s:%s:%s:%s:%s:%s%s:%s" % (GetAttr(x, "uid"),\
-                 PwdMarker,\
-                 GetAttr(x, "uidNumber"), GetAttr(x, "gidNumber"),\
-                 GetAttr(x, "gecos"), HomePrefix, GetAttr(x, "uid"),\
-                 GetAttr(x, "loginShell"))
-     
-         Line = Sanitize(Line) + "\n"
-         F.write("0%u %s" % (I, Line))
-         F.write(".%s %s" % (GetAttr(x, "uid"), Line))
-         F.write("=%s %s" % (GetAttr(x, "uidNumber"), Line))
-         I = I + 1
-  
+         if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
+
+         userlist[a['uid']] = a['gidNumber']
+         line = "%s:%s:%d:%d:%s:%s%s:%s" % (
+                 a['uid'],
+                 PwdMarker,
+                 a['uidNumber'],
+                 a['gidNumber'],
+                 a['gecos'],
+                 HomePrefix, a['uid'],
+                 a['loginShell'])
+         line = Sanitize(line) + "\n"
+         F.write("0%u %s" % (i, line))
+         F.write(".%s %s" % (a['uid'], line))
+         F.write("=%d %s" % (a['uidNumber'], line))
+         i = i + 1
+
    # Oops, something unspeakable happened.
    except:
       Die(File, None, F)
    # Oops, something unspeakable happened.
    except:
       Die(File, None, F)
@@ -216,22 +241,36 @@ def GenPasswd(File, HomePrefix, PwdMarker):
    # Return the list of users so we know which keys to export
    return userlist
 
    # Return the list of users so we know which keys to export
    return userlist
 
+def GenAllUsers(accounts, file):
+   f = None
+   try:
+      OldMask = os.umask(0022)
+      f = open(file + ".tmp", "w", 0644)
+      os.umask(OldMask)
+
+      all = []
+      for a in accounts:
+         all.append( { 'uid': a['uid'],
+                       'uidNumber': a['uidNumber'],
+                       'active': a.pw_active() and a.shadow_active() } )
+      json.dump(all, f)
+
+   # Oops, something unspeakable happened.
+   except:
+      Die(file, f, None)
+      raise
+   Done(file, f, None)
+
 # Generate the shadow list
 # Generate the shadow list
-def GenShadow(File):
+def GenShadow(accounts, File):
    F = None
    try:
       OldMask = os.umask(0077)
       F = open(File + ".tdb.tmp", "w", 0600)
       os.umask(OldMask)
    F = None
    try:
       OldMask = os.umask(0077)
       F = open(File + ".tdb.tmp", "w", 0600)
       os.umask(OldMask)
-     
-      # Fetch all the users
-      global PasswdAttrs
-     
-      i = 0
-      for x in PasswdAttrs:
-         a = UDLdap.Account(x[0], x[1])
-         if not IsInGroup(x): continue
 
 
+      i = 0
+      for a in accounts:
          # If the account is locked, mark it as such in shadow
          # See Debian Bug #308229 for why we set it to 1 instead of 0
          if not a.pw_active():     ShadowExpire = '1'
          # If the account is locked, mark it as such in shadow
          # See Debian Bug #308229 for why we set it to 1 instead of 0
          if not a.pw_active():     ShadowExpire = '1'
@@ -258,24 +297,18 @@ def GenShadow(File):
    Done(File, None, F)
 
 # Generate the sudo passwd file
    Done(File, None, F)
 
 # Generate the sudo passwd file
-def GenShadowSudo(File, untrusted):
+def GenShadowSudo(accounts, File, untrusted, current_host):
    F = None
    try:
       OldMask = os.umask(0077)
       F = open(File + ".tmp", "w", 0600)
       os.umask(OldMask)
    F = None
    try:
       OldMask = os.umask(0077)
       F = open(File + ".tmp", "w", 0600)
       os.umask(OldMask)
-     
-      # Fetch all the users
-      global PasswdAttrs
-     
-      for x in PasswdAttrs:
-         a = UDLdap.Account(x[0], x[1])
+
+      for a in accounts:
          Pass = '*'
          Pass = '*'
-         if not IsInGroup(x): continue
-     
          if 'sudoPassword' in a:
             for entry in a['sudoPassword']:
          if 'sudoPassword' in a:
             for entry in a['sudoPassword']:
-               Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
+               Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*-]+) ([^ ]+)$').match(entry)
                if Match == None:
                   continue
                uuid = Match.group(1)
                if Match == None:
                   continue
                uuid = Match.group(1)
@@ -286,7 +319,7 @@ def GenShadowSudo(File, untrusted):
                if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
                   continue
                for_all = hosts == "*"
                if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
                   continue
                for_all = hosts == "*"
-               for_this_host = CurrentHost in hosts.split(',')
+               for_this_host = current_host in hosts.split(',')
                if not (for_all or for_this_host):
                   continue
                # ignore * passwords for untrusted hosts, but copy host specific passwords
                if not (for_all or for_this_host):
                   continue
                # ignore * passwords for untrusted hosts, but copy host specific passwords
@@ -308,51 +341,142 @@ def GenShadowSudo(File, untrusted):
       raise
    Done(File, F, None)
 
       raise
    Done(File, F, None)
 
+# Generate the sudo passwd file
+def GenSSHGitolite(accounts, hosts, File, sshcommand=None, current_host=None):
+   F = None
+   if sshcommand is None:
+      sshcommand = GitoliteSSHCommand
+   try:
+      OldMask = os.umask(0022)
+      F = open(File + ".tmp", "w", 0600)
+      os.umask(OldMask)
+
+      if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
+         for a in accounts:
+            if not 'sshRSAAuthKey' in a: continue
+
+            User = a['uid']
+            prefix = GitoliteSSHRestrictions
+            prefix = prefix.replace('@@COMMAND@@', sshcommand)
+            prefix = prefix.replace('@@USER@@', User)
+            for I in a["sshRSAAuthKey"]:
+               if I.startswith("allowed_hosts=") and ' ' in line:
+                  if current_host is None:
+                     continue
+                  machines, I = I.split('=', 1)[1].split(' ', 1)
+                  if current_host not in machines.split(','):
+                     continue # skip this key
+
+               if I.startswith('ssh-'):
+                  line = "%s %s"%(prefix, I)
+               else:
+                  continue # do not allow keys with other restrictions that might conflict
+               line = Sanitize(line) + "\n"
+               F.write(line)
+
+         for dn, attrs in hosts:
+            if not 'sshRSAHostKey' in attrs: continue
+            hostname = "host-" + attrs['hostname'][0]
+            prefix = GitoliteSSHRestrictions
+            prefix = prefix.replace('@@COMMAND@@', sshcommand)
+            prefix = prefix.replace('@@USER@@', hostname)
+            for I in attrs["sshRSAHostKey"]:
+               line = "%s %s"%(prefix, I)
+               line = Sanitize(line) + "\n"
+               F.write(line)
+
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
+
 # Generate the shadow list
 # Generate the shadow list
-def GenSSHShadow():
+def GenSSHShadow(global_dir, accounts):
    # Fetch all the users
    # Fetch all the users
-   userfiles = []
+   userkeys = {}
 
 
-   global PasswdAttrs
+   for a in accounts:
+      if not 'sshRSAAuthKey' in a: continue
 
 
-   safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
-   safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
+      contents = []
+      for I in a['sshRSAAuthKey']:
+         MultipleLine = "%s" % I
+         MultipleLine = Sanitize(MultipleLine)
+         contents.append(MultipleLine)
+      userkeys[a['uid']] = contents
+   return userkeys
 
 
-   for x in PasswdAttrs:
-      a = UDLdap.Account(x[0], x[1])
-      if not 'sshRSAAuthKey' in a: continue
+# Generate the webPassword list
+def GenWebPassword(accounts, File):
+   F = None
+   try:
+      OldMask = os.umask(0077)
+      F = open(File, "w", 0600)
+      os.umask(OldMask)
 
 
-      F = None
-      try:
-         OldMask = os.umask(0077)
-         File = os.path.join(GlobalDir, 'userkeys', a['uid'])
-         F = open(File + ".tmp", "w", 0600)
-         os.umask(OldMask)
+      for a in accounts:
+         if not 'webPassword' in a: continue
+         if not a.pw_active(): continue
 
 
-         for I in a['sshRSAAuthKey']:
-            MultipleLine = "%s" % I
-            MultipleLine = Sanitize(MultipleLine) + "\n"
-            F.write(MultipleLine)
+         Pass = str(a['webPassword'])
+         Line = "%s:%s" % (a['uid'], Pass)
+         Line = Sanitize(Line) + "\n"
+         F.write("%s" % (Line))
 
 
-         Done(File, F, None)
-         userfiles.append(os.path.basename(File))
+   except:
+      Die(File, None, F)
+      raise
 
 
-      # Oops, something unspeakable happened.
-      except IOError:
-         Die(File, F, None)
-         # As neither masterFileName nor masterFile are defined at any point
-         # this will raise a NameError.
-         Die(masterFileName, masterFile, None)
-         raise
+# Generate the rtcPassword list
+def GenRtcPassword(accounts, File):
+   F = None
+   try:
+      OldMask = os.umask(0077)
+      F = open(File, "w", 0600)
+      os.umask(OldMask)
+
+      for a in accounts:
+         if a.is_guest_account(): continue
+         if not 'rtcPassword' in a: continue
+         if not a.pw_active(): continue
+
+         Line = "%s%s:%s:%s:AUTHORIZED" % (a['uid'], rtc_append, str(a['rtcPassword']), rtc_realm)
+         Line = Sanitize(Line) + "\n"
+         F.write("%s" % (Line))
+
+   except:
+      Die(File, None, F)
+      raise
 
 
-   return userfiles
+# Generate the TOTP auth file
+def GenTOTPSeed(accounts, File):
+   F = None
+   try:
+      OldMask = os.umask(0077)
+      F = open(File, "w", 0600)
+      os.umask(OldMask)
 
 
-def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
+      F.write("# Option User Prefix Seed\n")
+      for a in accounts:
+         if a.is_guest_account(): continue
+         if not 'totpSeed' in a: continue
+         if not a.pw_active(): continue
+
+         Line = "HOTP/T30/6 %s - %s" % (a['uid'], a['totpSeed'])
+         Line = Sanitize(Line) + "\n"
+         F.write("%s" % (Line))
+   except:
+      Die(File, None, F)
+      raise
+
+
+def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
    OldMask = os.umask(0077)
    OldMask = os.umask(0077)
-   tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
+   tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
    os.umask(OldMask)
    os.umask(OldMask)
-   for f in userlist.keys():
-      if f not in SSHFiles:
+   for f in userlist:
+      if f not in ssh_userkeys:
          continue
       # If we're not exporting their primary group, don't export
       # the key and warn
          continue
       # If we're not exporting their primary group, don't export
       # the key and warn
@@ -370,10 +494,21 @@ def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
             pass
 
       if grname is None:
             pass
 
       if grname is None:
-         print "User %s is supposed to have their key exported to host %s but their primary group (gid: %d) isn't in LDAP" % (f, CurrentHost, userlist[f])
+         print "User %s is supposed to have their key exported to host %s but their primary group (gid: %d) isn't in LDAP" % (f, current_host, userlist[f])
          continue
 
          continue
 
-      to = tf.gettarinfo(os.path.join(GlobalDir, 'userkeys', f), f)
+      lines = []
+      for line in ssh_userkeys[f]:
+         if line.startswith("allowed_hosts=") and ' ' in line:
+            machines, line = line.split('=', 1)[1].split(' ', 1)
+            if current_host not in machines.split(','):
+               continue # skip this key
+         lines.append(line)
+      if not lines:
+         continue # no keys for this host
+      contents = "\n".join(lines) + "\n"
+
+      to = tarfile.TarInfo(name=f)
       # These will only be used where the username doesn't
       # exist on the target system for some reason; hence,
       # in those cases, the safest thing is for the file to
       # These will only be used where the username doesn't
       # exist on the target system for some reason; hence,
       # in those cases, the safest thing is for the file to
@@ -389,33 +524,23 @@ def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
       to.uname = f
       to.gname = grname
       to.mode  = 0400
       to.uname = f
       to.gname = grname
       to.mode  = 0400
-
-      contents = file(os.path.join(GlobalDir, 'userkeys', f)).read()
-      lines = []
-      for line in contents.splitlines():
-         if line.startswith("allowed_hosts=") and ' ' in line:
-            machines, line = line.split('=', 1)[1].split(' ', 1)
-            if CurrentHost not in machines.split(','):
-               continue # skip this key
-         lines.append(line)
-      if not lines:
-         continue # no keys for this host
-      contents = "\n".join(lines) + "\n"
+      to.mtime = int(time.time())
       to.size = len(contents)
       to.size = len(contents)
+
       tf.addfile(to, StringIO(contents))
 
    tf.close()
       tf.addfile(to, StringIO(contents))
 
    tf.close()
-   os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
+   os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
 
 # add a list of groups to existing groups,
 # including all subgroups thereof, recursively.
 # basically this proceduces the transitive hull of the groups in
 # addgroups.
 
 # add a list of groups to existing groups,
 # including all subgroups thereof, recursively.
 # basically this proceduces the transitive hull of the groups in
 # addgroups.
-def addGroups(existingGroups, newGroups, uid):
+def addGroups(existingGroups, newGroups, uid, current_host):
    for group in newGroups:
       # if it's a <group>@host, split it and verify it's on the current host.
       s = group.split('@', 1)
    for group in newGroups:
       # if it's a <group>@host, split it and verify it's on the current host.
       s = group.split('@', 1)
-      if len(s) == 2 and s[1] != CurrentHost:
+      if len(s) == 2 and s[1] != current_host:
          continue
       group = s[0]
 
          continue
       group = s[0]
 
@@ -430,10 +555,10 @@ def addGroups(existingGroups, newGroups, uid):
       existingGroups.append(group)
 
       if SubGroupMap.has_key(group):
       existingGroups.append(group)
 
       if SubGroupMap.has_key(group):
-         addGroups(existingGroups, SubGroupMap[group], uid)
+         addGroups(existingGroups, SubGroupMap[group], uid, current_host)
 
 # Generate the group list
 
 # Generate the group list
-def GenGroup(File):
+def GenGroup(accounts, File, current_host):
    grouprevmap = {}
    F = None
    try:
    grouprevmap = {}
    F = None
    try:
@@ -441,29 +566,24 @@ def GenGroup(File):
      
       # Generate the GroupMap
       GroupMap = {}
      
       # Generate the GroupMap
       GroupMap = {}
-      for x in GroupIDMap.keys():
+      for x in GroupIDMap:
          GroupMap[x] = []
       GroupHasPrimaryMembers = {}
          GroupMap[x] = []
       GroupHasPrimaryMembers = {}
-     
-      # Fetch all the users
-      global PasswdAttrs
-     
+
       # Sort them into a list of groups having a set of users
       # Sort them into a list of groups having a set of users
-      for x in PasswdAttrs:
-         a = UDLdap.Account(x[0], x[1])
+      for a in accounts:
          GroupHasPrimaryMembers[ a['gidNumber'] ] = True
          GroupHasPrimaryMembers[ a['gidNumber'] ] = True
-         if not IsInGroup(x): continue
          if not 'supplementaryGid' in a: continue
 
          supgroups=[]
          if not 'supplementaryGid' in a: continue
 
          supgroups=[]
-         addGroups(supgroups, a['supplementaryGid'], a['uid'])
+         addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
          for g in supgroups:
             GroupMap[g].append(a['uid'])
 
       # Output the group file.
       J = 0
       for x in GroupMap.keys():
          for g in supgroups:
             GroupMap[g].append(a['uid'])
 
       # Output the group file.
       J = 0
       for x in GroupMap.keys():
-         if GroupIDMap.has_key(x) == 0:
+         if not x in GroupIDMap:
             continue
 
          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
             continue
 
          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
@@ -490,60 +610,51 @@ def GenGroup(File):
   
    return grouprevmap
 
   
    return grouprevmap
 
-def CheckForward():
-   global PasswdAttrs
-   for x in PasswdAttrs:
-      if x[1].has_key("emailForward") == 0:
-         continue
-   
-      if not IsInGroup(x):
-         x[1].pop("emailForward")
-         continue
+def CheckForward(accounts):
+   for a in accounts:
+      if not 'emailForward' in a: continue
 
 
-      # Do not allow people to try to buffer overflow busted parsers
-      if len(GetAttr(x, "emailForward")) > 200:
-         x[1].pop("emailForward")
-         continue
+      delete = False
 
 
+      # Do not allow people to try to buffer overflow busted parsers
+      if len(a['emailForward']) > 200: delete = True
       # Check the forwarding address
       # Check the forwarding address
-      if EmailCheck.match(GetAttr(x, "emailForward")) == None:
-         x[1].pop("emailForward")
+      elif EmailCheck.match(a['emailForward']) is None: delete = True
+
+      if delete:
+         a.delete_mailforward()
 
 # Generate the email forwarding list
 
 # Generate the email forwarding list
-def GenForward(File):
+def GenForward(accounts, File):
    F = None
    try:
       OldMask = os.umask(0022)
       F = open(File + ".tmp", "w", 0644)
       os.umask(OldMask)
    F = None
    try:
       OldMask = os.umask(0022)
       F = open(File + ".tmp", "w", 0644)
       os.umask(OldMask)
-     
-      # Fetch all the users
-      global PasswdAttrs
-     
-      # Write out the email address for each user
-      for x in PasswdAttrs:
-         a = UDLdap.Account(x[0], x[1])
+
+      for a in accounts:
          if not 'emailForward' in a: continue
          Line = "%s: %s" % (a['uid'], a['emailForward'])
          Line = Sanitize(Line) + "\n"
          F.write(Line)
          if not 'emailForward' in a: continue
          Line = "%s: %s" % (a['uid'], a['emailForward'])
          Line = Sanitize(Line) + "\n"
          F.write(Line)
-  
+
    # Oops, something unspeakable happened.
    except:
       Die(File, F, None)
       raise
    Done(File, F, None)
 
    # Oops, something unspeakable happened.
    except:
       Die(File, F, None)
       raise
    Done(File, F, None)
 
-def GenCDB(File, Users, key):
+def GenCDB(accounts, File, key):
    Fdb = None
    try:
       OldMask = os.umask(0022)
    Fdb = None
    try:
       OldMask = os.umask(0022)
-      Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
+      # nothing else does the fsync stuff, so why do it here?
+      prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
+      Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
       os.umask(OldMask)
 
       # Write out the email address for each user
       os.umask(OldMask)
 
       # Write out the email address for each user
-      for x in Users:
-         a = UDLdap.Account(x[0], x[1])
+      for a in accounts:
          if not key in a: continue
          value = a[key]
          user = a['uid']
          if not key in a: continue
          value = a[key]
          user = a['uid']
@@ -557,18 +668,42 @@ def GenCDB(File, Users, key):
    if Fdb.close() != None:
       raise "cdbmake gave an error"
 
    if Fdb.close() != None:
       raise "cdbmake gave an error"
 
+def GenDBM(accounts, File, key):
+   Fdb = None
+   OldMask = os.umask(0022)
+   fn = os.path.join(File).encode('ascii', 'ignore')
+   try:
+      posix.remove(fn)
+   except:
+      pass
+
+   try:
+      Fdb = dbm.open(fn, "c")
+      os.umask(OldMask)
+
+      # Write out the email address for each user
+      for a in accounts:
+         if not key in a: continue
+         value = a[key]
+         user = a['uid']
+         Fdb[user] = value
+
+      Fdb.close()
+   except:
+      # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
+      os.remove(File + ".db")
+      raise
+   # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
+   os.rename (File + ".db", File)
+
 # Generate the anon XEarth marker file
 # Generate the anon XEarth marker file
-def GenMarkers(File):
+def GenMarkers(accounts, File):
    F = None
    try:
       F = open(File + ".tmp", "w")
    F = None
    try:
       F = open(File + ".tmp", "w")
-     
-      # Fetch all the users
-      global PasswdAttrs
-     
+
       # Write out the position for each user
       # Write out the position for each user
-      for x in PasswdAttrs:
-         a = UDLdap.Account(x[0], x[1])
+      for a in accounts:
          if not ('latitude' in a and 'longitude' in a): continue
          try:
             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
          if not ('latitude' in a and 'longitude' in a): continue
          try:
             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
@@ -584,18 +719,15 @@ def GenMarkers(File):
    Done(File, F, None)
 
 # Generate the debian-private subscription list
    Done(File, F, None)
 
 # Generate the debian-private subscription list
-def GenPrivate(File):
+def GenPrivate(accounts, File):
    F = None
    try:
       F = open(File + ".tmp", "w")
    F = None
    try:
       F = open(File + ".tmp", "w")
-     
-      # Fetch all the users
-      global DebianDDUsers
-     
+
       # Write out the position for each user
       # Write out the position for each user
-      for x in DebianDDUsers:
-         a = UDLdap.Account(x[0], x[1])
+      for a in accounts:
          if not a.is_active_user(): continue
          if not a.is_active_user(): continue
+         if a.is_guest_account(): continue
          if not 'privateSub' in a: continue
          try:
             Line = "%s"%(a['privateSub'])
          if not 'privateSub' in a: continue
          try:
             Line = "%s"%(a['privateSub'])
@@ -611,21 +743,17 @@ def GenPrivate(File):
    Done(File, F, None)
 
 # Generate a list of locked accounts
    Done(File, F, None)
 
 # Generate a list of locked accounts
-def GenDisabledAccounts(File):
+def GenDisabledAccounts(accounts, File):
    F = None
    try:
       F = open(File + ".tmp", "w")
    F = None
    try:
       F = open(File + ".tmp", "w")
-     
+      disabled_accounts = []
+
       # Fetch all the users
       # Fetch all the users
-      global PasswdAttrs
-      global DisabledUsers
-     
-      I = 0
-      for x in PasswdAttrs:
-         a = UDLdap.Account(x[0], x[1])
+      for a in accounts:
          if a.pw_active(): continue
          Line = "%s:%s" % (a['uid'], "Account is locked")
          if a.pw_active(): continue
          Line = "%s:%s" % (a['uid'], "Account is locked")
-         DisabledUsers.append(x)
+         disabled_accounts.append(a)
          F.write(Sanitize(Line) + "\n")
 
    # Oops, something unspeakable happened.
          F.write(Sanitize(Line) + "\n")
 
    # Oops, something unspeakable happened.
@@ -633,23 +761,20 @@ def GenDisabledAccounts(File):
       Die(File, F, None)
       raise
    Done(File, F, None)
       Die(File, F, None)
       raise
    Done(File, F, None)
+   return disabled_accounts
 
 # Generate the list of local addresses that refuse all mail
 
 # Generate the list of local addresses that refuse all mail
-def GenMailDisable(File):
+def GenMailDisable(accounts, File):
    F = None
    try:
       F = open(File + ".tmp", "w")
    F = None
    try:
       F = open(File + ".tmp", "w")
-     
-      # Fetch all the users
-      global PasswdAttrs
-     
-      for x in PasswdAttrs:
-         a = UDLdap.Account(x[0], x[1])
+
+      for a in accounts:
          if not 'mailDisableMessage' in a: continue
          Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
          Line = Sanitize(Line) + "\n"
          F.write(Line)
          if not 'mailDisableMessage' in a: continue
          Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
          Line = Sanitize(Line) + "\n"
          F.write(Line)
-  
+
    # Oops, something unspeakable happened.
    except:
       Die(File, F, None)
    # Oops, something unspeakable happened.
    except:
       Die(File, F, None)
@@ -657,16 +782,12 @@ def GenMailDisable(File):
    Done(File, F, None)
 
 # Generate a list of uids that should have boolean affects applied
    Done(File, F, None)
 
 # Generate a list of uids that should have boolean affects applied
-def GenMailBool(File, key):
+def GenMailBool(accounts, File, key):
    F = None
    try:
       F = open(File + ".tmp", "w")
    F = None
    try:
       F = open(File + ".tmp", "w")
-     
-      # Fetch all the users
-      global PasswdAttrs
-     
-      for x in PasswdAttrs:
-         a = UDLdap.Account(x[0], x[1])
+
+      for a in accounts:
          if not key in a: continue
          if not a[key] == 'TRUE': continue
          Line = "%s"%(a['uid'])
          if not key in a: continue
          if not a[key] == 'TRUE': continue
          Line = "%s"%(a['uid'])
@@ -680,19 +801,15 @@ def GenMailBool(File, key):
    Done(File, F, None)
 
 # Generate a list of hosts for RBL or whitelist purposes.
    Done(File, F, None)
 
 # Generate a list of hosts for RBL or whitelist purposes.
-def GenMailList(File, key):
+def GenMailList(accounts, File, key):
    F = None
    try:
       F = open(File + ".tmp", "w")
    F = None
    try:
       F = open(File + ".tmp", "w")
-     
-      # Fetch all the users
-      global PasswdAttrs
-     
+
       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
       else:                      validregex = re.compile('^[-\w.]+$')
 
       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
       else:                      validregex = re.compile('^[-\w.]+$')
 
-      for x in PasswdAttrs:
-         a = UDLdap.Account(x[0], x[1])
+      for a in accounts:
          if not key in a: continue
 
          filtered = filter(lambda z: validregex.match(z), a[key])
          if not key in a: continue
 
          filtered = filter(lambda z: validregex.match(z), a[key])
@@ -708,64 +825,53 @@ def GenMailList(File, key):
       raise
    Done(File, F, None)
 
       raise
    Done(File, F, None)
 
-def isRoleAccount(pwEntry):
-   if not pwEntry.has_key("objectClass"):
-      raise "pwEntry has no objectClass"
-   oc =  pwEntry['objectClass']
-   try:
-      i = oc.index('debianRoleAccount')
-      return True
-   except ValueError:
-      return False
+def isRoleAccount(account):
+   return 'debianRoleAccount' in account['objectClass']
 
 # Generate the DNS Zone file
 
 # Generate the DNS Zone file
-def GenDNS(File):
+def GenDNS(accounts, File):
    F = None
    try:
       F = open(File + ".tmp", "w")
    F = None
    try:
       F = open(File + ".tmp", "w")
-     
+
       # Fetch all the users
       # Fetch all the users
-      global PasswdAttrs
       RRs = {}
       RRs = {}
-     
+
       # Write out the zone file entry for each user
       # Write out the zone file entry for each user
-      for x in PasswdAttrs:
-         if x[1].has_key("dnsZoneEntry") == 0:
-            continue
-     
-         # If the account has no PGP key, do not write it
-         if x[1].has_key("keyFingerPrint") == 0 and not isRoleAccount(x[1]):
-            continue
+      for a in accounts:
+         if not 'dnsZoneEntry' in a: continue
+         if not a.is_active_user() and not isRoleAccount(a): continue
+         if a.is_guest_account(): continue
+
          try:
          try:
-            F.write("; %s\n"%(EmailAddress(x)))
-            for z in x[1]["dnsZoneEntry"]:
+            F.write("; %s\n"%(a.email_address()))
+            for z in a["dnsZoneEntry"]:
                Split = z.lower().split()
                if Split[1].lower() == 'in':
                Split = z.lower().split()
                if Split[1].lower() == 'in':
-                  for y in range(0, len(Split)):
-                     if Split[y] == "$":
-                        Split[y] = "\n\t"
                   Line = " ".join(Split) + "\n"
                   F.write(Line)
                   Line = " ".join(Split) + "\n"
                   F.write(Line)
-     
+
                   Host = Split[0] + DNSZone
                   if BSMTPCheck.match(Line) != None:
                      F.write("; Has BSMTP\n")
                   Host = Split[0] + DNSZone
                   if BSMTPCheck.match(Line) != None:
                      F.write("; Has BSMTP\n")
-     
+
                   # Write some identification information
                   if not RRs.has_key(Host):
                      if Split[2].lower() in ["a", "aaaa"]:
                   # Write some identification information
                   if not RRs.has_key(Host):
                      if Split[2].lower() in ["a", "aaaa"]:
-                        Line = "%s IN TXT \"%s\"\n"%(Split[0], EmailAddress(x))
-                        for y in x[1]["keyFingerPrint"]:
+                        Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
+                        for y in a["keyFingerPrint"]:
                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
                            F.write(Line)
                         RRs[Host] = 1
                else:
                   Line = "; Err %s"%(str(Split))
                   F.write(Line)
                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
                            F.write(Line)
                         RRs[Host] = 1
                else:
                   Line = "; Err %s"%(str(Split))
                   F.write(Line)
-     
+
             F.write("\n")
             F.write("\n")
-         except:
-            F.write("; Errors\n")
+         except Exception, e:
+            F.write("; Errors:\n")
+            for line in str(e).split("\n"):
+               F.write("; %s\n"%(line))
             pass
   
    # Oops, something unspeakable happened.
             pass
   
    # Oops, something unspeakable happened.
@@ -774,7 +880,15 @@ def GenDNS(File):
       raise
    Done(File, F, None)
 
       raise
    Done(File, F, None)
 
+def is_ipv6_addr(i):
+   try:
+      socket.inet_pton(socket.AF_INET6, i)
+   except socket.error:
+      return False
+   return True
+
 def ExtractDNSInfo(x):
 def ExtractDNSInfo(x):
+   hostname = GetAttr(x, "hostname")
 
    TTLprefix="\t"
    if 'dnsTTL' in x[1]:
 
    TTLprefix="\t"
    if 'dnsTTL' in x[1]:
@@ -783,88 +897,75 @@ def ExtractDNSInfo(x):
    DNSInfo = []
    if x[1].has_key("ipHostNumber"):
       for I in x[1]["ipHostNumber"]:
    DNSInfo = []
    if x[1].has_key("ipHostNumber"):
       for I in x[1]["ipHostNumber"]:
-         if IsV6Addr.match(I) != None:
-            DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
+         if is_ipv6_addr(I):
+            DNSInfo.append("%s.\t%sIN\tAAAA\t%s" % (hostname, TTLprefix, I))
          else:
          else:
-            DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
+            DNSInfo.append("%s.\t%sIN\tA\t%s" % (hostname, TTLprefix, I))
 
    Algorithm = None
 
 
    Algorithm = None
 
+   ssh_hostnames = [ hostname ]
+   if x[1].has_key("sshfpHostname"):
+      ssh_hostnames += [ h for h in x[1]["sshfpHostname"] ]
+
    if 'sshRSAHostKey' in x[1]:
       for I in x[1]["sshRSAHostKey"]:
          Split = I.split()
    if 'sshRSAHostKey' in x[1]:
       for I in x[1]["sshRSAHostKey"]:
          Split = I.split()
-         if Split[0] == 'ssh-rsa':
+         key_prefix = Split[0]
+         key = base64.decodestring(Split[1])
+
+         # RFC4255
+         # https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
+         if key_prefix == 'ssh-rsa':
             Algorithm = 1
             Algorithm = 1
-         if Split[0] == 'ssh-dss':
+         if key_prefix == 'ssh-dss':
             Algorithm = 2
             Algorithm = 2
+         if key_prefix == 'ssh-ed25519':
+            Algorithm = 4
          if Algorithm == None:
             continue
          if Algorithm == None:
             continue
-         Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
-         DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
+         # and more from the registry
+         sshfp_digest_codepoints = [ (1, 'sha1'), (2, 'sha256') ]
+
+         fingerprints = [ ( digest_codepoint, hashlib.new(algorithm, key).hexdigest() ) for digest_codepoint, algorithm in sshfp_digest_codepoints ]
+         for h in ssh_hostnames:
+            for digest_codepoint, fingerprint in fingerprints:
+               DNSInfo.append("%s.\t%sIN\tSSHFP\t%u %d %s" % (h, TTLprefix, Algorithm, digest_codepoint, fingerprint))
 
    if 'architecture' in x[1]:
       Arch = GetAttr(x, "architecture")
       Mach = ""
       if x[1].has_key("machine"):
          Mach = " " + GetAttr(x, "machine")
 
    if 'architecture' in x[1]:
       Arch = GetAttr(x, "architecture")
       Mach = ""
       if x[1].has_key("machine"):
          Mach = " " + GetAttr(x, "machine")
-      DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
+      DNSInfo.append("%s.\t%sIN\tHINFO\t\"%s%s\" \"%s\"" % (hostname, TTLprefix, Arch, Mach, "Debian"))
 
    if x[1].has_key("mXRecord"):
       for I in x[1]["mXRecord"]:
 
    if x[1].has_key("mXRecord"):
       for I in x[1]["mXRecord"]:
-         DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
+         if I in MX_remap:
+            for e in MX_remap[I]:
+               DNSInfo.append("%s.\t%sIN\tMX\t%s" % (hostname, TTLprefix, e))
+         else:
+            DNSInfo.append("%s.\t%sIN\tMX\t%s" % (hostname, TTLprefix, I))
 
    return DNSInfo
 
 # Generate the DNS records
 
    return DNSInfo
 
 # Generate the DNS records
-def GenZoneRecords(File):
+def GenZoneRecords(host_attrs, File):
    F = None
    try:
       F = open(File + ".tmp", "w")
 
       # Fetch all the hosts
    F = None
    try:
       F = open(File + ".tmp", "w")
 
       # Fetch all the hosts
-      global HostAttrs
-
-      for x in HostAttrs:
+      for x in host_attrs:
          if x[1].has_key("hostname") == 0:
             continue
 
          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
             continue
 
          if x[1].has_key("hostname") == 0:
             continue
 
          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
             continue
 
-         DNSInfo = ExtractDNSInfo(x)
-         start = True
-         for Line in DNSInfo:
-            if start == True:
-               Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
-               start = False
-            else:
-               Line = "\t\t\t%s" % (Line)
-
+         for Line in ExtractDNSInfo(x):
             F.write(Line + "\n")
 
             F.write(Line + "\n")
 
-        # this would write sshfp lines for services on machines
-        # but we can't yet, since some are cnames and we'll make
-        # an invalid zonefile
-        #
-        # for i in x[1].get("purpose", []):
-        #    m = PurposeHostField.match(i)
-        #    if m:
-        #       m = m.group(1)
-        #       # we ignore [[*..]] entries
-        #       if m.startswith('*'):
-        #          continue
-        #       if m.startswith('-'):
-        #          m = m[1:]
-        #       if m:
-        #          if not m.endswith(HostDomain):
-        #             continue
-        #          if not m.endswith('.'):
-        #             m = m + "."
-        #          for Line in DNSInfo:
-        #             if isSSHFP.match(Line):
-        #                Line = "%s\t%s" % (m, Line)
-        #                F.write(Line + "\n")
-
    # Oops, something unspeakable happened.
    except:
       Die(File, F, None)
    # Oops, something unspeakable happened.
    except:
       Die(File, F, None)
@@ -872,24 +973,18 @@ def GenZoneRecords(File):
    Done(File, F, None)
 
 # Generate the BSMTP file
    Done(File, F, None)
 
 # Generate the BSMTP file
-def GenBSMTP(File, HomePrefix):
+def GenBSMTP(accounts, File, HomePrefix):
    F = None
    try:
       F = open(File + ".tmp", "w")
      
    F = None
    try:
       F = open(File + ".tmp", "w")
      
-      # Fetch all the users
-      global PasswdAttrs
-     
       # Write out the zone file entry for each user
       # Write out the zone file entry for each user
-      for x in PasswdAttrs:
-         if x[1].has_key("dnsZoneEntry") == 0:
-            continue
-     
-         # If the account has no PGP key, do not write it
-         if x[1].has_key("keyFingerPrint") == 0:
-            continue
+      for a in accounts:
+         if not 'dnsZoneEntry' in a: continue
+         if not a.is_active_user(): continue
+
          try:
          try:
-            for z in x[1]["dnsZoneEntry"]:
+            for z in a["dnsZoneEntry"]:
                Split = z.lower().split()
                if Split[1].lower() == 'in':
                   for y in range(0, len(Split)):
                Split = z.lower().split()
                if Split[1].lower() == 'in':
                   for y in range(0, len(Split)):
@@ -900,7 +995,7 @@ def GenBSMTP(File, HomePrefix):
                   Host = Split[0] + DNSZone
                   if BSMTPCheck.match(Line) != None:
                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
                   Host = Split[0] + DNSZone
                   if BSMTPCheck.match(Line) != None:
                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
-                                  GetAttr(x, "uid"), HomePrefix, GetAttr(x, "uid"), Host))
+                                  a['uid'], HomePrefix, a['uid'], Host))
      
          except:
             F.write("; Errors\n")
      
          except:
             F.write("; Errors\n")
@@ -919,22 +1014,20 @@ def HostToIP(Host, mapped=True):
    if Host[1].has_key("ipHostNumber"):
       for addr in Host[1]["ipHostNumber"]:
          IPAdresses.append(addr)
    if Host[1].has_key("ipHostNumber"):
       for addr in Host[1]["ipHostNumber"]:
          IPAdresses.append(addr)
-         if IsV6Addr.match(addr) is None and mapped == "True":
+         if not is_ipv6_addr(addr) and mapped == "True":
             IPAdresses.append("::ffff:"+addr)
 
    return IPAdresses
 
 # Generate the ssh known hosts file
             IPAdresses.append("::ffff:"+addr)
 
    return IPAdresses
 
 # Generate the ssh known hosts file
-def GenSSHKnown(File, mode=None):
+def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
    F = None
    try:
       OldMask = os.umask(0022)
       F = open(File + ".tmp", "w", 0644)
       os.umask(OldMask)
      
    F = None
    try:
       OldMask = os.umask(0022)
       F = open(File + ".tmp", "w", 0644)
       os.umask(OldMask)
      
-      global HostAttrs
-     
-      for x in HostAttrs:
+      for x in host_attrs:
          if x[1].has_key("hostname") == 0 or \
             x[1].has_key("sshRSAHostKey") == 0:
             continue
          if x[1].has_key("hostname") == 0 or \
             x[1].has_key("sshRSAHostKey") == 0:
             continue
@@ -967,7 +1060,9 @@ def GenSSHKnown(File, mode=None):
                hosts = HostToIP(x)
                if 'sshdistAuthKeysHost' in x[1]:
                   hosts += x[1]['sshdistAuthKeysHost']
                hosts = HostToIP(x)
                if 'sshdistAuthKeysHost' in x[1]:
                   hosts += x[1]['sshdistAuthKeysHost']
-               Line = 'command="rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (Host, ",".join(hosts), I)
+               clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
+               clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
+               Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
             else:
                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
             Line = Sanitize(Line) + "\n"
             else:
                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
             Line = Sanitize(Line) + "\n"
@@ -979,7 +1074,7 @@ def GenSSHKnown(File, mode=None):
    Done(File, F, None)
 
 # Generate the debianhosts file (list of all IP addresses)
    Done(File, F, None)
 
 # Generate the debianhosts file (list of all IP addresses)
-def GenHosts(File):
+def GenHosts(host_attrs, File):
    F = None
    try:
       OldMask = os.umask(0022)
    F = None
    try:
       OldMask = os.umask(0022)
@@ -988,9 +1083,7 @@ def GenHosts(File):
      
       seen = set()
 
      
       seen = set()
 
-      global HostAttrs
-
-      for x in HostAttrs:
+      for x in host_attrs:
 
          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
             continue
 
          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
             continue
@@ -1011,109 +1104,150 @@ def GenHosts(File):
       raise
    Done(File, F, None)
 
       raise
    Done(File, F, None)
 
+def replaceTree(src, dst_basedir):
+   bn = os.path.basename(src)
+   dst = os.path.join(dst_basedir, bn)
+   safe_rmtree(dst)
+   shutil.copytree(src, dst)
+
 def GenKeyrings(OutDir):
    for k in Keyrings:
 def GenKeyrings(OutDir):
    for k in Keyrings:
-      shutil.copy(k, OutDir)
-
-# Connect to the ldap server
-l = connectLDAP()
-# for testing purposes it's sometimes useful to pass username/password
-# via the environment
-if 'UD_CREDENTIALS' in os.environ:
-   Pass = os.environ['UD_CREDENTIALS'].split()
-else:
-   F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
-   Pass = F.readline().strip().split(" ")
-   F.close()
-l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
-
-# Fetch all the groups
-GroupIDMap = {}
-Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
-                  ["gid", "gidNumber", "subGroup"])
-
-# Generate the SubGroupMap and GroupIDMap
-for x in Attrs:
-   if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
-      continue
-   if x[1].has_key("gidNumber") == 0:
-      continue
-   GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
-   if x[1].has_key("subGroup") != 0:
-      SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
-
-# Fetch all the users
-PasswdAttrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0)))",\
-                ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
-                 "gecos", "loginShell", "userPassword", "shadowLastChange",\
-                 "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
-                 "shadowExpire", "emailForward", "latitude", "longitude",\
-                 "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
-                 "keyFingerPrint", "privateSub", "mailDisableMessage",\
-                 "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
-                 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
-                 "mailContentInspectionAction"])
-
-if PasswdAttrs is None:
-   raise UDEmptyList, "No Users"
-
-PasswdAttrs.sort(lambda x, y: cmp((GetAttr(x, "uid")).lower(), (GetAttr(y, "uid")).lower()))
-
-# Fetch all the hosts
-HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
-                ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
-                 "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
-
-if HostAttrs == None:
-   raise UDEmptyList, "No Hosts"
-
-HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
-
-# override globaldir for testing
-if 'UD_GENERATEDIR' in os.environ:
-   GenerateDir = os.environ['UD_GENERATEDIR']
-
-# Generate global things
-GlobalDir = GenerateDir + "/"
-GenDisabledAccounts(GlobalDir + "disabled-accounts")
-
-PasswdAttrs = filter(lambda x: not IsRetired(x), PasswdAttrs)
-DebianDDUsers = filter(lambda x: IsGidDebian(x), PasswdAttrs)
-
-CheckForward()
-
-GenMailDisable(GlobalDir + "mail-disable")
-GenCDB(GlobalDir + "mail-forward.cdb", PasswdAttrs, 'emailForward')
-GenCDB(GlobalDir + "mail-contentinspectionaction.cdb", PasswdAttrs, 'mailContentInspectionAction')
-GenPrivate(GlobalDir + "debian-private")
-GenSSHKnown(GlobalDir+"authorized_keys", 'authorized_keys')
-GenMailBool(GlobalDir + "mail-greylist", "mailGreylisting")
-GenMailBool(GlobalDir + "mail-callout", "mailCallout")
-GenMailList(GlobalDir + "mail-rbl", "mailRBL")
-GenMailList(GlobalDir + "mail-rhsbl", "mailRHSBL")
-GenMailList(GlobalDir + "mail-whitelist", "mailWhitelist")
-GenKeyrings(GlobalDir)
-
-# Compatibility.
-GenForward(GlobalDir + "forward-alias")
-
-PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
-
-SSHFiles = GenSSHShadow()
-GenMarkers(GlobalDir + "markers")
-GenSSHKnown(GlobalDir + "ssh_known_hosts")
-GenHosts(GlobalDir + "debianhosts")
-
-for host in HostAttrs:
-   if not "hostname" in host[1]:
-      continue
-
-   CurrentHost = host[1]['hostname'][0]
-   OutDir = GenerateDir + '/' + CurrentHost + '/'
-   try:
+      if os.path.isdir(k):
+         replaceTree(k, OutDir)
+      else:
+         shutil.copy(k, OutDir)
+
+
+def get_accounts(ldap_conn):
+   # Fetch all the users
+   passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
+                   ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
+                    "gecos", "loginShell", "userPassword", "shadowLastChange",\
+                    "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
+                    "shadowExpire", "emailForward", "latitude", "longitude",\
+                    "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
+                    "keyFingerPrint", "privateSub", "mailDisableMessage",\
+                    "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
+                    "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
+                    "mailContentInspectionAction", "webPassword", "rtcPassword",\
+                    "bATVToken", "totpSeed"])
+
+   if passwd_attrs is None:
+      raise UDEmptyList, "No Users"
+   accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
+   accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
+
+   return accounts
+
+def get_hosts(ldap_conn):
+   # Fetch all the hosts
+   HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
+                   ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
+                    "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture",
+                    "sshfpHostname"])
+
+   if HostAttrs == None:
+      raise UDEmptyList, "No Hosts"
+
+   HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
+
+   return HostAttrs
+
+
+def make_ldap_conn():
+   # Connect to the ldap server
+   l = connectLDAP()
+   # for testing purposes it's sometimes useful to pass username/password
+   # via the environment
+   if 'UD_CREDENTIALS' in os.environ:
+      Pass = os.environ['UD_CREDENTIALS'].split()
+   else:
+      F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
+      Pass = F.readline().strip().split(" ")
+      F.close()
+   l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
+
+   return l
+
+
+
+def setup_group_maps(l):
+   # Fetch all the groups
+   group_id_map = {}
+   subgroup_map = {}
+   attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
+                     ["gid", "gidNumber", "subGroup"])
+
+   # Generate the subgroup_map and group_id_map
+   for x in attrs:
+      if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
+         continue
+      if x[1].has_key("gidNumber") == 0:
+         continue
+      group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
+      if x[1].has_key("subGroup") != 0:
+         subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
+
+   global SubGroupMap
+   global GroupIDMap
+   SubGroupMap = subgroup_map
+   GroupIDMap = group_id_map
+
+def generate_all(global_dir, ldap_conn):
+   accounts = get_accounts(ldap_conn)
+   host_attrs = get_hosts(ldap_conn)
+
+   global_dir += '/'
+   # Generate global things
+   accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
+
+   accounts = filter(lambda x: not IsRetired(x), accounts)
+
+   CheckForward(accounts)
+
+   GenMailDisable(accounts, global_dir + "mail-disable")
+   GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
+   GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
+   GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
+   GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
+   GenPrivate(accounts, global_dir + "debian-private")
+   GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
+   GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
+   GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
+   GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
+   GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
+   GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
+   GenWebPassword(accounts, global_dir + "web-passwords")
+   GenRtcPassword(accounts, global_dir + "rtc-passwords")
+   GenTOTPSeed(accounts, global_dir + "users.oath")
+   GenKeyrings(global_dir)
+
+   # Compatibility.
+   GenForward(accounts, global_dir + "forward-alias")
+
+   GenAllUsers(accounts, global_dir + 'all-accounts.json')
+   accounts = filter(lambda a: not a in accounts_disabled, accounts)
+
+   ssh_userkeys = GenSSHShadow(global_dir, accounts)
+   GenMarkers(accounts, global_dir + "markers")
+   GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
+   GenHosts(host_attrs, global_dir + "debianhosts")
+
+   GenDNS(accounts, global_dir + "dns-zone")
+   GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
+
+   setup_group_maps(ldap_conn)
+
+   for host in host_attrs:
+      if not "hostname" in host[1]:
+         continue
+      generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
+
+def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
+   current_host = host[1]['hostname'][0]
+   OutDir = global_dir + current_host + '/'
+   if not os.path.isdir(OutDir):
       os.mkdir(OutDir)
       os.mkdir(OutDir)
-   except: 
-      pass
 
    # Get the group list and convert any named groups to numerics
    GroupList = {}
 
    # Get the group list and convert any named groups to numerics
    GroupList = {}
@@ -1131,70 +1265,248 @@ for host in HostAttrs:
       for extra in host[1]['exportOptions']:
          ExtraList[extra.upper()] = True
 
       for extra in host[1]['exportOptions']:
          ExtraList[extra.upper()] = True
 
-   Allowed = GroupList
-   if Allowed == {}:
-      Allowed = None
+   if GroupList != {}:
+      accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
 
 
-   DoLink(GlobalDir, OutDir, "debianhosts")
-   DoLink(GlobalDir, OutDir, "ssh_known_hosts")
-   DoLink(GlobalDir, OutDir, "disabled-accounts")
+   DoLink(global_dir, OutDir, "debianhosts")
+   DoLink(global_dir, OutDir, "ssh_known_hosts")
+   DoLink(global_dir, OutDir, "disabled-accounts")
 
    sys.stdout.flush()
    if 'NOPASSWD' in ExtraList:
 
    sys.stdout.flush()
    if 'NOPASSWD' in ExtraList:
-      userlist = GenPasswd(OutDir + "passwd", HomePrefix, "*")
+      userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
    else:
    else:
-      userlist = GenPasswd(OutDir + "passwd", HomePrefix, "x")
+      userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
    sys.stdout.flush()
    sys.stdout.flush()
-   grouprevmap = GenGroup(OutDir + "group")
-   GenShadowSudo(OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
+   grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
+   GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
 
    # Now we know who we're allowing on the machine, export
    # the relevant ssh keys
 
    # Now we know who we're allowing on the machine, export
    # the relevant ssh keys
-   GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
+   GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
 
    if not 'NOPASSWD' in ExtraList:
 
    if not 'NOPASSWD' in ExtraList:
-      GenShadow(OutDir + "shadow")
+      GenShadow(accounts, OutDir + "shadow")
 
    # Link in global things
    if not 'NOMARKERS' in ExtraList:
 
    # Link in global things
    if not 'NOMARKERS' in ExtraList:
-      DoLink(GlobalDir, OutDir, "markers")
-   DoLink(GlobalDir, OutDir, "mail-forward.cdb")
-   DoLink(GlobalDir, OutDir, "mail-contentinspectionaction.cdb")
-   DoLink(GlobalDir, OutDir, "mail-disable")
-   DoLink(GlobalDir, OutDir, "mail-greylist")
-   DoLink(GlobalDir, OutDir, "mail-callout")
-   DoLink(GlobalDir, OutDir, "mail-rbl")
-   DoLink(GlobalDir, OutDir, "mail-rhsbl")
-   DoLink(GlobalDir, OutDir, "mail-whitelist")
-   GenCDB(OutDir + "user-forward.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'emailForward')
-   GenCDB(OutDir + "batv-tokens.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'bATVToken')
-   GenCDB(OutDir + "default-mail-options.cdb", filter(lambda x: IsInGroup(x), PasswdAttrs), 'mailDefaultOptions')
+      DoLink(global_dir, OutDir, "markers")
+   DoLink(global_dir, OutDir, "mail-forward.cdb")
+   DoLink(global_dir, OutDir, "mail-forward.db")
+   DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
+   DoLink(global_dir, OutDir, "mail-contentinspectionaction.db")
+   DoLink(global_dir, OutDir, "mail-disable")
+   DoLink(global_dir, OutDir, "mail-greylist")
+   DoLink(global_dir, OutDir, "mail-callout")
+   DoLink(global_dir, OutDir, "mail-rbl")
+   DoLink(global_dir, OutDir, "mail-rhsbl")
+   DoLink(global_dir, OutDir, "mail-whitelist")
+   DoLink(global_dir, OutDir, "all-accounts.json")
+   GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
+   GenDBM(accounts, OutDir + "user-forward.db", 'emailForward')
+   GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
+   GenDBM(accounts, OutDir + "batv-tokens.db", 'bATVToken')
+   GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
+   GenDBM(accounts, OutDir + "default-mail-options.db", 'mailDefaultOptions')
 
    # Compatibility.
 
    # Compatibility.
-   DoLink(GlobalDir, OutDir, "forward-alias")
+   DoLink(global_dir, OutDir, "forward-alias")
 
    if 'DNS' in ExtraList:
 
    if 'DNS' in ExtraList:
-      GenDNS(OutDir + "dns-zone")
-      GenZoneRecords(OutDir + "dns-sshfp")
+      DoLink(global_dir, OutDir, "dns-zone")
+      DoLink(global_dir, OutDir, "dns-sshfp")
 
    if 'AUTHKEYS' in ExtraList:
 
    if 'AUTHKEYS' in ExtraList:
-      DoLink(GlobalDir, OutDir, "authorized_keys")
+      DoLink(global_dir, OutDir, "authorized_keys")
 
    if 'BSMTP' in ExtraList:
 
    if 'BSMTP' in ExtraList:
-      GenBSMTP(OutDir + "bsmtp", HomePrefix)
+      GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
 
    if 'PRIVATE' in ExtraList:
 
    if 'PRIVATE' in ExtraList:
-      DoLink(GlobalDir, OutDir, "debian-private")
+      DoLink(global_dir, OutDir, "debian-private")
+
+   if 'GITOLITE' in ExtraList:
+      GenSSHGitolite(all_accounts, all_hosts, OutDir + "ssh-gitolite", current_host=current_host)
+   if 'exportOptions' in host[1]:
+      for entry in host[1]['exportOptions']:
+         v = entry.split('=',1)
+         if v[0] != 'GITOLITE' or len(v) != 2: continue
+         options = v[1].split(',')
+         group = options.pop(0);
+         gitolite_accounts = filter(lambda x: IsInGroup(x, [group], current_host), all_accounts)
+         if not 'nohosts' in options:
+            gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
+         else:
+            gitolite_hosts = []
+         command = None
+         for opt in options:
+            if opt.startswith('sshcmd='):
+               command = opt.split('=',1)[1]
+         GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(group,), sshcommand=command, current_host=current_host)
+
+   if 'WEB-PASSWORDS' in ExtraList:
+      DoLink(global_dir, OutDir, "web-passwords")
+
+   if 'RTC-PASSWORDS' in ExtraList:
+      DoLink(global_dir, OutDir, "rtc-passwords")
+
+   if 'TOTP' in ExtraList:
+      DoLink(global_dir, OutDir, "users.oath")
 
    if 'KEYRING' in ExtraList:
       for k in Keyrings:
 
    if 'KEYRING' in ExtraList:
       for k in Keyrings:
-        DoLink(GlobalDir, OutDir, os.path.basename(k))
+         bn = os.path.basename(k)
+         if os.path.isdir(k):
+            src = os.path.join(global_dir, bn)
+            replaceTree(src, OutDir)
+         else:
+            DoLink(global_dir, OutDir, bn)
    else:
       for k in Keyrings:
    else:
       for k in Keyrings:
-         try: 
-            posix.remove(OutDir + os.path.basename(k))
+         try:
+            bn = os.path.basename(k)
+            target = os.path.join(OutDir, bn)
+            if os.path.isdir(target):
+               safe_rmtree(dst)
+            else:
+               posix.remove(target)
          except:
             pass
          except:
             pass
+   DoLink(global_dir, OutDir, "last_update.trace")
+
+
+def getLastLDAPChangeTime(l):
+   mods = l.search_s('cn=log',
+         ldap.SCOPE_ONELEVEL,
+         '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
+         ['reqEnd'])
+
+   last = 0
+
+   # Sort the list by reqEnd
+   sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
+   # Take the last element in the array
+   last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
+
+   return last
+
+def getLastKeyringChangeTime():
+   krmod = 0
+   for k in Keyrings:
+      mt = os.path.getmtime(k)
+      if mt > krmod:
+         krmod = mt
+
+   return int(krmod)
+
+def getLastBuildTime(gdir):
+   cache_last_ldap_mod = 0
+   cache_last_unix_mod = 0
+   cache_last_run = 0
+
+   try:
+      fd = open(os.path.join(gdir, "last_update.trace"), "r")
+      cache_last_mod=fd.read().split()
+      try:
+         cache_last_ldap_mod = cache_last_mod[0]
+         cache_last_unix_mod = int(cache_last_mod[1])
+         cache_last_run = int(cache_last_mod[2])
+      except IndexError, ValueError:
+         pass
+      fd.close()
+   except IOError, e:
+      if e.errno == errno.ENOENT:
+         pass
+      else:
+         raise e
+
+   return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
+
+def mq_notify(options, message):
+   options.section = 'dsa-udgenerate'
+   options.config = '/etc/dsa/pubsub.conf'
+
+   config = Config(options)
+   conf = {
+      'rabbit_userid': config.username,
+      'rabbit_password': config.password,
+      'rabbit_virtual_host': config.vhost,
+      'rabbit_hosts': ['pubsub02.debian.org', 'pubsub01.debian.org'],
+      'use_ssl': False
+   }
+
+   msg = {
+      'message': message,
+      'timestamp': int(time.time())
+   }
+   conn = None
+   try:
+      conn = Connection(conf=conf)
+      conn.topic_send(config.topic,
+            json.dumps(msg),
+            exchange_name=config.exchange,
+            timeout=5)
+   finally:
+      if conn:
+         conn.close()
+
+def ud_generate():
+   parser = optparse.OptionParser()
+   parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
+     help="Output directory.")
+   parser.add_option("-f", "--force", dest="force", action="store_true",
+     help="Force generation, even if no update to LDAP has happened.")
+
+   (options, args) = parser.parse_args()
+   if len(args) > 0:
+      parser.print_help()
+      sys.exit(1)
+
+   if options.generatedir is not None:
+      generate_dir = os.environ['UD_GENERATEDIR']
+   elif 'UD_GENERATEDIR' in os.environ:
+      generate_dir = os.environ['UD_GENERATEDIR']
+   else:
+      generate_dir = GenerateDir
+
+
+   lockf = os.path.join(generate_dir, 'ud-generate.lock')
+   lock = get_lock( lockf )
+   if lock is None:
+      sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
+      sys.exit(1)
+
+   l = make_ldap_conn()
+
+   time_started = int(time.time())
+   ldap_last_mod = getLastLDAPChangeTime(l)
+   unix_last_mod = getLastKeyringChangeTime()
+   cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
+
+   need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod) or (time_started - last_run > MAX_UD_AGE)
+
+   fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
+   if need_update or options.force:
+      msg = 'Update forced' if options.force else 'Update needed'
+      generate_all(generate_dir, l)
+      if use_mq:
+         mq_notify(options, msg)
+      last_run = int(time.time())
+   fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
+   fd.close()
+   sys.exit(0)
+
+
+if __name__ == "__main__":
+   if 'UD_PROFILE' in os.environ:
+      import cProfile
+      import pstats
+      cProfile.run('ud_generate()', "udg_prof")
+      p = pstats.Stats('udg_prof')
+      ##p.sort_stats('time').print_stats()
+      p.sort_stats('cumulative').print_stats()
+   else:
+      ud_generate()
 
 # vim:set et:
 # vim:set ts=3:
 
 # vim:set et:
 # vim:set ts=3: