Allow incoming-mx remapping in 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
65 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)(,\s*([^ <>@]+@[^ ,<>@]+))*$")
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 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 ExtractDNSInfo(x):
823
824    TTLprefix="\t"
825    if 'dnsTTL' in x[1]:
826       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
827
828    DNSInfo = []
829    if x[1].has_key("ipHostNumber"):
830       for I in x[1]["ipHostNumber"]:
831          if IsV6Addr.match(I) != None:
832             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
833          else:
834             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
835
836    Algorithm = None
837
838    if 'sshRSAHostKey' in x[1]:
839       for I in x[1]["sshRSAHostKey"]:
840          Split = I.split()
841          if Split[0] == 'ssh-rsa':
842             Algorithm = 1
843          if Split[0] == 'ssh-dss':
844             Algorithm = 2
845          if Algorithm == None:
846             continue
847          Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
848          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
849
850    if 'architecture' in x[1]:
851       Arch = GetAttr(x, "architecture")
852       Mach = ""
853       if x[1].has_key("machine"):
854          Mach = " " + GetAttr(x, "machine")
855       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
856
857    if x[1].has_key("mXRecord"):
858       for I in x[1]["mXRecord"]:
859          if I in MX_remap:
860             for e in MX_remap[I]:
861                DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
862          else:
863             DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
864
865    return DNSInfo
866
867 # Generate the DNS records
868 def GenZoneRecords(host_attrs, File):
869    F = None
870    try:
871       F = open(File + ".tmp", "w")
872
873       # Fetch all the hosts
874       for x in host_attrs:
875          if x[1].has_key("hostname") == 0:
876             continue
877
878          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
879             continue
880
881          DNSInfo = ExtractDNSInfo(x)
882          start = True
883          for Line in DNSInfo:
884             if start == True:
885                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
886                start = False
887             else:
888                Line = "\t\t\t%s" % (Line)
889
890             F.write(Line + "\n")
891
892         # this would write sshfp lines for services on machines
893         # but we can't yet, since some are cnames and we'll make
894         # an invalid zonefile
895         #
896         # for i in x[1].get("purpose", []):
897         #    m = PurposeHostField.match(i)
898         #    if m:
899         #       m = m.group(1)
900         #       # we ignore [[*..]] entries
901         #       if m.startswith('*'):
902         #          continue
903         #       if m.startswith('-'):
904         #          m = m[1:]
905         #       if m:
906         #          if not m.endswith(HostDomain):
907         #             continue
908         #          if not m.endswith('.'):
909         #             m = m + "."
910         #          for Line in DNSInfo:
911         #             if isSSHFP.match(Line):
912         #                Line = "%s\t%s" % (m, Line)
913         #                F.write(Line + "\n")
914
915    # Oops, something unspeakable happened.
916    except:
917       Die(File, F, None)
918       raise
919    Done(File, F, None)
920
921 # Generate the BSMTP file
922 def GenBSMTP(accounts, File, HomePrefix):
923    F = None
924    try:
925       F = open(File + ".tmp", "w")
926      
927       # Write out the zone file entry for each user
928       for a in accounts:
929          if not 'dnsZoneEntry' in a: continue
930          if not a.is_active_user(): continue
931
932          try:
933             for z in a["dnsZoneEntry"]:
934                Split = z.lower().split()
935                if Split[1].lower() == 'in':
936                   for y in range(0, len(Split)):
937                      if Split[y] == "$":
938                         Split[y] = "\n\t"
939                   Line = " ".join(Split) + "\n"
940      
941                   Host = Split[0] + DNSZone
942                   if BSMTPCheck.match(Line) != None:
943                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
944                                   a['uid'], HomePrefix, a['uid'], Host))
945      
946          except:
947             F.write("; Errors\n")
948             pass
949   
950    # Oops, something unspeakable happened.
951    except:
952       Die(File, F, None)
953       raise
954    Done(File, F, None)
955   
956 def HostToIP(Host, mapped=True):
957
958    IPAdresses = []
959
960    if Host[1].has_key("ipHostNumber"):
961       for addr in Host[1]["ipHostNumber"]:
962          IPAdresses.append(addr)
963          if IsV6Addr.match(addr) is None and mapped == "True":
964             IPAdresses.append("::ffff:"+addr)
965
966    return IPAdresses
967
968 # Generate the ssh known hosts file
969 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
970    F = None
971    try:
972       OldMask = os.umask(0022)
973       F = open(File + ".tmp", "w", 0644)
974       os.umask(OldMask)
975      
976       for x in host_attrs:
977          if x[1].has_key("hostname") == 0 or \
978             x[1].has_key("sshRSAHostKey") == 0:
979             continue
980          Host = GetAttr(x, "hostname")
981          HostNames = [ Host ]
982          if Host.endswith(HostDomain):
983             HostNames.append(Host[:-(len(HostDomain) + 1)])
984      
985          # in the purpose field [[host|some other text]] (where some other text is optional)
986          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
987          # file.  But so that we don't have to add everything we link we can add an asterisk
988          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
989          # http linking it we also support [[-hostname]] entries.
990          for i in x[1].get("purpose", []):
991             m = PurposeHostField.match(i)
992             if m:
993                m = m.group(1)
994                # we ignore [[*..]] entries
995                if m.startswith('*'):
996                   continue
997                if m.startswith('-'):
998                   m = m[1:]
999                if m:
1000                   HostNames.append(m)
1001                   if m.endswith(HostDomain):
1002                      HostNames.append(m[:-(len(HostDomain) + 1)])
1003      
1004          for I in x[1]["sshRSAHostKey"]:
1005             if mode and mode == 'authorized_keys':
1006                hosts = HostToIP(x)
1007                if 'sshdistAuthKeysHost' in x[1]:
1008                   hosts += x[1]['sshdistAuthKeysHost']
1009                clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1010                clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1011                Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1012             else:
1013                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1014             Line = Sanitize(Line) + "\n"
1015             F.write(Line)
1016    # Oops, something unspeakable happened.
1017    except:
1018       Die(File, F, None)
1019       raise
1020    Done(File, F, None)
1021
1022 # Generate the debianhosts file (list of all IP addresses)
1023 def GenHosts(host_attrs, File):
1024    F = None
1025    try:
1026       OldMask = os.umask(0022)
1027       F = open(File + ".tmp", "w", 0644)
1028       os.umask(OldMask)
1029      
1030       seen = set()
1031
1032       for x in host_attrs:
1033
1034          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1035             continue
1036
1037          if not 'ipHostNumber' in x[1]:
1038             continue
1039
1040          addrs = x[1]["ipHostNumber"]
1041          for addr in addrs:
1042             if addr not in seen:
1043                seen.add(addr)
1044                addr = Sanitize(addr) + "\n"
1045                F.write(addr)
1046
1047    # Oops, something unspeakable happened.
1048    except:
1049       Die(File, F, None)
1050       raise
1051    Done(File, F, None)
1052
1053 def replaceTree(src, dst_basedir):
1054    bn = os.path.basename(src)
1055    dst = os.path.join(dst_basedir, bn)
1056    safe_rmtree(dst)
1057    shutil.copytree(src, dst)
1058
1059 def GenKeyrings(OutDir):
1060    for k in Keyrings:
1061       if os.path.isdir(k):
1062          replaceTree(k, OutDir)
1063       else:
1064          shutil.copy(k, OutDir)
1065
1066
1067 def get_accounts(ldap_conn):
1068    # Fetch all the users
1069    passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1070                    ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1071                     "gecos", "loginShell", "userPassword", "shadowLastChange",\
1072                     "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1073                     "shadowExpire", "emailForward", "latitude", "longitude",\
1074                     "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1075                     "keyFingerPrint", "privateSub", "mailDisableMessage",\
1076                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1077                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1078                     "mailContentInspectionAction", "webPassword", "voipPassword"])
1079
1080    if passwd_attrs is None:
1081       raise UDEmptyList, "No Users"
1082    accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1083    accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1084
1085    return accounts
1086
1087 def get_hosts(ldap_conn):
1088    # Fetch all the hosts
1089    HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1090                    ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1091                     "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1092
1093    if HostAttrs == None:
1094       raise UDEmptyList, "No Hosts"
1095
1096    HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1097
1098    return HostAttrs
1099
1100
1101 def make_ldap_conn():
1102    # Connect to the ldap server
1103    l = connectLDAP()
1104    # for testing purposes it's sometimes useful to pass username/password
1105    # via the environment
1106    if 'UD_CREDENTIALS' in os.environ:
1107       Pass = os.environ['UD_CREDENTIALS'].split()
1108    else:
1109       F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1110       Pass = F.readline().strip().split(" ")
1111       F.close()
1112    l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1113
1114    return l
1115
1116
1117
1118 def setup_group_maps(l):
1119    # Fetch all the groups
1120    group_id_map = {}
1121    subgroup_map = {}
1122    attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1123                      ["gid", "gidNumber", "subGroup"])
1124
1125    # Generate the subgroup_map and group_id_map
1126    for x in attrs:
1127       if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1128          continue
1129       if x[1].has_key("gidNumber") == 0:
1130          continue
1131       group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1132       if x[1].has_key("subGroup") != 0:
1133          subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1134
1135    global SubGroupMap
1136    global GroupIDMap
1137    SubGroupMap = subgroup_map
1138    GroupIDMap = group_id_map
1139
1140 def generate_all(global_dir, ldap_conn):
1141    accounts = get_accounts(ldap_conn)
1142    host_attrs = get_hosts(ldap_conn)
1143
1144    global_dir += '/'
1145    # Generate global things
1146    accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1147
1148    accounts = filter(lambda x: not IsRetired(x), accounts)
1149    #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1150
1151    CheckForward(accounts)
1152
1153    GenMailDisable(accounts, global_dir + "mail-disable")
1154    GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1155    GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1156    GenPrivate(accounts, global_dir + "debian-private")
1157    GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1158    GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1159    GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1160    GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1161    GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1162    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1163    GenWebPassword(accounts, global_dir + "web-passwords")
1164    GenVoipPassword(accounts, global_dir + "voip-passwords")
1165    GenKeyrings(global_dir)
1166
1167    # Compatibility.
1168    GenForward(accounts, global_dir + "forward-alias")
1169
1170    GenAllUsers(accounts, global_dir + 'all-accounts.json')
1171    accounts = filter(lambda a: not a in accounts_disabled, accounts)
1172
1173    ssh_userkeys = GenSSHShadow(global_dir, accounts)
1174    GenMarkers(accounts, global_dir + "markers")
1175    GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1176    GenHosts(host_attrs, global_dir + "debianhosts")
1177    GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1178
1179    GenDNS(accounts, global_dir + "dns-zone")
1180    GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1181
1182    setup_group_maps(ldap_conn)
1183
1184    for host in host_attrs:
1185       if not "hostname" in host[1]:
1186          continue
1187       generate_host(host, global_dir, accounts, ssh_userkeys)
1188
1189 def generate_host(host, global_dir, all_accounts, ssh_userkeys):
1190    current_host = host[1]['hostname'][0]
1191    OutDir = global_dir + current_host + '/'
1192    if not os.path.isdir(OutDir):
1193       os.mkdir(OutDir)
1194
1195    # Get the group list and convert any named groups to numerics
1196    GroupList = {}
1197    for groupname in AllowedGroupsPreload.strip().split(" "):
1198       GroupList[groupname] = True
1199    if 'allowedGroups' in host[1]:
1200       for groupname in host[1]['allowedGroups']:
1201          GroupList[groupname] = True
1202    for groupname in GroupList.keys():
1203       if groupname in GroupIDMap:
1204          GroupList[str(GroupIDMap[groupname])] = True
1205
1206    ExtraList = {}
1207    if 'exportOptions' in host[1]:
1208       for extra in host[1]['exportOptions']:
1209          ExtraList[extra.upper()] = True
1210
1211    if GroupList != {}:
1212       accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1213
1214    DoLink(global_dir, OutDir, "debianhosts")
1215    DoLink(global_dir, OutDir, "ssh_known_hosts")
1216    DoLink(global_dir, OutDir, "disabled-accounts")
1217
1218    sys.stdout.flush()
1219    if 'NOPASSWD' in ExtraList:
1220       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1221    else:
1222       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1223    sys.stdout.flush()
1224    grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1225    GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1226
1227    # Now we know who we're allowing on the machine, export
1228    # the relevant ssh keys
1229    GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1230
1231    if not 'NOPASSWD' in ExtraList:
1232       GenShadow(accounts, OutDir + "shadow")
1233
1234    # Link in global things
1235    if not 'NOMARKERS' in ExtraList:
1236       DoLink(global_dir, OutDir, "markers")
1237    DoLink(global_dir, OutDir, "mail-forward.cdb")
1238    DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1239    DoLink(global_dir, OutDir, "mail-disable")
1240    DoLink(global_dir, OutDir, "mail-greylist")
1241    DoLink(global_dir, OutDir, "mail-callout")
1242    DoLink(global_dir, OutDir, "mail-rbl")
1243    DoLink(global_dir, OutDir, "mail-rhsbl")
1244    DoLink(global_dir, OutDir, "mail-whitelist")
1245    DoLink(global_dir, OutDir, "all-accounts.json")
1246    GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1247    GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1248    GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1249
1250    # Compatibility.
1251    DoLink(global_dir, OutDir, "forward-alias")
1252
1253    if 'DNS' in ExtraList:
1254       DoLink(global_dir, OutDir, "dns-zone")
1255       DoLink(global_dir, OutDir, "dns-sshfp")
1256
1257    if 'AUTHKEYS' in ExtraList:
1258       DoLink(global_dir, OutDir, "authorized_keys")
1259
1260    if 'BSMTP' in ExtraList:
1261       GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1262
1263    if 'PRIVATE' in ExtraList:
1264       DoLink(global_dir, OutDir, "debian-private")
1265
1266    if 'GITOLITE' in ExtraList:
1267       DoLink(global_dir, OutDir, "ssh-gitolite")
1268    if 'exportOptions' in host[1]:
1269       for entry in host[1]['exportOptions']:
1270          v = entry.split('=',1)
1271          if v[0] != 'GITOLITE' or len(v) != 2: continue
1272          gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1273          GenSSHGitolite(gitolite_accounts, OutDir + "ssh-gitolite-%s"%(v[1],))
1274
1275    if 'WEB-PASSWORDS' in ExtraList:
1276       DoLink(global_dir, OutDir, "web-passwords")
1277
1278    if 'VOIP-PASSWORDS' in ExtraList:
1279       DoLink(global_dir, OutDir, "voip-passwords")
1280
1281    if 'KEYRING' in ExtraList:
1282       for k in Keyrings:
1283          bn = os.path.basename(k)
1284          if os.path.isdir(k):
1285             src = os.path.join(global_dir, bn)
1286             replaceTree(src, OutDir)
1287          else:
1288             DoLink(global_dir, OutDir, bn)
1289    else:
1290       for k in Keyrings:
1291          try:
1292             bn = os.path.basename(k)
1293             target = os.path.join(OutDir, bn)
1294             if os.path.isdir(target):
1295                safe_rmtree(dst)
1296             else:
1297                posix.remove(target)
1298          except:
1299             pass
1300    DoLink(global_dir, OutDir, "last_update.trace")
1301
1302
1303 def getLastLDAPChangeTime(l):
1304    mods = l.search_s('cn=log',
1305          ldap.SCOPE_ONELEVEL,
1306          '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1307          ['reqEnd'])
1308
1309    last = 0
1310
1311    # Sort the list by reqEnd
1312    sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1313    # Take the last element in the array
1314    last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1315
1316    return last
1317
1318 def getLastKeyringChangeTime():
1319    krmod = 0
1320    for k in Keyrings:
1321       mt = os.path.getmtime(k)
1322       if mt > krmod:
1323          krmod = mt
1324
1325    return krmod
1326
1327 def getLastBuildTime(gdir):
1328    cache_last_ldap_mod = 0
1329    cache_last_unix_mod = 0
1330
1331    try:
1332       fd = open(os.path.join(gdir, "last_update.trace"), "r")
1333       cache_last_mod=fd.read().split()
1334       try:
1335          cache_last_ldap_mod = cache_last_mod[0]
1336          cache_last_unix_mod = int(cache_last_mod[1])
1337       except IndexError, ValueError:
1338          pass
1339       fd.close()
1340    except IOError, e:
1341       if e.errno == errno.ENOENT:
1342          pass
1343       else:
1344          raise e
1345
1346    return (cache_last_ldap_mod, cache_last_unix_mod)
1347
1348 def ud_generate():
1349    parser = optparse.OptionParser()
1350    parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1351      help="Output directory.")
1352    parser.add_option("-f", "--force", dest="force", action="store_true",
1353      help="Force generation, even if no update to LDAP has happened.")
1354
1355    (options, args) = parser.parse_args()
1356    if len(args) > 0:
1357       parser.print_help()
1358       sys.exit(1)
1359
1360    if options.generatedir is not None:
1361       generate_dir = os.environ['UD_GENERATEDIR']
1362    elif 'UD_GENERATEDIR' in os.environ:
1363       generate_dir = os.environ['UD_GENERATEDIR']
1364    else:
1365       generate_dir = GenerateDir
1366
1367
1368    lockf = os.path.join(generate_dir, 'ud-generate.lock')
1369    lock = get_lock( lockf )
1370    if lock is None:
1371       sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1372       sys.exit(1)
1373
1374    l = make_ldap_conn()
1375
1376    time_started = int(time.time())
1377    ldap_last_mod = getLastLDAPChangeTime(l)
1378    unix_last_mod = getLastKeyringChangeTime()
1379    cache_last_ldap_mod, cache_last_unix_mod = getLastBuildTime(generate_dir)
1380
1381    need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod)
1382
1383    if not options.force and not need_update:
1384       fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1385       fd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1386       fd.close()
1387       sys.exit(0)
1388
1389    tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1390    generate_all(generate_dir, l)
1391    tracefd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1392    tracefd.close()
1393
1394
1395 if __name__ == "__main__":
1396    if 'UD_PROFILE' in os.environ:
1397       import cProfile
1398       import pstats
1399       cProfile.run('ud_generate()', "udg_prof")
1400       p = pstats.Stats('udg_prof')
1401       ##p.sort_stats('time').print_stats()
1402       p.sort_stats('cumulative').print_stats()
1403    else:
1404       ud_generate()
1405
1406 # vim:set et:
1407 # vim:set ts=3:
1408 # vim:set shiftwidth=3: