Merge branch 'master' of ssh://git.onelab.eu/git/nodemanager
[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     # bind mount root side dir to sliver side
127     # needs to be done before sliver starts, in the vserver case at least
128     def expose_ssh_dir (self):
129         try:
130             root_ssh="/home/%s/.ssh"%self.name
131             sliver_ssh="/vservers/%s/home/%s/.ssh"%(self.name,self.name)
132             # any of both might not exist yet
133             for path in [root_ssh,sliver_ssh]:
134                 if not os.path.exists (path):
135                     os.mkdir(path)
136                 if not os.path.isdir (path):
137                     raise Exception
138             mounts=file('/proc/mounts').read()
139             if mounts.find(sliver_ssh)<0:
140                 # xxx perform mount
141                 subprocess.call("mount --bind -o ro %s %s"%(root_ssh,sliver_ssh),shell=True)
142                 logger.log("expose_ssh_dir: %s mounted into slice %s"%(root_ssh,self.name))
143         except:
144             logger.log_exc("expose_ssh_dir with slice %s failed"%self.name)
145
146 class Worker:
147
148     def __init__(self, name):
149         self.name = name  # username
150         self._acct = None  # the account object currently associated with this worker
151
152     def ensure_created(self, rec):
153         """Check account type is still valid.  If not, recreate sliver.
154 If still valid, check if running and configure/start if not."""
155         logger.log_data_in_file(rec,"/var/lib/nodemanager/%s.rec.txt"%rec['name'],
156                                 'raw rec captured in ensure_created',logger.LOG_VERBOSE)
157         curr_class = self._get_class()
158         next_class = type_acct_class[rec['type']]
159         if next_class != curr_class:
160             self._destroy(curr_class)
161             create_sem.acquire()
162             try: next_class.create(self.name, rec)
163             finally: create_sem.release()
164         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
165         logger.verbose("account.Worker.ensure_created: %s, running=%r"%(self.name,self.is_running()))
166
167         # reservation_alive is set on reervable nodes, and its value is a boolean
168         if 'reservation_alive' in rec:
169             # reservable nodes
170             if rec['reservation_alive']:
171                 # this sliver has the lease, it is safe to start it
172                 if not self.is_running(): self.start(rec)
173                 else: self.configure(rec)
174             else:
175                 # not having the lease, do not start it
176                 self.configure(rec)
177         # usual nodes - preserve old code
178         # xxx it's not clear what to do when a sliver changes type/class
179         # in a reservable node
180         else:
181             if not self.is_running() or next_class != curr_class:
182                 self.start(rec)
183             else: self.configure(rec)
184
185     def ensure_destroyed(self): self._destroy(self._get_class())
186
187     def start(self, rec, d = 0):
188         self._acct.configure(rec)
189         self._acct.start(delay=d)
190
191     def configure(self, rec):
192         self._acct.configure(rec)
193
194     def stop(self): self._acct.stop()
195
196     def is_running(self):
197         if (self._acct != None) and self._acct.is_running():
198             status = True
199         else:
200             status = False
201             logger.verbose("account: Worker(%s): is not running" % self.name)
202         return status
203
204     def _destroy(self, curr_class):
205         self._acct = None
206         if curr_class:
207             destroy_sem.acquire()
208             try: curr_class.destroy(self.name)
209             finally: destroy_sem.release()
210
211     def _get_class(self):
212         try: shell = pwd.getpwnam(self.name)[6]
213         except KeyError: return None
214         return shell_acct_class[shell]
215