c064ec65210c1bd944d85e640aec2a37f70d0671
[nodemanager.git] / accounts.py
1 # $Id$
2 # $URL$
3
4 """Functionality common to all account classes.
5
6 Each subclass of Account must provide five methods: 
7   (*) create() and destroy(), which are static; 
8   (*) configure(), start(), and stop(), which are not.  
9
10 configure(), which takes a record as its only argument, does
11 things like set up ssh keys. In addition, an Account subclass must
12 provide static member variables SHELL, which contains the unique shell
13 that it uses; and TYPE, a string that is used by the account creation
14 code.  For no particular reason, TYPE is divided hierarchically by
15 periods; at the moment the only convention is that all sliver accounts
16 have type that begins with sliver.
17
18 There are any number of race conditions that may result from the fact
19 that account names are not unique over time.  Moreover, it's a bad
20 idea to perform lengthy operations while holding the database lock.
21 In order to deal with both of these problems, we use a worker thread
22 for each account name that ever exists.  On 32-bit systems with large
23 numbers of accounts, this may cause the NM process to run out of
24 *virtual* memory!  This problem may be remedied by decreasing the
25 maximum stack size.
26 """
27
28 #import Queue
29 import os
30 import pwd, grp
31 import threading
32
33 import logger
34 import tools
35
36
37 # When this variable is true, start after any ensure_created
38 Startingup = False
39 # shell path -> account class association
40 shell_acct_class = {}
41 # account type -> account class association
42 type_acct_class = {}
43
44 # these semaphores are acquired before creating/destroying an account
45 create_sem = threading.Semaphore(1)
46 destroy_sem = threading.Semaphore(1)
47
48 def register_class(acct_class):
49     """Call once for each account class. This method adds the class
50 to the dictionaries used to look up account classes by shell and
51 type."""
52     shell_acct_class[acct_class.SHELL] = acct_class
53     type_acct_class[acct_class.TYPE] = acct_class
54
55
56 # private account name -> worker object association and associated lock
57 name_worker_lock = threading.Lock()
58 name_worker = {}
59
60 def allpwents():
61     return [pw_ent for pw_ent in pwd.getpwall() if pw_ent[6] in shell_acct_class]
62
63 def all():
64     """Return the names of all accounts on the system with recognized shells."""
65     return [pw_ent[0] for pw_ent in allpwents()]
66
67 def get(name):
68     """Return the worker object for a particular username.  If no such object exists, create it first."""
69     name_worker_lock.acquire()
70     try:
71         if name not in name_worker: name_worker[name] = Worker(name)
72         return name_worker[name]
73     finally: name_worker_lock.release()
74
75
76 class Account:
77     def __init__(self, rec):
78         logger.verbose('accounts: Initing account %s'%rec['name'])
79         self.name = rec['name']
80         self.keys = ''
81         self.initscriptchanged = False
82         self.configure(rec)
83
84     @staticmethod
85     def create(name, vref = None): abstract
86
87     @staticmethod
88     def destroy(name): abstract
89
90     def configure(self, rec):
91         """Write <rec['keys']> to my authorized_keys file."""
92         logger.verbose('accounts: configuring %s'%self.name)
93         new_keys = rec['keys']
94         if new_keys != self.keys:
95             # get the unix account info
96             gid = grp.getgrnam("slices")[2]
97             pw_info = pwd.getpwnam(self.name)
98             uid = pw_info[2]
99             pw_dir = pw_info[5]
100
101             # write out authorized_keys file and conditionally create
102             # the .ssh subdir if need be.
103             dot_ssh = os.path.join(pw_dir,'.ssh')
104             if not os.path.isdir(dot_ssh):
105                 if not os.path.isdir(pw_dir):
106                     logger.verbose('accounts: WARNING: homedir %s does not exist for %s!'%(pw_dir,self.name))
107                     os.mkdir(pw_dir)
108                     os.chown(pw_dir, uid, gid)
109                 os.mkdir(dot_ssh)
110
111             auth_keys = os.path.join(dot_ssh,'authorized_keys')
112             tools.write_file(auth_keys, lambda f: f.write(new_keys))
113
114             # set access permissions and ownership properly
115             os.chmod(dot_ssh, 0700)
116             os.chown(dot_ssh, uid, gid)
117             os.chmod(auth_keys, 0600)
118             os.chown(auth_keys, uid, gid)
119
120             # set self.keys to new_keys only when all of the above ops succeed
121             self.keys = new_keys
122
123             logger.log('accounts: %s: installed ssh keys' % self.name)
124
125     def start(self, delay=0): pass
126     def stop(self): pass
127     def is_running(self): pass
128
129 class Worker:
130
131     def __init__(self, name):
132         self.name = name  # username
133         self._acct = None  # the account object currently associated with this worker
134
135     def ensure_created(self, rec, startingup = Startingup):
136         """Check account type is still valid.  If not, recreate sliver.  
137 If still valid, check if running and configure/start if not."""
138         curr_class = self._get_class()
139         next_class = type_acct_class[rec['type']]
140         if next_class != curr_class:
141             self._destroy(curr_class)
142             create_sem.acquire()
143             try: next_class.create(self.name, rec['vref'])
144             finally: create_sem.release()
145         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
146         if startingup or \
147           not self.is_running() or \
148           next_class != curr_class or \
149           self._acct.initscriptchanged:
150             self.start(rec)
151         else: self._acct.configure(rec)
152
153     def ensure_destroyed(self): self._destroy(self._get_class())
154
155     def start(self, rec, d = 0): 
156         self._acct.configure(rec)
157         self._acct.start(delay=d)
158
159     def stop(self): self._acct.stop()
160
161     def is_running(self): 
162         if (self._acct != None) and self._acct.is_running():
163             status = True
164         else:
165             status = False
166             logger.verbose("accounts: Worker(%s): is not running" % self.name)
167         return status
168
169     def _destroy(self, curr_class):
170         self._acct = None
171         if curr_class:
172             destroy_sem.acquire()
173             try: curr_class.destroy(self.name)
174             finally: destroy_sem.release()
175
176     def _get_class(self):
177         try: shell = pwd.getpwnam(self.name)[6]
178         except KeyError: return None
179         return shell_acct_class[shell]