Пример #1
0
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
Пример #2
0
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