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
diff --git a/checkbox_ng/launcher/master.py b/checkbox_ng/launcher/master.py
index 7fb87f7..de46dc9 100644
--- a/checkbox_ng/launcher/master.py
+++ b/checkbox_ng/launcher/master.py
@@ -19,7 +19,9 @@
19This module contains implementation of the master end of the remote execution19This module contains implementation of the master end of the remote execution
20functionality.20functionality.
21"""21"""
22import getpass
22import gettext23import gettext
24import ipaddress
23import logging25import logging
24import os26import os
25import select27import select
@@ -43,7 +45,7 @@ from checkbox_ng.urwid_ui import CategoryBrowser
43from checkbox_ng.urwid_ui import ReRunBrowser45from checkbox_ng.urwid_ui import ReRunBrowser
44from checkbox_ng.urwid_ui import interrupt_dialog46from checkbox_ng.urwid_ui import interrupt_dialog
45from checkbox_ng.urwid_ui import resume_dialog47from checkbox_ng.urwid_ui import resume_dialog
46from checkbox_ng.launcher.run import NormalUI48from checkbox_ng.launcher.run import NormalUI, ReRunJob
47from checkbox_ng.launcher.stages import MainLoopStage49from checkbox_ng.launcher.stages import MainLoopStage
48from checkbox_ng.launcher.stages import ReportsStage50from checkbox_ng.launcher.stages import ReportsStage
49_ = gettext.gettext51_ = gettext.gettext
@@ -122,6 +124,7 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
122 self._is_bootstrapping = False124 self._is_bootstrapping = False
123 self._target_host = ctx.args.host125 self._target_host = ctx.args.host
124 self._sudo_provider = None126 self._sudo_provider = None
127 self._normal_user = ''
125 self.launcher = DefaultLauncherDefinition()128 self.launcher = DefaultLauncherDefinition()
126 if ctx.args.launcher:129 if ctx.args.launcher:
127 expanded_path = os.path.expanduser(ctx.args.launcher)130 expanded_path = os.path.expanduser(ctx.args.launcher)
@@ -131,11 +134,18 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
131 with open(expanded_path, 'rt') as f:134 with open(expanded_path, 'rt') as f:
132 self._launcher_text = f.read()135 self._launcher_text = f.read()
133 self.launcher.read_string(self._launcher_text)136 self.launcher.read_string(self._launcher_text)
137 if 'local' in ctx.args.host:
138 ctx.args.host = '127.0.0.1'
139 if ipaddress.ip_address(ctx.args.host).is_loopback:
140 self._normal_user = getpass.getuser()
141 if ctx.args.user:
142 self._normal_user = ctx.args.user
134 timeout = 600143 timeout = 600
135 deadline = time.time() + timeout144 deadline = time.time() + timeout
136 port = ctx.args.port145 port = ctx.args.port
137 print(_("Connecting to {}:{}. Timeout: {}s").format(146 if not ipaddress.ip_address(ctx.args.host).is_loopback:
138 ctx.args.host, port, timeout))147 print(_("Connecting to {}:{}. Timeout: {}s").format(
148 ctx.args.host, port, timeout))
139 while time.time() < deadline:149 while time.time() < deadline:
140 try:150 try:
141 self.connect_and_run(ctx.args.host, port)151 self.connect_and_run(ctx.args.host, port)
@@ -149,6 +159,7 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
149 def connect_and_run(self, host, port=18871):159 def connect_and_run(self, host, port=18871):
150 config = rpyc.core.protocol.DEFAULT_CONFIG.copy()160 config = rpyc.core.protocol.DEFAULT_CONFIG.copy()
151 config['allow_all_attrs'] = True161 config['allow_all_attrs'] = True
162 config['sync_request_timeout'] = 60
152 keep_running = False163 keep_running = False
153 self._prepare_transports()164 self._prepare_transports()
154 interrupted = False165 interrupted = False
@@ -217,6 +228,7 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
217 _logger.info("master: Starting new session.")228 _logger.info("master: Starting new session.")
218 configuration = dict()229 configuration = dict()
219 configuration['launcher'] = self._launcher_text230 configuration['launcher'] = self._launcher_text
231 configuration['normal_user'] = self._normal_user
220232
221 tps = self.sa.start_session(configuration)233 tps = self.sa.start_session(configuration)
222 _logger.debug("master: Session started. Available TPs:\n%s",234 _logger.debug("master: Session started. Available TPs:\n%s",
@@ -296,6 +308,8 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
296 "launcher definition file to use"))308 "launcher definition file to use"))
297 parser.add_argument('--port', type=int, default=18871, help=_(309 parser.add_argument('--port', type=int, default=18871, help=_(
298 "port to connect to"))310 "port to connect to"))
311 parser.add_argument('-u', '--user', help=_(
312 "normal user to run non-root jobs"))
299313
300 def _handle_interrupt(self):314 def _handle_interrupt(self):
301 """315 """
@@ -478,37 +492,68 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
478 print(_("Category: {0}").format(job['category_name']))492 print(_("Category: {0}").format(job['category_name']))
479 SimpleUI.horiz_line()493 SimpleUI.horiz_line()
480 next_job = False494 next_job = False
481 for interaction in self.sa.run_job(job['id']):495 while next_job is False:
482 if interaction.kind == 'sudo_input':496 for interaction in self.sa.run_job(job['id']):
483 self.sa.save_password(497 if interaction.kind == 'sudo_input':
484 self._sudo_provider.encrypted_password)498 self.sa.save_password(
485 if interaction.kind == 'purpose':499 self._sudo_provider.encrypted_password)
486 SimpleUI.description(_('Purpose:'), interaction.message)500 if interaction.kind == 'purpose':
487 elif interaction.kind in ['description', 'steps']:501 SimpleUI.description(_('Purpose:'),
488 SimpleUI.description(_('Steps:'), interaction.message)502 interaction.message)
489 if job['command'] is None:503 elif interaction.kind == 'description':
490 cmd = 'run'504 SimpleUI.description(_('Description:'),
491 else:505 interaction.message)
492 cmd = SimpleUI(None).wait_for_interaction_prompt(None)506 if job['command'] is None:
493 if cmd == 'skip':507 cmd = 'run'
508 else:
509 cmd = SimpleUI(
510 None).wait_for_interaction_prompt(None)
511 if cmd == 'skip':
512 next_job = True
513 self.sa.remember_users_response(cmd)
514 self.wait_for_job(dont_finish=True)
515 elif interaction.kind in 'steps':
516 SimpleUI.description(_('Steps:'), interaction.message)
517 if job['command'] is None:
518 cmd = 'run'
519 else:
520 cmd = SimpleUI(
521 None).wait_for_interaction_prompt(None)
522 if cmd == 'skip':
523 next_job = True
524 self.sa.remember_users_response(cmd)
525 elif interaction.kind == 'verification':
526 self.wait_for_job(dont_finish=True)
527 if interaction.message:
528 SimpleUI.description(
529 _('Verification:'), interaction.message)
530 JobAdapter = namedtuple('job_adapter', ['command'])
531 job_lite = JobAdapter(job['command'])
532 try:
533 cmd = SimpleUI(None)._interaction_callback(
534 job_lite, interaction.extra._builder)
535 self.sa.remember_users_response(cmd)
536 self.finish_job(
537 interaction.extra._builder.get_result())
538 next_job = True
539 break
540 except ReRunJob:
541 next_job = False
542 self.sa.rerun_job(
543 job['id'],
544 interaction.extra._builder.get_result())
545 break
546 elif interaction.kind == 'comment':
547 new_comment = input(SimpleUI.C.BLUE(
548 _('Please enter your comments:') + '\n'))
549 self.sa.remember_users_response(new_comment + '\n')
550 elif interaction.kind == 'skip':
551 self.finish_job(
552 interaction.extra._builder.get_result())
494 next_job = True553 next_job = True
495 self.sa.remember_users_response(cmd)554 break
496 elif interaction.kind == 'verification':555 else:
497 self.wait_for_job(dont_finish=True)556 self.wait_for_job()
498 if interaction.message:557 break
499 SimpleUI.description(
500 _('Verification:'), interaction.message)
501 JobAdapter = namedtuple('job_adapter', ['command'])
502 job = JobAdapter(job['command'])
503 cmd = SimpleUI(None)._interaction_callback(
504 job, interaction.extra)
505 self.sa.remember_users_response(cmd)
506 self.finish_job(interaction.extra.get_result())
507 next_job = True
508 elif interaction.kind == 'comment':
509 new_comment = input(SimpleUI.C.BLUE(
510 _('Please enter your comments:') + '\n'))
511 self.sa.remember_users_response(new_comment + '\n')
512 if next_job:558 if next_job:
513 continue559 continue
514 self.wait_for_job()
diff --git a/plainbox/impl/execution.py b/plainbox/impl/execution.py
index 38c1221..8c93f16 100644
--- a/plainbox/impl/execution.py
+++ b/plainbox/impl/execution.py
@@ -265,7 +265,9 @@ class UnifiedRunner(IJobRunner):
265 extcmd_popen._delegate.on_end(proc.returncode)265 extcmd_popen._delegate.on_end(proc.returncode)
266 return proc.returncode266 return proc.returncode
267 if not os.path.isdir(os.path.join(self._session_dir, "CHECKBOX_DATA")):267 if not os.path.isdir(os.path.join(self._session_dir, "CHECKBOX_DATA")):
268 oldmask = os.umask(000)
268 os.makedirs(os.path.join(self._session_dir, "CHECKBOX_DATA"))269 os.makedirs(os.path.join(self._session_dir, "CHECKBOX_DATA"))
270 os.umask(oldmask)
269 # Setup the executable nest directory271 # Setup the executable nest directory
270 with self.configured_filesystem(job) as nest_dir:272 with self.configured_filesystem(job) as nest_dir:
271 # Get the command and the environment.273 # Get the command and the environment.
diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
index abcd58c..f941793 100644
--- a/plainbox/impl/session/assistant.py
+++ b/plainbox/impl/session/assistant.py
@@ -1321,7 +1321,9 @@ class SessionAssistant:
1321 checkbox_data_dir = os.path.join(1321 checkbox_data_dir = os.path.join(
1322 self.get_session_dir(), 'CHECKBOX_DATA')1322 self.get_session_dir(), 'CHECKBOX_DATA')
1323 if not os.path.exists(checkbox_data_dir):1323 if not os.path.exists(checkbox_data_dir):
1324 oldmask = os.umask(000)
1324 os.mkdir(checkbox_data_dir)1325 os.mkdir(checkbox_data_dir)
1326 os.umask(oldmask)
1325 respawn_cmd_file = os.path.join(1327 respawn_cmd_file = os.path.join(
1326 checkbox_data_dir, '__respawn_checkbox')1328 checkbox_data_dir, '__respawn_checkbox')
1327 if self._restart_cmd_callback:1329 if self._restart_cmd_callback:
diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py
index 35a5b11..48b2ac0 100644
--- a/plainbox/impl/session/remote_assistant.py
+++ b/plainbox/impl/session/remote_assistant.py
@@ -25,12 +25,10 @@ import time
25import sys25import sys
26from collections import namedtuple26from collections import namedtuple
27from threading import Thread, Lock27from threading import Thread, Lock
28from subprocess import DEVNULL, CalledProcessError, check_call
2928
30from plainbox.impl.execution import UnifiedRunner29from plainbox.impl.execution import UnifiedRunner
31from plainbox.impl.session.assistant import SessionAssistant30from plainbox.impl.session.assistant import SessionAssistant
32from plainbox.impl.session.assistant import SA_RESTARTABLE31from plainbox.impl.session.assistant import SA_RESTARTABLE
33from plainbox.impl.session.jobs import InhibitionCause
34from plainbox.impl.secure.sudo_broker import SudoBroker, EphemeralKey32from plainbox.impl.secure.sudo_broker import SudoBroker, EphemeralKey
35from plainbox.impl.secure.sudo_broker import is_passwordless_sudo33from plainbox.impl.secure.sudo_broker import is_passwordless_sudo
36from plainbox.impl.secure.sudo_broker import validate_pass34from plainbox.impl.secure.sudo_broker import validate_pass
@@ -103,7 +101,7 @@ class BackgroundExecutor(Thread):
103 def wait(self):101 def wait(self):
104 while self.is_alive() or not self._started_real_run:102 while self.is_alive() or not self._started_real_run:
105 # return control to RPC server103 # return control to RPC server
106 time.sleep(0.1)104 time.sleep(0.01)
107 self.join()105 self.join()
108 return self._builder106 return self._builder
109107
@@ -213,6 +211,8 @@ class RemoteSessionAssistant():
213 self._sa.load_providers()211 self._sa.load_providers()
214212
215 self._normal_user = self._launcher.normal_user213 self._normal_user = self._launcher.normal_user
214 if configuration['normal_user']:
215 self._normal_user = configuration['normal_user']
216 pass_provider = (None if self._passwordless_sudo else216 pass_provider = (None if self._passwordless_sudo else
217 self.get_decrypted_password)217 self.get_decrypted_password)
218 runner_kwargs = {218 runner_kwargs = {
@@ -275,6 +275,13 @@ class RemoteSessionAssistant():
275 self._jobs_count = len(self._sa.get_dynamic_todo_list())275 self._jobs_count = len(self._sa.get_dynamic_todo_list())
276 self._state = TestsSelected276 self._state = TestsSelected
277277
278 @allowed_when(Interacting)
279 def rerun_job(self, job_id, result):
280 self._sa.use_job_result(job_id, result)
281 self.session_change_lock.acquire(blocking=False)
282 self.session_change_lock.release()
283 self._state = TestsSelected
284
278 @allowed_when(TestsSelected)285 @allowed_when(TestsSelected)
279 def run_job(self, job_id):286 def run_job(self, job_id):
280 """287 """
@@ -299,7 +306,8 @@ class RemoteSessionAssistant():
299 yield from self.interact(306 yield from self.interact(
300 Interaction('purpose', job.tr_purpose()))307 Interaction('purpose', job.tr_purpose()))
301 if job.tr_steps():308 if job.tr_steps():
302 yield from self.interact(Interaction('steps', job.tr_steps()))309 yield from self.interact(
310 Interaction('steps', job.tr_steps()))
303 if self._last_response == 'comment':311 if self._last_response == 'comment':
304 yield from self.interact(Interaction('comment'))312 yield from self.interact(Interaction('comment'))
305 if self._last_response:313 if self._last_response:
@@ -307,13 +315,16 @@ class RemoteSessionAssistant():
307 may_comment = True315 may_comment = True
308 continue316 continue
309 if self._last_response == 'skip':317 if self._last_response == 'skip':
310 result_builder = JobResultBuilder(318 def skipped_builder(*args, **kwargs):
311 outcome=IJobResult.OUTCOME_SKIP,319 result_builder = JobResultBuilder(
312 comments=_("Explicitly skipped before execution"))320 outcome=IJobResult.OUTCOME_SKIP,
313 if self._current_comments != "":321 comments=_("Explicitly skipped before execution"))
314 result_builder.comments = self._current_comments322 if self._current_comments != "":
315 self.finish_job(result_builder.get_result())323 result_builder.comments = self._current_comments
316 return324 return result_builder
325 self._be = BackgroundExecutor(self, job_id, skipped_builder)
326 yield from self.interact(
327 Interaction('skip', job.verification, self._be))
317 if job.command:328 if job.command:
318 if (job.user and not self._passwordless_sudo329 if (job.user and not self._passwordless_sudo
319 and not self._sudo_password):330 and not self._sudo_password):
@@ -338,14 +349,8 @@ class RemoteSessionAssistant():
338 self._be = BackgroundExecutor(self, job_id, undecided_builder)349 self._be = BackgroundExecutor(self, job_id, undecided_builder)
339 if self._sa.get_job(self._currently_running_job).plugin in [350 if self._sa.get_job(self._currently_running_job).plugin in [
340 'manual', 'user-interact-verify']:351 'manual', 'user-interact-verify']:
341 rb = self._be.wait()
342 # by this point the ui will handle adding comments via
343 # ResultBuilder.add_comment method that adds \n in front
344 # of the addition, let's rstrip it
345 rb.comments = self._current_comments.rstrip()
346 yield from self.interact(352 yield from self.interact(
347 Interaction('verification', job.verification, rb))353 Interaction('verification', job.verification, self._be))
348 self.finish_job(rb.get_result())
349354
350 @allowed_when(Started, Bootstrapping)355 @allowed_when(Started, Bootstrapping)
351 def run_bootstrapping_job(self, job_id):356 def run_bootstrapping_job(self, job_id):

Subscribers

People subscribed via source and target branches