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