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