Merge branch 'ipv6'
[nodemanager.git] / account.py
1 ### 
2
3 """Functionality common to all account classes.
4
5 Each subclass of Account must provide five methods:
6   (*) create() and destroy(), which are static;
7   (*) configure(), start(), and stop(), which are not.
8
9 configure(), which takes a record as its only argument, does
10 things like set up ssh keys. In addition, an Account subclass must
11 provide static member variables SHELL, which contains the unique shell
12 that it uses; and TYPE, a string that is used by the account creation
13 code.  For no particular reason, TYPE is divided hierarchically by
14 periods; at the moment the only convention is that all sliver accounts
15 have type that begins with sliver.
16
17 There are any number of race conditions that may result from the fact
18 that account names are not unique over time.  Moreover, it's a bad
19 idea to perform lengthy operations while holding the database lock.
20 In order to deal with both of these problems, we use a worker thread
21 for each account name that ever exists.  On 32-bit systems with large
22 numbers of accounts, this may cause the NM process to run out of
23 *virtual* memory!  This problem may be remedied by decreasing the
24 maximum stack size.
25 """
26
27 import os
28 import pwd, grp
29 import threading
30 import subprocess
31
32 import logger
33 import tools
34
35
36 # shell path -> account class association
37 shell_acct_class = {}
38 # account type -> account class association
39 type_acct_class = {}
40
41 # these semaphores are acquired before creating/destroying an account
42 create_sem = threading.Semaphore(1)
43 destroy_sem = threading.Semaphore(1)
44
45 def register_class(acct_class):
46     """Call once for each account class. This method adds the class
47 to the dictionaries used to look up account classes by shell and
48 type."""
49     shell_acct_class[acct_class.SHELL] = acct_class
50     type_acct_class[acct_class.TYPE] = acct_class
51
52
53 # private account name -> worker object association and associated lock
54 name_worker_lock = threading.Lock()
55 name_worker = {}
56
57 def allpwents():
58     return [pw_ent for pw_ent in pwd.getpwall() if pw_ent[6] in shell_acct_class]
59
60 def all():
61     """Return the names of all accounts on the system with recognized shells."""
62     return [pw_ent[0] for pw_ent in allpwents()]
63
64 def get(name):
65     """Return the worker object for a particular username.  If no such object exists, create it first."""
66     name_worker_lock.acquire()
67     try:
68         if name not in name_worker: name_worker[name] = Worker(name)
69         return name_worker[name]
70     finally: name_worker_lock.release()
71
72
73 # xxx strictly speaking this class should not use self.name that in fact
74 # is accidentally inited by the subclasses constructor...
75 class Account:
76     def __init__(self, name):
77         self.name = name
78         self.keys = ''
79         logger.verbose('account: Initing account %s'%name)
80
81     @staticmethod
82     def create(name, vref = None): abstract
83
84     @staticmethod
85     def destroy(name): abstract
86
87     def configure(self, rec):
88         """Write <rec['keys']> to my authorized_keys file."""
89         logger.verbose('account: configuring %s'%self.name)
90         new_keys = rec['keys']
91         if new_keys != self.keys:
92             # get the unix account info
93             gid = grp.getgrnam("slices")[2]
94             pw_info = pwd.getpwnam(self.name)
95             uid = pw_info[2]
96             pw_dir = pw_info[5]
97
98             # write out authorized_keys file and conditionally create
99             # the .ssh subdir if need be.
100             dot_ssh = os.path.join(pw_dir,'.ssh')
101             if not os.path.isdir(dot_ssh):
102                 if not os.path.isdir(pw_dir):
103                     logger.verbose('account: WARNING: homedir %s does not exist for %s!'%(pw_dir,self.name))
104                     os.mkdir(pw_dir)
105                     os.chown(pw_dir, uid, gid)
106                 os.mkdir(dot_ssh)
107
108             auth_keys = os.path.join(dot_ssh,'authorized_keys')
109             tools.write_file(auth_keys, lambda f: f.write(new_keys))
110
111             # set access permissions and ownership properly
112             os.chmod(dot_ssh, 0700)
113             os.chown(dot_ssh, uid, gid)
114             os.chmod(auth_keys, 0600)
115             os.chown(auth_keys, uid, gid)
116
117             # set self.keys to new_keys only when all of the above ops succeed
118             self.keys = new_keys
119
120             logger.log('account: %s: installed ssh keys' % self.name)
121
122     def start(self, delay=0): pass
123     def stop(self): pass
124     def is_running(self): pass
125
126     ### this used to be a plain method but because it needs to be invoked by destroy
127     # which is a static method, they need to become static as well
128     # needs to be done before sliver starts (checked with vs and lxc)
129     @staticmethod
130     def mount_ssh_dir (slicename): return Account._manage_ssh_dir (slicename, do_mount=True)
131     @staticmethod
132     def umount_ssh_dir (slicename): return Account._manage_ssh_dir (slicename, do_mount=False)
133
134     # bind mount / umount root side dir to sliver side
135     @staticmethod
136     def _manage_ssh_dir (slicename, do_mount):
137         logger.log ("_manage_ssh_dir, requested to "+("mount" if do_mount else "umount")+" ssh dir for "+ slicename)
138         try:
139             root_ssh="/home/%s/.ssh"%slicename
140             sliver_ssh="/vservers/%s/home/%s/.ssh"%(slicename,slicename)
141             def is_mounted (root_ssh):
142                 for mount_line in file('/proc/mounts').readlines():
143                     if mount_line.find (root_ssh)>=0: return True
144                 return False
145             if do_mount:
146                 # any of both might not exist yet
147                 for path in [root_ssh,sliver_ssh]:
148                     if not os.path.exists (path):
149                         os.mkdir(path)
150                     if not os.path.isdir (path):
151                         raise Exception
152                 if not is_mounted(root_ssh):
153                     command=['mount','--bind','-o','ro',root_ssh,sliver_ssh]
154                     mounted=logger.log_call (command)
155                     msg="OK" if mounted else "WARNING: FAILED"
156                     logger.log("_manage_ssh_dir: mounted %s into slice %s - %s"%(root_ssh,slicename,msg))
157             else:
158                 if is_mounted (sliver_ssh):
159                     command=['umount',sliver_ssh]
160                     umounted=logger.log_call(command)
161                     msg="OK" if umounted else "WARNING: FAILED"
162                     logger.log("_manage_ssh_dir: umounted %s - %s"%(sliver_ssh,msg))
163         except:
164             logger.log_exc("_manage_ssh_dir failed",name=slicename)
165
166 class Worker:
167
168     def __init__(self, name):
169         self.name = name  # username
170         self._acct = None  # the account object currently associated with this worker
171
172     def ensure_created(self, rec):
173         """Check account type is still valid.  If not, recreate sliver.
174 If still valid, check if running and configure/start if not."""
175         logger.log_data_in_file(rec,"/var/lib/nodemanager/%s.rec.txt"%rec['name'],
176                                 'raw rec captured in ensure_created',logger.LOG_VERBOSE)
177         curr_class = self._get_class()
178         next_class = type_acct_class[rec['type']]
179         if next_class != curr_class:
180             self._destroy(curr_class)
181             create_sem.acquire()
182             try: next_class.create(self.name, rec)
183             finally: create_sem.release()
184         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
185         logger.verbose("account.Worker.ensure_created: %s, running=%r"%(self.name,self.is_running()))
186
187         # reservation_alive is set on reservable nodes, and its value is a boolean
188         if 'reservation_alive' in rec:
189             # reservable nodes
190             if rec['reservation_alive']:
191                 # this sliver has the lease, it is safe to start it
192                 if not self.is_running(): self.start(rec)
193                 else: self.configure(rec)
194             else:
195                 # not having the lease, do not start it
196                 self.configure(rec)
197         # usual nodes - preserve old code
198         # xxx it's not clear what to do when a sliver changes type/class
199         # in a reservable node
200         else:
201             if not self.is_running() or next_class != curr_class:
202                 self.start(rec)
203             else: self.configure(rec)
204
205     def ensure_destroyed(self): self._destroy(self._get_class())
206
207     def start(self, rec, d = 0):
208         self._acct.configure(rec)
209         self._acct.start(delay=d)
210
211     def configure(self, rec):
212         self._acct.configure(rec)
213
214     def stop(self): self._acct.stop()
215
216     def is_running(self):
217         if (self._acct != None) and self._acct.is_running():
218             status = True
219         else:
220             status = False
221             logger.verbose("account: Worker(%s): is not running" % self.name)
222         return status
223
224     def _destroy(self, curr_class):
225         self._acct = None
226         if curr_class:
227             destroy_sem.acquire()
228             try: curr_class.destroy(self.name)
229             finally: destroy_sem.release()
230
231     def _get_class(self):
232         try: shell = pwd.getpwnam(self.name)[6]
233         except KeyError: return None
234         return shell_acct_class[shell]
235