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