no change, just nicer
[nodemanager.git] / account.py
1 ### 
2
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 import subprocess
32
33 import logger
34 import tools
35
36
37 # shell path -> account class association
38 shell_acct_class = {}
39 # account type -> account class association
40 type_acct_class = {}
41
42 # these semaphores are acquired before creating/destroying an account
43 create_sem = threading.Semaphore(1)
44 destroy_sem = threading.Semaphore(1)
45
46 def register_class(acct_class):
47     """
48     Call once for each account class. This method adds the class
49     to the dictionaries used to look up account classes
50     by shell and type.
51     """
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     """
69     Return the worker object for a particular username.
70     If no such object exists, create it first.
71     """
72     name_worker_lock.acquire()
73     try:
74         if name not in name_worker: name_worker[name] = Worker(name)
75         return name_worker[name]
76     finally: name_worker_lock.release()
77
78
79 class Account:
80     """
81     Base class for all types of account
82     """
83
84     def __init__(self, name):
85         self.name = name
86         self.keys = ''
87         logger.verbose('account: Initing account %s'%name)
88
89     @staticmethod
90     def create(name, vref = None):
91         abstract
92
93     @staticmethod
94     def destroy(name):
95         abstract
96
97     def configure(self, rec):
98         """
99         Write <rec['keys']> to my authorized_keys file.
100         """
101         new_keys = rec['keys']
102         logger.verbose('account: configuring {} with {} keys'.format(self.name, len(new_keys)))
103         if new_keys != self.keys:
104             # get the unix account info
105             gid = grp.getgrnam("slices")[2]
106             pw_info = pwd.getpwnam(self.name)
107             uid = pw_info[2]
108             pw_dir = pw_info[5]
109
110             # write out authorized_keys file and conditionally create
111             # the .ssh subdir if need be.
112             dot_ssh = os.path.join(pw_dir, '.ssh')
113             if not os.path.isdir(dot_ssh):
114                 if not os.path.isdir(pw_dir):
115                     logger.verbose('account: WARNING: homedir %s does not exist for %s!'%(pw_dir, self.name))
116                     os.mkdir(pw_dir)
117                     os.chown(pw_dir, uid, gid)
118                 os.mkdir(dot_ssh)
119
120             auth_keys = os.path.join(dot_ssh, 'authorized_keys')
121             tools.write_file(auth_keys, lambda f: f.write(new_keys))
122
123             # set access permissions and ownership properly
124             os.chmod(dot_ssh, 0700)
125             os.chown(dot_ssh, uid, gid)
126             os.chmod(auth_keys, 0600)
127             os.chown(auth_keys, uid, gid)
128
129             # set self.keys to new_keys only when all of the above ops succeed
130             self.keys = new_keys
131
132             logger.log('account: %s: installed ssh keys' % self.name)
133
134     def start(self, delay=0):
135         pass
136     def stop(self):
137         pass
138     def is_running(self):
139         pass
140
141     ### this used to be a plain method but because it needs to be invoked by destroy
142     # which is a static method, they need to become static as well
143     # needs to be done before sliver starts (checked with vs and lxc)
144     @staticmethod
145     def mount_ssh_dir (slicename): return Account._manage_ssh_dir (slicename, do_mount=True)
146     @staticmethod
147     def umount_ssh_dir (slicename): return Account._manage_ssh_dir (slicename, do_mount=False)
148
149     # bind mount / umount root side dir to sliver side
150     @staticmethod
151     def _manage_ssh_dir (slicename, do_mount):
152         logger.log ("_manage_ssh_dir, requested to "+("mount" if do_mount else "umount")+" ssh dir for "+ slicename)
153         try:
154             root_ssh = "/home/%s/.ssh"%slicename
155             sliver_ssh = "/vservers/%s/home/%s/.ssh"%(slicename, slicename)
156             def is_mounted (root_ssh):
157                 for mount_line in file('/proc/mounts').readlines():
158                     if mount_line.find (root_ssh) >= 0:
159                         return True
160                 return False
161             if do_mount:
162                 # any of both might not exist yet
163                 for path in [root_ssh, sliver_ssh]:
164                     if not os.path.exists (path):
165                         os.mkdir(path)
166                     if not os.path.isdir (path):
167                         raise Exception
168                 if not is_mounted(root_ssh):
169                     command = ['mount', '--bind', '-o', 'ro', root_ssh, sliver_ssh]
170                     mounted = logger.log_call (command)
171                     msg = "OK" if mounted else "WARNING: FAILED"
172                     logger.log("_manage_ssh_dir: mounted %s into slice %s - %s"%(root_ssh, slicename, msg))
173             else:
174                 if is_mounted (sliver_ssh):
175                     command = ['umount', sliver_ssh]
176                     umounted = logger.log_call(command)
177                     msg = "OK" if umounted else "WARNING: FAILED"
178                     logger.log("_manage_ssh_dir: umounted %s - %s"%(sliver_ssh, msg))
179         except:
180             logger.log_exc("_manage_ssh_dir failed", name=slicename)
181
182 class Worker:
183
184     def __init__(self, name):
185         self.name = name  # username
186         self._acct = None  # the account object currently associated with this worker
187
188     def ensure_created(self, rec):
189         """
190         Check account type is still valid.  If not, recreate sliver.
191         If still valid, check if running and configure/start if not.
192         """
193         logger.log_data_in_file(rec, "/var/lib/nodemanager/%s.rec.txt"%rec['name'],
194                                 'raw rec captured in ensure_created', logger.LOG_VERBOSE)
195         curr_class = self._get_class()
196         next_class = type_acct_class[rec['type']]
197         if next_class != curr_class:
198             self._destroy(curr_class)
199             create_sem.acquire()
200             try: next_class.create(self.name, rec)
201             finally: create_sem.release()
202         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
203         logger.verbose("account.Worker.ensure_created: %s, running=%r"%(self.name, self.is_running()))
204
205         # reservation_alive is set on reservable nodes, and its value is a boolean
206         if 'reservation_alive' in rec:
207             # reservable nodes
208             if rec['reservation_alive']:
209                 # this sliver has the lease, it is safe to start it
210                 if not self.is_running():
211                     self.start(rec)
212                 else: self.configure(rec)
213             else:
214                 # not having the lease, do not start it
215                 self.configure(rec)
216         # usual nodes - preserve old code
217         # xxx it's not clear what to do when a sliver changes type/class
218         # in a reservable node
219         else:
220             if not self.is_running() or next_class != curr_class:
221                 self.start(rec)
222             else:
223                 self.configure(rec)
224
225     def ensure_destroyed(self):
226         self._destroy(self._get_class())
227
228     # take rec as an arg here for api_calls
229     def start(self, rec, d = 0):
230         self._acct.configure(rec)
231         self._acct.start(delay=d)
232
233     def configure(self, rec):
234         self._acct.configure(rec)
235
236     def stop(self):
237         self._acct.stop()
238
239     def is_running(self):
240         if (self._acct != None) and self._acct.is_running():
241             status = True
242         else:
243             status = False
244             logger.verbose("account: Worker(%s): is not running" % self.name)
245         return status
246
247     def _destroy(self, curr_class):
248         self._acct = None
249         if curr_class:
250             destroy_sem.acquire()
251             try: curr_class.destroy(self.name)
252             finally: destroy_sem.release()
253
254     def _get_class(self):
255         try: shell = pwd.getpwnam(self.name)[6]
256         except KeyError: return None
257         return shell_acct_class[shell]
258