...
[nodemanager.git] / accounts.py
1 """Functionality common to all account classes.
2
3 Each subclass of Account must provide five methods: create(),
4 destroy(), configure(), start(), and stop().  In addition, it must
5 provide static member variables SHELL, which contains the unique shell
6 that it uses; and TYPE, which contains a description of the type that
7 it uses.  TYPE is divided hierarchically by periods; at the moment the
8 only convention is that all sliver accounts have type that begins with
9 sliver.
10
11 There are any number of race conditions that may result from the fact
12 that account names are not unique over time.  Moreover, it's a bad
13 idea to perform lengthy operations while holding the database lock.
14 In order to deal with both of these problems, we use a worker thread
15 for each account name that ever exists.  On 32-bit systems with large
16 numbers of accounts, this may cause the NM process to run out of
17 *virtual* memory!  This problem may be remedied by decreasing the
18 maximum stack size.
19 """
20
21 import Queue
22 import os
23 import pwd
24 import threading
25
26 import logger
27 import tools
28
29
30 # shell path -> account class association
31 shell_acct_class = {}
32 # account type -> account class association
33 type_acct_class = {}
34
35 def register_class(acct_class):
36     """Call once for each account class.  This method adds the class to the dictionaries used to look up account classes by shell and type."""
37     shell_acct_class[acct_class.SHELL] = acct_class
38     type_acct_class[acct_class.TYPE] = acct_class
39
40
41 # private account name -> worker object association and associated lock
42 name_worker_lock = threading.Lock()
43 name_worker = {}
44
45 def all():
46     """Return the names of all accounts on the system with recognized shells."""
47     return [pw_ent[0] for pw_ent in pwd.getpwall() if pw_ent[6] in shell_acct_class]
48
49 def get(name):
50     """Return the worker object for a particular username.  If no such object exists, create it first."""
51     name_worker_lock.acquire()
52     try:
53         if name not in name_worker: name_worker[name] = Worker(name)
54         return name_worker[name]
55     finally: name_worker_lock.release()
56
57
58 class Account:
59     def __init__(self, rec):
60         self.name = rec['name']
61         self.keys = ''
62         self.configure(rec)
63
64     @staticmethod
65     def create(name): abstract
66     @staticmethod
67     def destroy(name): abstract
68
69     def configure(self, rec):
70         """Write <rec['keys']> to my authorized_keys file."""
71         new_keys = rec['keys']
72         if new_keys != self.keys:
73             self.keys = new_keys
74             dot_ssh = '/home/%s/.ssh' % self.name
75             def do_installation():
76                 if not os.access(dot_ssh, os.F_OK): os.mkdir(dot_ssh)
77                 tools.write_file(dot_ssh + '/authorized_keys', lambda f: f.write(new_keys))
78             logger.log('%s: installing ssh keys' % self.name)
79             tools.fork_as(self.name, do_installation)
80
81     def start(self): pass
82     def stop(self): pass
83
84
85 class Worker:
86     # these semaphores are acquired before creating/destroying an account
87     _create_sem = threading.Semaphore(1)
88     _destroy_sem = threading.Semaphore(1)
89
90     def __init__(self, name):
91         self.name = name  # username
92         self._acct = None  # the account object currently associated with this worker
93         # task list
94         # outsiders request operations by putting (fn, args...) tuples on _q
95         # the worker thread (created below) will perform these operations in order
96         self._q = Queue.Queue()
97         tools.as_daemon_thread(self._run)
98
99     def ensure_created(self, rec):
100         """Cause the account specified by <rec> to exist if it doesn't already."""
101         self._q.put((self._ensure_created, rec.copy()))
102
103     def _ensure_created(self, rec):
104         curr_class = self._get_class()
105         next_class = type_acct_class[rec['type']]
106         if next_class != curr_class:
107             self._destroy(curr_class)
108             self._create_sem.acquire()
109             try: next_class.create(self.name)
110             finally: self._create_sem.release()
111         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
112         else: self._acct.configure(rec)
113         if next_class != curr_class: self._acct.start()
114
115     def ensure_destroyed(self): self._q.put((self._ensure_destroyed,))
116     def _ensure_destroyed(self): self._destroy(self._get_class())
117
118     def start(self): self._q.put((self._start,))
119     def _start(self): self._acct.start()
120
121     def stop(self): self._q.put((self._stop,))
122     def _stop(self): self._acct.stop()
123
124     def _destroy(self, curr_class):
125         self._acct = None
126         if curr_class:
127             self._destroy_sem.acquire()
128             try: curr_class.destroy(self.name)
129             finally: self._destroy_sem.release()
130
131     def _get_class(self):
132         try: shell = pwd.getpwnam(self.name)[6]
133         except KeyError: return None
134         return shell_acct_class[shell]
135
136     def _run(self):
137         while True:
138             try:
139                 cmd = self._q.get()
140                 cmd[0](*cmd[1:])
141             except: logger.log_exc()