also produce dbm files additionaly to cdb
[mirror/userdir-ldap.git] / ud-generate
1 #!/usr/bin/env python
2 # -*- mode: python -*-
3 # Generates passwd, shadow and group files from the ldap directory.
4
5 #   Copyright (c) 2000-2001  Jason Gunthorpe <jgg@debian.org>
6 #   Copyright (c) 2003-2004  James Troup <troup@debian.org>
7 #   Copyright (c) 2004-2005,7  Joey Schulze <joey@infodrom.org>
8 #   Copyright (c) 2001-2007  Ryan Murray <rmurray@debian.org>
9 #   Copyright (c) 2008,2009,2010,2011 Peter Palfrader <peter@palfrader.org>
10 #   Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 #   Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 #   Copyright (c) 2008 Luk Claes <luk@debian.org>
13 #   Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 #   Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 #   Copyright (c) 2010 Helmut Grohne <helmut@subdivi.de>
16 #
17 #   This program is free software; you can redistribute it and/or modify
18 #   it under the terms of the GNU General Public License as published by
19 #   the Free Software Foundation; either version 2 of the License, or
20 #   (at your option) any later version.
21 #
22 #   This program is distributed in the hope that it will be useful,
23 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
24 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25 #   GNU General Public License for more details.
26 #
27 #   You should have received a copy of the GNU General Public License
28 #   along with this program; if not, write to the Free Software
29 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
30
31 import string, re, time, ldap, optparse, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp, fcntl, dbm
32 from userdir_ldap import *
33 from userdir_exceptions import *
34 import UDLdap
35 from xml.etree.ElementTree import Element, SubElement, Comment
36 from xml.etree import ElementTree
37 from xml.dom import minidom
38 try:
39    from cStringIO import StringIO
40 except ImportError:
41    from StringIO import StringIO
42 try:
43    import simplejson as json
44 except ImportError:
45    import json
46    if not '__author__' in json.__dict__:
47       sys.stderr.write("Warning: This is probably the wrong json module.  We want python 2.6's json\n")
48       sys.stderr.write("module, or simplejson on pytyon 2.5.  Let's see if/how stuff blows up.\n")
49
50 if os.getuid() == 0:
51    sys.stderr.write("You should probably not run ud-generate as root.\n")
52    sys.exit(1)
53
54
55 #
56 # GLOBAL STATE
57 #
58 GroupIDMap = None
59 SubGroupMap = None
60
61
62
63 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
64 MAX_UD_AGE = 3600*24
65
66 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)(,\s*([^ <>@]+@[^ ,<>@]+))*$")
67 BSMTPCheck = re.compile(".*mx 0 (master)\.debian\.org\..*",re.DOTALL)
68 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
69 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
70 isSSHFP = re.compile("^\s*IN\s+SSHFP")
71 DNSZone = ".debian.net"
72 Keyrings = ConfModule.sync_keyrings.split(":")
73 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
74 GitoliteExportHosts = re.compile(getattr(ConfModule, "gitoliteexporthosts", "."))
75 MX_remap = json.loads(ConfModule.MX_remap)
76
77 def prettify(elem):
78    """Return a pretty-printed XML string for the Element.
79    """
80    rough_string = ElementTree.tostring(elem, 'utf-8')
81    reparsed = minidom.parseString(rough_string)
82    return reparsed.toprettyxml(indent="  ")
83
84 def safe_makedirs(dir):
85    try:
86       os.makedirs(dir)
87    except OSError, e:
88       if e.errno == errno.EEXIST:
89          pass
90       else:
91          raise e
92
93 def safe_rmtree(dir):
94    try:
95       shutil.rmtree(dir)
96    except OSError, e:
97       if e.errno == errno.ENOENT:
98          pass
99       else:
100          raise e
101
102 def get_lock(fn, wait=5*60):
103    f = open(fn, "w")
104    sl = 0.1
105    ends = time.time() + wait
106
107    while True:
108       success = False
109       try:
110          fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
111          return f
112       except IOError:
113          pass
114       if time.time() >= ends:
115          return None
116       sl = min(sl*2, 10, ends - time.time())
117       time.sleep(sl)
118    return None
119
120
121 def Sanitize(Str):
122    return Str.translate(string.maketrans("\n\r\t", "$$$"))
123
124 def DoLink(From, To, File):
125    try: 
126       posix.remove(To + File)
127    except: 
128       pass
129    posix.link(From + File, To + File)
130
131 def IsRetired(account):
132    """
133    Looks for accountStatus in the LDAP record and tries to
134    match it against one of the known retired statuses
135    """
136
137    status = account['accountStatus']
138
139    line = status.split()
140    status = line[0]
141
142    if status == "inactive":
143       return True
144
145    elif status == "memorial":
146       return True
147
148    elif status == "retiring":
149       # We'll give them a few extra days over what we said
150       age = 6 * 31 * 24 * 60 * 60
151       try:
152          return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
153       except IndexError:
154          return False
155       except ValueError:
156          return False
157
158    return False
159
160 #def IsGidDebian(account):
161 #   return account['gidNumber'] == 800
162
163 # See if this user is in the group list
164 def IsInGroup(account, allowed, current_host):
165   # See if the primary group is in the list
166   if str(account['gidNumber']) in allowed: return True
167
168   # Check the host based ACL
169   if account.is_allowed_by_hostacl(current_host): return True
170
171   # See if there are supplementary groups
172   if not 'supplementaryGid' in account: return False
173
174   supgroups=[]
175   addGroups(supgroups, account['supplementaryGid'], account['uid'], current_host)
176   for g in supgroups:
177      if g in allowed:
178         return True
179   return False
180
181 def Die(File, F, Fdb):
182    if F != None:
183       F.close()
184    if Fdb != None:
185       Fdb.close()
186    try: 
187       os.remove(File + ".tmp")
188    except:
189       pass
190    try: 
191       os.remove(File + ".tdb.tmp")
192    except: 
193       pass
194
195 def Done(File, F, Fdb):
196    if F != None:
197       F.close()
198       os.rename(File + ".tmp", File)
199    if Fdb != None:
200       Fdb.close()
201       os.rename(File + ".tdb.tmp", File + ".tdb")
202
203 # Generate the password list
204 def GenPasswd(accounts, File, HomePrefix, PwdMarker):
205    F = None
206    try:
207       F = open(File + ".tdb.tmp", "w")
208
209       userlist = {}
210       i = 0
211       for a in accounts:
212          # Do not let people try to buffer overflow some busted passwd parser.
213          if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
214
215          userlist[a['uid']] = a['gidNumber']
216          line = "%s:%s:%d:%d:%s:%s%s:%s" % (
217                  a['uid'],
218                  PwdMarker,
219                  a['uidNumber'],
220                  a['gidNumber'],
221                  a['gecos'],
222                  HomePrefix, a['uid'],
223                  a['loginShell'])
224          line = Sanitize(line) + "\n"
225          F.write("0%u %s" % (i, line))
226          F.write(".%s %s" % (a['uid'], line))
227          F.write("=%d %s" % (a['uidNumber'], line))
228          i = i + 1
229
230    # Oops, something unspeakable happened.
231    except:
232       Die(File, None, F)
233       raise
234    Done(File, None, F)
235
236    # Return the list of users so we know which keys to export
237    return userlist
238
239 def GenAllUsers(accounts, file):
240    f = None
241    try:
242       OldMask = os.umask(0022)
243       f = open(file + ".tmp", "w", 0644)
244       os.umask(OldMask)
245
246       all = []
247       for a in accounts:
248          all.append( { 'uid': a['uid'],
249                        'uidNumber': a['uidNumber'],
250                        'active': a.pw_active() and a.shadow_active() } )
251       json.dump(all, f)
252
253    # Oops, something unspeakable happened.
254    except:
255       Die(file, f, None)
256       raise
257    Done(file, f, None)
258
259 # Generate the shadow list
260 def GenShadow(accounts, File):
261    F = None
262    try:
263       OldMask = os.umask(0077)
264       F = open(File + ".tdb.tmp", "w", 0600)
265       os.umask(OldMask)
266
267       i = 0
268       for a in accounts:
269          # If the account is locked, mark it as such in shadow
270          # See Debian Bug #308229 for why we set it to 1 instead of 0
271          if not a.pw_active():     ShadowExpire = '1'
272          elif 'shadowExpire' in a: ShadowExpire = str(a['shadowExpire'])
273          else:                     ShadowExpire = ''
274
275          values = []
276          values.append(a['uid'])
277          values.append(a.get_password())
278          for key in 'shadowLastChange', 'shadowMin', 'shadowMax', 'shadowWarning', 'shadowInactive':
279             if key in a: values.append(a[key])
280             else:        values.append('')
281          values.append(ShadowExpire)
282          line = ':'.join(values)+':'
283          line = Sanitize(line) + "\n"
284          F.write("0%u %s" % (i, line))
285          F.write(".%s %s" % (a['uid'], line))
286          i = i + 1
287
288    # Oops, something unspeakable happened.
289    except:
290       Die(File, None, F)
291       raise
292    Done(File, None, F)
293
294 # Generate the sudo passwd file
295 def GenShadowSudo(accounts, File, untrusted, current_host):
296    F = None
297    try:
298       OldMask = os.umask(0077)
299       F = open(File + ".tmp", "w", 0600)
300       os.umask(OldMask)
301
302       for a in accounts:
303          Pass = '*'
304          if 'sudoPassword' in a:
305             for entry in a['sudoPassword']:
306                Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
307                if Match == None:
308                   continue
309                uuid = Match.group(1)
310                status = Match.group(2)
311                hosts = Match.group(3)
312                cryptedpass = Match.group(4)
313      
314                if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', a['uid'], uuid, hosts, cryptedpass):
315                   continue
316                for_all = hosts == "*"
317                for_this_host = current_host in hosts.split(',')
318                if not (for_all or for_this_host):
319                   continue
320                # ignore * passwords for untrusted hosts, but copy host specific passwords
321                if for_all and untrusted:
322                   continue
323                Pass = cryptedpass
324                if for_this_host: # this makes sure we take a per-host entry over the for-all entry
325                   break
326             if len(Pass) > 50:
327                Pass = '*'
328      
329          Line = "%s:%s" % (a['uid'], Pass)
330          Line = Sanitize(Line) + "\n"
331          F.write("%s" % (Line))
332   
333    # Oops, something unspeakable happened.
334    except:
335       Die(File, F, None)
336       raise
337    Done(File, F, None)
338
339 # Generate the sudo passwd file
340 def GenSSHGitolite(accounts, hosts, File):
341    F = None
342    try:
343       OldMask = os.umask(0022)
344       F = open(File + ".tmp", "w", 0600)
345       os.umask(OldMask)
346
347       if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
348          for a in accounts:
349             if not 'sshRSAAuthKey' in a: continue
350
351             User = a['uid']
352             prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
353             for I in a["sshRSAAuthKey"]:
354                if I.startswith('ssh-'):
355                   line = "%s %s"%(prefix, I)
356                else:
357                   line = "%s,%s"%(prefix, I)
358                line = Sanitize(line) + "\n"
359                F.write(line)
360
361          for dn, attrs in hosts:
362             if not 'sshRSAHostKey' in attrs: continue
363             hostname = "host-" + attrs['hostname'][0]
364             prefix = GitoliteSSHRestrictions.replace('@@USER@@', hostname)
365             for I in attrs["sshRSAHostKey"]:
366                line = "%s %s"%(prefix, I)
367                line = Sanitize(line) + "\n"
368                F.write(line)
369
370    # Oops, something unspeakable happened.
371    except:
372       Die(File, F, None)
373       raise
374    Done(File, F, None)
375
376 # Generate the shadow list
377 def GenSSHShadow(global_dir, accounts):
378    # Fetch all the users
379    userkeys = {}
380
381    for a in accounts:
382       if not 'sshRSAAuthKey' in a: continue
383
384       contents = []
385       for I in a['sshRSAAuthKey']:
386          MultipleLine = "%s" % I
387          MultipleLine = Sanitize(MultipleLine)
388          contents.append(MultipleLine)
389       userkeys[a['uid']] = contents
390    return userkeys
391
392 # Generate the webPassword list
393 def GenWebPassword(accounts, File):
394    F = None
395    try:
396       OldMask = os.umask(0077)
397       F = open(File, "w", 0600)
398       os.umask(OldMask)
399
400       for a in accounts:
401          if not 'webPassword' in a: continue
402          if not a.pw_active(): continue
403
404          Pass = str(a['webPassword'])
405          Line = "%s:%s" % (a['uid'], Pass)
406          Line = Sanitize(Line) + "\n"
407          F.write("%s" % (Line))
408
409    except:
410       Die(File, None, F)
411       raise
412
413 # Generate the voipPassword list
414 def GenVoipPassword(accounts, File):
415    F = None
416    try:
417       OldMask = os.umask(0077)
418       F = open(File, "w", 0600)
419       os.umask(OldMask)
420
421       root = Element('include')
422
423       for a in accounts:
424          if not 'voipPassword' in a: continue
425          if not a.pw_active(): continue
426
427          Pass = str(a['voipPassword'])
428          user = Element('user')
429          user.attrib['id'] = "%s" % (a['uid'])
430          root.append(user)
431          params = Element('params')
432          user.append(params)
433          param = Element('param')
434          params.append(param)
435          param.attrib['name'] = "a1-hash"
436          param.attrib['value'] = "%s" % (Pass)
437          variables = Element('variables')
438          user.append(variables)
439          variable = Element('variable')
440          variable.attrib['name'] = "toll_allow"
441          variable.attrib['value'] = "domestic,international,local"
442          variables.append(variable)
443
444       F.write("%s" % (prettify(root)))
445
446
447    except:
448       Die(File, None, F)
449       raise
450
451 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
452    OldMask = os.umask(0077)
453    tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
454    os.umask(OldMask)
455    for f in userlist:
456       if f not in ssh_userkeys:
457          continue
458       # If we're not exporting their primary group, don't export
459       # the key and warn
460       grname = None
461       if userlist[f] in grouprevmap.keys():
462          grname = grouprevmap[userlist[f]]
463       else:
464          try:
465             if int(userlist[f]) <= 100:
466                # In these cases, look it up in the normal way so we
467                # deal with cases where, for instance, users are in group
468                # users as their primary group.
469                grname = grp.getgrgid(userlist[f])[0]
470          except Exception, e:
471             pass
472
473       if grname is None:
474          print "User %s is supposed to have their key exported to host %s but their primary group (gid: %d) isn't in LDAP" % (f, current_host, userlist[f])
475          continue
476
477       lines = []
478       for line in ssh_userkeys[f]:
479          if line.startswith("allowed_hosts=") and ' ' in line:
480             machines, line = line.split('=', 1)[1].split(' ', 1)
481             if current_host not in machines.split(','):
482                continue # skip this key
483          lines.append(line)
484       if not lines:
485          continue # no keys for this host
486       contents = "\n".join(lines) + "\n"
487
488       to = tarfile.TarInfo(name=f)
489       # These will only be used where the username doesn't
490       # exist on the target system for some reason; hence,
491       # in those cases, the safest thing is for the file to
492       # be owned by root but group nobody.  This deals with
493       # the bloody obscure case where the group fails to exist
494       # whilst the user does (in which case we want to avoid
495       # ending up with a file which is owned user:root to avoid
496       # a fairly obvious attack vector)
497       to.uid = 0
498       to.gid = 65534
499       # Using the username / groupname fields avoids any need
500       # to give a shit^W^W^Wcare about the UIDoffset stuff.
501       to.uname = f
502       to.gname = grname
503       to.mode  = 0400
504       to.mtime = int(time.time())
505       to.size = len(contents)
506
507       tf.addfile(to, StringIO(contents))
508
509    tf.close()
510    os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
511
512 # add a list of groups to existing groups,
513 # including all subgroups thereof, recursively.
514 # basically this proceduces the transitive hull of the groups in
515 # addgroups.
516 def addGroups(existingGroups, newGroups, uid, current_host):
517    for group in newGroups:
518       # if it's a <group>@host, split it and verify it's on the current host.
519       s = group.split('@', 1)
520       if len(s) == 2 and s[1] != current_host:
521          continue
522       group = s[0]
523
524       # let's see if we handled this group already
525       if group in existingGroups:
526          continue
527
528       if not GroupIDMap.has_key(group):
529          print "Group", group, "does not exist but", uid, "is in it"
530          continue
531
532       existingGroups.append(group)
533
534       if SubGroupMap.has_key(group):
535          addGroups(existingGroups, SubGroupMap[group], uid, current_host)
536
537 # Generate the group list
538 def GenGroup(accounts, File, current_host):
539    grouprevmap = {}
540    F = None
541    try:
542       F = open(File + ".tdb.tmp", "w")
543      
544       # Generate the GroupMap
545       GroupMap = {}
546       for x in GroupIDMap:
547          GroupMap[x] = []
548       GroupHasPrimaryMembers = {}
549
550       # Sort them into a list of groups having a set of users
551       for a in accounts:
552          GroupHasPrimaryMembers[ a['gidNumber'] ] = True
553          if not 'supplementaryGid' in a: continue
554
555          supgroups=[]
556          addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
557          for g in supgroups:
558             GroupMap[g].append(a['uid'])
559
560       # Output the group file.
561       J = 0
562       for x in GroupMap.keys():
563          if not x in GroupIDMap:
564             continue
565
566          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
567             continue
568
569          grouprevmap[GroupIDMap[x]] = x
570
571          Line = "%s:x:%u:" % (x, GroupIDMap[x])
572          Comma = ''
573          for I in GroupMap[x]:
574             Line = Line + ("%s%s" % (Comma, I))
575             Comma = ','
576          Line = Sanitize(Line) + "\n"
577          F.write("0%u %s" % (J, Line))
578          F.write(".%s %s" % (x, Line))
579          F.write("=%u %s" % (GroupIDMap[x], Line))
580          J = J + 1
581   
582    # Oops, something unspeakable happened.
583    except:
584       Die(File, None, F)
585       raise
586    Done(File, None, F)
587   
588    return grouprevmap
589
590 def CheckForward(accounts):
591    for a in accounts:
592       if not 'emailForward' in a: continue
593
594       delete = False
595
596       # Do not allow people to try to buffer overflow busted parsers
597       if len(a['emailForward']) > 200: delete = True
598       # Check the forwarding address
599       elif EmailCheck.match(a['emailForward']) is None: delete = True
600
601       if delete:
602          a.delete_mailforward()
603
604 # Generate the email forwarding list
605 def GenForward(accounts, File):
606    F = None
607    try:
608       OldMask = os.umask(0022)
609       F = open(File + ".tmp", "w", 0644)
610       os.umask(OldMask)
611
612       for a in accounts:
613          if not 'emailForward' in a: continue
614          Line = "%s: %s" % (a['uid'], a['emailForward'])
615          Line = Sanitize(Line) + "\n"
616          F.write(Line)
617
618    # Oops, something unspeakable happened.
619    except:
620       Die(File, F, None)
621       raise
622    Done(File, F, None)
623
624 def GenCDB(accounts, File, key):
625    Fdb = None
626    try:
627       OldMask = os.umask(0022)
628       # nothing else does the fsync stuff, so why do it here?
629       prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
630       Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
631       os.umask(OldMask)
632
633       # Write out the email address for each user
634       for a in accounts:
635          if not key in a: continue
636          value = a[key]
637          user = a['uid']
638          Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
639
640       Fdb.write("\n")
641    # Oops, something unspeakable happened.
642    except:
643       Fdb.close()
644       raise
645    if Fdb.close() != None:
646       raise "cdbmake gave an error"
647
648 def GenDBM(accounts, File, key):
649    Fdb = None
650    try:
651       OldMask = os.umask(0022)
652       # nothing else does the fsync stuff, so why do it here?
653       fn = os.path.join(prefix,File).encode('ascii', 'ignore')
654       fntmp = os.path.join(prefix,File + '.tmp').encode('ascii', 'ignore')
655       try:
656           posix.remove(fntmp)
657       except:
658           pass
659
660       Fdb = dbm.open(fntmp, "c")
661       os.umask(OldMask)
662
663       # Write out the email address for each user
664       for a in accounts:
665          if not key in a: continue
666          value = a[key]
667          user = a['uid']
668          Fdb[user] = value
669
670       Fdb.close()
671       posix.rename(fntmp,fn)
672    # Oops, something unspeakable happened.
673    except:
674       Fdb.close()
675       raise
676
677 # Generate the anon XEarth marker file
678 def GenMarkers(accounts, File):
679    F = None
680    try:
681       F = open(File + ".tmp", "w")
682
683       # Write out the position for each user
684       for a in accounts:
685          if not ('latitude' in a and 'longitude' in a): continue
686          try:
687             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
688             Line = Sanitize(Line) + "\n"
689             F.write(Line)
690          except:
691             pass
692   
693    # Oops, something unspeakable happened.
694    except:
695       Die(File, F, None)
696       raise
697    Done(File, F, None)
698
699 # Generate the debian-private subscription list
700 def GenPrivate(accounts, File):
701    F = None
702    try:
703       F = open(File + ".tmp", "w")
704
705       # Write out the position for each user
706       for a in accounts:
707          if not a.is_active_user(): continue
708          if a.is_guest_account(): continue
709          if not 'privateSub' in a: continue
710          try:
711             Line = "%s"%(a['privateSub'])
712             Line = Sanitize(Line) + "\n"
713             F.write(Line)
714          except:
715             pass
716   
717    # Oops, something unspeakable happened.
718    except:
719       Die(File, F, None)
720       raise
721    Done(File, F, None)
722
723 # Generate a list of locked accounts
724 def GenDisabledAccounts(accounts, File):
725    F = None
726    try:
727       F = open(File + ".tmp", "w")
728       disabled_accounts = []
729
730       # Fetch all the users
731       for a in accounts:
732          if a.pw_active(): continue
733          Line = "%s:%s" % (a['uid'], "Account is locked")
734          disabled_accounts.append(a)
735          F.write(Sanitize(Line) + "\n")
736
737    # Oops, something unspeakable happened.
738    except:
739       Die(File, F, None)
740       raise
741    Done(File, F, None)
742    return disabled_accounts
743
744 # Generate the list of local addresses that refuse all mail
745 def GenMailDisable(accounts, File):
746    F = None
747    try:
748       F = open(File + ".tmp", "w")
749
750       for a in accounts:
751          if not 'mailDisableMessage' in a: continue
752          Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
753          Line = Sanitize(Line) + "\n"
754          F.write(Line)
755
756    # Oops, something unspeakable happened.
757    except:
758       Die(File, F, None)
759       raise
760    Done(File, F, None)
761
762 # Generate a list of uids that should have boolean affects applied
763 def GenMailBool(accounts, File, key):
764    F = None
765    try:
766       F = open(File + ".tmp", "w")
767
768       for a in accounts:
769          if not key in a: continue
770          if not a[key] == 'TRUE': continue
771          Line = "%s"%(a['uid'])
772          Line = Sanitize(Line) + "\n"
773          F.write(Line)
774
775    # Oops, something unspeakable happened.
776    except:
777       Die(File, F, None)
778       raise
779    Done(File, F, None)
780
781 # Generate a list of hosts for RBL or whitelist purposes.
782 def GenMailList(accounts, File, key):
783    F = None
784    try:
785       F = open(File + ".tmp", "w")
786
787       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
788       else:                      validregex = re.compile('^[-\w.]+$')
789
790       for a in accounts:
791          if not key in a: continue
792
793          filtered = filter(lambda z: validregex.match(z), a[key])
794          if len(filtered) == 0: continue
795          if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
796          line = a['uid'] + ': ' + ' : '.join(filtered)
797          line = Sanitize(line) + "\n"
798          F.write(line)
799
800    # Oops, something unspeakable happened.
801    except:
802       Die(File, F, None)
803       raise
804    Done(File, F, None)
805
806 def isRoleAccount(account):
807    return 'debianRoleAccount' in account['objectClass']
808
809 # Generate the DNS Zone file
810 def GenDNS(accounts, File):
811    F = None
812    try:
813       F = open(File + ".tmp", "w")
814
815       # Fetch all the users
816       RRs = {}
817
818       # Write out the zone file entry for each user
819       for a in accounts:
820          if not 'dnsZoneEntry' in a: continue
821          if not a.is_active_user() and not isRoleAccount(a): continue
822          if a.is_guest_account(): continue
823
824          try:
825             F.write("; %s\n"%(a.email_address()))
826             for z in a["dnsZoneEntry"]:
827                Split = z.lower().split()
828                if Split[1].lower() == 'in':
829                   Line = " ".join(Split) + "\n"
830                   F.write(Line)
831
832                   Host = Split[0] + DNSZone
833                   if BSMTPCheck.match(Line) != None:
834                      F.write("; Has BSMTP\n")
835
836                   # Write some identification information
837                   if not RRs.has_key(Host):
838                      if Split[2].lower() in ["a", "aaaa"]:
839                         Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
840                         for y in a["keyFingerPrint"]:
841                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
842                            F.write(Line)
843                         RRs[Host] = 1
844                else:
845                   Line = "; Err %s"%(str(Split))
846                   F.write(Line)
847
848             F.write("\n")
849          except Exception, e:
850             F.write("; Errors:\n")
851             for line in str(e).split("\n"):
852                F.write("; %s\n"%(line))
853             pass
854   
855    # Oops, something unspeakable happened.
856    except:
857       Die(File, F, None)
858       raise
859    Done(File, F, None)
860
861 def is_ipv6_addr(i):
862    try:
863       socket.inet_pton(socket.AF_INET6, i)
864    except socket.error:
865       return False
866    return True
867
868 def ExtractDNSInfo(x):
869
870    TTLprefix="\t"
871    if 'dnsTTL' in x[1]:
872       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
873
874    DNSInfo = []
875    if x[1].has_key("ipHostNumber"):
876       for I in x[1]["ipHostNumber"]:
877          if is_ipv6_addr(I):
878             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
879          else:
880             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
881
882    Algorithm = None
883
884    if 'sshRSAHostKey' in x[1]:
885       for I in x[1]["sshRSAHostKey"]:
886          Split = I.split()
887          if Split[0] == 'ssh-rsa':
888             Algorithm = 1
889          if Split[0] == 'ssh-dss':
890             Algorithm = 2
891          if Algorithm == None:
892             continue
893          Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
894          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
895
896    if 'architecture' in x[1]:
897       Arch = GetAttr(x, "architecture")
898       Mach = ""
899       if x[1].has_key("machine"):
900          Mach = " " + GetAttr(x, "machine")
901       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
902
903    if x[1].has_key("mXRecord"):
904       for I in x[1]["mXRecord"]:
905          if I in MX_remap:
906             for e in MX_remap[I]:
907                DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
908          else:
909             DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
910
911    return DNSInfo
912
913 # Generate the DNS records
914 def GenZoneRecords(host_attrs, File):
915    F = None
916    try:
917       F = open(File + ".tmp", "w")
918
919       # Fetch all the hosts
920       for x in host_attrs:
921          if x[1].has_key("hostname") == 0:
922             continue
923
924          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
925             continue
926
927          DNSInfo = ExtractDNSInfo(x)
928          start = True
929          for Line in DNSInfo:
930             if start == True:
931                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
932                start = False
933             else:
934                Line = "\t\t\t%s" % (Line)
935
936             F.write(Line + "\n")
937
938         # this would write sshfp lines for services on machines
939         # but we can't yet, since some are cnames and we'll make
940         # an invalid zonefile
941         #
942         # for i in x[1].get("purpose", []):
943         #    m = PurposeHostField.match(i)
944         #    if m:
945         #       m = m.group(1)
946         #       # we ignore [[*..]] entries
947         #       if m.startswith('*'):
948         #          continue
949         #       if m.startswith('-'):
950         #          m = m[1:]
951         #       if m:
952         #          if not m.endswith(HostDomain):
953         #             continue
954         #          if not m.endswith('.'):
955         #             m = m + "."
956         #          for Line in DNSInfo:
957         #             if isSSHFP.match(Line):
958         #                Line = "%s\t%s" % (m, Line)
959         #                F.write(Line + "\n")
960
961    # Oops, something unspeakable happened.
962    except:
963       Die(File, F, None)
964       raise
965    Done(File, F, None)
966
967 # Generate the BSMTP file
968 def GenBSMTP(accounts, File, HomePrefix):
969    F = None
970    try:
971       F = open(File + ".tmp", "w")
972      
973       # Write out the zone file entry for each user
974       for a in accounts:
975          if not 'dnsZoneEntry' in a: continue
976          if not a.is_active_user(): continue
977
978          try:
979             for z in a["dnsZoneEntry"]:
980                Split = z.lower().split()
981                if Split[1].lower() == 'in':
982                   for y in range(0, len(Split)):
983                      if Split[y] == "$":
984                         Split[y] = "\n\t"
985                   Line = " ".join(Split) + "\n"
986      
987                   Host = Split[0] + DNSZone
988                   if BSMTPCheck.match(Line) != None:
989                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
990                                   a['uid'], HomePrefix, a['uid'], Host))
991      
992          except:
993             F.write("; Errors\n")
994             pass
995   
996    # Oops, something unspeakable happened.
997    except:
998       Die(File, F, None)
999       raise
1000    Done(File, F, None)
1001   
1002 def HostToIP(Host, mapped=True):
1003
1004    IPAdresses = []
1005
1006    if Host[1].has_key("ipHostNumber"):
1007       for addr in Host[1]["ipHostNumber"]:
1008          IPAdresses.append(addr)
1009          if not is_ipv6_addr(addr) and mapped == "True":
1010             IPAdresses.append("::ffff:"+addr)
1011
1012    return IPAdresses
1013
1014 # Generate the ssh known hosts file
1015 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1016    F = None
1017    try:
1018       OldMask = os.umask(0022)
1019       F = open(File + ".tmp", "w", 0644)
1020       os.umask(OldMask)
1021      
1022       for x in host_attrs:
1023          if x[1].has_key("hostname") == 0 or \
1024             x[1].has_key("sshRSAHostKey") == 0:
1025             continue
1026          Host = GetAttr(x, "hostname")
1027          HostNames = [ Host ]
1028          if Host.endswith(HostDomain):
1029             HostNames.append(Host[:-(len(HostDomain) + 1)])
1030      
1031          # in the purpose field [[host|some other text]] (where some other text is optional)
1032          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1033          # file.  But so that we don't have to add everything we link we can add an asterisk
1034          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
1035          # http linking it we also support [[-hostname]] entries.
1036          for i in x[1].get("purpose", []):
1037             m = PurposeHostField.match(i)
1038             if m:
1039                m = m.group(1)
1040                # we ignore [[*..]] entries
1041                if m.startswith('*'):
1042                   continue
1043                if m.startswith('-'):
1044                   m = m[1:]
1045                if m:
1046                   HostNames.append(m)
1047                   if m.endswith(HostDomain):
1048                      HostNames.append(m[:-(len(HostDomain) + 1)])
1049      
1050          for I in x[1]["sshRSAHostKey"]:
1051             if mode and mode == 'authorized_keys':
1052                hosts = HostToIP(x)
1053                if 'sshdistAuthKeysHost' in x[1]:
1054                   hosts += x[1]['sshdistAuthKeysHost']
1055                clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1056                clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1057                Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1058             else:
1059                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1060             Line = Sanitize(Line) + "\n"
1061             F.write(Line)
1062    # Oops, something unspeakable happened.
1063    except:
1064       Die(File, F, None)
1065       raise
1066    Done(File, F, None)
1067
1068 # Generate the debianhosts file (list of all IP addresses)
1069 def GenHosts(host_attrs, File):
1070    F = None
1071    try:
1072       OldMask = os.umask(0022)
1073       F = open(File + ".tmp", "w", 0644)
1074       os.umask(OldMask)
1075      
1076       seen = set()
1077
1078       for x in host_attrs:
1079
1080          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1081             continue
1082
1083          if not 'ipHostNumber' in x[1]:
1084             continue
1085
1086          addrs = x[1]["ipHostNumber"]
1087          for addr in addrs:
1088             if addr not in seen:
1089                seen.add(addr)
1090                addr = Sanitize(addr) + "\n"
1091                F.write(addr)
1092
1093    # Oops, something unspeakable happened.
1094    except:
1095       Die(File, F, None)
1096       raise
1097    Done(File, F, None)
1098
1099 def replaceTree(src, dst_basedir):
1100    bn = os.path.basename(src)
1101    dst = os.path.join(dst_basedir, bn)
1102    safe_rmtree(dst)
1103    shutil.copytree(src, dst)
1104
1105 def GenKeyrings(OutDir):
1106    for k in Keyrings:
1107       if os.path.isdir(k):
1108          replaceTree(k, OutDir)
1109       else:
1110          shutil.copy(k, OutDir)
1111
1112
1113 def get_accounts(ldap_conn):
1114    # Fetch all the users
1115    passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1116                    ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1117                     "gecos", "loginShell", "userPassword", "shadowLastChange",\
1118                     "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1119                     "shadowExpire", "emailForward", "latitude", "longitude",\
1120                     "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1121                     "keyFingerPrint", "privateSub", "mailDisableMessage",\
1122                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1123                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1124                     "mailContentInspectionAction", "webPassword", "voipPassword"])
1125
1126    if passwd_attrs is None:
1127       raise UDEmptyList, "No Users"
1128    accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1129    accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1130
1131    return accounts
1132
1133 def get_hosts(ldap_conn):
1134    # Fetch all the hosts
1135    HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1136                    ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1137                     "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1138
1139    if HostAttrs == None:
1140       raise UDEmptyList, "No Hosts"
1141
1142    HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1143
1144    return HostAttrs
1145
1146
1147 def make_ldap_conn():
1148    # Connect to the ldap server
1149    l = connectLDAP()
1150    # for testing purposes it's sometimes useful to pass username/password
1151    # via the environment
1152    if 'UD_CREDENTIALS' in os.environ:
1153       Pass = os.environ['UD_CREDENTIALS'].split()
1154    else:
1155       F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1156       Pass = F.readline().strip().split(" ")
1157       F.close()
1158    l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1159
1160    return l
1161
1162
1163
1164 def setup_group_maps(l):
1165    # Fetch all the groups
1166    group_id_map = {}
1167    subgroup_map = {}
1168    attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1169                      ["gid", "gidNumber", "subGroup"])
1170
1171    # Generate the subgroup_map and group_id_map
1172    for x in attrs:
1173       if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1174          continue
1175       if x[1].has_key("gidNumber") == 0:
1176          continue
1177       group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1178       if x[1].has_key("subGroup") != 0:
1179          subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1180
1181    global SubGroupMap
1182    global GroupIDMap
1183    SubGroupMap = subgroup_map
1184    GroupIDMap = group_id_map
1185
1186 def generate_all(global_dir, ldap_conn):
1187    accounts = get_accounts(ldap_conn)
1188    host_attrs = get_hosts(ldap_conn)
1189
1190    global_dir += '/'
1191    # Generate global things
1192    accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1193
1194    accounts = filter(lambda x: not IsRetired(x), accounts)
1195    #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1196
1197    CheckForward(accounts)
1198
1199    GenMailDisable(accounts, global_dir + "mail-disable")
1200    GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1201    GenDBM(accounts, global_dir + "mail-forward.dbm", 'emailForward')
1202    GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1203    GenDBM(accounts, global_dir + "mail-contentinspectionaction.dbm", 'mailContentInspectionAction')
1204    GenPrivate(accounts, global_dir + "debian-private")
1205    GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1206    GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1207    GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1208    GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1209    GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1210    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1211    GenWebPassword(accounts, global_dir + "web-passwords")
1212    GenVoipPassword(accounts, global_dir + "voip-passwords")
1213    GenKeyrings(global_dir)
1214
1215    # Compatibility.
1216    GenForward(accounts, global_dir + "forward-alias")
1217
1218    GenAllUsers(accounts, global_dir + 'all-accounts.json')
1219    accounts = filter(lambda a: not a in accounts_disabled, accounts)
1220
1221    ssh_userkeys = GenSSHShadow(global_dir, accounts)
1222    GenMarkers(accounts, global_dir + "markers")
1223    GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1224    GenHosts(host_attrs, global_dir + "debianhosts")
1225    GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
1226
1227    GenDNS(accounts, global_dir + "dns-zone")
1228    GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1229
1230    setup_group_maps(ldap_conn)
1231
1232    for host in host_attrs:
1233       if not "hostname" in host[1]:
1234          continue
1235       generate_host(host, global_dir, accounts, host_attrs, ssh_userkeys)
1236
1237 def generate_host(host, global_dir, all_accounts, all_hosts, ssh_userkeys):
1238    current_host = host[1]['hostname'][0]
1239    OutDir = global_dir + current_host + '/'
1240    if not os.path.isdir(OutDir):
1241       os.mkdir(OutDir)
1242
1243    # Get the group list and convert any named groups to numerics
1244    GroupList = {}
1245    for groupname in AllowedGroupsPreload.strip().split(" "):
1246       GroupList[groupname] = True
1247    if 'allowedGroups' in host[1]:
1248       for groupname in host[1]['allowedGroups']:
1249          GroupList[groupname] = True
1250    for groupname in GroupList.keys():
1251       if groupname in GroupIDMap:
1252          GroupList[str(GroupIDMap[groupname])] = True
1253
1254    ExtraList = {}
1255    if 'exportOptions' in host[1]:
1256       for extra in host[1]['exportOptions']:
1257          ExtraList[extra.upper()] = True
1258
1259    if GroupList != {}:
1260       accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1261
1262    DoLink(global_dir, OutDir, "debianhosts")
1263    DoLink(global_dir, OutDir, "ssh_known_hosts")
1264    DoLink(global_dir, OutDir, "disabled-accounts")
1265
1266    sys.stdout.flush()
1267    if 'NOPASSWD' in ExtraList:
1268       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1269    else:
1270       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1271    sys.stdout.flush()
1272    grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1273    GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1274
1275    # Now we know who we're allowing on the machine, export
1276    # the relevant ssh keys
1277    GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1278
1279    if not 'NOPASSWD' in ExtraList:
1280       GenShadow(accounts, OutDir + "shadow")
1281
1282    # Link in global things
1283    if not 'NOMARKERS' in ExtraList:
1284       DoLink(global_dir, OutDir, "markers")
1285    DoLink(global_dir, OutDir, "mail-forward.cdb")
1286    DoLink(global_dir, OutDir, "mail-forward.dbm")
1287    DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1288    DoLink(global_dir, OutDir, "mail-contentinspectionaction.dbm")
1289    DoLink(global_dir, OutDir, "mail-disable")
1290    DoLink(global_dir, OutDir, "mail-greylist")
1291    DoLink(global_dir, OutDir, "mail-callout")
1292    DoLink(global_dir, OutDir, "mail-rbl")
1293    DoLink(global_dir, OutDir, "mail-rhsbl")
1294    DoLink(global_dir, OutDir, "mail-whitelist")
1295    DoLink(global_dir, OutDir, "all-accounts.json")
1296    GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1297    GenDBM(accounts, OutDir + "user-forward.dbm", 'emailForward')
1298    GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1299    GenDBM(accounts, OutDir + "batv-tokens.dbm", 'bATVToken')
1300    GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1301    GenDBM(accounts, OutDir + "default-mail-options.dbm", 'mailDefaultOptions')
1302
1303    # Compatibility.
1304    DoLink(global_dir, OutDir, "forward-alias")
1305
1306    if 'DNS' in ExtraList:
1307       DoLink(global_dir, OutDir, "dns-zone")
1308       DoLink(global_dir, OutDir, "dns-sshfp")
1309
1310    if 'AUTHKEYS' in ExtraList:
1311       DoLink(global_dir, OutDir, "authorized_keys")
1312
1313    if 'BSMTP' in ExtraList:
1314       GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1315
1316    if 'PRIVATE' in ExtraList:
1317       DoLink(global_dir, OutDir, "debian-private")
1318
1319    if 'GITOLITE' in ExtraList:
1320       DoLink(global_dir, OutDir, "ssh-gitolite")
1321    if 'exportOptions' in host[1]:
1322       for entry in host[1]['exportOptions']:
1323          v = entry.split('=',1)
1324          if v[0] != 'GITOLITE' or len(v) != 2: continue
1325          gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1326          gitolite_hosts = filter(lambda x: GitoliteExportHosts.match(x[1]["hostname"][0]), all_hosts)
1327          GenSSHGitolite(gitolite_accounts, gitolite_hosts, OutDir + "ssh-gitolite-%s"%(v[1],))
1328
1329    if 'WEB-PASSWORDS' in ExtraList:
1330       DoLink(global_dir, OutDir, "web-passwords")
1331
1332    if 'VOIP-PASSWORDS' in ExtraList:
1333       DoLink(global_dir, OutDir, "voip-passwords")
1334
1335    if 'KEYRING' in ExtraList:
1336       for k in Keyrings:
1337          bn = os.path.basename(k)
1338          if os.path.isdir(k):
1339             src = os.path.join(global_dir, bn)
1340             replaceTree(src, OutDir)
1341          else:
1342             DoLink(global_dir, OutDir, bn)
1343    else:
1344       for k in Keyrings:
1345          try:
1346             bn = os.path.basename(k)
1347             target = os.path.join(OutDir, bn)
1348             if os.path.isdir(target):
1349                safe_rmtree(dst)
1350             else:
1351                posix.remove(target)
1352          except:
1353             pass
1354    DoLink(global_dir, OutDir, "last_update.trace")
1355
1356
1357 def getLastLDAPChangeTime(l):
1358    mods = l.search_s('cn=log',
1359          ldap.SCOPE_ONELEVEL,
1360          '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1361          ['reqEnd'])
1362
1363    last = 0
1364
1365    # Sort the list by reqEnd
1366    sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1367    # Take the last element in the array
1368    last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1369
1370    return last
1371
1372 def getLastKeyringChangeTime():
1373    krmod = 0
1374    for k in Keyrings:
1375       mt = os.path.getmtime(k)
1376       if mt > krmod:
1377          krmod = mt
1378
1379    return int(krmod)
1380
1381 def getLastBuildTime(gdir):
1382    cache_last_ldap_mod = 0
1383    cache_last_unix_mod = 0
1384    cache_last_run = 0
1385
1386    try:
1387       fd = open(os.path.join(gdir, "last_update.trace"), "r")
1388       cache_last_mod=fd.read().split()
1389       try:
1390          cache_last_ldap_mod = cache_last_mod[0]
1391          cache_last_unix_mod = int(cache_last_mod[1])
1392          cache_last_run = int(cache_last_mod[2])
1393       except IndexError, ValueError:
1394          pass
1395       fd.close()
1396    except IOError, e:
1397       if e.errno == errno.ENOENT:
1398          pass
1399       else:
1400          raise e
1401
1402    return (cache_last_ldap_mod, cache_last_unix_mod, cache_last_run)
1403
1404 def ud_generate():
1405    parser = optparse.OptionParser()
1406    parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1407      help="Output directory.")
1408    parser.add_option("-f", "--force", dest="force", action="store_true",
1409      help="Force generation, even if no update to LDAP has happened.")
1410
1411    (options, args) = parser.parse_args()
1412    if len(args) > 0:
1413       parser.print_help()
1414       sys.exit(1)
1415
1416    if options.generatedir is not None:
1417       generate_dir = os.environ['UD_GENERATEDIR']
1418    elif 'UD_GENERATEDIR' in os.environ:
1419       generate_dir = os.environ['UD_GENERATEDIR']
1420    else:
1421       generate_dir = GenerateDir
1422
1423
1424    lockf = os.path.join(generate_dir, 'ud-generate.lock')
1425    lock = get_lock( lockf )
1426    if lock is None:
1427       sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1428       sys.exit(1)
1429
1430    l = make_ldap_conn()
1431
1432    time_started = int(time.time())
1433    ldap_last_mod = getLastLDAPChangeTime(l)
1434    unix_last_mod = getLastKeyringChangeTime()
1435    cache_last_ldap_mod, cache_last_unix_mod, last_run = getLastBuildTime(generate_dir)
1436
1437    need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod) or (time_started - last_run > MAX_UD_AGE)
1438
1439    if not options.force and not need_update:
1440       fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1441       fd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, last_run))
1442       fd.close()
1443       sys.exit(0)
1444
1445    tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1446    generate_all(generate_dir, l)
1447    tracefd.write("%s\n%s\n%s\n" % (ldap_last_mod, unix_last_mod, time_started))
1448    tracefd.close()
1449
1450
1451 if __name__ == "__main__":
1452    if 'UD_PROFILE' in os.environ:
1453       import cProfile
1454       import pstats
1455       cProfile.run('ud_generate()', "udg_prof")
1456       p = pstats.Stats('udg_prof')
1457       ##p.sort_stats('time').print_stats()
1458       p.sort_stats('cumulative').print_stats()
1459    else:
1460       ud_generate()
1461
1462 # vim:set et:
1463 # vim:set ts=3:
1464 # vim:set shiftwidth=3: