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