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