ud-generate: No longer expand $ in dnsZoneEntry data to a \n\t.
[mirror/userdir-ldap.git] / ud-generate
index bbddc2f..27088e9 100755 (executable)
@@ -6,11 +6,13 @@
 #   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 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 Thomas Viehmann <tv@beamnet.de>
 #   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 Thomas Viehmann <tv@beamnet.de>
+#   Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
+#   Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
 #
 #   This program is free software; you can redistribute it and/or modify
 #   it under the terms of the GNU General Public License as published by
 #
 #   This program is free software; you can redistribute it and/or modify
 #   it under the terms of the GNU General Public License as published by
 #   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 userdir_ldap import *;
+import string, re, time, ldap, getopt, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp
+import lockfile
+from userdir_ldap import *
+from userdir_exceptions import *
+import UDLdap
+try:
+   from cStringIO import StringIO
+except ImportError:
+   from StringIO import StringIO
+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)
+
+
+#
+# GLOBAL STATE
+#
+GroupIDMap = {}
+SubGroupMap = {}
+Allowed = None
+CurrentHost = ""
 
 
-global Allowed;
-global CurrentHost;
 
 
-PasswdAttrs = None;
-disabledusers = []
-GroupIDMap = {};
-SubGroupMap = {};
-Allowed = None;
-CurrentHost = "";
 
 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}'
 
-EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$");
-BSMTPCheck = re.compile(".*mx 0 (gluck)\.debian\.org\..*",re.DOTALL);
-PurposeHostField = re.compile(r"\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
+EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
+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(":")
 DNSZone = ".debian.net"
 Keyrings = ConfModule.sync_keyrings.split(":")
+GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
+
 
 def safe_makedirs(dir):
 
 def safe_makedirs(dir):
-    try:
-        os.makedirs(dir)
-    except OSError, e:
-        if e.errno == errno.EEXIST:
-            pass
-        else:
-            raise e
+   try:
+      os.makedirs(dir)
+   except OSError, e:
+      if e.errno == errno.EEXIST:
+         pass
+      else:
+         raise e
 
 def safe_rmtree(dir):
 
 def safe_rmtree(dir):
-    try:
-        shutil.rmtree(dir)
-    except OSError, e:
-        if e.errno == errno.ENOENT:
-            pass
-        else:
-            raise e
+   try:
+      shutil.rmtree(dir)
+   except OSError, e:
+      if e.errno == errno.ENOENT:
+         pass
+      else:
+         raise e
+
+def get_lock(fn, wait=5*60, max_age=3600*6):
+   try:
+      stat = os.stat(fn + '.lock')
+      if stat.st_mtime < time.time() - max_age:
+         sys.stderr.write("Removing stale lock %s"%(fn + '.lock'))
+         os.unlink(fn + '.lock')
+   except OSError, error:
+      if error.errno == errno.ENOENT:
+         pass
+      else:
+         raise
+
+   lock = lockfile.FileLock(fn)
+   try:
+      lock.acquire(timeout=wait)
+   except lockfile.LockTimeout:
+      return None
+
+   return lock
+
 
 def Sanitize(Str):
 
 def Sanitize(Str):
-  return Str.translate(string.maketrans("\n\r\t","$$$"))
+   return Str.translate(string.maketrans("\n\r\t", "$$$"))
 
 
-def DoLink(From,To,File):
-   try: posix.remove(To+File);
-   except: pass;
-   posix.link(From+File,To+File);
+def DoLink(From, To, File):
+   try: 
+      posix.remove(To + File)
+   except: 
+      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]
 
 
-   if status.find("inactive") != -1:
+   if status == "inactive":
       return True
 
       return True
 
-   if status.find("memorial") != -1:
+   elif status == "memorial":
       return True
 
       return True
 
-   if status.find("retiring") != -1:
-      line = status.split()
+   elif status == "retiring":
       # We'll give them a few extra days over what we said
       age = 6 * 31 * 24 * 60 * 60
       # We'll give them a few extra days over what we said
       age = 6 * 31 * 24 * 60 * 60
