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