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