-      if (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d")) > (age):
-            return True
+      try:
+         return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
+      except IndexError:
+         return False
+      except ValueError:
+         return False
 
    return False
 
 
    return False
 
+#def IsGidDebian(account):
+#   return account['gidNumber'] == 800
+
 # See if this user is in the group list
 # See if this user is in the group list
-def IsInGroup(DnRecord):
-  if Allowed == None:
-     return 1;
+def IsInGroup(account):
+  if Allowed is None:
+     return True
 
   # 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 1;
+  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:
-     for I in DnRecord[1]["allowedHost"]:
-        if CurrentHost == I:
-           return 1;
+  if account.is_allowed_by_hostacl(CurrentHost): return True
 
   # See if there are supplementary groups
 
   # See if there are supplementary groups
-  if DnRecord[1].has_key("supplementaryGid") == 0:
-     return 0;
+  if not 'supplementaryGid' in account: return False
 
   supgroups=[]
 
   supgroups=[]
-  addGroups(supgroups, DnRecord[1]["supplementaryGid"], GetAttr(DnRecord,"uid"))
+  addGroups(supgroups, account['supplementaryGid'], account['uid'])
   for g in supgroups:
      if Allowed.has_key(g):
   for g in supgroups:
      if Allowed.has_key(g):
-        return 1;
-  return 0;
+        return True
+  return False
 
 
-def Die(File,F,Fdb):
+def Die(File, F, Fdb):
+   if F != None:
+      F.close()
+   if Fdb != None:
+      Fdb.close()
+   try: 
+      os.remove(File + ".tmp")
+   except:
+      pass
+   try: 
+      os.remove(File + ".tdb.tmp")
+   except: 
+      pass
+
+def Done(File, F, Fdb):
    if F != None:
    if F != None:
-      F.close();
+      F.close()
+      os.rename(File + ".tmp", File)
    if Fdb != None:
    if Fdb != None:
-      Fdb.close();
-   try: os.remove(File + ".tmp");
-   except: pass;
-   try: os.remove(File + ".tdb.tmp");
-   except: pass;
-
-def Done(File,F,Fdb):
-  if F != None:
-    F.close();
-    os.rename(File + ".tmp",File);
-  if Fdb != None:
-    Fdb.close();
-    os.rename(File + ".tdb.tmp",File+".tdb");
+      Fdb.close()
+      os.rename(File + ".tdb.tmp", File + ".tdb")
 
 # Generate the password list
 
 # Generate the password list
-def GenPasswd(l,File,HomePrefix,PwdMarker):
-  F = None;
-  try:
-   F = open(File + ".tdb.tmp","w");
-
-   userlist = {}
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
-
-   I = 0;
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
-
-      if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
-         continue;
-
-      # 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;
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,None,F);
-   raise;
-  Done(File,None,F);
-
-  # Return the list of users so we know which keys to export
-  return userlist
+def GenPasswd(accounts, File, HomePrefix, PwdMarker):
+   F = None
+   try:
+      F = open(File + ".tdb.tmp", "w")
+
+      userlist = {}
+      i = 0
+      for a in accounts:
+         if not IsInGroup(a): continue
+
+         # Do not let people try to buffer overflow some busted passwd parser.
+         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)
+      raise
+   Done(File, None, F)
+
+   # 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(l,File):
-  F = None;
-  try:
-   OldMask = os.umask(0077);
-   F = open(File + ".tdb.tmp","w",0600);
-   os.umask(OldMask);
-
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
-
-   I = 0;
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
-
-      if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
-         continue;
-
-      Pass = GetAttr(x,"userPassword");
-      if Pass[0:7] != "{crypt}" or len(Pass) > 50:
-         Pass = '*';
-      else:
-         Pass = Pass[7:];
-
-      # 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 (GetAttr(x,"userPassword").find("*LK*") != -1) \
-          or GetAttr(x,"userPassword").startswith("!"):
-         ShadowExpire = '1'
-      else:
-         ShadowExpire = GetAttr(x,"shadowExpire")
-
-      Line = "%s:%s:%s:%s:%s:%s:%s:%s:" % (GetAttr(x,"uid"),\
-              Pass,GetAttr(x,"shadowLastChange"),\
-              GetAttr(x,"shadowMin"),GetAttr(x,"shadowMax"),\
-              GetAttr(x,"shadowWarning"),GetAttr(x,"shadowInactive"),\
-              ShadowExpire);
-      Line = Sanitize(Line) + "\n";
-      F.write("0%u %s" % (I,Line));
-      F.write(".%s %s" % (GetAttr(x,"uid"),Line));
-      I = I + 1;
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,None,F);
-   raise;
-  Done(File,None,F);
+def GenShadow(accounts, File):
+   F = None
+   try:
+      OldMask = os.umask(0077)
+      F = open(File + ".tdb.tmp", "w", 0600)
+      os.umask(OldMask)
+
+      i = 0
+      for a in accounts:
+         Pass = '*'
+         if not IsInGroup(a): continue
+
+         # 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'
+         elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
+         else:                     ShadowExpire = ''
+
+         values = []
+         values.append(a['uid'])
+         values.append(a.get_password())
+         for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
+            if key in a: values.append(a[key])
+            else:        values.append('')
+         values.append(ShadowExpire)
+         line = ':'.join(values)+':'
+         line = Sanitize(line) + "\n"
+         F.write("0%u %s" % (i, line))
+         F.write(".%s %s" % (a['uid'], line))
+         i = i + 1
+
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, None, F)
+      raise
+   Done(File, None, F)
 
 # Generate the sudo passwd file
 
 # Generate the sudo passwd file
-def GenShadowSudo(l,File, untrusted):
-  F = None;
-  try:
-   OldMask = os.umask(0077);
-   F = open(File + ".tmp","w",0600);
-   os.umask(OldMask);
-
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
-
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
+def GenShadowSudo(accounts, File, untrusted):
+   F = None
+   try:
+      OldMask = os.umask(0077)
+      F = open(File + ".tmp", "w", 0600)
+      os.umask(OldMask)
+
+      for a in accounts:
+         Pass = '*'
+         if not IsInGroup(a): continue
+     
+         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)
+               if Match == None:
+                  continue
+               uuid = Match.group(1)
+               status = Match.group(2)
+               hosts = Match.group(3)
+               cryptedpass = Match.group(4)
+     
+               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(',')
+               if not (for_all or for_this_host):
+                  continue
+               # ignore * passwords for untrusted hosts, but copy host specific passwords
+               if for_all and untrusted:
+                  continue
+               Pass = cryptedpass
+               if for_this_host: # this makes sure we take a per-host entry over the for-all entry
+                  break
+            if len(Pass) > 50:
+               Pass = '*'
+     
+         Line = "%s:%s" % (a['uid'], Pass)
+         Line = Sanitize(Line) + "\n"
+         F.write("%s" % (Line))
+  
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
 
 
-      Pass = '*'
-      if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
-         continue;
-
-      if x[1].has_key('sudoPassword'):
-         for entry in x[1]['sudoPassword']:
-            Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
-            if Match == None:
-               continue
-            uuid = Match.group(1)
-            status = Match.group(2)
-            hosts = Match.group(3)
-            cryptedpass = Match.group(4)
-
-            if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', x[1]['uid'][0], uuid, hosts, cryptedpass):
-               continue
-            for_all = hosts == "*"
-            for_this_host = CurrentHost in hosts.split(',')
-            if not (for_all or for_this_host):
-               continue
-            # ignore * passwords for untrusted hosts, but copy host specific passwords
-            if for_all and untrusted:
-               continue
-            Pass = cryptedpass
-            if for_this_host: # this makes sure we take a per-host entry over the for-all entry
-              break
-         if len(Pass) > 50:
-            Pass = '*'
-
-      Line = "%s:%s" % (GetAttr(x,"uid"), Pass)
-      Line = Sanitize(Line) + "\n";
-      F.write("%s" % (Line));
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,F,None);
-   raise;
-  Done(File,F,None);
+# Generate the sudo passwd file
+def GenSSHGitolite(accounts, File):
+   F = None
+   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.replace('@@USER@@', User)
+            for I in a["sshRSAAuthKey"]:
+               if I.startswith('ssh-'):
+                  line = "%s %s"%(prefix, I)
+               else:
+                  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(l):
+def GenSSHShadow(global_dir, accounts):
    # Fetch all the users
    # Fetch all the users
-   singlefile = None
    userfiles = []
 
    userfiles = []
 
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
+   safe_rmtree(os.path.join(global_dir, 'userkeys'))
+   safe_makedirs(os.path.join(global_dir, 'userkeys'))
 
 
-   safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
-   safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
-
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
-
-      # If the account is locked, do not write it.
-      # This is a partial stop-gap. The ssh also needs to change this
-      # to ignore ~/.ssh/authorized* files.
-      if (GetAttr(x,"userPassword").find("*LK*") != -1) \
-             or GetAttr(x,"userPassword").startswith("!"):
-         continue;
-
-      if x[1].has_key("uidNumber") == 0 or \
-         x[1].has_key("sshRSAAuthKey") == 0:
-         continue;
-      User = GetAttr(x,"uid");
-      F = None;
+   for a in accounts:
+      if not 'sshRSAAuthKey' in a: continue
 
 
+      F = None
       try:
       try:
-         OldMask = os.umask(0077);
-         File = os.path.join(GlobalDir, 'userkeys', User)
-         F = open(File + ".tmp","w",0600);
-         os.umask(OldMask);
+         OldMask = os.umask(0077)
+         File = os.path.join(global_dir, 'userkeys', a['uid'])
+         F = open(File + ".tmp", "w", 0600)
+         os.umask(OldMask)
 
 
-         for I in x[1]["sshRSAAuthKey"]:
+         for I in a['sshRSAAuthKey']:
             MultipleLine = "%s" % I
             MultipleLine = Sanitize(MultipleLine) + "\n"
             F.write(MultipleLine)
 
             MultipleLine = "%s" % I
             MultipleLine = Sanitize(MultipleLine) + "\n"
             F.write(MultipleLine)
 
-         Done(File,F,None);
+         Done(File, F, None)
          userfiles.append(os.path.basename(File))
 
       # Oops, something unspeakable happened.
       except IOError:
          userfiles.append(os.path.basename(File))
 
       # Oops, something unspeakable happened.
       except IOError:
-          Die(File,F,None)
-          Die(masterFileName,masterFile,None)
-          raise;
+         Die(File, F, None)
+         # As neither masterFileName nor masterFile are defined at any point
+         # this will raise a NameError.
+         Die(masterFileName, masterFile, None)
+         raise
 
    return userfiles
 
 
    return userfiles
 
-def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
-   OldMask = os.umask(0077);
-   tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
-   os.umask(OldMask);
+# Generate the webPassword list
+def GenWebPassword(accounts, File):
+   F = None
+   try:
+      OldMask = os.umask(0077)
+      F = open(File, "w", 0600)
+      os.umask(OldMask)
+
+      for a in accounts:
+         if not 'webPassword' in a: continue
+         if not a.pw_active(): continue
+
+         Pass = str(a['webPassword'])
+         Line = "%s:%s" % (a['uid'], Pass)
+         Line = Sanitize(Line) + "\n"
+         F.write("%s" % (Line))
+
+   except:
+      Die(File, None, F)
+      raise
+
+def GenSSHtarballs(global_dir, userlist, SSHFiles, grouprevmap, target):
+   OldMask = os.umask(0077)
+   tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
+   os.umask(OldMask)
    for f in userlist.keys():
       if f not in SSHFiles:
          continue
    for f in userlist.keys():
       if f not in SSHFiles:
          continue
@@ -373,7 +451,7 @@ def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
          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])
          continue
 
          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])
          continue
 
-      to = tf.gettarinfo(os.path.join(GlobalDir, 'userkeys', f), f)
+      to = tf.gettarinfo(os.path.join(global_dir, 'userkeys', f), 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,10 +467,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
-      tf.addfile(to, file(os.path.join(GlobalDir, 'userkeys', f)))
+
+      contents = file(os.path.join(global_dir, '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.size = len(contents)
+      tf.addfile(to, StringIO(contents))
 
    tf.close()
 
    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' % CurrentHost), target)
 
 # add a list of groups to existing groups,
 # including all subgroups thereof, recursively.
 
 # add a list of groups to existing groups,
 # including all subgroups thereof, recursively.
@@ -403,12 +494,12 @@ def addGroups(existingGroups, newGroups, uid):
       # 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 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:
-         continue;
+         continue
       group = s[0]
 
       # let's see if we handled this group already
       if group in existingGroups:
       group = s[0]
 
       # let's see if we handled this group already
       if group in existingGroups:
-        continue
+         continue
 
       if not GroupIDMap.has_key(group):
          print "Group", group, "does not exist but", uid, "is in it"
 
       if not GroupIDMap.has_key(group):
          print "Group", group, "does not exist but", uid, "is in it"
@@ -420,793 +511,815 @@ def addGroups(existingGroups, newGroups, uid):
          addGroups(existingGroups, SubGroupMap[group], uid)
 
 # Generate the group list
          addGroups(existingGroups, SubGroupMap[group], uid)
 
 # Generate the group list
-def GenGroup(l,File):
-  grouprevmap = {}
-  F = None;
-  try:
-   F = open(File + ".tdb.tmp","w");
-
-   # Generate the GroupMap
-   GroupMap = {};
-   for x in GroupIDMap.keys():
-      GroupMap[x] = [];
-
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
-
-   # Sort them into a list of groups having a set of users
-   for x in PasswdAttrs:
-      uid = GetAttr(x,"uid")
-      if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
-         continue;
-      if x[1].has_key("supplementaryGid") == 0:
-         continue;
-
-      supgroups=[]
-      addGroups(supgroups, x[1]["supplementaryGid"], uid)
-      for g in supgroups:
-         GroupMap[g].append(uid);
-
-   # Output the group file.
-   J = 0;
-   for x in GroupMap.keys():
-      grouprevmap[GroupIDMap[x]] = x
-      if GroupIDMap.has_key(x) == 0:
-         continue;
-      Line = "%s:x:%u:" % (x,GroupIDMap[x]);
-      Comma = '';
-      for I in GroupMap[x]:
-        Line = Line + ("%s%s" % (Comma,I));
-        Comma = ',';
-      Line = Sanitize(Line) + "\n";
-      F.write("0%u %s" % (J,Line));
-      F.write(".%s %s" % (x,Line));
-      F.write("=%u %s" % (GroupIDMap[x],Line));
-      J = J + 1;
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,None,F);
-   raise;
-  Done(File,None,F);
-
-  return grouprevmap
-
-# Generate the email forwarding list
-def GenForward(l,File):
-  F = None;
-  try:
-   OldMask = os.umask(0022);
-   F = open(File + ".tmp","w",0644);
-   os.umask(OldMask);
-
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
+def GenGroup(accounts, File):
+   grouprevmap = {}
+   F = None
+   try:
+      F = open(File + ".tdb.tmp", "w")
+     
+      # Generate the GroupMap
+      GroupMap = {}
+      for x in GroupIDMap.keys():
+         GroupMap[x] = []
+      GroupHasPrimaryMembers = {}
+
+      # Sort them into a list of groups having a set of users
+      for a in accounts:
+         GroupHasPrimaryMembers[ a['gidNumber'] ] = True
+         if not IsInGroup(a): continue
+         if not 'supplementaryGid' in a: continue
+
+         supgroups=[]
+         addGroups(supgroups, a['supplementaryGid'], a['uid'])
+         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:
+            continue
 
 
-   # Write out the email address for each user
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
+         if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
+            continue
 
 
-      if x[1].has_key("emailForward") == 0 or IsInGroup(x) == 0:
-         continue;
+         grouprevmap[GroupIDMap[x]] = x
 
 
+         Line = "%s:x:%u:" % (x, GroupIDMap[x])
+         Comma = ''
+         for I in GroupMap[x]:
+            Line = Line + ("%s%s" % (Comma, I))
+            Comma = ','
+         Line = Sanitize(Line) + "\n"
+         F.write("0%u %s" % (J, Line))
+         F.write(".%s %s" % (x, Line))
+         F.write("=%u %s" % (GroupIDMap[x], Line))
+         J = J + 1
+  
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, None, F)
+      raise
+   Done(File, None, F)
+  
+   return grouprevmap
+
+def CheckForward(accounts):
+   for a in accounts:
+      if not 'emailForward' in a: continue
+
+
+      delete = False
+
+      if not IsInGroup(a): delete = True
       # Do not allow people to try to buffer overflow busted parsers
       # Do not allow people to try to buffer overflow busted parsers
-      if len(GetAttr(x,"emailForward")) > 200:
-         continue;
-
+      elif len(a['emailForward']) > 200: delete = True
       # Check the forwarding address
       # Check the forwarding address
-      if EmailCheck.match(GetAttr(x,"emailForward")) == None:
-         continue;
-      Line = "%s: %s" % (GetAttr(x,"uid"),GetAttr(x,"emailForward"));
-      Line = Sanitize(Line) + "\n";
-      F.write(Line);
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,F,None);
-   raise;
-  Done(File,F,None);
-
-def GenAllForward(l,File):
-  Fdb = None;
-  try:
-   OldMask = os.umask(0022);
-   Fdb = os.popen("cdbmake %s %s.tmp"%(File,File),"w");
-   os.umask(OldMask);
+      elif EmailCheck.match(a['emailForward']) is None: delete = True
 
 
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
+      if delete:
+         a.delete_mailforward()
 
 
-   # Write out the email address for each user
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
+# Generate the email forwarding list
+def GenForward(accounts, File):
+   F = None
+   try:
+      OldMask = os.umask(0022)
+      F = open(File + ".tmp", "w", 0644)
+      os.umask(OldMask)
 
 
-      if x[1].has_key("emailForward") == 0:
-         continue;
+      for a in accounts:
+         if not 'emailForward' in a: continue
+         Line = "%s: %s" % (a['uid'], a['emailForward'])
+         Line = Sanitize(Line) + "\n"
+         F.write(Line)
 
 
-      # Do not allow people to try to buffer overflow busted parsers
-      Forward = GetAttr(x,"emailForward");
-      if len(Forward) > 200:
-         continue;
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
 
 
-      # Check the forwarding address
-      if EmailCheck.match(Forward) == None:
-         continue;
-
-      User = GetAttr(x,"uid");
-      Fdb.write("+%d,%d:%s->%s\n"%(len(User),len(Forward),User,Forward));
-   Fdb.write("\n");
-  # Oops, something unspeakable happened.
-  except:
-    Fdb.close();
-    raise;
-  if Fdb.close() != None:
-    raise "cdbmake gave an error";
+def GenCDB(accounts, File, key):
+   Fdb = None
+   try:
+      OldMask = os.umask(0022)
+      Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
+      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.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
+
+      Fdb.write("\n")
+   # Oops, something unspeakable happened.
+   except:
+      Fdb.close()
+      raise
+   if Fdb.close() != None:
+      raise "cdbmake gave an error"
 
 # Generate the anon XEarth marker file
 
 # Generate the anon XEarth marker file
