expose_ssh_dir relocated from Worker to the Account class where it belongs
[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
31 import logger
32 import tools
33
34
35 # shell path -> account class association
36 shell_acct_class = {}
37 # account type -> account class association
38 type_acct_class = {}
39
40 # these semaphores are acquired before creating/destroying an account
41 create_sem = threading.Semaphore(1)
42 destroy_sem = threading.Semaphore(1)
43
44 def register_class(acct_class):
45     """Call once for each account class. This method adds the class
46 to the dictionaries used to look up account classes by shell and
47 type."""
48     shell_acct_class[acct_class.SHELL] = acct_class
49     type_acct_class[acct_class.TYPE] = acct_class
50
51
52 # private account name -> worker object association and associated lock
53 name_worker_lock = threading.Lock()
54 name_worker = {}
55
56 def allpwents():
57     return [pw_ent for pw_ent in pwd.getpwall() if pw_ent[6] in shell_acct_class]
58
59 def all():
60     """Return the names of all accounts on the system with recognized shells."""
61     return [pw_ent[0] for pw_ent in allpwents()]
62
63 def get(name):
64     """Return the worker object for a particular username.  If no such object exists, create it first."""
65     name_worker_lock.acquire()
66     try:
67         if name not in name_worker: name_worker[name] = Worker(name)
68         return name_worker[name]
69     finally: name_worker_lock.release()
70
71
72 # xxx strictly speaking this class should not use self.name that in fact
73 # is accidentally inited by the subclasses constructor...
74 class Account:
75     def __init__(self, name):
76         self.name = name
77         self.keys = ''
78         logger.verbose('account: Initing account %s'%name)
79
80     @staticmethod
81     def create(name, vref = None): abstract
82
83     @staticmethod
84     def destroy(name): abstract
85
86     def configure(self, rec):
87         """Write <rec['keys']> to my authorized_keys file."""
88         logger.verbose('account: configuring %s'%self.name)
89         new_keys = rec['keys']
90         if new_keys != self.keys:
91             # get the unix account info
92             gid = grp.getgrnam("slices")[2]
93             pw_info = pwd.getpwnam(self.name)
94             uid = pw_info[2]
95             pw_dir = pw_info[5]
96
97             # write out authorized_keys file and conditionally create
98             # the .ssh subdir if need be.
99             dot_ssh = os.path.join(pw_dir,'.ssh')
100             if not os.path.isdir(dot_ssh):
101                 if not os.path.isdir(pw_dir):
102                     logger.verbose('account: WARNING: homedir %s does not exist for %s!'%(pw_dir,self.name))
103                     os.mkdir(pw_dir)
104                     os.chown(pw_dir, uid, gid)
105                 os.mkdir(dot_ssh)
106
107             auth_keys = os.path.join(dot_ssh,'authorized_keys')
108             tools.write_file(auth_keys, lambda f: f.write(new_keys))
109
110             # set access permissions and ownership properly
111             os.chmod(dot_ssh, 0700)
112             os.chown(dot_ssh, uid, gid)
113             os.chmod(auth_keys, 0600)
114             os.chown(auth_keys, uid, gid)
115
116             # set self.keys to new_keys only when all of the above ops succeed
117             self.keys = new_keys
118
119             logger.log('account: %s: installed ssh keys' % self.name)
120
121     def start(self, delay=0): pass
122     def stop(self): pass
123     def is_running(self): pass
124
125     # bind mount root side dir to sliver side
126     # needs to be done before sliver starts, in the vserver case at least
127     def expose_ssh_dir (self):
128         try:
129             root_ssh="/home/%s/.ssh"%self.name
130             sliver_ssh="/vservers/%s/home/%s/.ssh"%(self.name,self.name)
131             # any of both might not exist yet
132             for path in [root_ssh,sliver_ssh]:
133                 if not os.path.exists (path):
134                     os.mkdir(path)
135                 if not os.path.isdir (path):
136                     raise Exception
137             mounts=file('/proc/mounts').read()
138             if mounts.find(sliver_ssh)<0:
139                 # xxx perform mount
140                 subprocess.call("mount --bind -o ro %s %s"%(root_ssh,sliver_ssh),shell=True)
141                 logger.log("expose_ssh_dir: %s mounted into slice %s"%(root_ssh,self.name))
142         except:
143             logger.log_exc("expose_ssh_dir with slice %s failed"%self.name)
144
145 class Worker:
146
147     def __init__(self, name):
148         self.name = name  # username
149         self._acct = None  # the account object currently associated with this worker
150
151     def ensure_created(self, rec):
152         """Check account type is still valid.  If not, recreate sliver.
153 If still valid, check if running and configure/start if not."""
154         logger.log_data_in_file(rec,"/var/lib/nodemanager/%s.rec.txt"%rec['name'],
155                                 'raw rec captured in ensure_created',logger.LOG_VERBOSE)
156         curr_class = self._get_class()
157         next_class = type_acct_class[rec['type']]
158         if next_class != curr_class:
159             self._destroy(curr_class)
160             create_sem.acquire()
161             try: next_class.create(self.name, rec)
162             finally: create_sem.release()
163         if not isinstance(self._acct, next_class): self._acct = next_class(rec)
164         logger.verbose("account.Worker.ensure_created: %s, running=%r"%(self.name,self.is_running()))
165
166         # reservation_alive is set on reervable nodes, and its value is a boolean
167         if 'reservation_alive' in rec:
168             # reservable nodes
169             if rec['reservation_alive']:
170                 # this sliver has the lease, it is safe to start it
171                 if not self.is_running(): self.start(rec)
172                 else: self.configure(rec)
173             else:
174                 # not having the lease, do not start it
175                 self.configure(rec)
176         # usual nodes - preserve old code
177         # xxx it's not clear what to do when a sliver changes type/class
178         # in a reservable node
179         else:
180             if not self.is_running() or next_class != curr_class:
181                 self.start(rec)
182             else: self.configure(rec)
183
184     def ensure_destroyed(self): self._destroy(self._get_class())
185
186     def start(self, rec, d = 0):
187         self._acct.configure(rec)
188         self._acct.start(delay=d)
189
190     def configure(self, rec):
191         self._acct.configure(rec)
192
193     def stop(self): self._acct.stop()
194
195     def is_running(self):
196         if (self._acct != None) and self._acct.is_running():
197             status = True
198         else:
199             status = False
200             logger.verbose("account: Worker(%s): is not running" % self.name)
201         return status
202
203     def _destroy(self, curr_class):
204         self._acct = None
205         if curr_class:
206             destroy_sem.acquire()
207             try: curr_class.destroy(self.name)
208             finally: destroy_sem.release()
209
210     def _get_class(self):
211         try: shell = pwd.getpwnam(self.name)[6]
212         except KeyError: return None
213         return shell_acct_class[shell]
214