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

Proposed by Sylvain Pineau
Status: Work in progress
Proposed branch: ~sylvain-pineau/checkbox-ng:remote-interact-fixes
Merge into: checkbox-ng:master
Diff against target: 304 lines (+106/-52)
4 files modified
checkbox_ng/launcher/master.py (+79/-34)
plainbox/impl/execution.py (+2/-0)
plainbox/impl/session/assistant.py (+2/-0)
plainbox/impl/session/remote_assistant.py (+23/-18)
Reviewer Review Type Date Requested Status
Checkbox Developers Pending
Review via email: mp+374857@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.

Unmerged commits

32b2d59... by Sylvain Pineau

remote:sa: pep8

05688f1... by Sylvain Pineau

remote:master: extend the socket timeout to 60s

37b1d3b... by Sylvain Pineau

remote: Fix user-interact jobs command not streamed and rerun interactions

- Properly catch ReRunJob exception on master side
- Always yield something from the run_job generator
- on master keep looping until next_job is False
- skip results are now handled on master side (interaction.kind == 'skip')

b2083cf... by Sylvain Pineau

session:remote_assistant: Only sleep 0.01s to stream faster

8fa0e77... by Sylvain Pineau

session:assistant: Ensure all jobs can write to CHECKBOX_DATA

21efbb1... by Sylvain Pineau

execution: Ensure all jobs can write to CHECKBOX_DATA

2f78a4f... by Sylvain Pineau

master: Add a --user to specify the target (i.e slave) normal user

c6bdeb3... by Sylvain Pineau

master: Hide connection info when running on localhost

e41ebe0... by Sylvain Pineau

master: Use getpass.getuser() to find the normal user when running on localhost

f897c6e... by Sylvain Pineau

