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