Added NodeManager API calls for inspecting the slicename -> XID mapping and slice...
[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 # shell path -> account class association
33 shell_acct_class = {}
34 # account type -> account class association
35 type_acct_class = {}
36
37 def register_class(acct_class):
38     """Call once for each account class.  This method adds the class to the dictionaries used to look up account classes by shell and type."""
39     shell_acct_class[acct_class.SHELL] = acct_class
40     type_acct_class[acct_class.TYPE] = acct_class
41
42
43 # private account name -> worker object association and associated lock
44 name_worker_lock = threading.Lock()
45 name_worker = {}
46
47 def allpwents():
48     return [pw_ent for pw_ent in pwd.getpwall() if pw_ent[6] in shell_acct_class]
49
50 def all():
51     """Return the names of all accounts on the system with recognized shells."""
52     return [pw_ent[0] for pw_ent in allpwents()]
53
54 def get(name):
55     """Return the worker object for a particular username.  If no such object exists, create it first."""
56     name_worker_lock.acquire()
57     try:
58         if name not in name_worker: name_worker[name] = Worker(name)
59         return name_worker[name]
60     finally: name_worker_lock.release()
61
62
63 class Account:
64     def __init__(self, rec):
65         self.name = rec['name']
66         self.keys = ''
67         self.configure(rec)
68
69     @staticmethod
70     def create(name): abstract
71     @staticmethod
72     def destroy(name): abstract
73
74     def configure(self, rec):
75         """Write <rec['keys']> to my authorized_keys file."""
76         new_keys = rec['keys']
77         if new_keys != self.keys:
78             self.keys = new_keys
79             dot_ssh = '/home/%s/.ssh' % self.name
80             def do_installation():
81                 if not os.access(dot_ssh, os.F_OK): os.mkdir(dot_ssh)
82                 os.chmod(dot_ssh, 0700)
83                 tools.write_file(dot_ssh + '/authorized_keys', lambda f: f.write(new_keys))
84             logger.log('%s: installing ssh keys' % self.name)
85             tools.fork_as(self.name, do_installation)
86
87     def start(self, delay=0): pass
88     def stop(self): pass
89
90
91 class Worker:
92     # these semaphores are acquired before creating/destroying an account
93     _create_sem = threading.Semaphore(1)
94     _destroy_sem = threading.Semaphore(1)
95
96     def __init__(self, name):
97         self.name = name  # username
98         self._acct = None  # the account object currently associated with this worker
99         # task list
100         # outsiders request operations by putting (fn, args...) tuples on _q
101         # the worker thread (created below) will perform these operations in order
102         self._q = Queue.Queue()
103         tools.as_daemon_thread(self._run)
104
105     def ensure_created(self, rec):
106         """Cause the account specified by <rec> to exist if it doesn't already."""
107         self._q.put((self._ensure_created, rec.copy()))
108
109     def _ensure_created(self, rec):
110         curr_class = self._get_class()
111         next_class = type_acct_class[rec['type']]
112         if next_class != curr_class:
113             self._destroy(curr_class)
114             self._create_sem.acquire()
115             try: next_class.create(self.name, rec['vref'])
116             finally: self._create_sem.release()
117         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
118         else: self._acct.configure(rec)
119         if next_class != curr_class: self._acct.start()
120
121     def ensure_destroyed(self): self._q.put((self._ensure_destroyed,))
122     def _ensure_destroyed(self): self._destroy(self._get_class())
123
124     def start(self, delay=0): self._q.put((self._start, delay))
125     def _start(self, d): self._acct.start(delay=d)
126
127     def stop(self): self._q.put((self._stop,))
128     def _stop(self): self._acct.stop()
129
130     def _destroy(self, curr_class):
131         self._acct = None
132         if curr_class:
133             self._destroy_sem.acquire()
134             try: curr_class.destroy(self.name)
135             finally: self._destroy_sem.release()
136
137     def _get_class(self):
138         try: shell = pwd.getpwnam(self.name)[6]
139         except KeyError: return None
140         return shell_acct_class[shell]
141
142     def _run(self):
143         """Repeatedly pull commands off the queue and execute.  If memory usage becomes an issue, it might be wise to terminate after a while."""
144         while True:
145             try:
146                 cmd = self._q.get()
147                 cmd[0](*cmd[1:])
148             except: logger.log_exc()