execution: pep8

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 7fb87f7..de46dc9 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@@ -149,6 +159,7 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
54 def connect_and_run(self, host, port=18871):
55 config = rpyc.core.protocol.DEFAULT_CONFIG.copy()
56 config['allow_all_attrs'] = True
57+ config['sync_request_timeout'] = 60
58 keep_running = False
59 self._prepare_transports()
60 interrupted = False
61@@ -217,6 +228,7 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
62 _logger.info("master: Starting new session.")
63 configuration = dict()
64 configuration['launcher'] = self._launcher_text
65+ configuration['normal_user'] = self._normal_user
66
67 tps = self.sa.start_session(configuration)
68 _logger.debug("master: Session started. Available TPs:\n%s",
69@@ -296,6 +308,8 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
70 "launcher definition file to use"))
71 parser.add_argument('--port', type=int, default=18871, help=_(
72 "port to connect to"))
73+ parser.add_argument('-u', '--user', help=_(
74+ "normal user to run non-root jobs"))
75
76 def _handle_interrupt(self):
77 """
78@@ -478,37 +492,68 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
79 print(_("Category: {0}").format(job['category_name']))
80 SimpleUI.horiz_line()
81 next_job = False
82- for interaction in self.sa.run_job(job['id']):
83- if interaction.kind == 'sudo_input':
84- self.sa.save_password(
85- self._sudo_provider.encrypted_password)
86- if interaction.kind == 'purpose':
87- SimpleUI.description(_('Purpose:'), interaction.message)
88- elif interaction.kind in ['description', 'steps']:
89- SimpleUI.description(_('Steps:'), interaction.message)
90- if job['command'] is None:
91- cmd = 'run'
92- else:
93- cmd = SimpleUI(None).wait_for_interaction_prompt(None)
94- if cmd == 'skip':
95+ while next_job is False:
96+ for interaction in self.sa.run_job(job['id']):
97+ if interaction.kind == 'sudo_input':
98+ self.sa.save_password(
99+ self._sudo_provider.encrypted_password)
100+ if interaction.kind == 'purpose':
101+ SimpleUI.description(_('Purpose:'),
102+ interaction.message)
103+ elif interaction.kind == 'description':
104+ SimpleUI.description(_('Description:'),
105+ interaction.message)
106+ if job['command'] is None:
107+ cmd = 'run'
108+ else:
109+ cmd = SimpleUI(
110+ None).wait_for_interaction_prompt(None)
111+ if cmd == 'skip':
112+ next_job = True
113+ self.sa.remember_users_response(cmd)
114+ self.wait_for_job(dont_finish=True)
115+ elif interaction.kind in 'steps':
116+ SimpleUI.description(_('Steps:'), interaction.message)
117+ if job['command'] is None:
118+ cmd = 'run'
119+ else:
120+ cmd = SimpleUI(
121+ None).wait_for_interaction_prompt(None)
122+ if cmd == 'skip':
123+ next_job = True
124+ self.sa.remember_users_response(cmd)
125+ elif interaction.kind == 'verification':
126+ self.wait_for_job(dont_finish=True)
127+ if interaction.message:
128+ SimpleUI.description(
129+ _('Verification:'), interaction.message)
130+ JobAdapter = namedtuple('job_adapter', ['command'])
131+ job_lite = JobAdapter(job['command'])
132+ try:
133+ cmd = SimpleUI(None)._interaction_callback(
134+ job_lite, interaction.extra._builder)
135+ self.sa.remember_users_response(cmd)
136+ self.finish_job(
137+ interaction.extra._builder.get_result())
138+ next_job = True
139+ break
140+ except ReRunJob:
141+ next_job = False
142+ self.sa.rerun_job(
143+ job['id'],
144+ interaction.extra._builder.get_result())
145+ break
146+ elif interaction.kind == 'comment':
147+ new_comment = input(SimpleUI.C.BLUE(
148+ _('Please enter your comments:') + '\n'))
149+ self.sa.remember_users_response(new_comment + '\n')
150+ elif interaction.kind == 'skip':
151+ self.finish_job(
152+ interaction.extra._builder.get_result())
153 next_job = True
154- self.sa.remember_users_response(cmd)
155- elif interaction.kind == 'verification':
156- self.wait_for_job(dont_finish=True)
157- if interaction.message:
158- SimpleUI.description(
159- _('Verification:'), interaction.message)
160- JobAdapter = namedtuple('job_adapter', ['command'])
161- job = JobAdapter(job['command'])
162- cmd = SimpleUI(None)._interaction_callback(
163- job, interaction.extra)
164- self.sa.remember_users_response(cmd)
165- self.finish_job(interaction.extra.get_result())
166- next_job = True
167- elif interaction.kind == 'comment':
168- new_comment = input(SimpleUI.C.BLUE(
169- _('Please enter your comments:') + '\n'))
170- self.sa.remember_users_response(new_comment + '\n')
171+ break
172+ else:
173+ self.wait_for_job()
174+ break
175 if next_job:
176 continue
177- self.wait_for_job()
178diff --git a/plainbox/impl/execution.py b/plainbox/impl/execution.py
179index 38c1221..8c93f16 100644
180--- a/plainbox/impl/execution.py
181+++ b/plainbox/impl/execution.py
182@@ -265,7 +265,9 @@ class UnifiedRunner(IJobRunner):
183 extcmd_popen._delegate.on_end(proc.returncode)
184 return proc.returncode
185 if not os.path.isdir(os.path.join(self._session_dir, "CHECKBOX_DATA")):
186+ oldmask = os.umask(000)
187 os.makedirs(os.path.join(self._session_dir, "CHECKBOX_DATA"))
188+ os.umask(oldmask)
189 # Setup the executable nest directory
190 with self.configured_filesystem(job) as nest_dir:
191 # Get the command and the environment.
192diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
193index abcd58c..f941793 100644
194--- a/plainbox/impl/session/assistant.py
195+++ b/plainbox/impl/session/assistant.py
196@@ -1321,7 +1321,9 @@ class SessionAssistant:
197 checkbox_data_dir = os.path.join(
198 self.get_session_dir(), 'CHECKBOX_DATA')
199 if not os.path.exists(checkbox_data_dir):
200+ oldmask = os.umask(000)
201 os.mkdir(checkbox_data_dir)
202+ os.umask(oldmask)
203 respawn_cmd_file = os.path.join(
204 checkbox_data_dir, '__respawn_checkbox')
205 if self._restart_cmd_callback:
206diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py
207index 35a5b11..48b2ac0 100644
208--- a/plainbox/impl/session/remote_assistant.py
209+++ b/plainbox/impl/session/remote_assistant.py
210@@ -25,12 +25,10 @@ import time
211 import sys
212 from collections import namedtuple
213 from threading import Thread, Lock
214-from subprocess import DEVNULL, CalledProcessError, check_call
215
216 from plainbox.impl.execution import UnifiedRunner
217 from plainbox.impl.session.assistant import SessionAssistant
218 from plainbox.impl.session.assistant import SA_RESTARTABLE
219-from plainbox.impl.session.jobs import InhibitionCause
220 from plainbox.impl.secure.sudo_broker import SudoBroker, EphemeralKey
221 from plainbox.impl.secure.sudo_broker import is_passwordless_sudo
222 from plainbox.impl.secure.sudo_broker import validate_pass
223@@ -103,7 +101,7 @@ class BackgroundExecutor(Thread):
224 def wait(self):
225 while self.is_alive() or not self._started_real_run:
226 # return control to RPC server
227- time.sleep(0.1)
228+ time.sleep(0.01)
229 self.join()
230 return self._builder
231
232@@ -213,6 +211,8 @@ class RemoteSessionAssistant():
233 self._sa.load_providers()
234
235 self._normal_user = self._launcher.normal_user
236+ if configuration['normal_user']:
237+ self._normal_user = configuration['normal_user']
238 pass_provider = (None if self._passwordless_sudo else
239 self.get_decrypted_password)
240 runner_kwargs = {
241@@ -275,6 +275,13 @@ class RemoteSessionAssistant():
242 self._jobs_count = len(self._sa.get_dynamic_todo_list())
243 self._state = TestsSelected
244
245+ @allowed_when(Interacting)
246+ def rerun_job(self, job_id, result):
247+ self._sa.use_job_result(job_id, result)
248+ self.session_change_lock.acquire(blocking=False)
249+ self.session_change_lock.release()
250+ self._state = TestsSelected
251+
252 @allowed_when(TestsSelected)
253 def run_job(self, job_id):
254 """
255@@ -299,7 +306,8 @@ class RemoteSessionAssistant():
256 yield from self.interact(
257 Interaction('purpose', job.tr_purpose()))
258 if job.tr_steps():
259- yield from self.interact(Interaction('steps', job.tr_steps()))
260+ yield from self.interact(
261+ Interaction('steps', job.tr_steps()))
262 if self._last_response == 'comment':
263 yield from self.interact(Interaction('comment'))
264 if self._last_response:
265@@ -307,13 +315,16 @@ class RemoteSessionAssistant():
266 may_comment = True
267 continue
268 if self._last_response == 'skip':
269- result_builder = JobResultBuilder(
270- outcome=IJobResult.OUTCOME_SKIP,
271- comments=_("Explicitly skipped before execution"))
272- if self._current_comments != "":
273- result_builder.comments = self._current_comments
274- self.finish_job(result_builder.get_result())
275- return
276+ def skipped_builder(*args, **kwargs):
277+ result_builder = JobResultBuilder(
278+ outcome=IJobResult.OUTCOME_SKIP,
279+ comments=_("Explicitly skipped before execution"))
280+ if self._current_comments != "":
281+ result_builder.comments = self._current_comments
282+ return result_builder
283+ self._be = BackgroundExecutor(self, job_id, skipped_builder)
284+ yield from self.interact(
285+ Interaction('skip', job.verification, self._be))
286 if job.command:
287 if (job.user and not self._passwordless_sudo
288 and not self._sudo_password):
289@@ -338,14 +349,8 @@ class RemoteSessionAssistant():
290 self._be = BackgroundExecutor(self, job_id, undecided_builder)
291 if self._sa.get_job(self._currently_running_job).plugin in [
292 'manual', 'user-interact-verify']:
293- rb = self._be.wait()
294- # by this point the ui will handle adding comments via
295- # ResultBuilder.add_comment method that adds \n in front
296- # of the addition, let's rstrip it
297- rb.comments = self._current_comments.rstrip()
298 yield from self.interact(
299- Interaction('verification', job.verification, rb))
300- self.finish_job(rb.get_result())
301+ Interaction('verification', job.verification, self._be))
302
303 @allowed_when(Started, Bootstrapping)
304 def run_bootstrapping_job(self, job_id):

Subscribers

People subscribed via source and target branches