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]
76 finally: name_worker_lock.release()
81 Base class for all types of account
84 def __init__(self, name):
87 logger.verbose('account: Initing account {}'.format(name))
90 def create(name, vref = None):
97 def configure(self, rec):
99 Write <rec['keys']> to my authorized_keys file.
101 new_keys = rec['keys']
102 logger.verbose('account: configuring {} with {} keys'.format(self.name, len(new_keys)))
103 if new_keys != self.keys:
104 # get the unix account info
105 gid = grp.getgrnam("slices")[2]
106 pw_info = pwd.getpwnam(self.name)
110 # write out authorized_keys file and conditionally create
111 # the .ssh subdir if need be.
112 dot_ssh = os.path.join(pw_dir, '.ssh')
113 if not os.path.isdir(dot_ssh):
114 if not os.path.isdir(pw_dir):
115 logger.verbose('account: WARNING: homedir {} does not exist for {}!'
116 .format(pw_dir, self.name))
118 os.chown(pw_dir, uid, gid)
121 auth_keys = os.path.join(dot_ssh, 'authorized_keys')
122 tools.write_file(auth_keys, lambda f: f.write(new_keys))
124 # set access permissions and ownership properly
125 os.chmod(dot_ssh, 0700)
126 os.chown(dot_ssh, uid, gid)
127 os.chmod(auth_keys, 0600)
128 os.chown(auth_keys, uid, gid)
130 # set self.keys to new_keys only when all of the above ops succeed
133 logger.log('account: {}: installed ssh keys'.format(self.name))
135 def start(self, delay=0):
139 def is_running(self):
141 def needs_reimage(self, target_slicefamily):
142 stampname = "/vservers/{}/etc/slicefamily".format(self.name)
144 with open(stampname) as f:
145 current_slicefamily = f.read().strip()
146 return current_slicefamily != target_slicefamily
148 logger.verbose("Account.needs_reimage: missing slicefamily {} - left as-is"
152 ### this used to be a plain method but because it needs to be invoked by destroy
153 # which is a static method, they need to become static as well
154 # needs to be done before sliver starts (checked with vs and lxc)
156 def mount_ssh_dir (slicename): return Account._manage_ssh_dir (slicename, do_mount=True)
158 def umount_ssh_dir (slicename): return Account._manage_ssh_dir (slicename, do_mount=False)
160 # bind mount / umount root side dir to sliver side
162 def _manage_ssh_dir (slicename, do_mount):
163 logger.log("_manage_ssh_dir, requested to "+("mount" if do_mount else "umount")+" ssh dir for "+ slicename)
165 root_ssh = "/home/{}/.ssh".format(slicename)
166 sliver_ssh = "/vservers/{}/home/{}/.ssh".format(slicename, slicename)
167 def is_mounted (root_ssh):
168 with open('/proc/mounts') as mountsfile:
169 for mount_line in mountsfile.readlines():
170 if mount_line.find (root_ssh) >= 0:
174 # any of both might not exist yet
175 for path in [root_ssh, sliver_ssh]:
176 if not os.path.exists (path):
178 if not os.path.isdir (path):
180 if not is_mounted(root_ssh):
181 command = ['mount', '--bind', '-o', 'ro', root_ssh, sliver_ssh]
182 mounted = logger.log_call (command)
183 msg = "OK" if mounted else "WARNING: FAILED"
184 logger.log("_manage_ssh_dir: mounted {} into slice {} - {}"
185 .format(root_ssh, slicename, msg))
187 if is_mounted (sliver_ssh):
188 command = ['umount', sliver_ssh]
189 umounted = logger.log_call(command)
190 msg = "OK" if umounted else "WARNING: FAILED"
191 logger.log("_manage_ssh_dir: umounted {} - {}"
192 .format(sliver_ssh, msg))
194 logger.log_exc("_manage_ssh_dir failed", name=slicename)
198 def __init__(self, name):
199 self.name = name # username
200 self._acct = None # the account object currently associated with this worker
202 def ensure_created(self, rec):
204 Check account type is still valid. If not, recreate sliver.
205 If still valid, check if running and configure/start if not.
207 logger.log_data_in_file(rec, "/var/lib/nodemanager/{}.rec.txt".format(rec['name']),
208 'raw rec captured in ensure_created', logger.LOG_VERBOSE)
209 curr_class = self._get_class()
210 next_class = type_acct_class[rec['type']]
211 if next_class != curr_class:
212 self._destroy(curr_class)
214 try: next_class.create(self.name, rec)
215 finally: create_sem.release()
216 if not isinstance(self._acct, next_class):
217 self._acct = next_class(rec)
218 logger.verbose("account.Worker.ensure_created: {}, running={}"
219 .format(self.name, self.is_running()))
221 # reservation_alive is set on reservable nodes, and its value is a boolean
222 if 'reservation_alive' in rec:
224 if rec['reservation_alive']:
225 # this sliver has the lease, it is safe to start it
226 if not self.is_running():
228 else: self.configure(rec)
230 # not having the lease, do not start it
232 # usual nodes - preserve old code
233 # xxx it's not clear what to do when a sliver changes type/class
234 # in a reservable node
236 if not self.is_running() or self.needs_reimage(rec['vref']) or next_class != curr_class:
241 def ensure_destroyed(self):
242 self._destroy(self._get_class())
244 # take rec as an arg here for api_calls
245 def start(self, rec, d = 0):
246 self._acct.configure(rec)
247 self._acct.start(delay=d)
249 def configure(self, rec):
250 self._acct.configure(rec)
255 def is_running(self):
256 if self._acct and self._acct.is_running():
259 logger.verbose("Worker.is_running ({}) - no account or not running".format(self.name))
262 def needs_reimage(self, target_slicefamily):
264 logger.verbose("Worker.needs_reimage ({}) - no account -> True".format(self.name))
267 account_needs_reimage = self._acct.needs_reimage(target_slicefamily)
268 if account_needs_reimage:
269 logger.log("Worker.needs_reimage ({}) - account needs reimage (tmp: DRY RUN)".format(self.name))
271 logger.verbose("Worker.needs_reimage ({}) - everything fine".format(self.name))
272 return account_needs_reimage
274 def _destroy(self, curr_class):
277 destroy_sem.acquire()
278 try: curr_class.destroy(self.name)
279 finally: destroy_sem.release()
281 def _get_class(self):
282 try: shell = pwd.getpwnam(self.name)[6]
283 except KeyError: return None
284 return shell_acct_class[shell]