class RemoteSessionAssistant(): """Remote execution enabling wrapper for the SessionAssistant""" REMOTE_API_VERSION = 9 def __init__(self, cmd_callback): _logger.debug("__init__()") self._cmd_callback = cmd_callback self._sudo_broker = SudoBroker() self._sudo_password = None self._session_change_lock = Lock() self._operator_lock = Lock() self.buffered_ui = BufferedUI() self._input_piping = os.pipe() self._passwordless_sudo = is_passwordless_sudo() self.terminate_cb = None self._pipe_from_master = open(self._input_piping[1], 'w') self._pipe_to_subproc = open(self._input_piping[0]) self._reset_sa() self._currently_running_job = None def _reset_sa(self): _logger.info("Resetting RSA") self._state = Idle self._sa = SessionAssistant('service', api_flags={SA_RESTARTABLE}) self._sa.configure_application_restart(self._cmd_callback) self._be = None self._session_id = "" self._jobs_count = 0 self._job_index = 0 self._currently_running_job = None # XXX: yuck! self._last_job = None self._current_comments = "" self._last_response = None self._normal_user = '' self.session_change_lock.acquire(blocking=False) self.session_change_lock.release() @property def session_change_lock(self): return self._session_change_lock @property def config(self): return self._sa.config def allowed_when(*states): def wrap(f): def fun(self, *args): if self._state not in states: raise AssertionError("expected %s, is %s" % (states, self._state)) return f(self, *args) return fun return wrap def interact(self, interaction): self._state = Interacting self._current_interaction = interaction yield self._current_interaction @allowed_when(Interacting) def remember_users_response(self, response): if response == 'rollback': self._currently_running_job = None self.session_change_lock.acquire(blocking=False) self.session_change_lock.release() self._current_comments = "" self._state = TestsSelected return self._last_response = response self._state = Running def _prepare_display_without_psutil(self): try: value = check_output( 'strings /proc/*/environ 2>/dev/null | ' 'grep -m 1 -oP "(?<=DISPLAY=).*"', shell=True, universal_newlines=True).rstrip() return {'DISPLAY': value} except CalledProcessError: return None def prepare_extra_env(self): # If possible also set the DISPLAY env var # i.e when a user desktop session is running for p in psutil.pids(): try: p_environ = psutil.Process(p).environ() p_user = psutil.Process(p).username() except psutil.AccessDenied: continue except AttributeError: # psutil < 4.0.0 doesn't provide Process.environ() return self._prepare_display_without_psutil() except psutil.NoSuchProcess: # quietly ignore the process that died before we had a chance to # read the environment from them continue if ("DISPLAY" in p_environ and p_user != 'gdm'): # gdm uses :1024 return {'DISPLAY': p_environ['DISPLAY']} @allowed_when(Idle) def start_session(self, configuration): self._reset_sa() _logger.debug("start_session: %r", configuration) session_title = 'checkbox-slave' session_desc = 'checkbox-slave session' self._launcher = load_configs() if configuration['launcher']: self._launcher.read_string(configuration['launcher'], False) session_title = self._launcher.session_title session_desc = self._launcher.session_desc self._sa.use_alternate_configuration(self._launcher) self._normal_user = self._launcher.normal_user if configuration['normal_user']: self._normal_user = configuration['normal_user'] pass_provider = (None if self._passwordless_sudo else self.get_decrypted_password) runner_kwargs = { 'normal_user_provider': lambda: self._normal_user, 'password_provider': pass_provider, 'stdin': self._pipe_to_subproc, 'extra_env': self.prepare_extra_env(), } self._sa.start_new_session(session_title, UnifiedRunner, runner_kwargs) self._sa.update_app_blob( json.dumps({ 'description': session_desc, }).encode("UTF-8")) self._sa.update_app_blob( json.dumps({ 'launcher': configuration['launcher'], }).encode("UTF-8")) self._session_id = self._sa.get_session_id() tps = self._sa.get_test_plans() filtered_tps = set() for filter in self._launcher.test_plan_filters: filtered_tps.update(fnmatch.filter(tps, filter)) filtered_tps = list(filtered_tps) response = zip( filtered_tps, [self._sa.get_test_plan(tp).name for tp in filtered_tps]) self._state = Started self._available_testplans = sorted( response, key=lambda x: x[1]) # sorted by name return self._available_testplans @allowed_when(Started) def prepare_bootstrapping(self, test_plan_id): """ Go through the list of bootstrapping jobs, and return True if sudo password will be needed for any bootstrapping job. """ _logger.debug("prepare_bootstrapping: %r", test_plan_id) self._sa.update_app_blob( json.dumps({ 'testplan_id': test_plan_id, }).encode("UTF-8")) self._sa.select_test_plan(test_plan_id) for job_id in self._sa.get_bootstrap_todo_list(): job = self._sa.get_job(job_id) if job.user is not None: # job requires sudo controller return True return False @allowed_when(Started) def get_bootstrapping_todo_list(self): return self._sa.get_bootstrap_todo_list() def finish_bootstrap(self): self._sa.finish_bootstrap() self._state = Bootstrapped if self._launcher.auto_retry: for job_id in self._sa.get_static_todo_list(): job_state = self._sa.get_job_state(job_id) job_state.attempts = self._launcher.max_attempts return self._sa.get_static_todo_list() def get_manifest_repr(self): return self._sa.get_manifest_repr() def save_manifest(self, manifest_answers): return self._sa.save_manifest(manifest_answers) def modify_todo_list(self, chosen_jobs): self._sa.use_alternate_selection(chosen_jobs) def finish_job_selection(self): self._jobs_count = len(self._sa.get_dynamic_todo_list()) self._state = TestsSelected @allowed_when(Interacting) def rerun_job(self, job_id, result): self._sa.use_job_result(job_id, result) self.session_change_lock.acquire(blocking=False) self.session_change_lock.release() self._state = TestsSelected @allowed_when(TestsSelected) def run_job(self, job_id): """ Depending on the type of the job, run_job can yield different number of Interaction instances. """ _logger.debug("run_job: %r", job_id) self._job_index = self._jobs_count - len( self._sa.get_dynamic_todo_list()) + 1 self._currently_running_job = job_id self._current_comments = "" job = self._sa.get_job(job_id) if job.plugin in ['manual', 'user-interact-verify', 'user-interact']: may_comment = True while may_comment: may_comment = False if job.tr_description() and not job.tr_purpose(): yield from self.interact( Interaction('description', job.tr_description())) if job.tr_purpose(): yield from self.interact( Interaction('purpose', job.tr_purpose())) if job.tr_steps(): yield from self.interact( Interaction('steps', job.tr_steps())) if self._last_response == 'comment': yield from self.interact(Interaction('comment')) if self._last_response: self._current_comments += self._last_response may_comment = True continue if self._last_response == 'skip': def skipped_builder(*args, **kwargs): result_builder = JobResultBuilder( outcome=IJobResult.OUTCOME_SKIP, comments=_("Explicitly skipped before execution")) if self._current_comments != "": result_builder.comments = self._current_comments return result_builder self._be = BackgroundExecutor(self, job_id, skipped_builder) yield from self.interact( Interaction('skip', job.verification, self._be)) if job.command: if (job.user and not self._passwordless_sudo and not self._sudo_password): self._ephemeral_key = EphemeralKey() self._current_interaction = Interaction( 'sudo_input', self._ephemeral_key.public_key) pass_is_correct = False while not pass_is_correct: self.state = Interacting yield self._current_interaction pass_is_correct = validate_pass( self._sudo_broker.decrypt_password( self._sudo_password)) if not pass_is_correct: print(_('Sorry, try again.')) assert (self._sudo_password is not None) self._state = Running self._be = BackgroundExecutor(self, job_id, self._sa.run_job) else: def undecided_builder(*args, **kwargs): return JobResultBuilder(outcome=IJobResult.OUTCOME_UNDECIDED) self._be = BackgroundExecutor(self, job_id, undecided_builder) if self._sa.get_job(self._currently_running_job).plugin in [ 'manual', 'user-interact-verify' ]: yield from self.interact( Interaction('verification', job.verification, self._be)) @allowed_when(Started, Bootstrapping) def run_bootstrapping_job(self, job_id): self._currently_running_job = job_id self._state = Bootstrapping self._be = BackgroundExecutor(self, job_id, self._sa.run_job) @allowed_when(Running, Bootstrapping, Interacting, TestsSelected) def monitor_job(self): """ Check the state of the currently running job. :returns: (state, payload) tuple. Payload conveys detailed info that's characteristic to the current state. """ _logger.debug("monitor_job()") # either return [done, running, awaiting response] # TODO: handle awaiting_response (reading from stdin by the job) if self._be and self._be.is_alive(): return ('running', self.buffered_ui.get_output()) else: return ('done', self.buffered_ui.get_output()) def get_remote_api_version(self): return self.REMOTE_API_VERSION def whats_up(self): """ Check what is remote-service up to :returns: (state, payload) tuple. """ _logger.debug("whats_up()") payload = None if self._state == Running: payload = (self._job_index, self._jobs_count, self._currently_running_job) if self._state == TestsSelected and not self._currently_running_job: payload = {'last_job': self._last_job} elif self._state == Started: payload = self._available_testplans elif self._state == Interacting: payload = self._current_interaction elif self._state == Bootstrapped: payload = self._sa.get_static_todo_list() return self._state, payload def terminate(self): if self.terminate_cb: self.terminate_cb() def get_session_progress(self): """Return list of completed and not completed jobs in a dict.""" _logger.debug("get_session_progress()") return { "done": self._sa.get_dynamic_done_list(), "todo": self._sa.get_dynamic_todo_list(), } def get_master_public_key(self): """Expose the master public key""" return self._sudo_broker.master_public def save_password(self, cyphertext): """Store encrypted password""" if validate_pass(self._sudo_broker.decrypt_password(cyphertext)): self._sudo_password = cyphertext return True return False def get_decrypted_password(self): """Return decrypted password""" if self._passwordless_sudo: return '' assert (self._sudo_password) return self._sudo_broker.decrypt_password(self._sudo_password) def finish_job(self, result=None): # assert the thread completed self.session_change_lock.acquire(blocking=False) self.session_change_lock.release() if self._sa.get_job(self._currently_running_job).plugin in [ 'manual', 'user-interact-verify' ] and not result: # for manually verified jobs we don't set the outcome here # it is already determined return if not result: result = self._be.wait().get_result() self._sa.use_job_result(self._currently_running_job, result) if self._state != Bootstrapping: if not self._sa.get_dynamic_todo_list(): if (self._launcher.auto_retry and self.get_rerun_candidates('auto')): self._state = TestsSelected else: self._state = Idle else: self._state = TestsSelected return result def get_rerun_candidates(self, session_type='manual'): return self._sa.get_rerun_candidates(session_type) def prepare_rerun_candidates(self, rerun_candidates): candidates = self._sa.prepare_rerun_candidates(rerun_candidates) self._state = TestsSelected return candidates def get_job_result(self, job_id): return self._sa.get_job_state(job_id).result def get_jobs_repr(self, job_ids, offset=0): """ Translate jobs into a {'field': 'val'} representations. :param job_ids: list of job ids to get and translate :param offset: apply an offset to the job number if for instance the job list is being requested part way through a session :returns: list of dicts representing jobs """ test_info_list = tuple() for job_no, job_id in enumerate(job_ids, start=offset + 1): job = self._sa.get_job(job_id) cat_id = self._sa.get_job_state(job.id).effective_category_id duration_txt = _('No estimated duration provided for this job') if job.estimated_duration is not None: duration_txt = '{} {}'.format(job.estimated_duration, _('seconds')) # the next dict is only to get test_info generating code tidier automated_desc = { True: _('this job is fully automated'), False: _('this job requires some manual interaction') } test_info = { "id": job.id, "partial_id": job.partial_id, "name": job.tr_summary(), "category_id": cat_id, "category_name": self._sa.get_category(cat_id).tr_name(), "automated": automated_desc[job.automated], "duration": duration_txt, "description": (job.tr_description() or _('No description provided for this job')), "outcome": self._sa.get_job_state(job.id).result.outcome, "user": job.user, "command": job.command, "num": job_no, "plugin": job.plugin, } test_info_list = test_info_list + ((test_info, )) return test_info_list def resume_by_id(self, session_id=None): self._launcher = load_configs() resume_candidates = list(self._sa.get_resumable_sessions()) if not session_id: if not resume_candidates: print('No session to resume') return session_id = resume_candidates[0].id if session_id not in [s.id for s in resume_candidates]: print("Requested session not found") return _logger.warning("Resuming session: %r", session_id) self._normal_user = self._launcher.normal_user pass_provider = (None if self._passwordless_sudo else self.get_decrypted_password) runner_kwargs = { 'normal_user_provider': lambda: self._normal_user, 'password_provider': pass_provider, 'stdin': self._pipe_to_subproc, 'extra_env': self.prepare_extra_env(), } meta = self._sa.resume_session(session_id, runner_kwargs=runner_kwargs) app_blob = json.loads(meta.app_blob.decode("UTF-8")) launcher = app_blob['launcher'] self._launcher.read_string(launcher, False) self._sa.use_alternate_configuration(self._launcher) test_plan_id = app_blob['testplan_id'] self._sa.select_test_plan(test_plan_id) self._sa.bootstrap() self._last_job = meta.running_job_name result_dict = { 'outcome': IJobResult.OUTCOME_PASS, 'comments': _("Automatically passed after resuming execution"), } result_path = os.path.join(self._sa.get_session_dir(), 'CHECKBOX_DATA', '__result') if os.path.exists(result_path): try: with open(result_path, 'rt') as f: result_dict = json.load(f) # the only really important field in the result is # 'outcome' so let's make sure it doesn't contain # anything stupid if result_dict.get('outcome') not in [ 'pass', 'fail', 'skip' ]: result_dict['outcome'] = IJobResult.OUTCOME_PASS except json.JSONDecodeError as e: pass result = MemoryJobResult(result_dict) if self._last_job: try: self._sa.use_job_result(self._last_job, result, True) except KeyError: raise SystemExit(self._last_job) # some jobs have already been run, so we need to update the attempts # count for future auto-rerunning if self._launcher.auto_retry: for job_id in [ job.id for job in self.get_rerun_candidates('auto') ]: job_state = self._sa.get_job_state(job_id) job_state.attempts = self._launcher.max_attempts - len( job_state.result_history) self._state = TestsSelected def finalize_session(self): self._sa.finalize_session() self._reset_sa() def transmit_input(self, text): self._pipe_from_master.write(text) self._pipe_from_master.flush() def send_signal(self, signal): if not self._currently_running_job: return target_user = self._sa.get_job(self._currently_running_job).user self._sa.send_signal(signal, target_user) @property def manager(self): return self._sa._manager @property def passwordless_sudo(self): return self._passwordless_sudo @property def sideloaded_providers(self): return self._sa.sideloaded_providers
class RemoteSessionAssistant(): """Remote execution enabling wrapper for the SessionAssistant""" REMOTE_API_VERSION = 11 def __init__(self, cmd_callback): _logger.debug("__init__()") self._cmd_callback = cmd_callback self._session_change_lock = Lock() self._operator_lock = Lock() self._ui = BufferedUI() self._input_piping = os.pipe() self._passwordless_sudo = is_passwordless_sudo() self.terminate_cb = None self._pipe_from_master = open(self._input_piping[1], 'w') self._pipe_to_subproc = open(self._input_piping[0]) self._reset_sa() self._currently_running_job = None def _reset_sa(self): _logger.info("Resetting RSA") self._state = Idle self._sa = SessionAssistant('service', api_flags={SA_RESTARTABLE}) self._be = None self._session_id = "" self._jobs_count = 0 self._job_index = 0 self._currently_running_job = None # XXX: yuck! self._last_job = None self._current_comments = "" self._last_response = None self._normal_user = '' self.session_change_lock.acquire(blocking=False) self.session_change_lock.release() @property def session_change_lock(self): return self._session_change_lock @property def config(self): return self._sa.config def update_app_blob(self, app_blob): self._sa.update_app_blob(app_blob) def allowed_when(*states): def wrap(f): def fun(self, *args): if self._state not in states: raise AssertionError( "expected %s, is %s" % (states, self._state)) return f(self, *args) return fun return wrap def interact(self, interaction): self._state = Interacting self._current_interaction = interaction yield self._current_interaction @allowed_when(Interacting) def remember_users_response(self, response): if response == 'rollback': self._currently_running_job = None self.session_change_lock.acquire(blocking=False) self.session_change_lock.release() self._current_comments = "" self._state = TestsSelected return self._last_response = response self._state = Running def _prepare_display_without_psutil(self): try: display_value = check_output( 'strings /proc/*/environ 2>/dev/null | ' 'grep -m 1 -oP "(?<=DISPLAY=).*"', shell=True, universal_newlines=True).rstrip() xauth_value = check_output( 'strings /proc/*/environ 2>/dev/null | ' 'grep -m 1 -oP "(?<=XAUTHORITY=).*"', shell=True, universal_newlines=True).rstrip() return {'DISPLAY': display_value, 'XAUTHORITY': xauth_value} except CalledProcessError: return None def prepare_extra_env(self): # If possible also set the DISPLAY env var # i.e when a user desktop session is running for p in psutil.pids(): try: p_environ = psutil.Process(p).environ() p_user = psutil.Process(p).username() except psutil.AccessDenied: continue except AttributeError: # psutil < 4.0.0 doesn't provide Process.environ() return self._prepare_display_without_psutil() except psutil.NoSuchProcess: # quietly ignore the process that died before we had a chance # to read the environment from them continue if ( "DISPLAY" in p_environ and "XAUTHORITY" in p_environ and p_user != 'gdm' ): # gdm uses :1024 return { 'DISPLAY': p_environ['DISPLAY'], 'XAUTHORITY': p_environ['XAUTHORITY'] } @allowed_when(Idle) def start_session(self, configuration): self._reset_sa() _logger.info("start_session: %r", configuration) session_title = 'checkbox-slave' session_desc = 'checkbox-slave session' session_type = 'checkbox-slave' self._launcher = load_configs() if configuration['launcher']: self._launcher.read_string(configuration['launcher'], False) if self._launcher.session_title: session_title = self._launcher.session_title if self._launcher.session_desc: session_desc = self._launcher.session_desc self._sa.use_alternate_configuration(self._launcher) if configuration['normal_user']: self._normal_user = configuration['normal_user'] else: self._normal_user = self._launcher.normal_user if not self._normal_user: import pwd try: self._normal_user = pwd.getpwuid(1000).pw_name _logger.warning( ("normal_user not supplied via config(s). " "non-root jobs will run as %s"), self._normal_user) except KeyError: raise RuntimeError( ("normal_user not supplied via config(s). " "Username for uid 1000 not found")) runner_kwargs = { 'normal_user_provider': lambda: self._normal_user, 'stdin': self._pipe_to_subproc, 'extra_env': self.prepare_extra_env(), } self._sa.start_new_session(session_title, UnifiedRunner, runner_kwargs) new_blob = json.dumps({ 'description': session_desc, 'type': session_type, 'launcher': configuration['launcher'], 'effective_normal_user': self._normal_user, }).encode("UTF-8") self._sa.update_app_blob(new_blob) self._sa.configure_application_restart(self._cmd_callback) self._session_id = self._sa.get_session_id() tps = self._sa.get_test_plans() filtered_tps = set() for filter in self._launcher.test_plan_filters: filtered_tps.update(fnmatch.filter(tps, filter)) filtered_tps = list(filtered_tps) response = zip(filtered_tps, [self._sa.get_test_plan( tp).name for tp in filtered_tps]) self._state = Started self._available_testplans = sorted( response, key=lambda x: x[1]) # sorted by name return self._available_testplans @allowed_when(Started) def prepare_bootstrapping(self, test_plan_id): """Save picked test plan to the app blob.""" _logger.debug("prepare_bootstrapping: %r", test_plan_id) self._sa.update_app_blob(json.dumps( {'testplan_id': test_plan_id, }).encode("UTF-8")) self._sa.select_test_plan(test_plan_id) # TODO: REMOTE API RAPI: Change this API on the next RAPI bump # previously the function returned bool signifying the need for sudo # password. With slave being guaranteed to never need it anymor # we can make this funciton return nothing return False @allowed_when(Started) def get_bootstrapping_todo_list(self): return self._sa.get_bootstrap_todo_list() def finish_bootstrap(self): self._sa.finish_bootstrap() self._state = Bootstrapped if self._launcher.auto_retry: for job_id in self._sa.get_static_todo_list(): job_state = self._sa.get_job_state(job_id) job_state.attempts = self._launcher.max_attempts return self._sa.get_static_todo_list() def get_manifest_repr(self): return self._sa.get_manifest_repr() def save_manifest(self, manifest_answers): return self._sa.save_manifest(manifest_answers) def modify_todo_list(self, chosen_jobs): self._sa.use_alternate_selection(chosen_jobs) def finish_job_selection(self): self._jobs_count = len(self._sa.get_dynamic_todo_list()) self._state = TestsSelected @allowed_when(Interacting) def rerun_job(self, job_id, result): self._sa.use_job_result(job_id, result) self.session_change_lock.acquire(blocking=False) self.session_change_lock.release() self._state = TestsSelected def _get_ui_for_job(self, job): show_out = True if self._launcher.output == 'hide-resource-and-attachment': if job.plugin in ('local', 'resource', 'attachment'): show_out = False elif self._launcher.output in ['hide', 'hide-automated']: if job.plugin in ('shell', 'local', 'resource', 'attachment'): show_out = False if 'suppress-output' in job.get_flag_set(): show_out = False if show_out: self._ui = BufferedUI() else: self._ui = RemoteSilentUI() return self._ui @allowed_when(TestsSelected) def run_job(self, job_id): """ Depending on the type of the job, run_job can yield different number of Interaction instances. """ _logger.debug("run_job: %r", job_id) self._job_index = self._jobs_count - len( self._sa.get_dynamic_todo_list()) + 1 self._currently_running_job = job_id self._current_comments = "" job = self._sa.get_job(job_id) job_state = self._sa.get_job_state(job_id) if not job_state.can_start(): outcome = IJobResult.OUTCOME_NOT_SUPPORTED for inhibitor in job_state.readiness_inhibitor_list: if ( inhibitor.cause == InhibitionCause.FAILED_RESOURCE and 'fail-on-resource' in job.get_flag_set() ): outcome = IJobResult.OUTCOME_FAIL break elif inhibitor.cause != InhibitionCause.FAILED_DEP: continue related_job_state = self._sa._context.state.job_state_map[ inhibitor.related_job.id] if related_job_state.result.outcome == IJobResult.OUTCOME_SKIP: outcome = IJobResult.OUTCOME_SKIP def cant_start_builder(*args, **kwargs): result_builder = JobResultBuilder( outcome=outcome, comments=job_state.get_readiness_description()) return result_builder self._be = BackgroundExecutor(self, job_id, cant_start_builder) yield from self.interact(Interaction('skip', None, self._be)) if job.plugin in [ 'manual', 'user-interact-verify', 'user-interact']: may_comment = True while may_comment: may_comment = False if job.tr_description() and not job.tr_purpose(): yield from self.interact( Interaction('description', job.tr_description())) if job.tr_purpose(): yield from self.interact( Interaction('purpose', job.tr_purpose())) if job.tr_steps(): yield from self.interact( Interaction('steps', job.tr_steps())) if self._last_response == 'comment': yield from self.interact(Interaction('comment')) if self._last_response: self._current_comments += self._last_response may_comment = True continue if self._last_response == 'skip': def skipped_builder(*args, **kwargs): result_builder = JobResultBuilder( outcome=IJobResult.OUTCOME_SKIP, comments=_("Explicitly skipped before execution")) if self._current_comments != "": result_builder.comments = self._current_comments return result_builder self._be = BackgroundExecutor( self, job_id, skipped_builder) yield from self.interact( Interaction('skip', job.verification, self._be)) if job.command: self._state = Running ui = self._get_ui_for_job(job) self._be = BackgroundExecutor(self, job_id, self._sa.run_job, ui) else: def undecided_builder(*args, **kwargs): return JobResultBuilder(outcome=IJobResult.OUTCOME_UNDECIDED) self._be = BackgroundExecutor(self, job_id, undecided_builder) if self._sa.get_job(self._currently_running_job).plugin in [ 'manual', 'user-interact-verify']: yield from self.interact( Interaction('verification', job.verification, self._be)) @allowed_when(Started, Bootstrapping) def run_bootstrapping_job(self, job_id): self._currently_running_job = job_id self._state = Bootstrapping self._be = BackgroundExecutor(self, job_id, self._sa.run_job) @allowed_when(Running, Bootstrapping, Interacting, TestsSelected) def monitor_job(self): """ Check the state of the currently running job. :returns: (state, payload) tuple. Payload conveys detailed info that's characteristic to the current state. """ _logger.debug("monitor_job()") # either return [done, running, awaiting response] # TODO: handle awaiting_response (reading from stdin by the job) if self._be and self._be.is_alive(): return ('running', self._ui.get_output()) else: return ('done', self._ui.get_output()) def get_remote_api_version(self): return self.REMOTE_API_VERSION def whats_up(self): """ Check what is remote-service up to :returns: (state, payload) tuple. """ _logger.debug("whats_up() -> %r", self._state) payload = None if self._state == Running: payload = ( self._job_index, self._jobs_count, self._currently_running_job ) if self._state == TestsSelected and not self._currently_running_job: payload = {'last_job': self._last_job} elif self._state == Started: payload = self._available_testplans elif self._state == Interacting: payload = self._current_interaction elif self._state == Bootstrapped: payload = self._sa.get_static_todo_list() return self._state, payload def terminate(self): if self.terminate_cb: self.terminate_cb() def get_session_progress(self): """Return list of completed and not completed jobs in a dict.""" _logger.debug("get_session_progress()") return { "done": self._sa.get_dynamic_done_list(), "todo": self._sa.get_dynamic_todo_list(), } def get_master_public_key(self): # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump # this key is only for RAPI compliance. It will never be used as # this master requires slave to be completely sudoless return ( b'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMII' b'BCgKCAQEA5r0bjOA+IH5lDKkW3OYb\nDuEjf5VKgUlDSJJuyBlfLTBIXZ8j3s98' b'6AbV0zB62rAcgiFrBOzx51IzBDBmHI8V\nYYpEa+q4OP4yprYpSg6xzX6LRQapC' b'Iv9BAqN4MWrKBukGMzJyemIVEPv4BSHL5L/\nLY98Mwh4dAXxj5ZdsoVPqgeMo8' b'dxfYEOwVRJvSkseIhxRL6tvgP37c48ApUyjdUO\n3C2YgqJRx7mKKDyLOvhDVEl' b'MqkAfp6qS/8xcGBTEqn08dDQIgPl8KofpC9GXMGbK\nV9FGP+c1bpA3vMOfnpsE' b'WCju2qDoTSKJTm3VMZj88mqH7nOpbk7JI/Yz0EmtNXOM\n6QIDAQAB\n-----EN' b'D PUBLIC KEY-----') def save_password(self, password): """Store sudo password""" # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump # if the slave is running it means we don't need password # so we can consider call to this function as passing return True def finish_job(self, result=None): # assert the thread completed self.session_change_lock.acquire(blocking=False) self.session_change_lock.release() if self._sa.get_job(self._currently_running_job).plugin in [ 'manual', 'user-interact-verify'] and not result: # for manually verified jobs we don't set the outcome here # it is already determined return if not result: result = self._be.wait().get_result() self._sa.use_job_result(self._currently_running_job, result) if self._state != Bootstrapping: if not self._sa.get_dynamic_todo_list(): if ( self._launcher.auto_retry and self.get_rerun_candidates('auto') ): self._state = TestsSelected else: self._state = Idle else: self._state = TestsSelected return result def get_rerun_candidates(self, session_type='manual'): return self._sa.get_rerun_candidates(session_type) def prepare_rerun_candidates(self, rerun_candidates): candidates = self._sa.prepare_rerun_candidates(rerun_candidates) self._state = TestsSelected return candidates def get_job_result(self, job_id): return self._sa.get_job_state(job_id).result def get_jobs_repr(self, job_ids, offset=0): """ Translate jobs into a {'field': 'val'} representations. :param job_ids: list of job ids to get and translate :param offset: apply an offset to the job number if for instance the job list is being requested part way through a session :returns: list of dicts representing jobs """ test_info_list = tuple() for job_no, job_id in enumerate(job_ids, start=offset + 1): job = self._sa.get_job(job_id) cat_id = self._sa.get_job_state(job.id).effective_category_id duration_txt = _('No estimated duration provided for this job') if job.estimated_duration is not None: duration_txt = '{} {}'.format(job.estimated_duration, _( 'seconds')) # the next dict is only to get test_info generating code tidier automated_desc = { True: _('this job is fully automated'), False: _('this job requires some manual interaction') } test_info = { "id": job.id, "partial_id": job.partial_id, "name": job.tr_summary(), "category_id": cat_id, "category_name": self._sa.get_category(cat_id).tr_name(), "automated": automated_desc[job.automated], "duration": duration_txt, "description": (job.tr_description() or _('No description provided for this job')), "outcome": self._sa.get_job_state(job.id).result.outcome, "user": job.user, "command": job.command, "num": job_no, "plugin": job.plugin, } test_info_list = test_info_list + ((test_info, )) return json.dumps(test_info_list) def resume_by_id(self, session_id=None): _logger.info("resume_by_id: %r", session_id) self._launcher = load_configs() resume_candidates = list(self._sa.get_resumable_sessions()) if not session_id: if not resume_candidates: print('No session to resume') return session_id = resume_candidates[0].id if session_id not in [s.id for s in resume_candidates]: print("Requested session not found") return _logger.warning("Resuming session: %r", session_id) runner_kwargs = { 'normal_user_provider': lambda: self._normal_user, 'stdin': self._pipe_to_subproc, 'extra_env': self.prepare_extra_env(), } meta = self._sa.resume_session(session_id, runner_kwargs=runner_kwargs) app_blob = json.loads(meta.app_blob.decode("UTF-8")) launcher = app_blob['launcher'] self._launcher.read_string(launcher, False) self._sa.use_alternate_configuration(self._launcher) self._normal_user = app_blob.get( 'effective_normal_user', self._launcher.normal_user) _logger.info( "normal_user after loading metadata: %r", self._normal_user) test_plan_id = app_blob['testplan_id'] self._sa.select_test_plan(test_plan_id) self._sa.bootstrap() self._last_job = meta.running_job_name result_dict = { 'outcome': IJobResult.OUTCOME_PASS, 'comments': _("Automatically passed after resuming execution"), } session_share = WellKnownDirsHelper.session_share( self._sa._manager.storage.id) result_path = os.path.join(session_share, '__result') if os.path.exists(result_path): try: with open(result_path, 'rt') as f: result_dict = json.load(f) # the only really important field in the result is # 'outcome' so let's make sure it doesn't contain # anything stupid if result_dict.get('outcome') not in [ 'pass', 'fail', 'skip']: result_dict['outcome'] = IJobResult.OUTCOME_PASS except json.JSONDecodeError as e: pass result = MemoryJobResult(result_dict) if self._last_job: try: self._sa.use_job_result(self._last_job, result, True) except KeyError: raise SystemExit(self._last_job) # some jobs have already been run, so we need to update the attempts # count for future auto-rerunning if self._launcher.auto_retry: for job_id in [ job.id for job in self.get_rerun_candidates('auto')]: job_state = self._sa.get_job_state(job_id) job_state.attempts = self._launcher.max_attempts - len( job_state.result_history) self._state = TestsSelected def finalize_session(self): self._sa.finalize_session() self._reset_sa() def transmit_input(self, text): self._pipe_from_master.write(text) self._pipe_from_master.flush() def send_signal(self, signal): if not self._currently_running_job: return target_user = self._sa.get_job(self._currently_running_job).user self._sa.send_signal(signal, target_user) @property def manager(self): return self._sa._manager @property def passwordless_sudo(self): # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump # if the slave is still running it means it's very passwordless return True @property def sideloaded_providers(self): return self._sa.sideloaded_providers def exposed_cache_report(self, exporter_id, options): exporter = self._sa._manager.create_exporter(exporter_id, options) exported_stream = SpooledTemporaryFile(max_size=102400, mode='w+b') exporter.dump_from_session_manager(self._sa._manager, exported_stream) exported_stream.flush() return exported_stream