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