4 Functionality common to all account classes.
6 Each subclass of Account must provide five methods:
7 (*) create() and destroy(), which are static;
8 (*) configure(), start(), and stop(), which are not.
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.
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
37 # shell path -> account class association
39 # account type -> account class association
42 # these semaphores are acquired before creating/destroying an account
43 create_sem = threading.Semaphore(1)
44 destroy_sem = threading.Semaphore(1)
46 def register_class(acct_class):
48 Call once for each account class. This method adds the class
49 to the dictionaries used to look up account classes
52 shell_acct_class[acct_class.SHELL] = acct_class
53 type_acct_class[acct_class.TYPE] = acct_class
56 # private account name -> worker object association and associated lock
57 name_worker_lock = threading.Lock()
61 return [pw_ent for pw_ent in pwd.getpwall() if pw_ent[6] in shell_acct_class]
64 """Return the names of all accounts on the system with recognized shells."""
65 return [pw_ent[0] for pw_ent in allpwents()]
69 Return the worker object for a particular username.
70 If no such object exists, create it first.
72 name_worker_lock.acquire()
74 if name not in name_worker: name_worker[name] = Worker(name)
75 return name_worker[name]
77 name_worker_lock.release()
82 Base class for all types of account
85 def __init__(self, name):
88 logger.verbose('account: Initing account {}'.format(name))
91 def create(name, vref = None):
98 def configure(self, rec):
100 Write <rec['keys']> to my authorized_keys file.
102 new_keys = rec['keys']
103 logger.verbose('account: configuring {} with {} keys'.format(self.name, len(new_keys)))
104 if new_keys != self.keys:
105 # get the unix account info
106 gid = grp.getgrnam("slices")[2]
107 pw_info = pwd.getpwnam(self.name)
111 # write out authorized_keys file and conditionally create
112 # the .ssh subdir if need be.
113 dot_ssh = os.path.join(pw_dir, '.ssh')
114 if not os.path.isdir(dot_ssh):
115 if not os.path.isdir(pw_dir):
116 logger.verbose('account: WARNING: homedir {} does not exist for {}!'
117 .format(pw_dir, self.name))
119 os.chown(pw_dir, uid, gid)
122 auth_keys = os.path.join(dot_ssh, 'authorized_keys')
123 tools.write_file(auth_keys, lambda f: f.write(new_keys))
125 # set access permissions and ownership properly
126 os.chmod(dot_ssh, 0700)
127 os.chown(dot_ssh, uid, gid)
128 os.chmod(auth_keys, 0600)
129 os.chown(auth_keys, uid, gid)
131 # set self.keys to new_keys only when all of the above ops succeed
134 logger.log('account: {}: installed ssh keys'.format(self.name))
136 def start(self, delay=0):
140 def is_running(self):
142 def needs_reimage(self, target_slicefamily):
143 stampname = "/vservers/{}/etc/slicefamily".format(self.name)
145 with open(stampname) as f:
146 current_slicefamily = f.read().strip()
147 return current_slicefamily != target_slicefamily
149 logger.verbose("Account.needs_reimage: missing slicefamily {} - left as-is"
153 ### this used to be a plain method but because it needs to be invoked by destroy
154 # which is a static method, they need to become static as well
155 # needs to be done before sliver starts (checked with vs and lxc)
157 def mount_ssh_dir (slicename):
158 return Account._manage_ssh_dir (slicename, do_mount=True)
160 def umount_ssh_dir (slicename):
161 return Account._manage_ssh_dir (slicename, do_mount=False)
163 # bind mount / umount root side dir to sliver side
165 def _manage_ssh_dir (slicename, do_mount):
166 logger.log("_manage_ssh_dir, requested to "+("mount" if do_mount else "umount")+" ssh dir for "+ slicename)
168 root_ssh = "/home/{}/.ssh".format(slicename)
169 sliver_ssh = "/vservers/{}/home/{}/.ssh".format(slicename, slicename)
170 def is_mounted (root_ssh):
171 with open('/proc/mounts') as mountsfile:
172 for mount_line in mountsfile.readlines():
173 if mount_line.find (root_ssh) >= 0:
177 # any of both might not exist yet
178 for path in [root_ssh, sliver_ssh]:
179 if not os.path.exists (path):
181 if not os.path.isdir (path):
183 if not is_mounted(root_ssh):
184 command = ['mount', '--bind', '-o', 'ro', root_ssh, sliver_ssh]
185 mounted = logger.log_call (command)
186 msg = "OK" if mounted else "WARNING: FAILED"
187 logger.log("_manage_ssh_dir: mounted {} into slice {} - {}"
188 .format(root_ssh, slicename, msg))
190 if is_mounted (sliver_ssh):
191 command = ['umount', sliver_ssh]
192 umounted = logger.log_call(command)
193 msg = "OK" if umounted else "WARNING: FAILED"
194 logger.log("_manage_ssh_dir: umounted {} - {}"
195 .format(sliver_ssh, msg))
197 logger.log_exc("_manage_ssh_dir failed", name=slicename)
201 def __init__(self, name):
202 self.name = name # username
203 self._acct = None # the account object currently associated with this worker
205 def ensure_created(self, rec):
207 Check account type is still valid. If not, recreate sliver.
208 If still valid, check if running and configure/start if not.
210 logger.log_data_in_file(rec, "/var/lib/nodemanager/{}.rec.txt".format(rec['name']),
211 'raw rec captured in ensure_created', logger.LOG_VERBOSE)
212 curr_class = self._get_class()
213 next_class = type_acct_class[rec['type']]
214 if next_class != curr_class:
215 self._destroy(curr_class)
217 try: next_class.create(self.name, rec)
218 finally: create_sem.release()
219 if not isinstance(self._acct, next_class):
220 self._acct = next_class(rec)
221 logger.verbose("account.Worker.ensure_created: {}, running={}"
222 .format(self.name, self.is_running()))
224 # reservation_alive is set on reservable nodes, and its value is a boolean
225 if 'reservation_alive' in rec:
227 if rec['reservation_alive']:
228 # this sliver has the lease, it is safe to start it
229 if not self.is_running():
234 # not having the lease, do not start it
236 # usual nodes - preserve old code
237 # xxx it's not clear what to do when a sliver changes type/class
238 # in a reservable node
240 if not self.is_running() or self.needs_reimage(rec['vref']) or next_class != curr_class:
245 def ensure_destroyed(self):
246 self._destroy(self._get_class())
248 # take rec as an arg here for api_calls
249 def start(self, rec, d = 0):
250 self._acct.configure(rec)
251 self._acct.start(delay=d)
253 def configure(self, rec):
254 self._acct.configure(rec)
259 def is_running(self):
260 if self._acct and self._acct.is_running():
263 logger.verbose("Worker.is_running ({}) - no account or not running".format(self.name))
266 def needs_reimage(self, target_slicefamily):
268 logger.verbose("Worker.needs_reimage ({}) - no account -> True".format(self.name))
271 account_needs_reimage = self._acct.needs_reimage(target_slicefamily)
272 if account_needs_reimage:
273 logger.log("Worker.needs_reimage ({}) - account needs reimage (tmp: DRY RUN)"
276 logger.verbose("Worker.needs_reimage ({}) - everything fine"
278 return account_needs_reimage
280 def _destroy(self, curr_class):
283 destroy_sem.acquire()
285 logger.verbose("account._destroy is callling destroy from {}"
286 .format(curr_class.__name__))
287 curr_class.destroy(self.name)
289 destroy_sem.release()
291 def _get_class(self):
293 shell = pwd.getpwnam(self.name)[6]
296 return shell_acct_class[shell]