fix generation of voip-passwords file
[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('include')
411
412       for a in accounts:
413          if not 'voipPassword' in a: continue
414          if not a.pw_active(): continue
415
416          Pass = str(a['voipPassword'])
417          user = Element('user')
418          user.attrib['id'] = "%s" % (a['uid'])
419          root.append(user)
420          params = Element('params')
421          user.append(params)
422          param = Element('param')
423          params.append(param)
424          param.attrib['name'] = "a1-hash"
425          param.attrib['value'] = "%s" % (Pass)
426          variables = Element('variables')
427          user.append(variables)
428          variable = Element('variable')
429          variable.attrib['name'] = "toll_allow"
430          variable.attrib['value'] = "domestic,international,local"
431          variables.append(variable)
432
433       F.write("%s" % (prettify(root)))
434
435
436    except:
437       Die(File, None, F)
438       raise
439
440 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
441    OldMask = os.umask(0077)
442    tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
443    os.umask(OldMask)
444    for f in userlist:
445       if f not in ssh_userkeys:
446          continue
447       # If we're not exporting their primary group, don't export
448       # the key and warn
449       grname = None
450       if userlist[f] in grouprevmap.keys():
451          grname = grouprevmap[userlist[f]]
452       else:
453          try:
454             if int(userlist[f]) <= 100:
455                # In these cases, look it up in the normal way so we
456                # deal with cases where, for instance, users are in group
457                # users as their primary group.
458                grname = grp.getgrgid(userlist[f])[0]
459          except Exception, e:
460             pass
461
462       if grname is None:
463          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])
464          continue
465
466       lines = []
467       for line in ssh_userkeys[f]:
468          if line.startswith("allowed_hosts=") and ' ' in line:
469             machines, line = line.split('=', 1)[1].split(' ', 1)
470             if current_host not in machines.split(','):
471                continue # skip this key
472          lines.append(line)
473       if not lines:
474          continue # no keys for this host
475       contents = "\n".join(lines) + "\n"
476
477       to = tarfile.TarInfo(name=f)
478       # These will only be used where the username doesn't
479       # exist on the target system for some reason; hence,
480       # in those cases, the safest thing is for the file to
481       # be owned by root but group nobody.  This deals with
482       # the bloody obscure case where the group fails to exist
483       # whilst the user does (in which case we want to avoid
484       # ending up with a file which is owned user:root to avoid
485       # a fairly obvious attack vector)
486       to.uid = 0
487       to.gid = 65534
488       # Using the username / groupname fields avoids any need
489       # to give a shit^W^W^Wcare about the UIDoffset stuff.
490       to.uname = f
491       to.gname = grname
492       to.mode  = 0400
493       to.mtime = int(time.time())
494       to.size = len(contents)
495
496       tf.addfile(to, StringIO(contents))
497
498    tf.close()
499    os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
500
501 # add a list of groups to existing groups,
502 # including all subgroups thereof, recursively.
503 # basically this proceduces the transitive hull of the groups in
504 # addgroups.
505 def addGroups(existingGroups, newGroups, uid, current_host):
506    for group in newGroups:
507       # if it's a <group>@host, split it and verify it's on the current host.
508       s = group.split('@', 1)
509       if len(s) == 2 and s[1] != current_host:
510          continue
511       group = s[0]
512
513       # let's see if we handled this group already
514       if group in existingGroups:
515          continue
516
517       if not GroupIDMap.has_key(group):
518          print "Group", group, "does not exist but", uid, "is in it"
519          continue
520
521       existingGroups.append(group)
522
523       if SubGroupMap.has_key(group):
524          addGroups(existingGroups, SubGroupMap[group], uid, current_host)
525
526 # Generate the group list
527 def GenGroup(accounts, File, current_host):
528    grouprevmap = {}
529    F = None
530    try:
531       F = open(File + ".tdb.tmp", "w")
532      
533       # Generate the GroupMap
534       GroupMap = {}
535       for x in GroupIDMap:
536          GroupMap[x] = []
537       GroupHasPrimaryMembers = {}
538
539       # Sort them into a list of groups having a set of users
540       for a in accounts:
541          GroupHasPrimaryMembers[ a['gidNumber'] ] = True
542          if not 'supplementaryGid' in a: continue
543
544          supgroups=[]
545          addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
546          for g in supgroups:
547             GroupMap[g].append(a['uid'])
548
549       # Output the group file.
550       J = 0
551       for x in GroupMap.keys():
552          if not x in GroupIDMap:
553             continue
554
555          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
556             continue
557
558          grouprevmap[GroupIDMap[x]] = x
559
560          Line = "%s:x:%u:" % (x, GroupIDMap[x])
561          Comma = ''
562          for I in GroupMap[x]:
563             Line = Line + ("%s%s" % (Comma, I))
564             Comma = ','
565          Line = Sanitize(Line) + "\n"
566          F.write("0%u %s" % (J, Line))
567          F.write(".%s %s" % (x, Line))
568          F.write("=%u %s" % (GroupIDMap[x], Line))
569          J = J + 1
570   
571    # Oops, something unspeakable happened.
572    except:
573       Die(File, None, F)
574       raise
575    Done(File, None, F)
576   
577    return grouprevmap
578
579 def CheckForward(accounts):
580    for a in accounts:
581       if not 'emailForward' in a: continue
582
583       delete = False
584
585       # Do not allow people to try to buffer overflow busted parsers
586       if len(a['emailForward']) > 200: delete = True
587       # Check the forwarding address
588       elif EmailCheck.match(a['emailForward']) is None: delete = True
589
590       if delete:
591          a.delete_mailforward()
592
593 # Generate the email forwarding list
594 def GenForward(accounts, File):
595    F = None
596    try:
597       OldMask = os.umask(0022)
598       F = open(File + ".tmp", "w", 0644)
599       os.umask(OldMask)
600
601       for a in accounts:
602          if not 'emailForward' in a: continue
603          Line = "%s: %s" % (a['uid'], a['emailForward'])
604          Line = Sanitize(Line) + "\n"
605          F.write(Line)
606
607    # Oops, something unspeakable happened.
608    except:
609       Die(File, F, None)
610       raise
611    Done(File, F, None)
612
613 def GenCDB(accounts, File, key):
614    Fdb = None
615    try:
616       OldMask = os.umask(0022)
617       # nothing else does the fsync stuff, so why do it here?
618       prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
619       Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
620       os.umask(OldMask)
621
622       # Write out the email address for each user
623       for a in accounts:
624          if not key in a: continue
625          value = a[key]
626          user = a['uid']
627          Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
628
629       Fdb.write("\n")
630    # Oops, something unspeakable happened.
631    except:
632       Fdb.close()
633       raise
634    if Fdb.close() != None:
635       raise "cdbmake gave an error"
636
637 # Generate the anon XEarth marker file
638 def GenMarkers(accounts, File):
639    F = None
640    try:
641       F = open(File + ".tmp", "w")
642
643       # Write out the position for each user
644       for a in accounts:
645          if not ('latitude' in a and 'longitude' in a): continue
646          try:
647             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
648             Line = Sanitize(Line) + "\n"
649             F.write(Line)
650          except:
651             pass
652   
653    # Oops, something unspeakable happened.
654    except:
655       Die(File, F, None)
656       raise
657    Done(File, F, None)
658
659 # Generate the debian-private subscription list
660 def GenPrivate(accounts, File):
661    F = None
662    try:
663       F = open(File + ".tmp", "w")
664
665       # Write out the position for each user
666       for a in accounts:
667          if not a.is_active_user(): continue
668          if not 'privateSub' in a: continue
669          try:
670             Line = "%s"%(a['privateSub'])
671             Line = Sanitize(Line) + "\n"
672             F.write(Line)
673          except:
674             pass
675   
676    # Oops, something unspeakable happened.
677    except:
678       Die(File, F, None)
679       raise
680    Done(File, F, None)
681
682 # Generate a list of locked accounts
683 def GenDisabledAccounts(accounts, File):
684    F = None
685    try:
686       F = open(File + ".tmp", "w")
687       disabled_accounts = []
688
689       # Fetch all the users
690       for a in accounts:
691          if a.pw_active(): continue
692          Line = "%s:%s" % (a['uid'], "Account is locked")
693          disabled_accounts.append(a)
694          F.write(Sanitize(Line) + "\n")
695
696    # Oops, something unspeakable happened.
697    except:
698       Die(File, F, None)
699       raise
700    Done(File, F, None)
701    return disabled_accounts
702
703 # Generate the list of local addresses that refuse all mail
704 def GenMailDisable(accounts, File):
705    F = None
706    try:
707       F = open(File + ".tmp", "w")
708
709       for a in accounts:
710          if not 'mailDisableMessage' in a: continue
711          Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
712          Line = Sanitize(Line) + "\n"
713          F.write(Line)
714
715    # Oops, something unspeakable happened.
716    except:
717       Die(File, F, None)
718       raise
719    Done(File, F, None)
720
721 # Generate a list of uids that should have boolean affects applied
722 def GenMailBool(accounts, File, key):
723    F = None
724    try:
725       F = open(File + ".tmp", "w")
726
727       for a in accounts:
728          if not key in a: continue
729          if not a[key] == 'TRUE': continue
730          Line = "%s"%(a['uid'])
731          Line = Sanitize(Line) + "\n"
732          F.write(Line)
733
734    # Oops, something unspeakable happened.
735    except:
736       Die(File, F, None)
737       raise
738    Done(File, F, None)
739
740 # Generate a list of hosts for RBL or whitelist purposes.
741 def GenMailList(accounts, File, key):
742    F = None
743    try:
744       F = open(File + ".tmp", "w")
745
746       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
747       else:                      validregex = re.compile('^[-\w.]+$')
748
749       for a in accounts:
750          if not key in a: continue
751
752          filtered = filter(lambda z: validregex.match(z), a[key])
753          if len(filtered) == 0: continue
754          if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
755          line = a['uid'] + ': ' + ' : '.join(filtered)
756          line = Sanitize(line) + "\n"
757          F.write(line)
758
759    # Oops, something unspeakable happened.
760    except:
761       Die(File, F, None)
762       raise
763    Done(File, F, None)
764
765 def isRoleAccount(account):
766    return 'debianRoleAccount' in account['objectClass']
767
768 # Generate the DNS Zone file
769 def GenDNS(accounts, File):
770    F = None
771    try:
772       F = open(File + ".tmp", "w")
773
774       # Fetch all the users
775       RRs = {}
776
777       # Write out the zone file entry for each user
778       for a in accounts:
779          if not 'dnsZoneEntry' in a: continue
780          if not a.is_active_user() and not isRoleAccount(a): continue
781
782          try:
783             F.write("; %s\n"%(a.email_address()))
784             for z in a["dnsZoneEntry"]:
785                Split = z.lower().split()
786                if Split[1].lower() == 'in':
787                   Line = " ".join(Split) + "\n"
788                   F.write(Line)
789
790                   Host = Split[0] + DNSZone
791                   if BSMTPCheck.match(Line) != None:
792                      F.write("; Has BSMTP\n")
793
794                   # Write some identification information
795                   if not RRs.has_key(Host):
796                      if Split[2].lower() in ["a", "aaaa"]:
797                         Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
798                         for y in a["keyFingerPrint"]:
799                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
800                            F.write(Line)
801                         RRs[Host] = 1
802                else:
803                   Line = "; Err %s"%(str(Split))
804                   F.write(Line)
805
806             F.write("\n")
807          except Exception, e:
808             F.write("; Errors:\n")
809             for line in str(e).split("\n"):
810                F.write("; %s\n"%(line))
811             pass
812   
813    # Oops, something unspeakable happened.
814    except:
815       Die(File, F, None)
816       raise
817    Done(File, F, None)
818
819 def ExtractDNSInfo(x):
820
821    TTLprefix="\t"
822    if 'dnsTTL' in x[1]:
823       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
824
825    DNSInfo = []
826    if x[1].has_key("ipHostNumber"):
827       for I in x[1]["ipHostNumber"]:
828          if IsV6Addr.match(I) != None:
829             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
830          else:
831             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
832
833    Algorithm = None
834
835    if 'sshRSAHostKey' in x[1]:
836       for I in x[1]["sshRSAHostKey"]:
837          Split = I.split()
838          if Split[0] == 'ssh-rsa':
839             Algorithm = 1
840          if Split[0] == 'ssh-dss':
841             Algorithm = 2
842          if Algorithm == None:
843             continue
844          Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
845          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
846
847    if 'architecture' in x[1]:
848       Arch = GetAttr(x, "architecture")
849       Mach = ""
850       if x[1].has_key("machine"):
851          Mach = " " + GetAttr(x, "machine")
852       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
853
854    if x[1].has_key("mXRecord"):
855       for I in x[1]["mXRecord"]:
856          DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
857
858    return DNSInfo
859
860 # Generate the DNS records
861 def GenZoneRecords(host_attrs, File):
862    F = None
863    try:
864       F = open(File + ".tmp", "w")
865
866       # Fetch all the hosts
867       for x in host_attrs:
868          if x[1].has_key("hostname") == 0:
869             continue
870
871          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
872             continue
873
874          DNSInfo = ExtractDNSInfo(x)
875          start = True
876          for Line in DNSInfo:
877             if start == True:
878                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
879                start = False
880             else:
881                Line = "\t\t\t%s" % (Line)
882
883             F.write(Line + "\n")
884
885         # this would write sshfp lines for services on machines
886         # but we can't yet, since some are cnames and we'll make
887         # an invalid zonefile
888         #
889         # for i in x[1].get("purpose", []):
890         #    m = PurposeHostField.match(i)
891         #    if m:
892         #       m = m.group(1)
893         #       # we ignore [[*..]] entries
894         #       if m.startswith('*'):
895         #          continue
896         #       if m.startswith('-'):
897         #          m = m[1:]
898         #       if m:
899         #          if not m.endswith(HostDomain):
900         #             continue
901         #          if not m.endswith('.'):
902         #             m = m + "."
903         #          for Line in DNSInfo:
904         #             if isSSHFP.match(Line):
905         #                Line = "%s\t%s" % (m, Line)
906         #                F.write(Line + "\n")
907
908    # Oops, something unspeakable happened.
909    except:
910       Die(File, F, None)
911       raise
912    Done(File, F, None)
913
914 # Generate the BSMTP file
915 def GenBSMTP(accounts, File, HomePrefix):
916    F = None
917    try:
918       F = open(File + ".tmp", "w")
919      
920       # Write out the zone file entry for each user
921       for a in accounts:
922          if not 'dnsZoneEntry' in a: continue
923          if not a.is_active_user(): continue
924
925          try:
926             for z in a["dnsZoneEntry"]:
927                Split = z.lower().split()
928                if Split[1].lower() == 'in':
929                   for y in range(0, len(Split)):
930                      if Split[y] == "$":
931                         Split[y] = "\n\t"
932                   Line = " ".join(Split) + "\n"
933      
934                   Host = Split[0] + DNSZone
935                   if BSMTPCheck.match(Line) != None:
936                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
937                                   a['uid'], HomePrefix, a['uid'], Host))
938      
939          except:
940             F.write("; Errors\n")
941             pass
942   
943    # Oops, something unspeakable happened.
944    except:
945       Die(File, F, None)
946       raise
947    Done(File, F, None)
948   
949 def HostToIP(Host, mapped=True):
950
951    IPAdresses = []
952
953    if Host[1].has_key("ipHostNumber"):
954       for addr in Host[1]["ipHostNumber"]:
955          IPAdresses.append(addr)
956          if IsV6Addr.match(addr) is None and mapped == "True":
957             IPAdresses.append("::ffff:"+addr)
958
959    return IPAdresses
960
961 # Generate the ssh known hosts file
962 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
963    F = None
964    try:
965       OldMask = os.umask(0022)
966       F = open(File + ".tmp", "w", 0644)
967       os.umask(OldMask)
968      
969       for x in host_attrs:
970          if x[1].has_key("hostname") == 0 or \
971             x[1].has_key("sshRSAHostKey") == 0:
972             continue
973          Host = GetAttr(x, "hostname")
974          HostNames = [ Host ]
975          if Host.endswith(HostDomain):
976             HostNames.append(Host[:-(len(HostDomain) + 1)])
977      
978          # in the purpose field [[host|some other text]] (where some other text is optional)
979          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
980          # file.  But so that we don't have to add everything we link we can add an asterisk
981          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
982          # http linking it we also support [[-hostname]] entries.
983          for i in x[1].get("purpose", []):
984             m = PurposeHostField.match(i)
985             if m:
986                m = m.group(1)
987                # we ignore [[*..]] entries
988                if m.startswith('*'):
989                   continue
990                if m.startswith('-'):
991                   m = m[1:]
992                if m:
993                   HostNames.append(m)
994                   if m.endswith(HostDomain):
995                      HostNames.append(m[:-(len(HostDomain) + 1)])
996      
997          for I in x[1]["sshRSAHostKey"]:
998             if mode and mode == 'authorized_keys':
999                hosts = HostToIP(x)
1000                if 'sshdistAuthKeysHost' in x[1]:
1001                   hosts += x[1]['sshdistAuthKeysHost']
1002                clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1003                clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1004                Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1005             else:
1006                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1007             Line = Sanitize(Line) + "\n"
1008             F.write(Line)
1009    # Oops, something unspeakable happened.
1010    except:
1011       Die(File, F, None)
1012       raise
1013    Done(File, F, None)
1014
1015 # Generate the debianhosts file (list of all IP addresses)
1016 def GenHosts(host_attrs, File):
1017    F = None
1018    try:
1019       OldMask = os.umask(0022)
1020       F = open(File + ".tmp", "w", 0644)
1021       os.umask(OldMask)
1022      
1023       seen = set()
1024
1025       for x in host_attrs:
1026
1027          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1028             continue
1029
1030          if not 'ipHostNumber' in x[1]:
1031             continue
1032
1033          addrs = x[1]["ipHostNumber"]
1034          for addr in addrs:
1035             if addr not in seen:
1036                seen.add(addr)
1037                addr = Sanitize(addr) + "\n"
1038                F.write(addr)
1039
1040    # Oops, something unspeakable happened.
1041    except:
1042       Die(File, F, None)
1043       raise
1044    Done(File, F, None)
1045
1046 def replaceTree(src, dst_basedir):
1047    bn = os.path.basename(src)
1048    dst = os.path.join(dst_basedir, bn)
1049    safe_rmtree(dst)
1050    shutil.copytree(src, dst)
1051
1052 def GenKeyrings(OutDir):
1053    for k in Keyrings:
1054       if os.path.isdir(k):
1055          replaceTree(k, OutDir)
1056       else:
1057          shutil.copy(k, OutDir)
1058
1059
1060 def get_accounts(ldap_conn):
1061    # Fetch all the users
1062    passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1063                    ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1064                     "gecos", "loginShell", "userPassword", "shadowLastChange",\
1065                     "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1066                     "shadowExpire", "emailForward", "latitude", "longitude",\
1067                     "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1068                     "keyFingerPrint", "privateSub", "mailDisableMessage",\
1069                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1070                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1071                     "mailContentInspectionAction", "webPassword", "voipPassword"])
1072
1073    if passwd_attrs is None:
1074       raise UDEmptyList, "No Users"
1075    accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1076    accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1077
1078    return accounts
1079
1080 def get_hosts(ldap_conn):
1081    # Fetch all the hosts
1082    HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1083                    ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1084                     "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1085
1086    if HostAttrs == None:
1087       raise UDEmptyList, "No Hosts"
1088
1089    HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1090
1091    return HostAttrs
1092
1093
1094 def make_ldap_conn():
1095    # Connect to the ldap server
1096    l = connectLDAP()
1097    # for testing purposes it's sometimes useful to pass username/password
1098    # via the environment
1099    if 'UD_CREDENTIALS' in os.environ:
1100       Pass = os.environ['UD_CREDENTIALS'].split()
1101    else:
1102       F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1103       Pass = F.readline().strip().split(" ")
1104       F.close()
1105    l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1106
1107    return l
1108
1109
1110
1111 def setup_group_maps(l):
1112    # Fetch all the groups
1113    group_id_map = {}
1114    subgroup_map = {}
1115    attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1116                      ["gid", "gidNumber", "subGroup"])
1117
1118    # Generate the subgroup_map and group_id_map
1119    for x in attrs:
1120       if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1121          continue
1122       if x[1].has_key("gidNumber") == 0:
1123          continue
1124       group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1125       if x[1].has_key("subGroup") != 0:
1126          subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1127
1128    global SubGroupMap
1129    global GroupIDMap
1130    SubGroupMap = subgroup_map
1131    GroupIDMap = group_id_map
1132
1133 def generate_all(global_dir, ldap_conn):
1134    accounts = get_accounts(ldap_conn)
1135    host_attrs = get_hosts(ldap_conn)
1136
1137    global_dir += '/'
1138    # Generate global things
1139    accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1140
1141    accounts = filter(lambda x: not IsRetired(x), accounts)
1142    #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1143
1144    CheckForward(accounts)
1145
1146    GenMailDisable(accounts, global_dir + "mail-disable")
1147    GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1148    GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1149    GenPrivate(accounts, global_dir + "debian-private")
1150    GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1151    GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1152    GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1153    GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1154    GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1155    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1156    GenWebPassword(accounts, global_dir + "web-passwords")
1157    GenVoipPassword(accounts, global_dir + "voip-passwords")
1158    GenKeyrings(global_dir)
1159
1160    # Compatibility.
1161    GenForward(accounts, global_dir + "forward-alias")
1162
1163    GenAllUsers(accounts, global_dir + 'all-accounts.json')
1164    accounts = filter(lambda a: not a in accounts_disabled, accounts)
1165
1166    ssh_userkeys = GenSSHShadow(global_dir, accounts)
1167    GenMarkers(accounts, global_dir + "markers")
1168    GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1169    GenHosts(host_attrs, global_dir + "debianhosts")
1170    GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1171
1172    GenDNS(accounts, global_dir + "dns-zone")
1173    GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1174
1175    setup_group_maps(ldap_conn)
1176
1177    for host in host_attrs:
1178       if not "hostname" in host[1]:
1179          continue
1180       generate_host(host, global_dir, accounts, ssh_userkeys)
1181
1182 def generate_host(host, global_dir, accounts, ssh_userkeys):
1183    current_host = host[1]['hostname'][0]
1184    OutDir = global_dir + current_host + '/'
1185    if not os.path.isdir(OutDir):
1186       os.mkdir(OutDir)
1187
1188    # Get the group list and convert any named groups to numerics
1189    GroupList = {}
1190    for groupname in AllowedGroupsPreload.strip().split(" "):
1191       GroupList[groupname] = True
1192    if 'allowedGroups' in host[1]:
1193       for groupname in host[1]['allowedGroups']:
1194          GroupList[groupname] = True
1195    for groupname in GroupList.keys():
1196       if groupname in GroupIDMap:
1197          GroupList[str(GroupIDMap[groupname])] = True
1198
1199    ExtraList = {}
1200    if 'exportOptions' in host[1]:
1201       for extra in host[1]['exportOptions']:
1202          ExtraList[extra.upper()] = True
1203
1204    if GroupList != {}:
1205       accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), accounts)
1206
1207    DoLink(global_dir, OutDir, "debianhosts")
1208    DoLink(global_dir, OutDir, "ssh_known_hosts")
1209    DoLink(global_dir, OutDir, "disabled-accounts")
1210
1211    sys.stdout.flush()
1212    if 'NOPASSWD' in ExtraList:
1213       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1214    else:
1215       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1216    sys.stdout.flush()
1217    grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1218    GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1219
1220    # Now we know who we're allowing on the machine, export
1221    # the relevant ssh keys
1222    GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1223
1224    if not 'NOPASSWD' in ExtraList:
1225       GenShadow(accounts, OutDir + "shadow")
1226
1227    # Link in global things
1228    if not 'NOMARKERS' in ExtraList:
1229       DoLink(global_dir, OutDir, "markers")
1230    DoLink(global_dir, OutDir, "mail-forward.cdb")
1231    DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1232    DoLink(global_dir, OutDir, "mail-disable")
1233    DoLink(global_dir, OutDir, "mail-greylist")
1234    DoLink(global_dir, OutDir, "mail-callout")
1235    DoLink(global_dir, OutDir, "mail-rbl")
1236    DoLink(global_dir, OutDir, "mail-rhsbl")
1237    DoLink(global_dir, OutDir, "mail-whitelist")
1238    DoLink(global_dir, OutDir, "all-accounts.json")
1239    GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1240    GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1241    GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1242
1243    # Compatibility.
1244    DoLink(global_dir, OutDir, "forward-alias")
1245
1246    if 'DNS' in ExtraList:
1247       DoLink(global_dir, OutDir, "dns-zone")
1248       DoLink(global_dir, OutDir, "dns-sshfp")
1249
1250    if 'AUTHKEYS' in ExtraList:
1251       DoLink(global_dir, OutDir, "authorized_keys")
1252
1253    if 'BSMTP' in ExtraList:
1254       GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1255
1256    if 'PRIVATE' in ExtraList:
1257       DoLink(global_dir, OutDir, "debian-private")
1258
1259    if 'GITOLITE' in ExtraList:
1260       DoLink(global_dir, OutDir, "ssh-gitolite")
1261
1262    if 'WEB-PASSWORDS' in ExtraList:
1263       DoLink(global_dir, OutDir, "web-passwords")
1264
1265    if 'VOIP-PASSWORDS' in ExtraList:
1266       DoLink(global_dir, OutDir, "voip-passwords")
1267
1268    if 'KEYRING' in ExtraList:
1269       for k in Keyrings:
1270          bn = os.path.basename(k)
1271          if os.path.isdir(k):
1272             src = os.path.join(global_dir, bn)
1273             replaceTree(src, OutDir)
1274          else:
1275             DoLink(global_dir, OutDir, bn)
1276    else:
1277       for k in Keyrings:
1278          try:
1279             bn = os.path.basename(k)
1280             target = os.path.join(OutDir, bn)
1281             if os.path.isdir(target):
1282                safe_rmtree(dst)
1283             else:
1284                posix.remove(target)
1285          except:
1286             pass
1287    DoLink(global_dir, OutDir, "last_update.trace")
1288
1289
1290 def getLastLDAPChangeTime(l):
1291    mods = l.search_s('cn=log',
1292          ldap.SCOPE_ONELEVEL,
1293          '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1294          ['reqEnd'])
1295
1296    last = 0
1297
1298    # Sort the list by reqEnd
1299    sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1300    # Take the last element in the array
1301    last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1302
1303    return last
1304
1305 def getLastKeyringChangeTime():
1306    krmod = 0
1307    for k in Keyrings:
1308       mt = os.path.getmtime(k)
1309       if mt > krmod:
1310          krmod = mt
1311
1312    return krmod
1313
1314 def getLastBuildTime(gdir):
1315    cache_last_ldap_mod = 0
1316    cache_last_unix_mod = 0
1317
1318    try:
1319       fd = open(os.path.join(gdir, "last_update.trace"), "r")
1320       cache_last_mod=fd.read().split()
1321       try:
1322          cache_last_ldap_mod = cache_last_mod[0]
1323          cache_last_unix_mod = int(cache_last_mod[1])
1324       except IndexError, ValueError:
1325          pass
1326       fd.close()
1327    except IOError, e:
1328       if e.errno == errno.ENOENT:
1329          pass
1330       else:
1331          raise e
1332
1333    return (cache_last_ldap_mod, cache_last_unix_mod)
1334
1335 def ud_generate():
1336    parser = optparse.OptionParser()
1337    parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1338      help="Output directory.")
1339    parser.add_option("-f", "--force", dest="force", action="store_true",
1340      help="Force generation, even if not update to LDAP has happened.")
1341
1342    (options, args) = parser.parse_args()
1343    if len(args) > 0:
1344       parser.print_help()
1345       sys.exit(1)
1346
1347    if options.generatedir is not None:
1348       generate_dir = os.environ['UD_GENERATEDIR']
1349    elif 'UD_GENERATEDIR' in os.environ:
1350       generate_dir = os.environ['UD_GENERATEDIR']
1351    else:
1352       generate_dir = GenerateDir
1353
1354
1355    lockf = os.path.join(generate_dir, 'ud-generate.lock')
1356    lock = get_lock( lockf )
1357    if lock is None:
1358       sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1359       sys.exit(1)
1360
1361    l = make_ldap_conn()
1362
1363    time_started = int(time.time())
1364    ldap_last_mod = getLastLDAPChangeTime(l)
1365    unix_last_mod = getLastKeyringChangeTime()
1366    cache_last_ldap_mod, cache_last_unix_mod = getLastBuildTime(generate_dir)
1367
1368    need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod)
1369
1370    if not options.force and not need_update:
1371       fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1372       fd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1373       fd.close()
1374       sys.exit(0)
1375
1376    tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1377    generate_all(generate_dir, l)
1378    tracefd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1379    tracefd.close()
1380
1381
1382 if __name__ == "__main__":
1383    if 'UD_PROFILE' in os.environ:
1384       import cProfile
1385       import pstats
1386       cProfile.run('ud_generate()', "udg_prof")
1387       p = pstats.Stats('udg_prof')
1388       ##p.sort_stats('time').print_stats()
1389       p.sort_stats('cumulative').print_stats()
1390    else:
1391       ud_generate()
1392
1393 # vim:set et:
1394 # vim:set ts=3:
1395 # vim:set shiftwidth=3: