Merge ~sylvain-pineau/checkbox-ng:remote-interact-fixes-rebased into checkbox-ng:master

Proposed by Sylvain Pineau
Status: Merged
Approved by: Sylvain Pineau
Approved revision: 4ec41328950ead893c01145286984dea0cc1b2ab
Merged at revision: 2d621a85f3a798674f90f08563edd4f06f091e6b
Proposed branch: ~sylvain-pineau/checkbox-ng:remote-interact-fixes-rebased
Merge into: checkbox-ng:master
Diff against target: 268 lines (+101/-52)
2 files modified
checkbox_ng/launcher/master.py (+78/-34)
plainbox/impl/session/remote_assistant.py (+23/-18)
Reviewer Review Type Date Requested Status
Sylvain Pineau (community) Approve
Review via email: mp+374940@code.launchpad.net

Description of the change

Multiple fixes to support all interactions type for manual jobs (skip, rerun).

Fix user interact types jobs where command output was not streamed resulting in a re-connection after 30s.

To post a comment you must log in.
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

Rebased since several initial commits have been merged already.

review: Needs Resubmitting
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

self-approved

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/checkbox_ng/launcher/master.py b/checkbox_ng/launcher/master.py
2index 818e78d..82dc20d 100644
3--- a/checkbox_ng/launcher/master.py
4+++ b/checkbox_ng/launcher/master.py
5@@ -19,7 +19,9 @@
6 This module contains implementation of the master end of the remote execution
7 functionality.
8 """
9+import getpass
10 import gettext
11+import ipaddress
12 import logging
13 import os
14 import select
15@@ -43,7 +45,7 @@ from checkbox_ng.urwid_ui import CategoryBrowser
16 from checkbox_ng.urwid_ui import ReRunBrowser
17 from checkbox_ng.urwid_ui import interrupt_dialog
18 from checkbox_ng.urwid_ui import resume_dialog
19-from checkbox_ng.launcher.run import NormalUI
20+from checkbox_ng.launcher.run import NormalUI, ReRunJob
21 from checkbox_ng.launcher.stages import MainLoopStage
22 from checkbox_ng.launcher.stages import ReportsStage
23 _ = gettext.gettext
24@@ -122,6 +124,7 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
25 self._is_bootstrapping = False
26 self._target_host = ctx.args.host
27 self._sudo_provider = None
28+ self._normal_user = ''
29 self.launcher = DefaultLauncherDefinition()
30 if ctx.args.launcher:
31 expanded_path = os.path.expanduser(ctx.args.launcher)
32@@ -131,11 +134,18 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
33 with open(expanded_path, 'rt') as f:
34 self._launcher_text = f.read()
35 self.launcher.read_string(self._launcher_text)
36+ if 'local' in ctx.args.host:
37+ ctx.args.host = '127.0.0.1'
38+ if ipaddress.ip_address(ctx.args.host).is_loopback:
39+ self._normal_user = getpass.getuser()
40+ if ctx.args.user:
41+ self._normal_user = ctx.args.user
42 timeout = 600
43 deadline = time.time() + timeout
44 port = ctx.args.port
45- print(_("Connecting to {}:{}. Timeout: {}s").format(
46- ctx.args.host, port, timeout))
47+ if not ipaddress.ip_address(ctx.args.host).is_loopback:
48+ print(_("Connecting to {}:{}. Timeout: {}s").format(
49+ ctx.args.host, port, timeout))
50 while time.time() < deadline:
51 try:
52 self.connect_and_run(ctx.args.host, port)
53@@ -218,6 +228,7 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
54 _logger.info("master: Starting new session.")
55 configuration = dict()
56 configuration['launcher'] = self._launcher_text
57+ configuration['normal_user'] = self._normal_user
58
59 tps = self.sa.start_session(configuration)
60 _logger.debug("master: Session started. Available TPs:\n%s",
61@@ -295,6 +306,8 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
62 "launcher definition file to use"))
63 parser.add_argument('--port', type=int, default=18871, help=_(
64 "port to connect to"))
65+ parser.add_argument('-u', '--user', help=_(
66+ "normal user to run non-root jobs"))
67
68 def _handle_interrupt(self):
69 """
70@@ -477,37 +490,68 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
71 print(_("Category: {0}").format(job['category_name']))
72 SimpleUI.horiz_line()
73 next_job = False
74- for interaction in self.sa.run_job(job['id']):
75- if interaction.kind == 'sudo_input':
76- self.sa.save_password(
77- self._sudo_provider.encrypted_password)
78- if interaction.kind == 'purpose':
79- SimpleUI.description(_('Purpose:'), interaction.message)
80- elif interaction.kind in ['description', 'steps']:
81- SimpleUI.description(_('Steps:'), interaction.message)
82- if job['command'] is None:
83- cmd = 'run'
84- else:
85- cmd = SimpleUI(None).wait_for_interaction_prompt(None)
86- if cmd == 'skip':
87+ while next_job is False:
88+ for interaction in self.sa.run_job(job['id']):
89+ if interaction.kind == 'sudo_input':
90+ self.sa.save_password(
91+ self._sudo_provider.encrypted_password)
92+ if interaction.kind == 'purpose':
93+ SimpleUI.description(_('Purpose:'),
94+ interaction.message)
95+ elif interaction.kind == 'description':
96+ SimpleUI.description(_('Description:'),
97+ interaction.message)
98+ if job['command'] is None:
99+ cmd = 'run'
100+ else:
101+ cmd = SimpleUI(
102+ None).wait_for_interaction_prompt(None)
103+ if cmd == 'skip':
104+ next_job = True
105+ self.sa.remember_users_response(cmd)
106+ self.wait_for_job(dont_finish=True)
107+ elif interaction.kind in 'steps':
108+ SimpleUI.description(_('Steps:'), interaction.message)
109+ if job['command'] is None:
110+ cmd = 'run'
111+ else:
112+ cmd = SimpleUI(
113+ None).wait_for_interaction_prompt(None)
114+ if cmd == 'skip':
115+ next_job = True
116+ self.sa.remember_users_response(cmd)
117+ elif interaction.kind == 'verification':
118+ self.wait_for_job(dont_finish=True)
119+ if interaction.message:
120+ SimpleUI.description(
121+ _('Verification:'), interaction.message)
122+ JobAdapter = namedtuple('job_adapter', ['command'])
123+ job_lite = JobAdapter(job['command'])
124+ try:
125+ cmd = SimpleUI(None)._interaction_callback(
126+ job_lite, interaction.extra._builder)
127+ self.sa.remember_users_response(cmd)
128+ self.finish_job(
129+ interaction.extra._builder.get_result())
130+ next_job = True
131+ break
132+ except ReRunJob:
133+ next_job = False
134+ self.sa.rerun_job(
135+ job['id'],
136+ interaction.extra._builder.get_result())
137+ break
138+ elif interaction.kind == 'comment':
139+ new_comment = input(SimpleUI.C.BLUE(
140+ _('Please enter your comments:') + '\n'))
141+ self.sa.remember_users_response(new_comment + '\n')
142+ elif interaction.kind == 'skip':
143+ self.finish_job(
144+ interaction.extra._builder.get_result())
145 next_job = True
146- self.sa.remember_users_response(cmd)
147- elif interaction.kind == 'verification':
148- self.wait_for_job(dont_finish=True)
149- if interaction.message:
150- SimpleUI.description(
151- _('Verification:'), interaction.message)
152- JobAdapter = namedtuple('job_adapter', ['command'])
153- job = JobAdapter(job['command'])
154- cmd = SimpleUI(None)._interaction_callback(
155- job, interaction.extra)
156- self.sa.remember_users_response(cmd)
157- self.finish_job(interaction.extra.get_result())
158- next_job = True
159- elif interaction.kind == 'comment':
160- new_comment = input(SimpleUI.C.BLUE(
161- _('Please enter your comments:') + '\n'))
162- self.sa.remember_users_response(new_comment + '\n')
163+ break
164+ else:
165+ self.wait_for_job()
166+ break
167 if next_job:
168 continue
169- self.wait_for_job()
170diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py
171index 907bce2..bef07f3 100644
172--- a/plainbox/impl/session/remote_assistant.py
173+++ b/plainbox/impl/session/remote_assistant.py
174@@ -25,12 +25,11 @@ import time
175 import sys
176 from collections import namedtuple
177 from threading import Thread, Lock
178-from subprocess import DEVNULL, CalledProcessError, check_call, check_output
179+from subprocess import CalledProcessError, check_output
180
181 from plainbox.impl.execution import UnifiedRunner
182 from plainbox.impl.session.assistant import SessionAssistant
183 from plainbox.impl.session.assistant import SA_RESTARTABLE
184-from plainbox.impl.session.jobs import InhibitionCause
185 from plainbox.impl.secure.sudo_broker import SudoBroker, EphemeralKey
186 from plainbox.impl.secure.sudo_broker import is_passwordless_sudo
187 from plainbox.impl.secure.sudo_broker import validate_pass
188@@ -210,7 +209,6 @@ class RemoteSessionAssistant():
189 return self._prepare_display_without_psutil()
190 if ("DISPLAY" in p_environ and p_user != 'gdm'): # gdm uses :1024
191 return {'DISPLAY': p_environ['DISPLAY']}
192- break
193
194 @allowed_when(Idle)
195 def start_session(self, configuration):
196@@ -229,6 +227,8 @@ class RemoteSessionAssistant():
197 self._sa.load_providers()
198
199 self._normal_user = self._launcher.normal_user
200+ if configuration['normal_user']:
201+ self._normal_user = configuration['normal_user']
202 pass_provider = (None if self._passwordless_sudo else
203 self.get_decrypted_password)
204 runner_kwargs = {
205@@ -292,6 +292,13 @@ class RemoteSessionAssistant():
206 self._jobs_count = len(self._sa.get_dynamic_todo_list())
207 self._state = TestsSelected
208
209+ @allowed_when(Interacting)
210+ def rerun_job(self, job_id, result):
211+ self._sa.use_job_result(job_id, result)
212+ self.session_change_lock.acquire(blocking=False)
213+ self.session_change_lock.release()
214+ self._state = TestsSelected
215+
216 @allowed_when(TestsSelected)
217 def run_job(self, job_id):
218 """
219@@ -316,7 +323,8 @@ class RemoteSessionAssistant():
220 yield from self.interact(
221 Interaction('purpose', job.tr_purpose()))
222 if job.tr_steps():
223- yield from self.interact(Interaction('steps', job.tr_steps()))
224+ yield from self.interact(
225+ Interaction('steps', job.tr_steps()))
226 if self._last_response == 'comment':
227 yield from self.interact(Interaction('comment'))
228 if self._last_response:
229@@ -324,13 +332,16 @@ class RemoteSessionAssistant():
230 may_comment = True
231 continue
232 if self._last_response == 'skip':
233- result_builder = JobResultBuilder(
234- outcome=IJobResult.OUTCOME_SKIP,
235- comments=_("Explicitly skipped before execution"))
236- if self._current_comments != "":
237- result_builder.comments = self._current_comments
238- self.finish_job(result_builder.get_result())
239- return
240+ def skipped_builder(*args, **kwargs):
241+ result_builder = JobResultBuilder(
242+ outcome=IJobResult.OUTCOME_SKIP,
243+ comments=_("Explicitly skipped before execution"))
244+ if self._current_comments != "":
245+ result_builder.comments = self._current_comments
246+ return result_builder
247+ self._be = BackgroundExecutor(self, job_id, skipped_builder)
248+ yield from self.interact(
249+ Interaction('skip', job.verification, self._be))
250 if job.command:
251 if (job.user and not self._passwordless_sudo
252 and not self._sudo_password):
253@@ -355,14 +366,8 @@ class RemoteSessionAssistant():
254 self._be = BackgroundExecutor(self, job_id, undecided_builder)
255 if self._sa.get_job(self._currently_running_job).plugin in [
256 'manual', 'user-interact-verify']:
257- rb = self._be.wait()
258- # by this point the ui will handle adding comments via
259- # ResultBuilder.add_comment method that adds \n in front
260- # of the addition, let's rstrip it
261- rb.comments = self._current_comments.rstrip()
262 yield from self.interact(
263- Interaction('verification', job.verification, rb))
264- self.finish_job(rb.get_result())
265+ Interaction('verification', job.verification, self._be))
266
267 @allowed_when(Started, Bootstrapping)
268 def run_bootstrapping_job(self, job_id):

Subscribers

People subscribed via source and target branches