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