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