NM does not fork often. A stack limit is now unnecessary.
[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 grp
27 import threading
28
29 import logger
30 import tools
31
32
33 # When this variable is true, start after any ensure_created
34 Startingup = False
35 # shell path -> account class association
36 shell_acct_class = {}
37 # account type -> account class association
38 type_acct_class = {}
39
40 # these semaphores are acquired before creating/destroying an account
41 create_sem = threading.Semaphore(1)
42 destroy_sem = threading.Semaphore(1)
43
44 def register_class(acct_class):
45     """Call once for each account class.  This method adds the class to the dictionaries used to look up account classes by shell and type."""
46     shell_acct_class[acct_class.SHELL] = acct_class
47     type_acct_class[acct_class.TYPE] = acct_class
48
49
50 # private account name -> worker object association and associated lock
51 name_worker_lock = threading.Lock()
52 name_worker = {}
53
54 def allpwents():
55     return [pw_ent for pw_ent in pwd.getpwall() if pw_ent[6] in shell_acct_class]
56
57 def all():
58     """Return the names of all accounts on the system with recognized shells."""
59     return [pw_ent[0] for pw_ent in allpwents()]
60
61 def get(name):
62     """Return the worker object for a particular username.  If no such object exists, create it first."""
63     name_worker_lock.acquire()
64     try:
65         if name not in name_worker: name_worker[name] = Worker(name)
66         return name_worker[name]
67     finally: name_worker_lock.release()
68
69
70 class Account:
71     def __init__(self, rec):
72         logger.verbose('Initing account %s'%rec['name'])
73         self.name = rec['name']
74         self.keys = ''
75         self.initscriptchanged = False
76         self.configure(rec)
77
78     @staticmethod
79     def create(name, vref = None): abstract
80     @staticmethod
81     def destroy(name): abstract
82
83     def configure(self, rec):
84         """Write <rec['keys']> to my authorized_keys file."""
85         logger.verbose('%s: in accounts:configure'%self.name)
86         new_keys = rec['keys']
87         if new_keys != self.keys:
88             self.keys = new_keys
89             gid = grp.getgrnam("slices")[2]
90             pw_info = pwd.getpwnam(self.name)
91             uid = pw_info[2]
92             pw_dir = pw_info[5]
93
94             dot_ssh = pw_dir + '/.ssh'
95             if not os.access(dot_ssh, os.F_OK): os.mkdir(dot_ssh)
96
97             auth_keys = dot_ssh + '/authorized_keys'
98             tools.write_file(auth_keys, lambda f: f.write(new_keys))
99
100             os.chmod(dot_ssh, 0700)
101             os.chmod(auth_keys, 0600)
102
103             os.chown(dot_ssh, uid, gid)
104             os.chown(auth_keys, uid, gid)
105
106             logger.log('%s: installed ssh keys' % self.name)
107
108     def start(self, delay=0): pass
109     def stop(self): pass
110     def is_running(self): pass
111
112 class Worker:
113     def __init__(self, name):
114         self.name = name  # username
115         self._acct = None  # the account object currently associated with this worker
116
117     def ensure_created(self, rec, startingup = Startingup):
118         """Check account type is still valid.  If not, recreate sliver.  If still valid,
119         check if running and configure/start if not."""
120         curr_class = self._get_class()
121         next_class = type_acct_class[rec['type']]
122         if next_class != curr_class:
123             self._destroy(curr_class)
124             create_sem.acquire()
125             try: next_class.create(self.name, rec['vref'])
126             finally: create_sem.release()
127         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
128         if startingup or \
129           not self.is_running() or \
130           next_class != curr_class or \
131           self._acct.initscriptchanged:
132             self.start(rec)
133         else: self._acct.configure(rec)
134
135     def ensure_destroyed(self): self._destroy(self._get_class())
136
137     def start(self, rec, d = 0): 
138         self._acct.configure(rec)
139         self._acct.start(delay=d)
140
141     def stop(self): self._acct.stop()
142
143     def is_running(self): 
144         if (self._acct != None) and self._acct.is_running():
145             status = True
146         else:
147             status = False
148             logger.verbose("Worker(%s): is not running" % self.name)
149         return status
150
151     def _destroy(self, curr_class):
152         self._acct = None
153         if curr_class:
154             destroy_sem.acquire()
155             try: curr_class.destroy(self.name)
156             finally: destroy_sem.release()
157
158     def _get_class(self):
159         try: shell = pwd.getpwnam(self.name)[6]
160         except KeyError: return None
161         return shell_acct_class[shell]