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