Moving away from string exceptions
[mirror/userdir-ldap.git] / ud-generate
index 5515b98..8001012 100755 (executable)
@@ -9,6 +9,8 @@
 #   Copyright (c) 2008 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>
 #
 #   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
 
 import string, re, time, ldap, getopt, sys, os, pwd, posix, socket, base64, sha, shutil, errno, tarfile, grp
 from userdir_ldap import *;
+from userdir_exceptions import *
 
 global Allowed;
 global CurrentHost;
 
 PasswdAttrs = None;
+DisabledUsers = []
+RetiredUsers = []
 GroupIDMap = {};
+SubGroupMap = {};
 Allowed = None;
 CurrentHost = "";
 
@@ -39,6 +45,7 @@ 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.\-]*)(?:\|.*)?\]\]")
 DNSZone = ".debian.net"
 Keyrings = ConfModule.sync_keyrings.split(":")
 
@@ -68,6 +75,36 @@ def DoLink(From,To,File):
    except: pass;
    posix.link(From+File,To+File);
 
+def IsRetired(DnRecord):
+   """
+   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
+
+   line = status.split()
+   status = line[0]
+   
+   if status == "inactive":
+      return True
+
+   elif status == "memorial":
+      return True
+
+   elif status == "retiring":
+      # We'll give them a few extra days over what we said
+      age = 6 * 31 * 24 * 60 * 60
+      try:
+         if (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age:
+            return True
+      except IndexError:
+         return False
+
+   return False
+
 # See if this user is in the group list
 def IsInGroup(DnRecord):
   if Allowed == None:
@@ -87,9 +124,10 @@ def IsInGroup(DnRecord):
   if DnRecord[1].has_key("supplementaryGid") == 0:
      return 0;
 
-  # Check the supplementary groups
-  for I in DnRecord[1]["supplementaryGid"]:
-     if Allowed.has_key(I):
+  supgroups=[]
+  addGroups(supgroups, DnRecord[1]["supplementaryGid"], GetAttr(DnRecord,"uid"))
+  for g in supgroups:
+     if Allowed.has_key(g):
         return 1;
   return 0;
 
@@ -120,8 +158,6 @@ def GenPasswd(l,File,HomePrefix,PwdMarker):
    userlist = {}
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    I = 0;
    for x in PasswdAttrs:
@@ -164,8 +200,6 @@ def GenShadow(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    I = 0;
    for x in PasswdAttrs:
@@ -212,8 +246,6 @@ def GenShadowSudo(l,File, untrusted):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    for x in PasswdAttrs:
       Pass = '*'
@@ -230,7 +262,7 @@ def GenShadowSudo(l,File, untrusted):
             hosts = Match.group(3)
             cryptedpass = Match.group(4)
 
-            if status != 'confirmed:'+make_sudopasswd_hmac('password-is-confirmed', uuid, hosts, cryptedpass):
+            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(',')
@@ -256,65 +288,41 @@ def GenShadowSudo(l,File, untrusted):
   Done(File,F,None);
 
 # Generate the shadow list
-def GenSSHShadow(l,masterFileName):
+def GenSSHShadow(l):
    # Fetch all the users
    singlefile = None
    userfiles = []
-   # Depending on config, we write out either a single file,
-   # multiple files, or both
-   if SingleSSHFile:
-       try:
-           OldMask = os.umask(0077);
-           masterFile = open(masterFileName + ".tmp","w",0600);
-           os.umask(OldMask);
-       except IOError:
-           Die(masterFileName,masterFile,None)
-           raise
 
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
-   # If we're going to be dealing with multiple keys, empty the
-   # directory before we start to avoid old keys hanging around
-   if MultipleSSHFiles:
-      safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
-      safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
+   safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
+   safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
 
    for x in PasswdAttrs:
-      # 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 in DisabledUsers:
+         continue
 
       if x[1].has_key("uidNumber") == 0 or \
          x[1].has_key("sshRSAAuthKey") == 0:
          continue;
+
       User = GetAttr(x,"uid");
       F = None;
 
       try:
-         if MultipleSSHFiles:
-             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(GlobalDir, 'userkeys', User)
+         F = open(File + ".tmp","w",0600);
+         os.umask(OldMask);
 
          for I in x[1]["sshRSAAuthKey"]:
-             if MultipleSSHFiles:
-                 MultipleLine = "%s" % I
-                 MultipleLine = Sanitize(MultipleLine) + "\n"
-                 F.write(MultipleLine)
-             if SingleSSHFile:
-                 SingleLine = "%s: %s" % (User, I)
-                 SingleLine = Sanitize(SingleLine) + "\n"
-                 masterFile.write(SingleLine)
-
-         if MultipleSSHFiles:
-             Done(File,F,None);
-             userfiles.append(os.path.basename(File))
+            MultipleLine = "%s" % I
+            MultipleLine = Sanitize(MultipleLine) + "\n"
+            F.write(MultipleLine)
+
+         Done(File,F,None);
+         userfiles.append(os.path.basename(File))
 
       # Oops, something unspeakable happened.
       except IOError:
@@ -322,11 +330,79 @@ def GenSSHShadow(l,masterFileName):
           Die(masterFileName,masterFile,None)
           raise;
 
-   if SingleSSHFile:
-       Done(masterFileName,masterFile,None)
-       singlefile = os.path.basename(masterFileName)
+   return userfiles
 
-   return singlefile, 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);
+   for f in userlist.keys():
+      if f not in SSHFiles:
+         continue
+      # If we're not exporting their primary group, don't export
+      # the key and warn
+      grname = None
+      if userlist[f] in grouprevmap.keys():
+         grname = grouprevmap[userlist[f]]
+      else:
+         try:
+            if int(userlist[f]) <= 100:
+               # In these cases, look it up in the normal way so we
+               # deal with cases where, for instance, users are in group
+               # users as their primary group.
+               grname = grp.getgrgid(userlist[f])[0]
+         except Exception, e:
+            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])
+         continue
+
+      to = tf.gettarinfo(os.path.join(GlobalDir, '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
+      # be owned by root but group nobody.  This deals with
+      # the bloody obscure case where the group fails to exist
+      # whilst the user does (in which case we want to avoid
+      # ending up with a file which is owned user:root to avoid
+      # a fairly obvious attack vector)
+      to.uid = 0
+      to.gid = 65534
+      # Using the username / groupname fields avoids any need
+      # to give a shit^W^W^Wcare about the UIDoffset stuff.
+      to.uname = f
+      to.gname = grname
+      to.mode  = 0400
+      tf.addfile(to, file(os.path.join(GlobalDir, 'userkeys', f)))
+
+   tf.close()
+   os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), 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.
+def addGroups(existingGroups, newGroups, uid):
+   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:
+         continue;
+      group = s[0]
+
+      # let's see if we handled this group already
+      if group in existingGroups:
+        continue
+
+      if not GroupIDMap.has_key(group):
+         print "Group", group, "does not exist but", uid, "is in it"
+         continue
+
+      existingGroups.append(group)
+
+      if SubGroupMap.has_key(group):
+         addGroups(existingGroups, SubGroupMap[group], uid)
 
 # Generate the group list
 def GenGroup(l,File):
@@ -342,21 +418,19 @@ def GenGroup(l,File):
 
    # 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;
 
-      for I in x[1]["supplementaryGid"]:
-         if GroupMap.has_key(I):
-            GroupMap[I].append(GetAttr(x,"uid"));
-         else:
-            print "Group does not exist ",I,"but",GetAttr(x,"uid"),"is in it";
+      supgroups=[]
+      addGroups(supgroups, x[1]["supplementaryGid"], uid)
+      for g in supgroups:
+         GroupMap[g].append(uid);
 
    # Output the group file.
    J = 0;
@@ -393,8 +467,6 @@ def GenForward(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Write out the email address for each user
    for x in PasswdAttrs:
@@ -427,8 +499,6 @@ def GenAllForward(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Write out the email address for each user
    for x in PasswdAttrs:
@@ -462,8 +532,6 @@ def GenMarkers(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Write out the position for each user
    for x in PasswdAttrs:
@@ -490,19 +558,12 @@ def GenPrivate(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Write out the position for each user
    for x in PasswdAttrs:
       if x[1].has_key("privateSub") == 0:
          continue;
 
-      # If the account is locked, do not write it
-      if (GetAttr(x,"userPassword").find("*LK*") != -1) \
-             or GetAttr(x,"userPassword").startswith("!"):
-         continue;
-
       # If the account has no PGP key, do not write it
       if x[1].has_key("keyFingerPrint") == 0:
          continue;
@@ -532,8 +593,7 @@ def GenDisabledAccounts(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
+   global DisabledUsers
 
    I = 0;
    for x in PasswdAttrs:
@@ -551,6 +611,8 @@ def GenDisabledAccounts(l,File):
       if Line != "":
          F.write(Sanitize(Line) + "\n")
 
+      DisabledUsers.append(x)
+
   # Oops, something unspeakable happened.
   except:
    Die(File,F,None);
@@ -565,8 +627,6 @@ def GenMailDisable(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    for x in PasswdAttrs:
       Reason = None
@@ -601,8 +661,6 @@ def GenMailBool(l,File,Key):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    for x in PasswdAttrs:
       Reason = None
@@ -638,8 +696,6 @@ def GenMailList(l,File,Key):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    for x in PasswdAttrs:
       Reason = None
@@ -682,6 +738,16 @@ def GenMailList(l,File,Key):
    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
+
 # Generate the DNS Zone file
 def GenDNS(l,File,HomePrefix):
   F = None;
@@ -690,8 +756,6 @@ def GenDNS(l,File,HomePrefix):
 
    # 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:
@@ -699,7 +763,7 @@ def GenDNS(l,File,HomePrefix):
          continue;
 
       # If the account has no PGP key, do not write it
-      if x[1].has_key("keyFingerPrint") == 0:
+      if x[1].has_key("keyFingerPrint") == 0 and not isRoleAccount(x[1]):
          continue;
       try:
          F.write("; %s\n"%(EmailAddress(x)));
@@ -746,7 +810,7 @@ def GenSSHFP(l,File,HomePrefix):
    # Fetch all the hosts
    global HostAttrs
    if HostAttrs == None:
-      raise "No Hosts"
+      raise UDEmptyList, "No Hosts"
 
    for x in HostAttrs:
       if x[1].has_key("hostname") == 0 or \
@@ -780,8 +844,6 @@ def GenBSMTP(l,File,HomePrefix):
 
    # 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:
@@ -826,9 +888,10 @@ def HostToIP(Host):
         except socket.gaierror, (code):
             if code[0] != -2: raise
         IPAdresses = []
-        for addr in IPAdressesT:
-            if addr[0] == socket.AF_INET: IPAdresses += [addr[1], "::ffff:"+addr[1]]
-            else: IPAdresses += [addr[1]]
+        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]
 
@@ -843,7 +906,7 @@ def GenSSHKnown(l,File,mode=None):
 
    global HostAttrs
    if HostAttrs == None:
-      raise "No Hosts";
+      raise UDEmptyList, "No Hosts"
 
    for x in HostAttrs:
       if x[1].has_key("hostname") == 0 or \
@@ -851,8 +914,27 @@ def GenSSHKnown(l,File,mode=None):
          continue;
       Host = GetAttr(x,"hostname");
       HostNames = [ Host ]
-      SHost = Host.find(".")
-      if SHost != None: HostNames += [Host[0:SHost]]
+      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':
@@ -870,38 +952,50 @@ def GenSSHKnown(l,File,mode=None):
 
 # Generate the debianhosts file (list of all IP addresses)
 def GenHosts(l,File):
-  F = None;
+  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";
-
-   for x in HostNames:
-      if x[1].has_key("hostname") == 0:
-         continue;
-      Host = GetAttr(x,"hostname");
-      try:
-        Addr = socket.gethostbyname(Host);
-        F.write(Addr + "\n");
-      except:
-        pass
+    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 UDEmptyList, "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);
+    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");
@@ -912,13 +1006,15 @@ 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"]);
+                  ["gid","gidNumber","subGroup"]);
 
-# Generate the GroupMap and GroupIDMap
+# 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=*",\
@@ -929,10 +1025,14 @@ PasswdAttrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
                  "allowedHost","sshRSAAuthKey","dnsZoneEntry","cn","sn",\
                  "keyFingerPrint","privateSub","mailDisableMessage",\
                  "mailGreylisting","mailCallout","mailRBL","mailRHSBL",\
-                 "mailWhitelist", "sudoPassword"]);
+                 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus"])
+
+if PasswdAttrs is None:
+   raise UDEmptyList, "No Users"
+
 # Fetch all the hosts
 HostAttrs    = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"sshRSAHostKey=*",\
-                ["hostname","sshRSAHostKey"]);
+                ["hostname","sshRSAHostKey","purpose"]);
 
 # Open the control file
 if len(sys.argv) == 1:
@@ -942,7 +1042,15 @@ else:
 
 # Generate global things
 GlobalDir = GenerateDir+"/";
-SSHGlobal, SSHFiles = GenSSHShadow(l,GlobalDir+"ssh-rsa-shadow");
+GenMailDisable(l,GlobalDir+"mail-disable")
+
+for x in PasswdAttrs:
+   if IsRetired(x):
+      RetiredUsers.append(x)
+
+PasswdAttrs = filter(lambda x: not x in RetiredUsers, PasswdAttrs)
+
+SSHFiles = GenSSHShadow(l);
 GenAllForward(l,GlobalDir+"mail-forward.cdb");
 GenMarkers(l,GlobalDir+"markers");
 GenPrivate(l,GlobalDir+"debian-private");
@@ -950,7 +1058,6 @@ 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");
@@ -961,6 +1068,8 @@ 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 == "":
@@ -992,10 +1101,6 @@ while(1):
      Allowed = None
    CurrentHost = Split[0];
 
-   # If we're using a single SSH file, deal with it
-   if SSHGlobal is not None:
-      DoLink(GlobalDir, OutDir, SSHGlobal)
-
    DoLink(GlobalDir,OutDir,"debianhosts");
    DoLink(GlobalDir,OutDir,"ssh_known_hosts");
    DoLink(GlobalDir,OutDir,"disabled-accounts")
@@ -1011,53 +1116,7 @@ while(1):
 
    # Now we know who we're allowing on the machine, export
    # the relevant ssh keys
-   if MultipleSSHFiles:
-      OldMask = os.umask(0077);
-      tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
-      os.umask(OldMask);
-      for f in userlist.keys():
-        if f not in SSHFiles:
-            continue
-        # If we're not exporting their primary group, don't export
-        # the key and warn
-        grname = None
-        if userlist[f] in grouprevmap.keys():
-            grname = grouprevmap[userlist[f]]
-        else:
-            try:
-                if int(userlist[f]) <= 100:
-                    # In these cases, look it up in the normal way so we
-                    # deal with cases where, for instance, users are in group
-                    # users as their primary group.
-                    grname = grp.getgrgid(userlist[f])[0]
-            except Exception, e:
-                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])
-            continue
-
-        to = tf.gettarinfo(os.path.join(GlobalDir, '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
-        # be owned by root but group nobody.  This deals with
-        # the bloody obscure case where the group fails to exist
-        # whilst the user does (in which case we want to avoid
-        # ending up with a file which is owned user:root to avoid
-        # a fairly obvious attack vector)
-        to.uid = 0
-        to.gid = 65534
-        # Using the username / groupname fields avoids any need
-        # to give a shit^W^W^Wcare about the UIDoffset stuff.
-        to.uname = f
-        to.gname = grname
-        to.mode  = 0400
-        tf.addfile(to, file(os.path.join(GlobalDir, 'userkeys', f)))
-
-      tf.close()
-      os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost),
-                os.path.join(OutDir, 'ssh-keys.tar.gz'))
+   GenSSHtarballs(userlist, SSHFiles, 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."