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