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