def main(self, cycle): """ cycle is the duration in seconds of one cycle Corner cases: * cycle = None : fetch value from config_bases * cycle = 0 : run just once (for debug mostly) """ if cycle is None: cycle = Config().value('accounts', 'cycle') cycle = int(cycle) policy = Config().value('accounts', 'access_policy') if policy not in ('open', 'leased', 'closed'): logger.error("Unknown policy {} - using 'closed'" .format(policy)) policy = 'closed' # trick is if cycle != 0: self.run_forever(cycle, policy) else: logger.info("---------- rhubarbe accounts manager oneshot " "policy = {}" .format(policy)) self.manage_accounts(policy)
def run_forever(self, period): while True: beg = time.time() logger.info("---------- rhubarbe accounts manager (period {})" .format(period)) self.manage_accounts() now = time.time() duration = now - beg towait = period - duration logger.info("---------- rhubarbe accounts manager - sleeping for {}" .format(towait)) time.sleep(period - duration)
def replace_file_with_string(destination_path, new_contents, owner=None, chmod=None, remove_if_empty=False): """ Replace a file with new contents checks for changes does not do anything if previous state was already right can handle chmod/chown if requested can also remove resulting file if contents are void, if requested returns * True if a change occurred, or the file is deleted * False if the file has no change * None if the file could not be created (e.g. directory missing) """ try: with destination_path.open() as previous: current = previous.read() except IOError: current = "" if current == new_contents: # if turns out to be an empty string, and remove_if_empty is set, # then make sure to trash the file if it exists if remove_if_empty and not new_contents and destination_path.is_file(): logger.info( "replace_file_with_string: removing file {}" .format(destination_path)) try: destination_path.unlink() finally: return True # pylint: disable=w0150 # we're done and have nothing to do return False # overwrite file: create a temp in the same directory try: with destination_path.open('w') as new: new.write(new_contents) if chmod: destination_path.chmod(chmod) if owner: os.system("chown {} {}".format(owner, destination_path)) return True except IOError as exc: logger.error("Cannot create {}".format(destination_path)) return None
def create_account(self, slicename): """ Does useradd with the right options Plus, creates an empty .ssh dir with proper permissions NOTE that this addresses ubuntu for now, fedora 'useradd' being slightly different as far as I remember (at least wrt homedir creation, IIRC again) """ commands = [ "useradd --create-home --user-group {x} --shell /bin/bash", "mkdir /home/{x}/.ssh", "chmod 700 /home/{x}/.ssh", "chown -R {x}:{x} /home/{x}", ] for cmd in commands: command = cmd.format(x=slicename) logger.info("Running {}".format(command)) retcod = os.system(command) if retcod != 0: logger.error("{} -> {}".format(command, retcod))
def create_account(slicename): """ Does useradd with the right options Plus, creates an empty .ssh dir with proper permissions NOTE that this addresses ubuntu for now, fedora 'useradd' being slightly different as far as I remember (at least wrt homedir creation, IIRC again) """ commands = [ "useradd --create-home --user-group {x} --shell /bin/bash", "mkdir /home/{x}/.ssh", "chmod 700 /home/{x}/.ssh", "chown -R {x}:{x} /home/{x}", ] for cmd in commands: command = cmd.format(x=slicename) logger.info("Running {}".format(command)) retcod = os.system(command) if retcod != 0: logger.error("{} -> {}".format(command, retcod))
def replace_file_with_string(filename, new_contents, owner=None, chmod=None, remove_if_empty=False): """ Replace a file with new contents checks for changes does not do anything if previous state was already right can handle chmod/chown if requested can also remove resulting file if contents are void, if requested returns True if a change occurred, or the file is deleted """ try: with open(filename) as f: current = f.read() except: current = "" if current == new_contents: # if turns out to be an empty string, and remove_if_empty is set, # then make sure to trash the file if it exists if remove_if_empty and not new_contents and os.path.isfile(filename): logger.info( "replace_file_with_string: removing file {}".format(filename)) try: os.unlink(filename) finally: return True # we're done and have nothing to do return False # overwrite filename file: create a temp in the same directory path = os.path.dirname(filename) or '.' with open(filename, 'w') as f: f.write(new_contents) if chmod: os.chmod(filename, chmod) if owner: retcod = os.system("chown {} {}".format(owner, filename)) return True
def run_forever(self, cycle, policy): while True: beg = time.time() logger.info("---------- rhubarbe accounts manager " "policy = {}, cycle {}s" .format(policy, cycle)) self.manage_accounts(policy) now = time.time() duration = now - beg towait = cycle - duration if towait > 0: logger.info("---------- rhubarbe accounts manager - " "sleeping for {:.2f}s" .format(towait)) time.sleep(towait) else: logger.info("duration {}s exceeded cycle {}s - " "skipping sleep" .format(duration, cycle))
def manage_accounts(self): self._running = True # get context passwd_entries = self.all_passwd_entries() home_basenames = self.authorized_home_basenames() legacy_basenames = self.authorized_legacy_basenames() # get plcapi specification of what should be slices = self.proxy().GetSlices( {}, ['slice_id', 'name', 'expires', 'person_ids']) persons = self.proxy().GetPersons( {}, ['person_id', 'email', 'slice_ids', 'key_ids']) keys = self.proxy().GetKeys() persons_by_id = {p['person_id']: p for p in persons} keys_by_id = {k['key_id']: k for k in keys} active_slices = [] for slice in slices: slicename = slice['name'] authorized_keys = self.authorized_key_lines( slice, persons_by_id, keys_by_id) # don't bother to create an account if the slice has no key if authorized_keys: try: active_slices.append(slicename) # create account if missing if slicename not in passwd_entries: self.create_account(slicename) # dictate authorized_keys contents ssh_auth_keys = "/home/{x}/.ssh/authorized_keys".format( x=slicename) replace_file_with_string(ssh_auth_keys, authorized_keys, chmod=0o400, owner="{x}:{x}".format( x=slicename), remove_if_empty=True) self.create_ssh_config(slicename) except Exception as e: logger.exception("could not properly deal " "with active slice {x}" .format(x=slicename)) # find out about slices that currently have suthorized keys but should # not for slicename in home_basenames: if slicename not in active_slices: try: logger.info( "Removing authorized_keys for {x}".format(x=slicename)) ssh_auth_keys = "/home/{x}/.ssh/authorized_keys".format( x=slicename) os.unlink(ssh_auth_keys) except Exception as e: logger.exception("could not properly deal " "with inactive slice {x}" .format(x=slicename)) # a one-shot piece of code : turn off legacy slices for slicename in legacy_basenames: try: logger.info( "legacy slicename {x} " "needs to be shutdown".format(x=slicename)) ssh_auth_keys = "/home/{x}/.ssh/authorized_keys".format( x=slicename) # xxx enable the following line to tear down legacy slices # os.unlink(ssh_auth_keys) except Exception as e: logger.exception("could not properly deal " "with legacy slice {x}" .format(x=slicename)) self._running = False
def manage_accounts(self, policy): # pylint: disable=r0914 # get plcapi specification of what should be slices = self.proxy().GetSlices( {}, ['slice_id', 'name', 'expires', 'person_ids']) persons = self.proxy().GetPersons( {}, ['person_id', 'email', 'slice_ids', 'key_ids']) keys = self.proxy().GetKeys() current_leases = [] if policy == 'leased': now = int(time.time()) current_leases = self.proxy().GetLeases( {'alive': now}, ['name']) if (current_leases is None or slices is None or persons is None or keys is None): logger.info("PLCAPI unreachable - back to sleep") return # prepare data persons_by_id = {p['person_id']: p for p in persons} keys_by_id = {k['key_id']: k for k in keys} # current_slicenames will contain 0 or 1 item current_slicenames = [lease['name'] for lease in current_leases] # initialize with the slice names that are in /etc/passwd logins = self.slices_from_passwd() # initialize map login_name -> authorized_keys contents # this is where we handle the fact that obsolete slices # will effectively have their authorized_keys voided auths_by_login = {login: "" for login in logins} for sliceobj in slices: slicename = sliceobj['name'] # policy-dependant if policy == 'closed': authorized_keys = "" elif policy == 'leased': authorized_keys = "" if slicename in current_slicenames: authorized_keys = self.authorized_key_lines( sliceobj, persons_by_id, keys_by_id) # policy == 'open' else: authorized_keys = self.authorized_key_lines( sliceobj, persons_by_id, keys_by_id) auths_by_login[slicename] = authorized_keys # implement it for slicename, keys in auths_by_login.items(): try: if slicename not in logins: self.create_account(slicename) # do this always, allows to propagate later changes self.create_ssh_config(slicename) self.apply_keys(slicename, keys) except Exception: logger.exception("Could not deal with slice {}" .format(slicename))