Add ud-sync-accounts-to-afs, a script to sync accounts to an AFS protection database
authorPeter Palfrader <peter@palfrader.org>
Wed, 15 Sep 2010 10:49:26 +0000 (12:49 +0200)
committerPeter Palfrader <peter@palfrader.org>
Wed, 15 Sep 2010 10:49:26 +0000 (12:49 +0200)
debian/changelog
debian/install
ud-sync-accounts-to-afs [new file with mode: 0755]

index 2157b3a..a3316b7 100644 (file)
@@ -1,8 +1,9 @@
 userdir-ldap (0.3.7X) Xnstable; urgency=low
 
-  * XX
+  * Add ud-sync-accounts-to-afs, a script to sync accounts to an
+    AFS protection database.
 
- -- Peter Palfrader <weasel@debian.org>  Mon, 13 Sep 2010 19:14:21 +0200
+ -- Peter Palfrader <weasel@debian.org>  Wed, 15 Sep 2010 12:48:37 +0200
 
 userdir-ldap (0.3.78) unstable; urgency=low
 
index 31d24f8..5800025 100644 (file)
@@ -12,6 +12,7 @@ ud-ldapshow usr/bin
 ud-userimport usr/bin
 ud-mailgate usr/bin
 ud-krb-reset usr/bin
+ud-sync-accounts-to-afs usr/bin
 ud-generate usr/bin
 ud-passchk usr/bin
 ud-useradd usr/bin
diff --git a/ud-sync-accounts-to-afs b/ud-sync-accounts-to-afs
new file mode 100755 (executable)
index 0000000..6ce93ef
--- /dev/null
@@ -0,0 +1,261 @@
+#!/usr/bin/python
+
+# Copyright (c) 2010 Peter Palfrader
+
+# reads a list of active accounts from /var/lib/misc/thishost/all-accounts.json
+# and creates those accounts in AFS's protection database.
+# Furthermore it creates per-user scratch directories in
+# /afs/debian.org/scratch/eu/grnet (or whatever path is specified in a command
+# line option), owned by that user.
+
+import optparse
+import os
+import os.path
+import pwd
+import re
+import subprocess
+import sys
+import tempfile
+
+
+import json
+if not '__author__' in json.__dict__:
+   sys.stderr.write("Warning: This is probably the wrong json module.  We want python 2.6's json\n")
+   sys.stderr.write("module, or simplejson on python 2.5.  Let's see if/how stuff blows up.\n")
+   import simplejson as json
+
+class UserEntries:
+   def __init__(self):
+      self.entries = []
+      self.by_name = {}
+      self.by_id = {}
+
+   def append(self, name, idnumber, owner=None, creator=None):
+      if name in self.by_name: raise Exception("Name '%s' is not unique."%(name))
+      if idnumber in self.by_id: raise Exception("ID '%d' is not unique."%(idnumber))
+
+      h = { 'name': name, 'id': idnumber, 'owner': owner, 'creator': creator }
+      self.entries.append( h )
+      self.by_name[name] = h
+      self.by_id[idnumber] = h
+
+   def del_id(self, i):
+      h = self.by_id[i]
+      del self.by_id[i]
+      del self.by_name[h['name']]
+      self.entries.remove(h)
+
+   def del_name(self, n):
+      self.del_id(self.by_name[n]['id'])
+
+def load_expected():
+   accountsfile = '/var/lib/misc/thishost/all-accounts.json'
+
+   if not os.path.isfile(accountsfile):
+      print >> sys.stderr, "Accountsfile %s not found."%(accountsfile)
+   accounts_json = open(accountsfile, 'r').read()
+   accounts = json.loads(accounts_json)
+
+   entries = UserEntries()
+   for a in accounts:
+      if a['active']:
+         entries.append(a['uid'], a['uidNumber'])
+   return entries
+
+def load_existing():
+   entries = UserEntries()
+   l = subprocess.Popen(('pts', 'listentries', '-users'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
+   l.stdin.close()
+   l.stdout.readline() # headers
+   # Name                          ID  Owner Creator
+   for line in l.stdout:
+      line = line.strip()
+      m = re.match('([0-9a-z.-]+) +(\d+) +(-?\d+) +(-?\d+)$', line)
+      if m is None:
+         raise Exception("Cannot parse pts listentries line '%s'."%(line))
+      (name, afsid, owner, creator) = m.groups()
+      entries.append(name, int(afsid), int(owner), int(creator))
+   l.wait()
+   exitcode = l.returncode
+   if not exitcode == 0:
+      raise Exception("pts listentries -users exited with non-zero exit code %d."%(exitcode))
+   return entries
+
+class Krb:
+   def __init__(self, keytab, principal):
+      (fd_dummy, self.ccachefile) = tempfile.mkstemp(prefix='krb5cc')
+      os.environ['KRB5CCNAME'] = self.ccachefile
+      self.kinit(keytab, principal)
+
+   def kinit(self, keytab, principal):
+      subprocess.check_call( ('kinit', '-t', keytab, principal) )
+   def klist(self):
+      subprocess.check_call( ('klist') )
+   def kdestroy(self):
+      if os.path.exists(self.ccachefile):
+         subprocess.check_call( ('kdestroy') )
+      if os.path.exists(self.ccachefile):
+         os.unlink(self.ccachefile)
+
+def filter_common(a, b):
+   ids = a.by_id.keys()
+   for i in ids:
+      if i in b.by_id:
+         if a.by_id[i]['name'] == b.by_id[i]['name']:
+            #print "Common: %s (%d)"%(a.by_id[i]['name'], i)
+            a.del_id(i)
+            b.del_id(i)
+         else:
+            print >> sys.stderr, Excetption("ID %d has different names in our two databases ('%s' vs. '%s')."%(i, a.by_id[i]['name'], b.by_id[i]['name']))
+            sys.exit(1)
+
+   # just make sure there are not same names on both sides
+   # but with differend uids:
+   for n in a.by_name:
+      if n in b.by_name:
+         print >> sys.stderr, Excetption("Name %n has different IDs in our two databases ('%d' vs. '%d')."%(n, a.by_name[n]['id'], b.by_name[n]['id']))
+         sys.exit(1)
+
+def filter_have(a):
+   # removing from the list means we keep the account and
+   # do not delete it later on.
+   names = a.by_name.keys()
+   for n in names:
+      if n == 'anonymous': # keep account, so remove from the have list
+         a.del_name(n)
+         continue
+      m = re.match('[0-9a-z]+$', n)
+      if not m: # weird name, probably has dots like weasel.admin etc.
+         a.del_name(n)
+         continue
+
+def remove_extra(have, ifownedby):
+   for name in have.by_name:
+      if have.by_name[name]['creator'] == ifownedby:
+         subprocess.check_call( ('pts', 'delete', name) )
+         print "Deleted user %s(%d)."%(name, have.by_name[name]['id'])
+      else:
+         print >> sys.stderr, "Did not delete %s because it was not created by me(%d) but by %d."%(name, ifownedby, have.by_name[name]['creator'])
+
+def add_new(want):
+   #for name in want.by_name:
+   #   subprocess.check_call( ('pts', 'createuser', '-name', name, '-id', '%d'%(want.by_name[name]['id'])) )
+   #   print "Added user %s(%d)."%(name, want.by_name[name]['id'])
+   names = []
+   ids = []
+   for name in want.by_name:
+      names.append(name)
+      ids.append('%d'%(want.by_name[name]['id']))
+
+   if len(names) == 0: return
+
+   args = ['pts', 'createuser']
+   for n in names:
+      args.append('-name')
+      args.append(n)
+   for i in ids:
+      args.append('-id')
+      args.append(i)
+   subprocess.check_call(args)
+
+
+def do_accounts():
+   want = load_expected()
+   have = load_existing()
+
+   if not options.user in have.by_name:
+      print >> sys.stderr, "Cannot find our user, '%s', in pts listentries"%(options.user)
+      sys.exit(1)
+   me = have.by_name[options.user]
+
+   filter_common(have, want)
+   filter_have(have)
+   # just for the sake of it, make sure 'want' does not have weird names either.
+   # this gets rid of a few accounts with underscores in them, like buildd_$ARCH
+   # but we might not care about them in AFS anyway
+   filter_have(want)
+
+   remove_extra(have, me['id'])
+   add_new(want)
+
+   created_some = len(want.by_id) > 0
+   return created_some
+
+def do_scratchdir(d):
+   have = load_existing()
+   filter_have(have)
+
+   if not os.path.isdir(d):
+      print >> sys.stderr, "Path '%s' is not a directory"%(d)
+
+   for n in have.by_name:
+      tree = ( n[0], n[0:2] )
+
+      p = d
+      for t in tree:
+         p = os.path.join(p, t)
+         if not os.path.exists(p): os.mkdir(p)
+
+      p = os.path.join(p, n)
+      if os.path.exists(p): continue
+
+      print "Making directory %s"%(p)
+      os.mkdir(p)
+      subprocess.check_call(('fs', 'sa', '-dir', p, '-acl', n, 'all'))
+
+
+parser = optparse.OptionParser()
+parser.add_option("-p", "--principal", dest="principal", metavar="name",
+  help="Principal to auth as")
+parser.add_option("-k", "--keytab", dest="keytab", metavar="file",
+  help="keytab file location")
+parser.add_option("-P", "--PAGed", action="store_true",
+  help="already running in own PAG")
+parser.add_option("-s", "--self", dest="user", metavar="ownafsuser",
+  help="This principal's AFS user")
+parser.add_option("-d", "--dir", dest="scratchdir", action="append",
+  help="scratchdir to create directories in.")
+parser.add_option("-D", "--dircheck-force", dest="dircheck", action="store_true", default=False,
+  help="Check if all user scratch dirs exist even if no new users were created")
+
+(options, args) = parser.parse_args()
+if len(args) > 0:
+   parser.print_help()
+   sys.exit(1)
+
+if not options.PAGed:
+   #print >> sys.stderr, "running self in new PAG"
+   os.execlp('pagsh', 'pagsh', '-c', ' '.join(sys.argv)+" -P")
+
+if options.principal is None:
+   options.principal = "%s/admin"%( pwd.getpwuid(os.getuid())[0] )
+if options.keytab is None:
+   options.keytab = "/etc/userdir-ldap/keytab.%s"%(pwd.getpwuid(os.getuid())[0] )
+if options.user is None:
+   options.user = options.principal.replace('/', '.')
+if options.scratchdir is None:
+   options.scratchdir = ['/afs/debian.org/scratch/eu/grnet']
+
+k = None
+try:
+   k = Krb(options.keytab, options.principal)
+   subprocess.check_call( ('aklog') )
+   #k.klist()
+   #subprocess.check_call( ('tokens') )
+
+   created_some = do_accounts()
+   if created_some or options.dircheck:
+      for d in options.scratchdir:
+         do_scratchdir(d)
+finally:
+   try:
+      subprocess.check_call( ('unlog') )
+   except Exception, e:
+      print >> sys.stderr, "During unlog: %s"%(e)
+      pass
+   if k is not None: k.kdestroy()
+
+
+# vim:set et:
+# vim:set ts=3:
+# vim:set shiftwidth=3: