spaces after comma
[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 {}'.format(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 {} does not exist for {}!'
116                                    .format(pw_dir, self.name))
117                     os.mkdir(pw_dir)
118                     os.chown(pw_dir, uid, gid)
119                 os.mkdir(dot_ssh)
120
121             auth_keys = os.path.join(dot_ssh, 'authorized_keys')
122             tools.write_file(auth_keys, lambda f: f.write(new_keys))
123
124             # set access permissions and ownership properly
125             os.chmod(dot_ssh, 0700)
126             os.chown(dot_ssh, uid, gid)
127             os.chmod(auth_keys, 0600)
128             os.chown(auth_keys, uid, gid)
129
130             # set self.keys to new_keys only when all of the above ops succeed
131             self.keys = new_keys
132
133             logger.log('account: {}: installed ssh keys'.format(self.name))
134
135     def start(self, delay=0):
136         pass
137     def stop(self):
138         pass
139     def is_running(self):
140         pass
141     def needs_reimage(self, target_slicefamily):
142         stampname = "/vservers/{}/etc/slicefamily".format(self.name)
143         try:
144             with open(stampname) as f:
145                 current_slicefamily = f.read().strip()
146                 if current_slicefamily != target_slicefamily:
147                     logger.info("slice {} : new slice family - would require reimaging"
148                                 .format(self.name))
149                     return True
150                 else:
151                     return False
152         except IOError as e:
153             logger.verbose("Missing slicefamily {} - considered OK"
154                            .format(self.name))
155             return False
156
157     ### this used to be a plain method but because it needs to be invoked by destroy
158     # which is a static method, they need to become static as well
159     # needs to be done before sliver starts (checked with vs and lxc)
160     @staticmethod
161     def mount_ssh_dir (slicename): return Account._manage_ssh_dir (slicename, do_mount=True)
162     @staticmethod
163     def umount_ssh_dir (slicename): return Account._manage_ssh_dir (slicename, do_mount=False)
164
165     # bind mount / umount root side dir to sliver side
166     @staticmethod
167     def _manage_ssh_dir (slicename, do_mount):
168         logger.log ("_manage_ssh_dir, requested to "+("mount" if do_mount else "umount")+" ssh dir for "+ slicename)
169         try:
170             root_ssh = "/home/{}/.ssh".format(slicename)
171             sliver_ssh = "/vservers/{}/home/{}/.ssh".format(slicename, slicename)
172             def is_mounted (root_ssh):
173                 with open('/proc/mounts') as mountsfile:
174                     for mount_line in mountsfile.readlines():
175                         if mount_line.find (root_ssh) >= 0:
176                             return True
177                 return False
178             if do_mount:
179                 # any of both might not exist yet
180                 for path in [root_ssh, sliver_ssh]:
181                     if not os.path.exists (path):
182                         os.mkdir(path)
183                     if not os.path.isdir (path):
184                         raise Exception
185                 if not is_mounted(root_ssh):
186                     command = ['mount', '--bind', '-o', 'ro', root_ssh, sliver_ssh]
187                     mounted = logger.log_call (command)
188                     msg = "OK" if mounted else "WARNING: FAILED"
189                     logger.log("_manage_ssh_dir: mounted {} into slice {} - {}"
190                                .format(root_ssh, slicename, msg))
191             else:
192                 if is_mounted (sliver_ssh):
193                     command = ['umount', sliver_ssh]
194                     umounted = logger.log_call(command)
195                     msg = "OK" if umounted else "WARNING: FAILED"
196                     logger.log("_manage_ssh_dir: umounted {} - {}"
197                                .format(sliver_ssh, msg))
198         except:
199             logger.log_exc("_manage_ssh_dir failed", name=slicename)
200
201 class Worker:
202
203     def __init__(self, name):
204         self.name = name  # username
205         self._acct = None  # the account object currently associated with this worker
206
207     def ensure_created(self, rec):
208         """
209         Check account type is still valid.  If not, recreate sliver.
210         If still valid, check if running and configure/start if not.
211         """
212         logger.log_data_in_file(rec, "/var/lib/nodemanager/{}.rec.txt".format(rec['name']),
213                                 'raw rec captured in ensure_created', logger.LOG_VERBOSE)
214         curr_class = self._get_class()
215         next_class = type_acct_class[rec['type']]
216         if next_class != curr_class:
217             self._destroy(curr_class)
218             create_sem.acquire()
219             try: next_class.create(self.name, rec)
220             finally: create_sem.release()
221         if not isinstance(self._acct, next_class):
222             self._acct = next_class(rec)
223         logger.verbose("account.Worker.ensure_created: {}, running={}"
224                        .format(self.name, self.is_running()))
225
226         # reservation_alive is set on reservable nodes, and its value is a boolean
227         if 'reservation_alive' in rec:
228             # reservable nodes
229             if rec['reservation_alive']:
230                 # this sliver has the lease, it is safe to start it
231                 if not self.is_running():
232                     self.start(rec)
233                 else: self.configure(rec)
234             else:
235                 # not having the lease, do not start it
236                 self.configure(rec)
237         # usual nodes - preserve old code
238         # xxx it's not clear what to do when a sliver changes type/class
239         # in a reservable node
240         else:
241             if not self.is_running() or self.needs_reimage(ref['vref']) or next_class != curr_class:
242                 self.start(rec)
243             else:
244                 self.configure(rec)
245
246     def ensure_destroyed(self):
247         self._destroy(self._get_class())
248
249     # take rec as an arg here for api_calls
250     def start(self, rec, d = 0):
251         self._acct.configure(rec)
252         self._acct.start(delay=d)
253
254     def configure(self, rec):
255         self._acct.configure(rec)
256
257     def stop(self):
258         self._acct.stop()
259
260     def is_running(self):
261         if self._acct and self._acct.is_running():
262             return True
263         else:
264             logger.verbose("Worker.is_running ({}) - no account or not running".format(self.name))
265             return False
266
267     def needs_reimage(self, target_slicefamily):
268         if not self._acct:
269             logger.verbose("Worker.needs_reimage ({}) - no account".format(self.name))
270             return True
271         elif self._acct.needs_reimage(target_slicefamily):
272             logger.info("Worker.needs_reimage ({}) - account needs reimage (tmp: DRY RUN)".format(self.name))
273             return False
274         else:
275             return False
276     
277     def _destroy(self, curr_class):
278         self._acct = None
279         if curr_class:
280             destroy_sem.acquire()
281             try: curr_class.destroy(self.name)
282             finally: destroy_sem.release()
283
284     def _get_class(self):
285         try: shell = pwd.getpwnam(self.name)[6]
286         except KeyError: return None
287         return shell_acct_class[shell]
288