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