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