release 0.3.92
[mirror/userdir-ldap.git] / ud-sync-accounts-to-afs
1 #!/usr/bin/python
2
3 # Copyright (c) 2010 Peter Palfrader
4
5 # reads a list of active accounts from /var/lib/misc/thishost/all-accounts.json
6 # and creates those accounts in AFS's protection database.
7 # Furthermore it creates per-user scratch directories in
8 # /afs/debian.org/scratch/eu/grnet (or whatever path is specified in a command
9 # line option), owned by that user.
10
11 import optparse
12 import os
13 import os.path
14 import pwd
15 import re
16 import subprocess
17 import sys
18 import tempfile
19
20
21 try:
22    import simplejson as json
23 except ImportError:
24    import json # this better be pthon 2.6's json..
25
26 class UserEntries:
27    def __init__(self):
28       self.entries = []
29       self.by_name = {}
30       self.by_id = {}
31
32    def append(self, name, idnumber, owner=None, creator=None):
33       if name in self.by_name: raise Exception("Name '%s' is not unique."%(name))
34       if idnumber in self.by_id: raise Exception("ID '%d' is not unique."%(idnumber))
35
36       h = { 'name': name, 'id': idnumber, 'owner': owner, 'creator': creator }
37       self.entries.append( h )
38       self.by_name[name] = h
39       self.by_id[idnumber] = h
40
41    def del_id(self, i):
42       h = self.by_id[i]
43       del self.by_id[i]
44       del self.by_name[h['name']]
45       self.entries.remove(h)
46
47    def del_name(self, n):
48       self.del_id(self.by_name[n]['id'])
49
50 def load_expected():
51    accountsfile = '/var/lib/misc/thishost/all-accounts.json'
52
53    if not os.path.isfile(accountsfile):
54       print >> sys.stderr, "Accountsfile %s not found."%(accountsfile)
55    accounts_json = open(accountsfile, 'r').read()
56    accounts = json.loads(accounts_json)
57
58    entries = UserEntries()
59    for a in accounts:
60       if a['active']:
61          entries.append(a['uid'], a['uidNumber'])
62    return entries
63
64 def load_existing():
65    entries = UserEntries()
66    l = subprocess.Popen(('pts', 'listentries', '-users'), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None)
67    l.stdin.close()
68    l.stdout.readline() # headers
69    # Name                          ID  Owner Creator
70    for line in l.stdout:
71       line = line.strip()
72       m = re.match('([0-9a-z.-]+) +(\d+) +(-?\d+) +(-?\d+)$', line)
73       if m is None:
74          raise Exception("Cannot parse pts listentries line '%s'."%(line))
75       (name, afsid, owner, creator) = m.groups()
76       entries.append(name, int(afsid), int(owner), int(creator))
77    l.wait()
78    exitcode = l.returncode
79    if not exitcode == 0:
80       raise Exception("pts listentries -users exited with non-zero exit code %d."%(exitcode))
81    return entries
82
83 class Krb:
84    def __init__(self, keytab, principal):
85       (fd_dummy, self.ccachefile) = tempfile.mkstemp(prefix='krb5cc')
86       os.environ['KRB5CCNAME'] = self.ccachefile
87       self.kinit(keytab, principal)
88
89    def kinit(self, keytab, principal):
90       subprocess.check_call( ('kinit', '-t', keytab, principal) )
91    def klist(self):
92       subprocess.check_call( ('klist') )
93    def kdestroy(self):
94       if os.path.exists(self.ccachefile):
95          subprocess.check_call( ('kdestroy') )
96       if os.path.exists(self.ccachefile):
97          os.unlink(self.ccachefile)
98
99 def filter_common(a, b):
100    ids = a.by_id.keys()
101    for i in ids:
102       if i in b.by_id:
103          if a.by_id[i]['name'] == b.by_id[i]['name']:
104             #print "Common: %s (%d)"%(a.by_id[i]['name'], i)
105             a.del_id(i)
106             b.del_id(i)
107          else:
108             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']))
109             sys.exit(1)
110
111    # just make sure there are not same names on both sides
112    # but with differend uids:
113    for n in a.by_name:
114       if n in b.by_name:
115          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']))
116          sys.exit(1)
117
118 def filter_have(a):
119    # removing from the list means we keep the account and
120    # do not delete it later on.
121    names = a.by_name.keys()
122    for n in names:
123       if n == 'anonymous': # keep account, so remove from the have list
124          a.del_name(n)
125          continue
126       m = re.match('[0-9a-z-]+$', n)
127       if not m: # weird name, probably has dots like weasel.admin etc.
128          a.del_name(n)
129          continue
130
131 def remove_extra(have, ifownedby):
132    for name in have.by_name:
133       if have.by_name[name]['creator'] == ifownedby:
134          subprocess.check_call( ('pts', 'delete', name) )
135          print "Deleted user %s(%d)."%(name, have.by_name[name]['id'])
136       else:
137          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'])
138
139 def add_new(want):
140    #for name in want.by_name:
141    #   subprocess.check_call( ('pts', 'createuser', '-name', name, '-id', '%d'%(want.by_name[name]['id'])) )
142    #   print "Added user %s(%d)."%(name, want.by_name[name]['id'])
143    names = []
144    ids = []
145    for name in want.by_name:
146       names.append(name)
147       ids.append('%d'%(want.by_name[name]['id']))
148
149    if len(names) == 0: return
150
151    args = ['pts', 'createuser']
152    for n in names:
153       args.append('-name')
154       args.append(n)
155    for i in ids:
156       args.append('-id')
157       args.append(i)
158    subprocess.check_call(args)
159
160
161 def do_accounts():
162    want = load_expected()
163    have = load_existing()
164
165    if not options.user in have.by_name:
166       print >> sys.stderr, "Cannot find our user, '%s', in pts listentries"%(options.user)
167       sys.exit(1)
168    me = have.by_name[options.user]
169
170    filter_common(have, want)
171    filter_have(have)
172    # just for the sake of it, make sure 'want' does not have weird names either.
173    # this gets rid of a few accounts with underscores in them, like buildd_$ARCH
174    # but we might not care about them in AFS anyway
175    filter_have(want)
176
177    remove_extra(have, me['id'])
178    add_new(want)
179
180    created_some = len(want.by_id) > 0
181    return created_some
182
183 def do_scratchdir(d):
184    have = load_existing()
185    filter_have(have)
186
187    if not os.path.isdir(d):
188       print >> sys.stderr, "Path '%s' is not a directory"%(d)
189
190    for n in have.by_name:
191       tree = ( n[0], n[0:2] )
192
193       p = d
194       for t in tree:
195          p = os.path.join(p, t)
196          if not os.path.exists(p): os.mkdir(p)
197
198       p = os.path.join(p, n)
199       if os.path.exists(p): continue
200
201       print "Making directory %s"%(p)
202       os.mkdir(p)
203       subprocess.check_call(('fs', 'sa', '-dir', p, '-acl', n, 'all'))
204
205
206 parser = optparse.OptionParser()
207 parser.add_option("-p", "--principal", dest="principal", metavar="name",
208   help="Principal to auth as")
209 parser.add_option("-k", "--keytab", dest="keytab", metavar="file",
210   help="keytab file location")
211 parser.add_option("-P", "--PAGed", action="store_true",
212   help="already running in own PAG")
213 parser.add_option("-s", "--self", dest="user", metavar="ownafsuser",
214   help="This principal's AFS user")
215 parser.add_option("-d", "--dir", dest="scratchdir", action="append",
216   help="scratchdir to create directories in.")
217 parser.add_option("-D", "--dircheck-force", dest="dircheck", action="store_true", default=False,
218   help="Check if all user scratch dirs exist even if no new users were created")
219
220 (options, args) = parser.parse_args()
221 if len(args) > 0:
222    parser.print_help()
223    sys.exit(1)
224
225 if not options.PAGed:
226    #print >> sys.stderr, "running self in new PAG"
227    os.execlp('pagsh', 'pagsh', '-c', ' '.join(sys.argv)+" -P")
228
229 if options.principal is None:
230    options.principal = "%s/admin"%( pwd.getpwuid(os.getuid())[0] )
231 if options.keytab is None:
232    options.keytab = "/etc/userdir-ldap/keytab.%s"%(pwd.getpwuid(os.getuid())[0] )
233 if options.user is None:
234    options.user = options.principal.replace('/', '.')
235 if options.scratchdir is None:
236    options.scratchdir = ['/afs/debian.org/scratch/eu/grnet']
237
238 k = None
239 try:
240    k = Krb(options.keytab, options.principal)
241    subprocess.check_call( ('aklog') )
242    #k.klist()
243    #subprocess.check_call( ('tokens') )
244
245    created_some = do_accounts()
246    if created_some or options.dircheck:
247       for d in options.scratchdir:
248          do_scratchdir(d)
249 finally:
250    try:
251       subprocess.check_call( ('unlog') )
252    except Exception, e:
253       print >> sys.stderr, "During unlog: %s"%(e)
254       pass
255    if k is not None: k.kdestroy()
256
257
258 # vim:set et:
259 # vim:set ts=3:
260 # vim:set shiftwidth=3: