3 """Functionality common to all account classes.
5 Each subclass of Account must provide five methods:
6 (*) create() and destroy(), which are static;
7 (*) configure(), start(), and stop(), which are not.
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.
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
36 # shell path -> account class association
38 # account type -> account class association
41 # these semaphores are acquired before creating/destroying an account
42 create_sem = threading.Semaphore(1)
43 destroy_sem = threading.Semaphore(1)
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
49 shell_acct_class[acct_class.SHELL] = acct_class
50 type_acct_class[acct_class.TYPE] = acct_class
53 # private account name -> worker object association and associated lock
54 name_worker_lock = threading.Lock()
58 return [pw_ent for pw_ent in pwd.getpwall() if pw_ent[6] in shell_acct_class]
61 """Return the names of all accounts on the system with recognized shells."""
62 return [pw_ent[0] for pw_ent in allpwents()]
65 """Return the worker object for a particular username. If no such object exists, create it first."""
66 name_worker_lock.acquire()
68 if name not in name_worker: name_worker[name] = Worker(name)
69 return name_worker[name]
70 finally: name_worker_lock.release()
73 # xxx strictly speaking this class should not use self.name that in fact
74 # is accidentally inited by the subclasses constructor...
76 def __init__(self, name):
79 logger.verbose('account: Initing account %s'%name)
82 def create(name, vref = None): abstract
85 def destroy(name): abstract
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)
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))
105 os.chown(pw_dir, uid, gid)
108 auth_keys = os.path.join(dot_ssh,'authorized_keys')
109 tools.write_file(auth_keys, lambda f: f.write(new_keys))
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)
117 # set self.keys to new_keys only when all of the above ops succeed
120 logger.log('account: %s: installed ssh keys' % self.name)
122 def start(self, delay=0): pass
124 def is_running(self): pass
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)
130 def mount_ssh_dir (slicename): return Account._manage_ssh_dir (slicename, do_mount=True)
132 def umount_ssh_dir (slicename): return Account._manage_ssh_dir (slicename, do_mount=False)
134 # bind mount / umount root side dir to sliver side
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)
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
146 # any of both might not exist yet
147 for path in [root_ssh,sliver_ssh]:
148 if not os.path.exists (path):
150 if not os.path.isdir (path):
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))
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))
164 logger.log_exc("_manage_ssh_dir failed",name=slicename)
168 def __init__(self, name):
169 self.name = name # username
170 self._acct = None # the account object currently associated with this worker
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)
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()))
187 # reservation_alive is set on reservable nodes, and its value is a boolean
188 if 'reservation_alive' in rec:
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)
195 # not having the lease, do not start it
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
201 if not self.is_running() or next_class != curr_class:
203 else: self.configure(rec)
205 def ensure_destroyed(self): self._destroy(self._get_class())
207 def start(self, rec, d = 0):
208 self._acct.configure(rec)
209 self._acct.start(delay=d)
211 def configure(self, rec):
212 self._acct.configure(rec)
214 def stop(self): self._acct.stop()
216 def is_running(self):
217 if (self._acct != None) and self._acct.is_running():
221 logger.verbose("account: Worker(%s): is not running" % self.name)
224 def _destroy(self, curr_class):
227 destroy_sem.acquire()
228 try: curr_class.destroy(self.name)
229 finally: destroy_sem.release()
231 def _get_class(self):
232 try: shell = pwd.getpwnam(self.name)[6]
233 except KeyError: return None
234 return shell_acct_class[shell]