-def GenMarkers(l,File):
-  F = None;
-  try:
-   F = open(File + ".tmp","w");
+def GenMarkers(accounts, File):
+   F = None
+   try:
+      F = open(File + ".tmp", "w")
 
 
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
+      # Write out the position for each user
+      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))
+            Line = Sanitize(Line) + "\n"
+            F.write(Line)
+         except:
+            pass
+  
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
 
 
-   # Write out the position for each user
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
+# Generate the debian-private subscription list
+def GenPrivate(accounts, File):
+   F = None
+   try:
+      F = open(File + ".tmp", "w")
 
 
-      if x[1].has_key("latitude") == 0 or x[1].has_key("longitude") == 0:
-         continue;
-      try:
-         Line = "%8s %8s \"\""%(DecDegree(GetAttr(x,"latitude"),1),DecDegree(GetAttr(x,"longitude"),1));
-         Line = Sanitize(Line) + "\n";
-         F.write(Line);
-      except:
-         pass;
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,F,None);
-   raise;
-  Done(File,F,None);
+      # Write out the position for each user
+      for a in accounts:
+         if not a.is_active_user(): continue
+         if not 'privateSub' in a: continue
+         try:
+            Line = "%s"%(a['privateSub'])
+            Line = Sanitize(Line) + "\n"
+            F.write(Line)
+         except:
+            pass
+  
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
 
 
-# Generate the debian-private subscription list
-def GenPrivate(l,File):
-  F = None;
-  try:
-   F = open(File + ".tmp","w");
+# Generate a list of locked accounts
+def GenDisabledAccounts(accounts, File):
+   F = None
+   try:
+      F = open(File + ".tmp", "w")
+      disabled_accounts = []
+
+      # Fetch all the users
+      for a in accounts:
+         if a.pw_active(): continue
+         Line = "%s:%s" % (a['uid'], "Account is locked")
+         disabled_accounts.append(a)
+         F.write(Sanitize(Line) + "\n")
 
 
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
+   return disabled_accounts
 
 
-   # Write out the position for each user
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
+# Generate the list of local addresses that refuse all mail
+def GenMailDisable(accounts, File):
+   F = None
+   try:
+      F = open(File + ".tmp", "w")
 
 
-      if x[1].has_key("privateSub") == 0:
-         continue;
+      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 the account is locked, do not write it
-      if (GetAttr(x,"userPassword").find("*LK*") != -1) \
-             or GetAttr(x,"userPassword").startswith("!"):
-         continue;
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
 
 
-      # If the account has no PGP key, do not write it
-      if x[1].has_key("keyFingerPrint") == 0:
-         continue;
+# Generate a list of uids that should have boolean affects applied
+def GenMailBool(accounts, File, key):
+   F = None
+   try:
+      F = open(File + ".tmp", "w")
 
 
-      # Must be in the Debian group (yuk, hard coded for now)
-      if GetAttr(x,"gidNumber") != "800":
-         continue;
+      for a in accounts:
+         if not key in a: continue
+         if not a[key] == 'TRUE': continue
+         Line = "%s"%(a['uid'])
+         Line = Sanitize(Line) + "\n"
+         F.write(Line)
 
 
-      try:
-         Line = "%s"%(GetAttr(x,"privateSub"));
-         Line = Sanitize(Line) + "\n";
-         F.write(Line);
-      except:
-         pass;
-
-  # 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)
 
 
-# Generate a list of locked accounts
-def GenDisabledAccounts(l,File):
-  F = None;
-  try:
-   F = open(File + ".tmp","w");
+# Generate a list of hosts for RBL or whitelist purposes.
+def GenMailList(accounts, File, key):
+   F = None
+   try:
+      F = open(File + ".tmp", "w")
 
 
-   # Fetch all the users
-   global PasswdAttrs;
-   global disabledusers
-   if PasswdAttrs == None:
-      raise "No Users";
-
-   I = 0;
-   for x in PasswdAttrs:
-      if x[1].has_key("uidNumber") == 0:
-         continue;
-
-      Pass = GetAttr(x,"userPassword");
-      Line = ""
-      # *LK* is the reference value for a locked account
-      # password starting with ! is also a locked account
-      if Pass.find("*LK*") != -1 or Pass.startswith("!"):
-         # Format is <login>:<reason>
-         Line = "%s:%s" % (GetAttr(x,"uid"), "Account is locked")
-
-      if Line != "":
-         F.write(Sanitize(Line) + "\n")
+      if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
+      else:                      validregex = re.compile('^[-\w.]+$')
 
 
-      disabledusers.append(x)
+      for a in accounts:
+         if not key in a: continue
 
 
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,F,None);
-   raise;
-  Done(File,F,None);
+         filtered = filter(lambda z: validregex.match(z), a[key])
+         if len(filtered) == 0: continue
+         if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
+         line = a['uid'] + ': ' + ' : '.join(filtered)
+         line = Sanitize(line) + "\n"
+         F.write(line)
 
 
-# Generate the list of local addresses that refuse all mail
-def GenMailDisable(l,File):
-  F = None;
-  try:
-   F = open(File + ".tmp","w");
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
 
 
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
+def isRoleAccount(account):
+   return 'debianRoleAccount' in account['objectClass']
 
 
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
+# Generate the DNS Zone file
+def GenDNS(accounts, File):
+   F = None
+   try:
+      F = open(File + ".tmp", "w")
 
 
-      Reason = None
+      # Fetch all the users
+      RRs = {}
 
 
-      if x[1].has_key("mailDisableMessage"):
-         Reason = GetAttr(x,"mailDisableMessage")
-      else:
-         continue
+      # Write out the zone file entry for each user
+      for a in accounts:
+         if not 'dnsZoneEntry' in a: continue
+         if not a.is_active_user() and not isRoleAccount(a): continue
 
 
-      # Must be in the Debian group (yuk, hard coded for now)
-      if GetAttr(x,"gidNumber") != "800":
-         continue;
+         try:
+            F.write("; %s\n"%(a.email_address()))
+            for z in a["dnsZoneEntry"]:
+               Split = z.lower().split()
+               if Split[1].lower() == 'in':
+                  Line = " ".join(Split) + "\n"
+                  F.write(Line)
+
+                  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"]:
+                        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)
+
+            F.write("\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.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
+
+def ExtractDNSInfo(x):
+
+   TTLprefix="\t"
+   if 'dnsTTL' in x[1]:
+      TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
+
+   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))
+         else:
+            DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
 
 
-      try:
-         Line = "%s: %s"%(GetAttr(x,"uid"),Reason);
-         Line = Sanitize(Line) + "\n";
-         F.write(Line);
-      except:
-         pass;
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,F,None);
-   raise;
-  Done(File,F,None);
+   Algorithm = None
 
 
-# Generate a list of uids that should have boolean affects applied
-def GenMailBool(l,File,Key):
-  F = None;
-  try:
-   F = open(File + ".tmp","w");
+   if 'sshRSAHostKey' in x[1]:
+      for I in x[1]["sshRSAHostKey"]:
+         Split = I.split()
+         if Split[0] == 'ssh-rsa':
+            Algorithm = 1
+         if Split[0] == 'ssh-dss':
+            Algorithm = 2
+         if Algorithm == None:
+            continue
+         Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
+         DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
 
 
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
+   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"))
 
 
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
+   if x[1].has_key("mXRecord"):
+      for I in x[1]["mXRecord"]:
+         DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
 
 
-      Reason = None
+   return DNSInfo
 
 
-      if x[1].has_key(Key) == 0:
-         continue
+# Generate the DNS records
+def GenZoneRecords(host_attrs, File):
+   F = None
+   try:
+      F = open(File + ".tmp", "w")
 
 
-      # Must be in the Debian group (yuk, hard coded for now)
-      if GetAttr(x,"gidNumber") != "800":
-         continue
+      # Fetch all the hosts
+      for x in host_attrs:
+         if x[1].has_key("hostname") == 0:
+            continue
 
 
-      if GetAttr(x,Key) != "TRUE":
-         continue
+         if IsDebianHost.match(GetAttr(x, "hostname")) is None:
+            continue
 
 
-      try:
-         Line = "%s"%(GetAttr(x,"uid"));
-         Line = Sanitize(Line) + "\n";
-         F.write(Line);
-      except:
-         pass;
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,F,None);
-   raise;
-  Done(File,F,None);
+         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)
+
+            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)
+      raise
+   Done(File, F, None)
 
 
-# Generate a list of hosts for RBL or whitelist purposes.
-def GenMailList(l,File,Key):
-  F = None;
-  try:
-   F = open(File + ".tmp","w");
+# Generate the BSMTP file
+def GenBSMTP(accounts, File, HomePrefix):
+   F = None
+   try:
+      F = open(File + ".tmp", "w")
+     
+      # Write out the zone file entry for each user
+      for a in accounts:
+         if not 'dnsZoneEntry' in a: continue
+         if not a.is_active_user(): continue
 
 
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
+         try:
+            for z in a["dnsZoneEntry"]:
+               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"
+     
+                  Host = Split[0] + DNSZone
+                  if BSMTPCheck.match(Line) != None:
+                      F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
+                                  a['uid'], HomePrefix, a['uid'], Host))
+     
+         except:
+            F.write("; Errors\n")
+            pass
+  
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
+  
+def HostToIP(Host, mapped=True):
 
 
-   for x in PasswdAttrs:
-      if IsRetired(x):
-         continue
+   IPAdresses = []
 
 
-      Reason = None
+   if Host[1].has_key("ipHostNumber"):
+      for addr in Host[1]["ipHostNumber"]:
+         IPAdresses.append(addr)
+         if IsV6Addr.match(addr) is None and mapped == "True":
+            IPAdresses.append("::ffff:"+addr)
 
 
-      if x[1].has_key(Key) == 0:
-         continue
+   return IPAdresses
 
 
-      # Must be in the Debian group (yuk, hard coded for now)
-      if GetAttr(x,"gidNumber") != "800":
-         continue
+# Generate the ssh known hosts file
+def GenSSHKnown(host_attrs, File, mode=None):
+   F = None
+   try:
+      OldMask = os.umask(0022)
+      F = open(File + ".tmp", "w", 0644)
+      os.umask(OldMask)
+     
+      for x in host_attrs:
+         if x[1].has_key("hostname") == 0 or \
+            x[1].has_key("sshRSAHostKey") == 0:
+            continue
+         Host = GetAttr(x, "hostname")
+         HostNames = [ Host ]
+         if Host.endswith(HostDomain):
+            HostNames.append(Host[:-(len(HostDomain) + 1)])
+     
+         # in the purpose field [[host|some other text]] (where some other text is optional)
+         # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
+         # file.  But so that we don't have to add everything we link we can add an asterisk
+         # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
+         # http linking it we also support [[-hostname]] entries.
+         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:
+                  HostNames.append(m)
+                  if m.endswith(HostDomain):
+                     HostNames.append(m[:-(len(HostDomain) + 1)])
+     
+         for I in x[1]["sshRSAHostKey"]:
+            if mode and mode == 'authorized_keys':
+               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)
+            else:
+               Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
+            Line = Sanitize(Line) + "\n"
+            F.write(Line)
+   # Oops, something unspeakable happened.
+   except:
+      Die(File, F, None)
+      raise
+   Done(File, F, None)
 
 
-      try:
-         found = 0
-         Line = None
-         for z in x[1][Key]:
-             if Key == "mailWhitelist":
-                 if re.match('^[-\w.]+(/[\d]+)?$',z) == None:
-                     continue
-             else:
-                 if re.match('^[-\w.]+$',z) == None:
-                     continue
-             if found == 0:
-                 found = 1
-                 Line = GetAttr(x,"uid")
-             else:
-                 Line += " "
-             Line += ": " + z
-             if Key == "mailRHSBL":
-                 Line += "/$sender_address_domain"
-
-         if Line != None:
-             Line = Sanitize(Line) + "\n";
-             F.write(Line);
-      except:
-         pass;
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(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']
+# Generate the debianhosts file (list of all IP addresses)
+def GenHosts(host_attrs, File):
+   F = None
    try:
    try:
-      i = oc.index('debianRoleAccount')
-      return True
-   except ValueError:
-      return False
+      OldMask = os.umask(0022)
+      F = open(File + ".tmp", "w", 0644)
+      os.umask(OldMask)
+     
+      seen = set()
 
 
-# Generate the DNS Zone file
-def GenDNS(l,File,HomePrefix):
-  F = None;
-  try:
-   F = open(File + ".tmp","w");
+      for x in host_attrs:
 
 
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
-
-   # 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;
-      try:
-         F.write("; %s\n"%(EmailAddress(x)));
-         for z in x[1]["dnsZoneEntry"]:
-            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);
-
-               Host = Split[0] + DNSZone;
-               if BSMTPCheck.match(Line) != None:
-                   F.write("; Has BSMTP\n");
-
-               # Write some identification information
-               if Split[2].lower() == "a":
-                  Line = "%s IN TXT \"%s\"\n"%(Split[0],EmailAddress(x));
-                  for y in x[1]["keyFingerPrint"]:
-                     Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0],FormatPGPKey(y));
-                  F.write(Line);
-            else:
-               Line = "; Err %s"%(str(Split));
-               F.write(Line);
-
-         F.write("\n");
-      except:
-         F.write("; Errors\n");
-         pass;
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,F,None);
-   raise;
-  Done(File,F,None);
-
-# Generate the DNS SSHFP records
-def GenSSHFP(l,File,HomePrefix):
-  F = None
-  try:
-   F = open(File + ".tmp","w")
+         if IsDebianHost.match(GetAttr(x, "hostname")) is None:
+            continue
 
 
+         if not 'ipHostNumber' in x[1]:
+            continue
+
+         addrs = x[1]["ipHostNumber"]
+         for addr in addrs:
+            if addr not in seen:
+               seen.add(addr)
+               addr = Sanitize(addr) + "\n"
+               F.write(addr)
+
+   # Oops, something unspeakable happened.
+   except:
+      Die(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:
+      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"])
+
+   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
    # Fetch all the hosts
-   global HostAttrs
+   HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
+                   ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
+                    "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
+
    if HostAttrs == None:
    if HostAttrs == None:
-      raise "No Hosts"
+      raise UDEmptyList, "No Hosts"
 
 
-   for x in HostAttrs:
-      if x[1].has_key("hostname") == 0 or \
-         x[1].has_key("sshRSAHostKey") == 0:
-         continue
-      Host = GetAttr(x,"hostname");
-      Algorithm = None
-      for I in x[1]["sshRSAHostKey"]:
-         Split = I.split()
-         if Split[0] == 'ssh-rsa':
-            Algorithm = 1
-         if Split[0] == 'ssh-dss':
-            Algorithm = 2
-         if Algorithm == None:
-            continue
-         Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
-         Line = "%s. IN SSHFP %u 1 %s" % (Host,Algorithm,Fingerprint)
-         Line = Sanitize(Line) + "\n"
-         F.write(Line)
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,F,None)
-   raise;
-  Done(File,F,None)
+   HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
 
 
-# Generate the BSMTP file
-def GenBSMTP(l,File,HomePrefix):
-  F = None;
-  try:
-   F = open(File + ".tmp","w");
+   return HostAttrs
 
 
-   # Fetch all the users
-   global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
-
-   # 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;
-      try:
-         for z in x[1]["dnsZoneEntry"]:
-            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";
-
-               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));
-
-      except:
-         F.write("; Errors\n");
-         pass;
-
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,F,None);
-   raise;
-  Done(File,F,None);
-
-# cache IP adresses
-HostToIPCache = {}
-def HostToIP(Host):
-    global HostToIPCache
-    if not Host in HostToIPCache:
-        IPAdressesT = None
-        try:
-            IPAdressesT = list(set([ (a[0],a[4][0]) for a in socket.getaddrinfo(Host, None)]))
-        except socket.gaierror, (code):
-            if code[0] != -2: raise
-        IPAdresses = []
-        if not IPAdressesT is None:
-            for addr in IPAdressesT:
-               if addr[0] == socket.AF_INET: IPAdresses += [addr[1], "::ffff:"+addr[1]]
-               else: IPAdresses += [addr[1]]
-        HostToIPCache[Host] = IPAdresses
-    return HostToIPCache[Host]
 
 
+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 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)
+   #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
+
+   CheckForward(accounts)
+
+   GenMailDisable(accounts, global_dir + "mail-disable")
+   GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
+   GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
+   GenPrivate(accounts, global_dir + "debian-private")
+   GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys')
+   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")
+   GenKeyrings(global_dir)
+
+   # Compatibility.
+   GenForward(accounts, global_dir + "forward-alias")
 
 
-# Generate the ssh known hosts file
-def GenSSHKnown(l,File,mode=None):
-  F = None;
-  try:
-   OldMask = os.umask(0022);
-   F = open(File + ".tmp","w",0644);
-   os.umask(OldMask);
-
-   global HostAttrs
-   if HostAttrs == None:
-      raise "No Hosts";
-
-   for x in HostAttrs:
-      if x[1].has_key("hostname") == 0 or \
-         x[1].has_key("sshRSAHostKey") == 0:
-         continue;
-      Host = GetAttr(x,"hostname");
-      HostNames = [ Host ]
-      if Host.endswith(HostDomain):
-         HostNames.append(Host[:-(len(HostDomain)+1)])
-
-      # in the purpose field [[host|some other text]] (where some other text is optional)
-      # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
-      # file.  But so that we don't have to add everything we link we can add an asterisk
-      # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
-      # http linking it we also support [[-hostname]] entries.
-      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:
-               HostNames.append(m)
-               if m.endswith(HostDomain):
-                  HostNames.append(m[:-(len(HostDomain)+1)])
+   GenAllUsers(accounts, global_dir + 'all-accounts.json')
+   accounts = filter(lambda a: not a in accounts_disabled, accounts)
 
 
-      for I in x[1]["sshRSAHostKey"]:
-         if mode and mode == 'authorized_keys':
-            #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(HNames + HostToIP(Host)), I)
-            Line = 'command="rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding %s' % (Host,I)
-         else:
-            Line = "%s %s" %(",".join(HostNames + HostToIP(Host)), I);
-         Line = Sanitize(Line) + "\n";
-         F.write(Line);
-  # Oops, something unspeakable happened.
-  except:
-   Die(File,F,None);
-   raise;
-  Done(File,F,None);
+   ssh_files = GenSSHShadow(global_dir, accounts)
+   GenMarkers(accounts, global_dir + "markers")
+   GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
+   GenHosts(host_attrs, global_dir + "debianhosts")
+   GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
 
 
-# Generate the debianhosts file (list of all IP addresses)
-def GenHosts(l,File):
-  F = None
-  try:
-    OldMask = os.umask(0022)
-    F = open(File + ".tmp","w",0644)
-    os.umask(OldMask)
-
-    # Fetch all the hosts
-    hostnames = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "hostname=*",
-                           ["hostname"])
-
-    if hostnames == None:
-       raise "No Hosts"
-
-    seen = set()
-    for x in hostnames:
-      host = GetAttr(x,"hostname", None)
-      if host:
-        addrs = []
-        try:
-          addrs += socket.getaddrinfo(host, None, socket.AF_INET)
-        except socket.error:
-          pass
-        try:
-          addrs += socket.getaddrinfo(host, None, socket.AF_INET6)
-        except socket.error:
-          pass
-
-        for addrinfo in addrs:
-          if addrinfo[0] in (socket.AF_INET, socket.AF_INET6):
-            addr = addrinfo[4][0]
-            if addr not in seen:
-              print >> F, addrinfo[4][0]
-              seen.add(addr)
-  # Oops, something unspeakable happened.
-  except:
-    Die(File,F,None)
-    raise
-  Done(File,F,None)
-
-def GenKeyrings(l,OutDir):
-  for k in Keyrings:
-    shutil.copy(k, OutDir)
-
-
-# Connect to the ldap server
-l = connectLDAP()
-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]);
+   GenDNS(accounts, global_dir + "dns-zone")
+   GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
 
 
-# Fetch all the groups
-GroupIDMap = {};
-Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"gid=*",\
-                  ["gid","gidNumber","subGroup"]);
+   for host in host_attrs:
+      if not "hostname" in host[1]:
+         continue
+      generate_host(host, global_dir, accounts, ssh_files)
 
 
-# Generate the SubGroupMap and GroupIDMap
-for x in Attrs:
-   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=*",\
-                ["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"]);
-# Fetch all the hosts
-HostAttrs    = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"sshRSAHostKey=*",\
-                ["hostname","sshRSAHostKey","purpose"]);
-
-# Open the control file
-if len(sys.argv) == 1:
-   F = open(GenerateConf,"r");
-else:
-   F = open(sys.argv[1],"r")
-
-# Generate global things
-GlobalDir = GenerateDir+"/";
-SSHFiles = GenSSHShadow(l);
-GenAllForward(l,GlobalDir+"mail-forward.cdb");
-GenMarkers(l,GlobalDir+"markers");
-GenPrivate(l,GlobalDir+"debian-private");
-GenDisabledAccounts(l,GlobalDir+"disabled-accounts");
-GenSSHKnown(l,GlobalDir+"ssh_known_hosts");
-#GenSSHKnown(l,GlobalDir+"authorized_keys", 'authorized_keys');
-GenHosts(l,GlobalDir+"debianhosts");
-GenMailDisable(l,GlobalDir+"mail-disable");
-GenMailBool(l,GlobalDir+"mail-greylist","mailGreylisting");
-GenMailBool(l,GlobalDir+"mail-callout","mailCallout");
-GenMailList(l,GlobalDir+"mail-rbl","mailRBL");
-GenMailList(l,GlobalDir+"mail-rhsbl","mailRHSBL");
-GenMailList(l,GlobalDir+"mail-whitelist","mailWhitelist");
-GenKeyrings(l,GlobalDir);
-
-# Compatibility.
-GenForward(l,GlobalDir+"forward-alias");
-
-PasswdAttrs = filter(lambda x: not x in disabledusers, PasswdAttrs)
-
-while(1):
-   Line = F.readline();
-   if Line == "":
-      break;
-   Line = Line.strip()
-   if Line == "":
-      continue;
-   if Line[0] == '#':
-      continue;
-
-   Split = Line.split(" ")
-   OutDir = GenerateDir + '/' + Split[0] + '/';
-   try: os.mkdir(OutDir);
-   except: pass;
+def generate_host(host, global_dir, accounts, ssh_files):
+   global CurrentHost
+
+   CurrentHost = host[1]['hostname'][0]
+   OutDir = global_dir + CurrentHost + '/'
+   try:
+      os.mkdir(OutDir)
+   except:
+      pass
 
    # Get the group list and convert any named groups to numerics
 
    # Get the group list and convert any named groups to numerics
