Add option "use_mq" to allow disabling the use of mq_notify in ud-generate
[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 not 'rtcPassword' in a: continue
441          if not a.pw_active(): continue
442
443          Line = "%s%s:%s:%s:AUTHORIZED" % (a['uid'], rtc_append, str(a['rtcPassword']), rtc_realm)
444          Line = Sanitize(Line) + "\n"
445          F.write("%s" % (Line))
446
447    except:
448       Die(File, None, F)
449       raise
450
451 def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target, current_host):
452    OldMask = os.umask(0077)
453    tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), mode='w:gz')
454    os.umask(OldMask)
455    for f in userlist:
456       if f not in ssh_userkeys:
457          continue
458       # If we're not exporting their primary group, don't export
459       # the key and warn
460       grname = None
461       if userlist[f] in grouprevmap.keys():
462          grname = grouprevmap[userlist[f]]
463       else:
464          try:
465             if int(userlist[f]) <= 100:
466                # In these cases, look it up in the normal way so we
467                # deal with cases where, for instance, users are in group
468                # users as their primary group.
469                grname = grp.getgrgid(userlist[f])[0]
470          except Exception, e:
471             pass
472
473       if grname is None:
474          print "User %s is supposed to have their key exported to host %s but their primary group (gid: %d) isn't in LDAP" % (f, current_host, userlist[f])
475          continue
476
477       lines = []
478       for line in ssh_userkeys[f]:
479          if line.startswith("allowed_hosts=") and ' ' in line:
480             machines, line = line.split('=', 1)[1].split(' ', 1)
481             if current_host not in machines.split(','):
482                continue # skip this key
483          lines.append(line)
484       if not lines:
485          continue # no keys for this host
486       contents = "\n".join(lines) + "\n"
487
488       to = tarfile.TarInfo(name=f)
489       # These will only be used where the username doesn't
490       # exist on the target system for some reason; hence,
491       # in those cases, the safest thing is for the file to
492       # be owned by root but group nobody.  This deals with
493       # the bloody obscure case where the group fails to exist
494       # whilst the user does (in which case we want to avoid
495       # ending up with a file which is owned user:root to avoid
496       # a fairly obvious attack vector)
497       to.uid = 0
498       to.gid = 65534
499       # Using the username / groupname fields avoids any need
500       # to give a shit^W^W^Wcare about the UIDoffset stuff.
501       to.uname = f
502       to.gname = grname
503       to.mode  = 0400
504       to.mtime = int(time.time())
505       to.size = len(contents)
506
507       tf.addfile(to, StringIO(contents))
508
509    tf.close()
510    os.rename(os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % current_host), target)
511
512 # add a list of groups to existing groups,
513 # including all subgroups thereof, recursively.
514 # basically this proceduces the transitive hull of the groups in
515 # addgroups.
516 def addGroups(existingGroups, newGroups, uid, current_host):
517    for group in newGroups:
518       # if it's a <group>@host, split it and verify it's on the current host.
519       s = group.split('@', 1)
520       if len(s) == 2 and s[1] != current_host:
521          continue
522       group = s[0]
523
524       # let's see if we handled this group already
525       if group in existingGroups:
526          continue
527
528       if not GroupIDMap.has_key(group):
529          print "Group", group, "does not exist but", uid, "is in it"
530          continue
531
532       existingGroups.append(group)
533
534       if SubGroupMap.has_key(group):
535          addGroups(existingGroups, SubGroupMap[group], uid, current_host)
536
537 # Generate the group list
538 def GenGroup(accounts, File, current_host):
539    grouprevmap = {}
540    F = None
541    try:
542       F = open(File + ".tdb.tmp", "w")
543      
544       # Generate the GroupMap
545       GroupMap = {}
546       for x in GroupIDMap:
547          GroupMap[x] = []
548       GroupHasPrimaryMembers = {}
549
550       # Sort them into a list of groups having a set of users
551       for a in accounts:
552          GroupHasPrimaryMembers[ a['gidNumber'] ] = True
553          if not 'supplementaryGid' in a: continue
554
555          supgroups=[]
556          addGroups(supgroups, a['supplementaryGid'], a['uid'], current_host)
557          for g in supgroups:
558             GroupMap[g].append(a['uid'])
559
560       # Output the group file.
561       J = 0
562       for x in GroupMap.keys():
563          if not x in GroupIDMap:
564             continue
565
566          if len(GroupMap[x]) == 0 and GroupIDMap[x] not in GroupHasPrimaryMembers:
567             continue
568
569          grouprevmap[GroupIDMap[x]] = x
570
571          Line = "%s:x:%u:" % (x, GroupIDMap[x])
572          Comma = ''
573          for I in GroupMap[x]:
574             Line = Line + ("%s%s" % (Comma, I))
575             Comma = ','
576          Line = Sanitize(Line) + "\n"
577          F.write("0%u %s" % (J, Line))
578          F.write(".%s %s" % (x, Line))
579          F.write("=%u %s" % (GroupIDMap[x], Line))
580          J = J + 1
581   
582    # Oops, something unspeakable happened.
583    except:
584       Die(File, None, F)
585       raise
586    Done(File, None, F)
587   
588    return grouprevmap
589
590 def CheckForward(accounts):
591    for a in accounts:
592       if not 'emailForward' in a: continue
593
594       delete = False
595
596       # Do not allow people to try to buffer overflow busted parsers
597       if len(a['emailForward']) > 200: delete = True
598       # Check the forwarding address
599       elif EmailCheck.match(a['emailForward']) is None: delete = True
600
601       if delete:
602          a.delete_mailforward()
603
604 # Generate the email forwarding list
605 def GenForward(accounts, File):
606    F = None
607    try:
608       OldMask = os.umask(0022)
609       F = open(File + ".tmp", "w", 0644)
610       os.umask(OldMask)
611
612       for a in accounts:
613          if not 'emailForward' in a: continue
614          Line = "%s: %s" % (a['uid'], a['emailForward'])
615          Line = Sanitize(Line) + "\n"
616          F.write(Line)
617
618    # Oops, something unspeakable happened.
619    except:
620       Die(File, F, None)
621       raise
622    Done(File, F, None)
623
624 def GenCDB(accounts, File, key):
625    Fdb = None
626    try:
627       OldMask = os.umask(0022)
628       # nothing else does the fsync stuff, so why do it here?
629       prefix = "/usr/bin/eatmydata " if os.path.exists('/usr/bin/eatmydata') else ''
630       Fdb = os.popen("%scdbmake %s %s.tmp"%(prefix, File, File), "w")
631       os.umask(OldMask)
632
633       # Write out the email address for each user
634       for a in accounts:
635          if not key in a: continue
636          value = a[key]
637          user = a['uid']
638          Fdb.write("+%d,%d:%s->%s\n" % (len(user), len(value), user, value))
639
640       Fdb.write("\n")
641    # Oops, something unspeakable happened.
642    except:
643       Fdb.close()
644       raise
645    if Fdb.close() != None:
646       raise "cdbmake gave an error"
647
648 def GenDBM(accounts, File, key):
649    Fdb = None
650    OldMask = os.umask(0022)
651    fn = os.path.join(File).encode('ascii', 'ignore')
652    try:
653       posix.remove(fn)
654    except:
655       pass
656
657    try:
658       Fdb = dbm.open(fn, "c")
659       os.umask(OldMask)
660
661       # Write out the email address for each user
662       for a in accounts:
663          if not key in a: continue
664          value = a[key]
665          user = a['uid']
666          Fdb[user] = value
667
668       Fdb.close()
669    except:
670       # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
671       os.remove(File + ".db")
672       raise
673    # python-dbm names the files Fdb.db.db so we want to them to be Fdb.db
674    os.rename (File + ".db", File)
675
676 # Generate the anon XEarth marker file
677 def GenMarkers(accounts, File):
678    F = None
679    try:
680       F = open(File + ".tmp", "w")
681
682       # Write out the position for each user
683       for a in accounts:
684          if not ('latitude' in a and 'longitude' in a): continue
685          try:
686             Line = "%8s %8s \"\""%(a.latitude_dec(True), a.longitude_dec(True))
687             Line = Sanitize(Line) + "\n"
688             F.write(Line)
689          except:
690             pass
691   
692    # Oops, something unspeakable happened.
693    except:
694       Die(File, F, None)
695       raise
696    Done(File, F, None)
697
698 # Generate the debian-private subscription list
699 def GenPrivate(accounts, File):
700    F = None
701    try:
702       F = open(File + ".tmp", "w")
703
704       # Write out the position for each user
705       for a in accounts:
706          if not a.is_active_user(): continue
707          if a.is_guest_account(): continue
708          if not 'privateSub' in a: continue
709          try:
710             Line = "%s"%(a['privateSub'])
711             Line = Sanitize(Line) + "\n"
712             F.write(Line)
713          except:
714             pass
715   
716    # Oops, something unspeakable happened.
717    except:
718       Die(File, F, None)
719       raise
720    Done(File, F, None)
721
722 # Generate a list of locked accounts
723 def GenDisabledAccounts(accounts, File):
724    F = None
725    try:
726       F = open(File + ".tmp", "w")
727       disabled_accounts = []
728
729       # Fetch all the users
730       for a in accounts:
731          if a.pw_active(): continue
732          Line = "%s:%s" % (a['uid'], "Account is locked")
733          disabled_accounts.append(a)
734          F.write(Sanitize(Line) + "\n")
735
736    # Oops, something unspeakable happened.
737    except:
738       Die(File, F, None)
739       raise
740    Done(File, F, None)
741    return disabled_accounts
742
743 # Generate the list of local addresses that refuse all mail
744 def GenMailDisable(accounts, File):
745    F = None
746    try:
747       F = open(File + ".tmp", "w")
748
749       for a in accounts:
750          if not 'mailDisableMessage' in a: continue
751          Line = "%s: %s"%(a['uid'], a['mailDisableMessage'])
752          Line = Sanitize(Line) + "\n"
753          F.write(Line)
754
755    # Oops, something unspeakable happened.
756    except:
757       Die(File, F, None)
758       raise
759    Done(File, F, None)
760
761 # Generate a list of uids that should have boolean affects applied
762 def GenMailBool(accounts, File, key):
763    F = None
764    try:
765       F = open(File + ".tmp", "w")
766
767       for a in accounts:
768          if not key in a: continue
769          if not a[key] == 'TRUE': continue
770          Line = "%s"%(a['uid'])
771          Line = Sanitize(Line) + "\n"
772          F.write(Line)
773
774    # Oops, something unspeakable happened.
775    except:
776       Die(File, F, None)
777       raise
778    Done(File, F, None)
779
780 # Generate a list of hosts for RBL or whitelist purposes.
781 def GenMailList(accounts, File, key):
782    F = None
783    try:
784       F = open(File + ".tmp", "w")
785
786       if key == "mailWhitelist": validregex = re.compile('^[-\w.]+(/[\d]+)?$')
787       else:                      validregex = re.compile('^[-\w.]+$')
788
789       for a in accounts:
790          if not key in a: continue
791
792          filtered = filter(lambda z: validregex.match(z), a[key])
793          if len(filtered) == 0: continue
794          if key == "mailRHSBL": filtered = map(lambda z: z+"/$sender_address_domain", filtered)
795          line = a['uid'] + ': ' + ' : '.join(filtered)
796          line = Sanitize(line) + "\n"
797          F.write(line)
798
799    # Oops, something unspeakable happened.
800    except:
801       Die(File, F, None)
802       raise
803    Done(File, F, None)
804
805 def isRoleAccount(account):
806    return 'debianRoleAccount' in account['objectClass']
807
808 # Generate the DNS Zone file
809 def GenDNS(accounts, File):
810    F = None
811    try:
812       F = open(File + ".tmp", "w")
813
814       # Fetch all the users
815       RRs = {}
816
817       # Write out the zone file entry for each user
818       for a in accounts:
819          if not 'dnsZoneEntry' in a: continue
820          if not a.is_active_user() and not isRoleAccount(a): continue
821          if a.is_guest_account(): continue
822
823          try:
824             F.write("; %s\n"%(a.email_address()))
825             for z in a["dnsZoneEntry"]:
826                Split = z.lower().split()
827                if Split[1].lower() == 'in':
828                   Line = " ".join(Split) + "\n"
829                   F.write(Line)
830
831                   Host = Split[0] + DNSZone
832                   if BSMTPCheck.match(Line) != None:
833                      F.write("; Has BSMTP\n")
834
835                   # Write some identification information
836                   if not RRs.has_key(Host):
837                      if Split[2].lower() in ["a", "aaaa"]:
838                         Line = "%s IN TXT \"%s\"\n"%(Split[0], a.email_address())
839                         for y in a["keyFingerPrint"]:
840                            Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
841                            F.write(Line)
842                         RRs[Host] = 1
843                else:
844                   Line = "; Err %s"%(str(Split))
845                   F.write(Line)
846
847             F.write("\n")
848          except Exception, e:
849             F.write("; Errors:\n")
850             for line in str(e).split("\n"):
851                F.write("; %s\n"%(line))
852             pass
853   
854    # Oops, something unspeakable happened.
855    except:
856       Die(File, F, None)
857       raise
858    Done(File, F, None)
859
860 def is_ipv6_addr(i):
861    try:
862       socket.inet_pton(socket.AF_INET6, i)
863    except socket.error:
864       return False
865    return True
866
867 def ExtractDNSInfo(x):
868
869    TTLprefix="\t"
870    if 'dnsTTL' in x[1]:
871       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
872
873    DNSInfo = []
874    if x[1].has_key("ipHostNumber"):
875       for I in x[1]["ipHostNumber"]:
876          if is_ipv6_addr(I):
877             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
878          else:
879             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
880
881    Algorithm = None
882
883    if 'sshRSAHostKey' in x[1]:
884       for I in x[1]["sshRSAHostKey"]:
885          Split = I.split()
886          if Split[0] == 'ssh-rsa':
887             Algorithm = 1
888          if Split[0] == 'ssh-dss':
889             Algorithm = 2
890          if Split[0] == 'ssh-ed25519':
891             Algorithm = 4
892          if Algorithm == None:
893             continue
894          Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
895          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
896          Fingerprint = hashlib.new('sha256', base64.decodestring(Split[1])).hexdigest()
897          DNSInfo.append("%sIN\tSSHFP\t%u 2 %s" % (TTLprefix, Algorithm, Fingerprint))
898
899    if 'architecture' in x[1]:
900       Arch = GetAttr(x, "architecture")
901       Mach = ""
902       if x[1].has_key("machine"):
903          Mach = " " + GetAttr(x, "machine")
904       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian"))
905
906    if x[1].has_key("mXRecord"):
907       for I in x[1]["mXRecord"]:
908          if I in MX_remap:
909             for e in MX_remap[I]:
910                DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
911          else:
912             DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
913
914    return DNSInfo
915
916 # Generate the DNS records
917 def GenZoneRecords(host_attrs, File):
918    F = None
919    try:
920       F = open(File + ".tmp", "w")
921
922       # Fetch all the hosts
923       for x in host_attrs:
924          if x[1].has_key("hostname") == 0:
925             continue
926
927          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
928             continue
929
930          DNSInfo = ExtractDNSInfo(x)
931          start = True
932          for Line in DNSInfo:
933             if start == True:
934                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
935                start = False
936             else:
937                Line = "\t\t\t%s" % (Line)
938
939             F.write(Line + "\n")
940
941         # this would write sshfp lines for services on machines
942         # but we can't yet, since some are cnames and we'll make
943         # an invalid zonefile
944         #
945         # for i in x[1].get("purpose", []):
946         #    m = PurposeHostField.match(i)
947         #    if m:
948         #       m = m.group(1)
949         #       # we ignore [[*..]] entries
950         #       if m.startswith('*'):
951         #          continue
952         #       if m.startswith('-'):
953         #          m = m[1:]
954         #       if m:
955         #          if not m.endswith(HostDomain):
956         #             continue
957         #          if not m.endswith('.'):
958         #             m = m + "."
959         #          for Line in DNSInfo:
960         #             if isSSHFP.match(Line):
961         #                Line = "%s\t%s" % (m, Line)
962         #                F.write(Line + "\n")
963
964    # Oops, something unspeakable happened.
965    except:
966       Die(File, F, None)
967       raise
968    Done(File, F, None)
969
970 # Generate the BSMTP file
971 def GenBSMTP(accounts, File, HomePrefix):
972    F = None
973    try:
974       F = open(File + ".tmp", "w")
975      
976       # Write out the zone file entry for each user
977       for a in accounts:
978          if not 'dnsZoneEntry' in a: continue
979          if not a.is_active_user(): continue
980
981          try:
982             for z in a["dnsZoneEntry"]:
983                Split = z.lower().split()
984                if Split[1].lower() == 'in':
985                   for y in range(0, len(Split)):
986                      if Split[y] == "$":
987                         Split[y] = "\n\t"
988                   Line = " ".join(Split) + "\n"
989      
990                   Host = Split[0] + DNSZone
991                   if BSMTPCheck.match(Line) != None:
992                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
993                                   a['uid'], HomePrefix, a['uid'], Host))
994      
995          except:
996             F.write("; Errors\n")
997             pass
998   
999    # Oops, something unspeakable happened.
1000    except:
1001       Die(File, F, None)
1002       raise
1003    Done(File, F, None)
1004   
1005 def HostToIP(Host, mapped=True):
1006
1007    IPAdresses = []
1008
1009    if Host[1].has_key("ipHostNumber"):
1010       for addr in Host[1]["ipHostNumber"]:
1011          IPAdresses.append(addr)
1012          if not is_ipv6_addr(addr) and mapped == "True":
1013             IPAdresses.append("::ffff:"+addr)
1014
1015    return IPAdresses
1016
1017 # Generate the ssh known hosts file
1018 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
1019    F = None
1020    try:
1021       OldMask = os.umask(0022)
1022       F = open(File + ".tmp", "w", 0644)
1023       os.umask(OldMask)
1024      
1025       for x in host_attrs:
1026          if x[1].has_key("hostname") == 0 or \
1027             x[1].has_key("sshRSAHostKey") == 0:
1028             continue
1029          Host = GetAttr(x, "hostname")
1030          HostNames = [ Host ]
1031          if Host.endswith(HostDomain):
1032             HostNames.append(Host[:-(len(HostDomain) + 1)])
1033      
1034          # in the purpose field [[host|some other text]] (where some other text is optional)
1035          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
1036          # file.  But so that we don't have to add everything we link we can add an asterisk
1037          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
1038          # http linking it we also support [[-hostname]] entries.
1039          for i in x[1].get("purpose", []):
1040             m = PurposeHostField.match(i)
1041             if m:
1042                m = m.group(1)
1043                # we ignore [[*..]] entries
1044                if m.startswith('*'):
1045                   continue
1046                if m.startswith('-'):
1047                   m = m[1:]
1048                if m:
1049                   HostNames.append(m)
1050                   if m.endswith(HostDomain):
1051                      HostNames.append(m[:-(len(HostDomain) + 1)])
1052      
1053          for I in x[1]["sshRSAHostKey"]:
1054             if mode and mode == 'authorized_keys':
1055                hosts = HostToIP(x)
1056                if 'sshdistAuthKeysHost' in x[1]:
1057                   hosts += x[1]['sshdistAuthKeysHost']
1058                clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1059                clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1060                Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1061             else:
1062                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1063             Line = Sanitize(Line) + "\n"
1064             F.write(Line)
1065    # Oops, something unspeakable happened.
1066    except:
1067       Die(File, F, None)
1068       raise
1069    Done(File, F, None)
1070
1071 # Generate the debianhosts file (list of all IP addresses)
1072 def GenHosts(host_attrs, File):
1073    F = None
1074    try:
1075       OldMask = os.umask(0022)
1076       F = open(File + ".tmp", "w", 0644)
1077       os.umask(OldMask)
1078      
1079       seen = set()
1080
1081       for x in host_attrs:
1082
1083          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1084             continue
1085
1086          if not 'ipHostNumber' in x[1]:
1087             continue
1088
1089          addrs = x[1]["ipHostNumber"]
1090          for addr in addrs:
1091             if addr not in seen:
1092                seen.add(addr)
1093                addr = Sanitize(addr) + "\n"
1094                F.write(addr)
1095
1096    # Oops, something unspeakable happened.
1097    except:
1098       Die(File, F, None)
1099       raise
1100    Done(File, F, None)
1101
1102 def replaceTree(src, dst_basedir):
1103    bn = os.path.basename(src)
1104    dst = os.path.join(dst_basedir, bn)
1105    safe_rmtree(dst)
1106    shutil.copytree(src, dst)
1107
1108 def GenKeyrings(OutDir):
1109    for k in Keyrings:
1110       if os.path.isdir(k):
1111          replaceTree(k, OutDir)
1112       else:
1113          shutil.copy(k, OutDir)
1114
1115
1116 def get_accounts(ldap_conn):
1117    # Fetch all the users
1118    passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1119                    ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1120                     "gecos", "loginShell", "userPassword", "shadowLastChange",\
1121                     "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1122                     "shadowExpire", "emailForward", "latitude", "longitude",\
1123                     "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1124                     "keyFingerPrint", "privateSub", "mailDisableMessage",\
1125                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1126                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1127                     "mailContentInspectionAction", "webPassword", "rtcPassword",\
1128                     "bATVToken"])
1129
1130    if passwd_attrs is None:
1131       raise UDEmptyList, "No Users"
1132    accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1133    accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1134
1135    return accounts
1136
1137 def get_hosts(ldap_conn):
1138    # Fetch all the hosts
1139    HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1140                    ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1141                     "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1142
1143    if HostAttrs == None:
1144       raise UDEmptyList, "No Hosts"
1145
1146    HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1147
1148    return HostAttrs
1149
1150
1151 def make_ldap_conn():
1152    # Connect to the ldap server
1153    l = connectLDAP()
1154    # for testing purposes it's sometimes useful to pass username/password
1155    # via the environment
1156    if 'UD_CREDENTIALS' in os.environ:
1157       Pass = os.environ['UD_CREDENTIALS'].split()
1158    else:
1159       F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1160       Pass = F.readline().strip().split(" ")
1161       F.close()
1162    l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1163
1164    return l
1165
1166
1167
1168 def setup_group_maps(l):
1169    # Fetch all the groups
1170    group_id_map = {}
1171    subgroup_map = {}
1172    attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1173                      ["gid", "gidNumber", "subGroup"])
1174
1175    # Generate the subgroup_map and group_id_map
1176    for x in attrs:
1177       if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1178          continue
1179       if x[1].has_key("gidNumber") == 0:
1180          continue
1181       group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1182       if x[1].has_key("subGroup") != 0:
1183          subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1184
1185    global SubGroupMap
1186    global GroupIDMap
1187    SubGroupMap = subgroup_map
1188    GroupIDMap = group_id_map
1189
1190 def generate_all(global_dir, ldap_conn):
1191    accounts = get_accounts(ldap_conn)
1192    host_attrs = get_hosts(ldap_conn)
1193
1194    global_dir += '/'
1195    # Generate global things
1196    accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1197
1198    accounts = filter(lambda x: not IsRetired(x), accounts)
1199
1200    CheckForward(accounts)
1201
1202    GenMailDisable(accounts, global_dir + "mail-disable")
1203    GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1204    GenDBM(accounts, global_dir + "mail-forward.db", 'emailForward')
1205    GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1206    GenDBM(accounts, global_dir + "mail-contentinspectionaction.db", 'mailContentInspectionAction')
1207    GenPrivate(accounts, global_dir + "debian-private")
1208    GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1209    GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1210    GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1211    GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1212    GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1213    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1214    GenWebPassword(accounts, global_dir + "web-passwords")
1215    GenRtcPassword(accounts, global_dir + "rtc-passwords")
1216    GenKeyrings(global_dir)
1217
1218    # Compatibility.
1219    GenForward(accounts, global_dir + "forward-alias")
1220
1221    GenAllUsers(accounts, global_dir + 'all-accounts.json')
1222    accounts = filter(lambda a: not a in accounts_disabled, accounts)
1223
1224    ssh_userkeys = GenSSHShadow(global_dir, accounts)
1225    GenMarkers(accounts, global_dir + "markers")
1226    GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1227    GenHosts(host_attrs, global_dir + "debianhosts")
1228    GenSSHGitolite(accounts, host_attrs, global_dir + "ssh-gitolite")
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       DoLink(global_dir, OutDir, "ssh-gitolite")
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: