Fix ipv6 check
[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 IsDebianHost = re.compile(ConfModule.dns_hostmatch)
69 isSSHFP = re.compile("^\s*IN\s+SSHFP")
70 DNSZone = ".debian.net"
71 Keyrings = ConfModule.sync_keyrings.split(":")
72 GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
73 MX_remap = json.loads(ConfModule.MX_remap)
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 is_ipv6_addr(i):
822    try:
823       socket.inet_pton(socket.AF_INET6, i)
824    except socket.error:
825       return False
826    return True
827
828 def ExtractDNSInfo(x):
829
830    TTLprefix="\t"
831    if 'dnsTTL' in x[1]:
832       TTLprefix="%s\t"%(x[1]["dnsTTL"][0])
833
834    DNSInfo = []
835    if x[1].has_key("ipHostNumber"):
836       for I in x[1]["ipHostNumber"]:
837          if is_ipv6_addr(I):
838             DNSInfo.append("%sIN\tAAAA\t%s" % (TTLprefix, I))
839          else:
840             DNSInfo.append("%sIN\tA\t%s" % (TTLprefix, I))
841
842    Algorithm = None
843
844    if 'sshRSAHostKey' in x[1]:
845       for I in x[1]["sshRSAHostKey"]:
846          Split = I.split()
847          if Split[0] == 'ssh-rsa':
848             Algorithm = 1
849          if Split[0] == 'ssh-dss':
850             Algorithm = 2
851          if Algorithm == None:
852             continue
853          Fingerprint = hashlib.new('sha1', base64.decodestring(Split[1])).hexdigest()
854          DNSInfo.append("%sIN\tSSHFP\t%u 1 %s" % (TTLprefix, Algorithm, Fingerprint))
855
856    if 'architecture' in x[1]:
857       Arch = GetAttr(x, "architecture")
858       Mach = ""
859       if x[1].has_key("machine"):
860          Mach = " " + GetAttr(x, "machine")
861       DNSInfo.append("%sIN\tHINFO\t\"%s%s\" \"%s\"" % (TTLprefix, Arch, Mach, "Debian GNU/Linux"))
862
863    if x[1].has_key("mXRecord"):
864       for I in x[1]["mXRecord"]:
865          if I in MX_remap:
866             for e in MX_remap[I]:
867                DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, e))
868          else:
869             DNSInfo.append("%sIN\tMX\t%s" % (TTLprefix, I))
870
871    return DNSInfo
872
873 # Generate the DNS records
874 def GenZoneRecords(host_attrs, File):
875    F = None
876    try:
877       F = open(File + ".tmp", "w")
878
879       # Fetch all the hosts
880       for x in host_attrs:
881          if x[1].has_key("hostname") == 0:
882             continue
883
884          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
885             continue
886
887          DNSInfo = ExtractDNSInfo(x)
888          start = True
889          for Line in DNSInfo:
890             if start == True:
891                Line = "%s.\t%s" % (GetAttr(x, "hostname"), Line)
892                start = False
893             else:
894                Line = "\t\t\t%s" % (Line)
895
896             F.write(Line + "\n")
897
898         # this would write sshfp lines for services on machines
899         # but we can't yet, since some are cnames and we'll make
900         # an invalid zonefile
901         #
902         # for i in x[1].get("purpose", []):
903         #    m = PurposeHostField.match(i)
904         #    if m:
905         #       m = m.group(1)
906         #       # we ignore [[*..]] entries
907         #       if m.startswith('*'):
908         #          continue
909         #       if m.startswith('-'):
910         #          m = m[1:]
911         #       if m:
912         #          if not m.endswith(HostDomain):
913         #             continue
914         #          if not m.endswith('.'):
915         #             m = m + "."
916         #          for Line in DNSInfo:
917         #             if isSSHFP.match(Line):
918         #                Line = "%s\t%s" % (m, Line)
919         #                F.write(Line + "\n")
920
921    # Oops, something unspeakable happened.
922    except:
923       Die(File, F, None)
924       raise
925    Done(File, F, None)
926
927 # Generate the BSMTP file
928 def GenBSMTP(accounts, File, HomePrefix):
929    F = None
930    try:
931       F = open(File + ".tmp", "w")
932      
933       # Write out the zone file entry for each user
934       for a in accounts:
935          if not 'dnsZoneEntry' in a: continue
936          if not a.is_active_user(): continue
937
938          try:
939             for z in a["dnsZoneEntry"]:
940                Split = z.lower().split()
941                if Split[1].lower() == 'in':
942                   for y in range(0, len(Split)):
943                      if Split[y] == "$":
944                         Split[y] = "\n\t"
945                   Line = " ".join(Split) + "\n"
946      
947                   Host = Split[0] + DNSZone
948                   if BSMTPCheck.match(Line) != None:
949                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
950                                   a['uid'], HomePrefix, a['uid'], Host))
951      
952          except:
953             F.write("; Errors\n")
954             pass
955   
956    # Oops, something unspeakable happened.
957    except:
958       Die(File, F, None)
959       raise
960    Done(File, F, None)
961   
962 def HostToIP(Host, mapped=True):
963
964    IPAdresses = []
965
966    if Host[1].has_key("ipHostNumber"):
967       for addr in Host[1]["ipHostNumber"]:
968          IPAdresses.append(addr)
969          if not is_ipv6_addr(addr) and mapped == "True":
970             IPAdresses.append("::ffff:"+addr)
971
972    return IPAdresses
973
974 # Generate the ssh known hosts file
975 def GenSSHKnown(host_attrs, File, mode=None, lockfilename=None):
976    F = None
977    try:
978       OldMask = os.umask(0022)
979       F = open(File + ".tmp", "w", 0644)
980       os.umask(OldMask)
981      
982       for x in host_attrs:
983          if x[1].has_key("hostname") == 0 or \
984             x[1].has_key("sshRSAHostKey") == 0:
985             continue
986          Host = GetAttr(x, "hostname")
987          HostNames = [ Host ]
988          if Host.endswith(HostDomain):
989             HostNames.append(Host[:-(len(HostDomain) + 1)])
990      
991          # in the purpose field [[host|some other text]] (where some other text is optional)
992          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
993          # file.  But so that we don't have to add everything we link we can add an asterisk
994          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
995          # http linking it we also support [[-hostname]] entries.
996          for i in x[1].get("purpose", []):
997             m = PurposeHostField.match(i)
998             if m:
999                m = m.group(1)
1000                # we ignore [[*..]] entries
1001                if m.startswith('*'):
1002                   continue
1003                if m.startswith('-'):
1004                   m = m[1:]
1005                if m:
1006                   HostNames.append(m)
1007                   if m.endswith(HostDomain):
1008                      HostNames.append(m[:-(len(HostDomain) + 1)])
1009      
1010          for I in x[1]["sshRSAHostKey"]:
1011             if mode and mode == 'authorized_keys':
1012                hosts = HostToIP(x)
1013                if 'sshdistAuthKeysHost' in x[1]:
1014                   hosts += x[1]['sshdistAuthKeysHost']
1015                clientcommand='rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s'%(Host)
1016                clientcommand="flock -s %s -c '%s'"%(lockfilename, clientcommand)
1017                Line = 'command="%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (clientcommand, ",".join(hosts), I)
1018             else:
1019                Line = "%s %s" %(",".join(HostNames + HostToIP(x, False)), I)
1020             Line = Sanitize(Line) + "\n"
1021             F.write(Line)
1022    # Oops, something unspeakable happened.
1023    except:
1024       Die(File, F, None)
1025       raise
1026    Done(File, F, None)
1027
1028 # Generate the debianhosts file (list of all IP addresses)
1029 def GenHosts(host_attrs, File):
1030    F = None
1031    try:
1032       OldMask = os.umask(0022)
1033       F = open(File + ".tmp", "w", 0644)
1034       os.umask(OldMask)
1035      
1036       seen = set()
1037
1038       for x in host_attrs:
1039
1040          if IsDebianHost.match(GetAttr(x, "hostname")) is None:
1041             continue
1042
1043          if not 'ipHostNumber' in x[1]:
1044             continue
1045
1046          addrs = x[1]["ipHostNumber"]
1047          for addr in addrs:
1048             if addr not in seen:
1049                seen.add(addr)
1050                addr = Sanitize(addr) + "\n"
1051                F.write(addr)
1052
1053    # Oops, something unspeakable happened.
1054    except:
1055       Die(File, F, None)
1056       raise
1057    Done(File, F, None)
1058
1059 def replaceTree(src, dst_basedir):
1060    bn = os.path.basename(src)
1061    dst = os.path.join(dst_basedir, bn)
1062    safe_rmtree(dst)
1063    shutil.copytree(src, dst)
1064
1065 def GenKeyrings(OutDir):
1066    for k in Keyrings:
1067       if os.path.isdir(k):
1068          replaceTree(k, OutDir)
1069       else:
1070          shutil.copy(k, OutDir)
1071
1072
1073 def get_accounts(ldap_conn):
1074    # Fetch all the users
1075    passwd_attrs = ldap_conn.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "(&(uid=*)(!(uidNumber=0))(objectClass=shadowAccount))",\
1076                    ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1077                     "gecos", "loginShell", "userPassword", "shadowLastChange",\
1078                     "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1079                     "shadowExpire", "emailForward", "latitude", "longitude",\
1080                     "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1081                     "keyFingerPrint", "privateSub", "mailDisableMessage",\
1082                     "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1083                     "mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
1084                     "mailContentInspectionAction", "webPassword", "voipPassword"])
1085
1086    if passwd_attrs is None:
1087       raise UDEmptyList, "No Users"
1088    accounts = map(lambda x: UDLdap.Account(x[0], x[1]), passwd_attrs)
1089    accounts.sort(lambda x,y: cmp(x['uid'].lower(), y['uid'].lower()))
1090
1091    return accounts
1092
1093 def get_hosts(ldap_conn):
1094    # Fetch all the hosts
1095    HostAttrs    = ldap_conn.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "objectClass=debianServer",\
1096                    ["hostname", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions",\
1097                     "mXRecord", "ipHostNumber", "dnsTTL", "machine", "architecture"])
1098
1099    if HostAttrs == None:
1100       raise UDEmptyList, "No Hosts"
1101
1102    HostAttrs.sort(lambda x, y: cmp((GetAttr(x, "hostname")).lower(), (GetAttr(y, "hostname")).lower()))
1103
1104    return HostAttrs
1105
1106
1107 def make_ldap_conn():
1108    # Connect to the ldap server
1109    l = connectLDAP()
1110    # for testing purposes it's sometimes useful to pass username/password
1111    # via the environment
1112    if 'UD_CREDENTIALS' in os.environ:
1113       Pass = os.environ['UD_CREDENTIALS'].split()
1114    else:
1115       F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1116       Pass = F.readline().strip().split(" ")
1117       F.close()
1118    l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1119
1120    return l
1121
1122
1123
1124 def setup_group_maps(l):
1125    # Fetch all the groups
1126    group_id_map = {}
1127    subgroup_map = {}
1128    attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1129                      ["gid", "gidNumber", "subGroup"])
1130
1131    # Generate the subgroup_map and group_id_map
1132    for x in attrs:
1133       if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
1134          continue
1135       if x[1].has_key("gidNumber") == 0:
1136          continue
1137       group_id_map[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1138       if x[1].has_key("subGroup") != 0:
1139          subgroup_map.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1140
1141    global SubGroupMap
1142    global GroupIDMap
1143    SubGroupMap = subgroup_map
1144    GroupIDMap = group_id_map
1145
1146 def generate_all(global_dir, ldap_conn):
1147    accounts = get_accounts(ldap_conn)
1148    host_attrs = get_hosts(ldap_conn)
1149
1150    global_dir += '/'
1151    # Generate global things
1152    accounts_disabled = GenDisabledAccounts(accounts, global_dir + "disabled-accounts")
1153
1154    accounts = filter(lambda x: not IsRetired(x), accounts)
1155    #accounts_DDs = filter(lambda x: IsGidDebian(x), accounts)
1156
1157    CheckForward(accounts)
1158
1159    GenMailDisable(accounts, global_dir + "mail-disable")
1160    GenCDB(accounts, global_dir + "mail-forward.cdb", 'emailForward')
1161    GenCDB(accounts, global_dir + "mail-contentinspectionaction.cdb", 'mailContentInspectionAction')
1162    GenPrivate(accounts, global_dir + "debian-private")
1163    GenSSHKnown(host_attrs, global_dir+"authorized_keys", 'authorized_keys', global_dir+'ud-generate.lock')
1164    GenMailBool(accounts, global_dir + "mail-greylist", "mailGreylisting")
1165    GenMailBool(accounts, global_dir + "mail-callout", "mailCallout")
1166    GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
1167    GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
1168    GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
1169    GenWebPassword(accounts, global_dir + "web-passwords")
1170    GenVoipPassword(accounts, global_dir + "voip-passwords")
1171    GenKeyrings(global_dir)
1172
1173    # Compatibility.
1174    GenForward(accounts, global_dir + "forward-alias")
1175
1176    GenAllUsers(accounts, global_dir + 'all-accounts.json')
1177    accounts = filter(lambda a: not a in accounts_disabled, accounts)
1178
1179    ssh_userkeys = GenSSHShadow(global_dir, accounts)
1180    GenMarkers(accounts, global_dir + "markers")
1181    GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
1182    GenHosts(host_attrs, global_dir + "debianhosts")
1183    GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
1184
1185    GenDNS(accounts, global_dir + "dns-zone")
1186    GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
1187
1188    setup_group_maps(ldap_conn)
1189
1190    for host in host_attrs:
1191       if not "hostname" in host[1]:
1192          continue
1193       generate_host(host, global_dir, accounts, ssh_userkeys)
1194
1195 def generate_host(host, global_dir, all_accounts, ssh_userkeys):
1196    current_host = host[1]['hostname'][0]
1197    OutDir = global_dir + current_host + '/'
1198    if not os.path.isdir(OutDir):
1199       os.mkdir(OutDir)
1200
1201    # Get the group list and convert any named groups to numerics
1202    GroupList = {}
1203    for groupname in AllowedGroupsPreload.strip().split(" "):
1204       GroupList[groupname] = True
1205    if 'allowedGroups' in host[1]:
1206       for groupname in host[1]['allowedGroups']:
1207          GroupList[groupname] = True
1208    for groupname in GroupList.keys():
1209       if groupname in GroupIDMap:
1210          GroupList[str(GroupIDMap[groupname])] = True
1211
1212    ExtraList = {}
1213    if 'exportOptions' in host[1]:
1214       for extra in host[1]['exportOptions']:
1215          ExtraList[extra.upper()] = True
1216
1217    if GroupList != {}:
1218       accounts = filter(lambda x: IsInGroup(x, GroupList, current_host), all_accounts)
1219
1220    DoLink(global_dir, OutDir, "debianhosts")
1221    DoLink(global_dir, OutDir, "ssh_known_hosts")
1222    DoLink(global_dir, OutDir, "disabled-accounts")
1223
1224    sys.stdout.flush()
1225    if 'NOPASSWD' in ExtraList:
1226       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "*")
1227    else:
1228       userlist = GenPasswd(accounts, OutDir + "passwd", HomePrefix, "x")
1229    sys.stdout.flush()
1230    grouprevmap = GenGroup(accounts, OutDir + "group", current_host)
1231    GenShadowSudo(accounts, OutDir + "sudo-passwd", ('UNTRUSTED' in ExtraList) or ('NOPASSWD' in ExtraList), current_host)
1232
1233    # Now we know who we're allowing on the machine, export
1234    # the relevant ssh keys
1235    GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'), current_host)
1236
1237    if not 'NOPASSWD' in ExtraList:
1238       GenShadow(accounts, OutDir + "shadow")
1239
1240    # Link in global things
1241    if not 'NOMARKERS' in ExtraList:
1242       DoLink(global_dir, OutDir, "markers")
1243    DoLink(global_dir, OutDir, "mail-forward.cdb")
1244    DoLink(global_dir, OutDir, "mail-contentinspectionaction.cdb")
1245    DoLink(global_dir, OutDir, "mail-disable")
1246    DoLink(global_dir, OutDir, "mail-greylist")
1247    DoLink(global_dir, OutDir, "mail-callout")
1248    DoLink(global_dir, OutDir, "mail-rbl")
1249    DoLink(global_dir, OutDir, "mail-rhsbl")
1250    DoLink(global_dir, OutDir, "mail-whitelist")
1251    DoLink(global_dir, OutDir, "all-accounts.json")
1252    GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
1253    GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
1254    GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
1255
1256    # Compatibility.
1257    DoLink(global_dir, OutDir, "forward-alias")
1258
1259    if 'DNS' in ExtraList:
1260       DoLink(global_dir, OutDir, "dns-zone")
1261       DoLink(global_dir, OutDir, "dns-sshfp")
1262
1263    if 'AUTHKEYS' in ExtraList:
1264       DoLink(global_dir, OutDir, "authorized_keys")
1265
1266    if 'BSMTP' in ExtraList:
1267       GenBSMTP(accounts, OutDir + "bsmtp", HomePrefix)
1268
1269    if 'PRIVATE' in ExtraList:
1270       DoLink(global_dir, OutDir, "debian-private")
1271
1272    if 'GITOLITE' in ExtraList:
1273       DoLink(global_dir, OutDir, "ssh-gitolite")
1274    if 'exportOptions' in host[1]:
1275       for entry in host[1]['exportOptions']:
1276          v = entry.split('=',1)
1277          if v[0] != 'GITOLITE' or len(v) != 2: continue
1278          gitolite_accounts = filter(lambda x: IsInGroup(x, [v[1]], current_host), all_accounts)
1279          GenSSHGitolite(gitolite_accounts, OutDir + "ssh-gitolite-%s"%(v[1],))
1280
1281    if 'WEB-PASSWORDS' in ExtraList:
1282       DoLink(global_dir, OutDir, "web-passwords")
1283
1284    if 'VOIP-PASSWORDS' in ExtraList:
1285       DoLink(global_dir, OutDir, "voip-passwords")
1286
1287    if 'KEYRING' in ExtraList:
1288       for k in Keyrings:
1289          bn = os.path.basename(k)
1290          if os.path.isdir(k):
1291             src = os.path.join(global_dir, bn)
1292             replaceTree(src, OutDir)
1293          else:
1294             DoLink(global_dir, OutDir, bn)
1295    else:
1296       for k in Keyrings:
1297          try:
1298             bn = os.path.basename(k)
1299             target = os.path.join(OutDir, bn)
1300             if os.path.isdir(target):
1301                safe_rmtree(dst)
1302             else:
1303                posix.remove(target)
1304          except:
1305             pass
1306    DoLink(global_dir, OutDir, "last_update.trace")
1307
1308
1309 def getLastLDAPChangeTime(l):
1310    mods = l.search_s('cn=log',
1311          ldap.SCOPE_ONELEVEL,
1312          '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
1313          ['reqEnd'])
1314
1315    last = 0
1316
1317    # Sort the list by reqEnd
1318    sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
1319    # Take the last element in the array
1320    last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
1321
1322    return last
1323
1324 def getLastKeyringChangeTime():
1325    krmod = 0
1326    for k in Keyrings:
1327       mt = os.path.getmtime(k)
1328       if mt > krmod:
1329          krmod = mt
1330
1331    return krmod
1332
1333 def getLastBuildTime(gdir):
1334    cache_last_ldap_mod = 0
1335    cache_last_unix_mod = 0
1336
1337    try:
1338       fd = open(os.path.join(gdir, "last_update.trace"), "r")
1339       cache_last_mod=fd.read().split()
1340       try:
1341          cache_last_ldap_mod = cache_last_mod[0]
1342          cache_last_unix_mod = int(cache_last_mod[1])
1343       except IndexError, ValueError:
1344          pass
1345       fd.close()
1346    except IOError, e:
1347       if e.errno == errno.ENOENT:
1348          pass
1349       else:
1350          raise e
1351
1352    return (cache_last_ldap_mod, cache_last_unix_mod)
1353
1354 def ud_generate():
1355    parser = optparse.OptionParser()
1356    parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
1357      help="Output directory.")
1358    parser.add_option("-f", "--force", dest="force", action="store_true",
1359      help="Force generation, even if no update to LDAP has happened.")
1360
1361    (options, args) = parser.parse_args()
1362    if len(args) > 0:
1363       parser.print_help()
1364       sys.exit(1)
1365
1366    if options.generatedir is not None:
1367       generate_dir = os.environ['UD_GENERATEDIR']
1368    elif 'UD_GENERATEDIR' in os.environ:
1369       generate_dir = os.environ['UD_GENERATEDIR']
1370    else:
1371       generate_dir = GenerateDir
1372
1373
1374    lockf = os.path.join(generate_dir, 'ud-generate.lock')
1375    lock = get_lock( lockf )
1376    if lock is None:
1377       sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
1378       sys.exit(1)
1379
1380    l = make_ldap_conn()
1381
1382    time_started = int(time.time())
1383    ldap_last_mod = getLastLDAPChangeTime(l)
1384    unix_last_mod = getLastKeyringChangeTime()
1385    cache_last_ldap_mod, cache_last_unix_mod = getLastBuildTime(generate_dir)
1386
1387    need_update = (ldap_last_mod > cache_last_ldap_mod) or (unix_last_mod > cache_last_unix_mod)
1388
1389    if not options.force and not need_update:
1390       fd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1391       fd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1392       fd.close()
1393       sys.exit(0)
1394
1395    tracefd = open(os.path.join(generate_dir, "last_update.trace"), "w")
1396    generate_all(generate_dir, l)
1397    tracefd.write("%s\n%s\n" % (ldap_last_mod, time_started))
1398    tracefd.close()
1399
1400
1401 if __name__ == "__main__":
1402    if 'UD_PROFILE' in os.environ:
1403       import cProfile
1404       import pstats
1405       cProfile.run('ud_generate()', "udg_prof")
1406       p = pstats.Stats('udg_prof')
1407       ##p.sort_stats('time').print_stats()
1408       p.sort_stats('cumulative').print_stats()
1409    else:
1410       ud_generate()
1411
1412 # vim:set et:
1413 # vim:set ts=3:
1414 # vim:set shiftwidth=3: