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