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