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