-   GroupList = {};
-   ExtraList = {};
-   for I in Split[2:]:
-      if I[0] == '[':
-         ExtraList[I] = None;
-         continue;
-      GroupList[I] = None;
-      if GroupIDMap.has_key(I):
-         GroupList[str(GroupIDMap[I])] = None;
-
-   Allowed = GroupList;
+   GroupList = {}
+   for groupname in AllowedGroupsPreload.strip().split(" "):
+      GroupList[groupname] = True
+   if 'allowedGroups' in host[1]:
+      for groupname in host[1]['allowedGroups']:
+         GroupList[groupname] = True
+   for groupname in GroupList.keys():
+      if groupname in GroupIDMap:
+         GroupList[str(GroupIDMap[groupname])] = True
+
+   ExtraList = {}
+   if 'exportOptions' in host[1]:
+      for extra in host[1]['exportOptions']:
+         ExtraList[extra.upper()] = True
+
+   global Allowed
+   Allowed = GroupList
    if Allowed == {}:
    if Allowed == {}:
-     Allowed = None
-   CurrentHost = Split[0];
+      Allowed = None
 
 
-   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 ExtraList.has_key("[NOPASSWD]"):
-      userlist = GenPasswd(l,OutDir+"passwd",Split[1], "*");
+   sys.stdout.flush()
+   if 'NOPASSWD' in ExtraList:
+      userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
    else:
    else:
-      userlist = GenPasswd(l,OutDir+"passwd",Split[1], "x");
-   sys.stdout.flush();
-   grouprevmap = GenGroup(l,OutDir+"group");
-   GenShadowSudo(l, OutDir+"sudo-passwd", ExtraList.has_key("[UNTRUSTED]") or ExtraList.has_key("[NOPASSWD]"))
+      userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
+   sys.stdout.flush()
+   grouprevmap = GenGroup(accounts, OutDir + "group")
+   GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList))
 
    # 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_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
 
 
-   if ExtraList.has_key("[UNTRUSTED]"):
-     print "[UNTRUSTED] tag is obsolete and may be removed in the future."
-     continue;
-   if not ExtraList.has_key("[NOPASSWD]"):
-     GenShadow(l,OutDir+"shadow");
+   if not 'NOPASSWD' in ExtraList:
+      GenShadow(accounts, OutDir + "shadow")
 
    # Link in global things
 
    # Link in global things
-   if not ExtraList.has_key("[NOMARKERS]"):
-     DoLink(GlobalDir,OutDir,"markers");
-   DoLink(GlobalDir,OutDir,"mail-forward.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");
+   if not 'NOMARKERS' in ExtraList:
+      DoLink(global_dir, OutDir, "markers")
+   DoLink(global_dir, OutDir, "mail-forward.cdb")
+   DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
+   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(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
+   GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
+   GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
 
    # Compatibility.
 
    # Compatibility.
-   DoLink(GlobalDir,OutDir,"forward-alias");
+   DoLink(global_dir, OutDir, "forward-alias")
+
+   if 'DNS' in ExtraList:
+      DoLink(global_dir, OutDir, "dns-zone")
+      DoLink(global_dir, OutDir, "dns-sshfp")
+
+   if 'AUTHKEYS' in ExtraList:
+      DoLink(global_dir, OutDir, "authorized_keys")
+
+   if 'BSMTP' in ExtraList:
+      GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
 
 
-   if ExtraList.has_key("[DNS]"):
-      GenDNS(l,OutDir+"dns-zone",Split[1]);
-      GenSSHFP(l,OutDir+"dns-sshfp",Split[1])
+   if 'PRIVATE' in ExtraList:
+      DoLink(global_dir, OutDir, "debian-private")
 
 
-   if ExtraList.has_key("[BSMTP]"):
-      GenBSMTP(l,OutDir+"bsmtp",Split[1])
+   if 'GITOLITE' in ExtraList:
+      DoLink(global_dir, OutDir, "ssh-gitolite")
 
 
-   if ExtraList.has_key("[PRIVATE]"):
-      DoLink(GlobalDir,OutDir,"debian-private")
+   if 'WEB-PASSWORDS' in ExtraList:
+      DoLink(global_dir, OutDir, "web-passwords")
 
 
-   if ExtraList.has_key("[KEYRING]"):
+   if 'KEYRING' in ExtraList:
       for k in Keyrings:
       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:
    else:
-     for k in Keyrings:
-       try: posix.remove(OutDir+os.path.basename(k));
-       except: pass;
+      for k in Keyrings:
+         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
+   DoLink(global_dir, OutDir, "last_update.trace")
+
+l = make_ldap_conn()
+
+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]
+
+# override globaldir for testing
+if 'UD_GENERATEDIR' in os.environ:
+   GenerateDir = os.environ['UD_GENERATEDIR']
+
+cache_last_mod = 0
+
+try:
+   fd = open(os.path.join(GenerateDir, "last_update.trace"), "r")
+   cache_last_mod=fd.read().split()
+   try:
+      cache_last_mod = cache_last_mod[0]
+   except IndexError:
+      pass
+   fd.close()
+except IOError, e:
+   if e.errno == errno.ENOENT:
+      pass
+   else:
+      raise e
+
+if cache_last_mod >= last:
+   fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
+   fd.write("%s\n%s\n" % (last, int(time.time())))
+   fd.close()
+   sys.exit(0)
+
+# 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"])
+
+lock = None
+try:
+   lockf = os.path.join(GenerateDir, 'ud-generate.lock')
+   lock = get_lock( lockf )
+   if lock is None:
+      sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
+      sys.exit(1)
+
+   tracefd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
+   generate_all(GenerateDir, l)
+   tracefd.write("%s\n%s\n" % (last, int(time.time())))
+   tracefd.close()
+
+finally:
+   if lock is not None:
+      lock.release()
+
 
 # vim:set et:
 # vim:set ts=3:
 
 # vim:set et:
 # vim:set ts=3: