Merge ~sylvain-pineau/checkbox-ng:remote_deamon_controller into checkbox-ng:master

Proposed by Sylvain Pineau
Status: Work in progress
Proposed branch: ~sylvain-pineau/checkbox-ng:remote_deamon_controller
Merge into: checkbox-ng:master
Diff against target: 442 lines (+227/-50)
7 files modified
checkbox_ng/launcher/remote.py (+8/-1)
checkbox_ng/launcher/subcommands.py (+13/-1)
plainbox/impl/ctrl.py (+62/-1)
plainbox/impl/runner.py (+47/-40)
plainbox/impl/session/assistant.py (+67/-1)
plainbox/impl/session/remote_assistant.py (+28/-4)
plainbox/impl/test_ctrl.py (+2/-2)
Reviewer Review Type Date Requested Status
Jonathan Cave (community) Needs Information
Review via email: mp+360477@code.launchpad.net

Description of the change

The code that use the slave daemon to act as a new controller for root job commands

To post a comment you must log in.
Revision history for this message
Jonathan Cave (jocave) wrote :

I'm not sure I fully understand the concept of what this is looking to do yet.

The intention is to "hand over" jobs that want to run as the root user to the remote-slave service? Why is this much better that just using localhost remote (assuming know bugs are fixed)? The obvious disadvantage being that if you start a session locally, you can't disconnect the front end and then re-connect somewhere else.

review: Needs Information
Revision history for this message
Chris Wayne (cwayne) wrote :

I'm looking at it as an intermediary step until full local-remote is ready
for widespread use

On Thu, Dec 13, 2018, 11:11 AM Jonathan Cave <<email address hidden>
wrote:

> Review: Needs Information
>
> I'm not sure I fully understand the concept of what this is looking to do
> yet.
>
> The intention is to "hand over" jobs that want to run as the root user to
> the remote-slave service? Why is this much better that just using localhost
> remote (assuming know bugs are fixed)? The obvious disadvantage being that
> if you start a session locally, you can't disconnect the front end and then
> re-connect somewhere else.
> --
>
> https://code.launchpad.net/~sylvain-pineau/checkbox-ng/+git/checkbox-ng/+merge/360477
> Your team Checkbox Administrators is subscribed to branch
> checkbox-ng:master.
>

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

Remote for everyone is the prio #1, back to in progress. I'll re assess the need after xmas.

Unmerged commits

ca2e28b... by Sylvain Pineau

subcommands:run: Add support for sideloaded providers

322a34f... by Sylvain Pineau

runner: Only run the job command via remote, not the full job

Doing so keeps all the logic to skip or not jobs on master

3ff0225... by Sylvain Pineau

session:assistant: log "Finalizing session" events as info not warning msg

Too noisy when used with remote slave

7472231... by Sylvain Pineau

remote: Properly render stderr stream lines in red

1e8da85... by Sylvain Pineau

remote_assistant: Fix logger name

255d44a... by Sylvain Pineau

ctrl: New execution controller that gains root by using the remote deamon

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/checkbox_ng/launcher/remote.py b/checkbox_ng/launcher/remote.py
2index 70bdc60..34e8fec 100644
3--- a/checkbox_ng/launcher/remote.py
4+++ b/checkbox_ng/launcher/remote.py
5@@ -84,6 +84,9 @@ class SimpleUI(NormalUI, MainLoopStage):
6 def green_text(text, end='\n'):
7 print(SimpleUI.C.GREEN(text), end)
8
9+ def red_text(text, end='\n'):
10+ print(SimpleUI.C.RED(text), end)
11+
12 def horiz_line():
13 print(SimpleUI.C.WHITE('-' * 80))
14
15@@ -431,7 +434,11 @@ class RemoteMaster(Command, ReportsStage, MainLoopStage):
16 while True:
17 state, payload = self.sa.monitor_job()
18 if payload and not self._is_bootstrapping:
19- SimpleUI.green_text(payload, end='')
20+ for line in payload:
21+ if line[0] == 'stderr':
22+ SimpleUI.red_text(payload, end='')
23+ else:
24+ SimpleUI.green_text(payload, end='')
25 if state == 'running':
26 time.sleep(0.5)
27 while True:
28diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py
29index 169d38a..4e4c2cf 100644
30--- a/checkbox_ng/launcher/subcommands.py
31+++ b/checkbox_ng/launcher/subcommands.py
32@@ -771,8 +771,20 @@ class Run(Command, MainLoopStage):
33 "0.99",
34 ["restartable"],
35 )
36+ # side-load providers local-providers
37+ side_load_path = os.path.expandvars(os.path.join(
38+ '/home', '$USER', 'providers'))
39+ additional_providers = ()
40+ if os.path.exists(side_load_path):
41+ print(self._C.RED(_(
42+ "WARNING: using side-loaded providers")))
43+ os.environ['PROVIDERPATH'] = ''
44+ embedded_providers = EmbeddedProvider1PlugInCollection(
45+ side_load_path)
46+ additional_providers = embedded_providers.get_all_plugin_objects()
47 self._configure_restart()
48- try_selecting_providers(self.sa, '*')
49+ try_selecting_providers(
50+ self.sa, '*', additional_providers=additional_providers)
51 self.sa.start_new_session(self.ctx.args.title or 'checkbox-run')
52 tps = self.sa.get_test_plans()
53 self._configure_report()
54diff --git a/plainbox/impl/ctrl.py b/plainbox/impl/ctrl.py
55index ef08867..72a78b7 100644
56--- a/plainbox/impl/ctrl.py
57+++ b/plainbox/impl/ctrl.py
58@@ -44,6 +44,7 @@ import itertools
59 import json
60 import logging
61 import os
62+import time
63 import tempfile
64 import subprocess
65 import sys
66@@ -55,6 +56,7 @@ from plainbox.abc import IJobResult
67 from plainbox.abc import ISessionStateController
68 from plainbox.i18n import gettext as _
69 from plainbox.impl import get_plainbox_dir
70+from plainbox.impl.color import Colorizer
71 from plainbox.impl.depmgr import DependencyDuplicateError
72 from plainbox.impl.depmgr import DependencyMissingError
73 from plainbox.impl.resource import ExpressionCannotEvaluateError
74@@ -74,11 +76,14 @@ from plainbox.impl.unit.unit import MissingParam
75 from plainbox.impl.validation import Severity
76 from plainbox.vendor import morris
77 from plainbox.vendor import extcmd
78+from plainbox.vendor import rpyc
79+
80
81 __all__ = [
82 'CheckBoxSessionStateController',
83 'RootViaPTL1ExecutionController',
84 'RootViaPkexecExecutionController',
85+ 'RootViaRemoteDeamonController',
86 'RootViaSudoExecutionController',
87 'UserJobExecutionController',
88 'checkbox_session_state_ctrl',
89@@ -810,7 +815,7 @@ class UserJobExecutionController(CheckBoxExecutionController):
90 return -1
91 if job.user is not None:
92 if os.getuid() == 0:
93- return 4
94+ return 5
95 else:
96 return -1
97 return 1
98@@ -1305,6 +1310,62 @@ class RootViaPkexecExecutionController(
99 return 0
100
101
102+class RootViaRemoteDeamonController(
103+ CheckBoxDifferentialExecutionController):
104+ """
105+ Execution controller that gains root by using the remote deamon.
106+ """
107+
108+ _REMOTE_CONN = None
109+
110+ C = Colorizer()
111+
112+ def execute_remote_job(self, job, job_state, config):
113+ sa = self._REMOTE_CONN.root.get_sa()
114+ sa._reset_sa()
115+ configuration = dict()
116+ configuration['launcher'] = None
117+ sa.start_session(configuration)
118+ sa._sa.hand_pick_jobs([job.id])
119+ sa.save_todo_list([job.id])
120+ sa.run_job_command(job.id)
121+ while True:
122+ state, payload = sa.monitor_job()
123+ if payload:
124+ for line in payload:
125+ if line[0] == 'stderr':
126+ print(self.C.RED(line[1]), end='')
127+ else:
128+ print(self.C.GREEN(line[1]), end='')
129+ if state == 'running':
130+ time.sleep(0.1)
131+ else:
132+ result = sa.finish_job_command()
133+ sa.finalize_session()
134+ return result
135+
136+ def get_execution_command(self, job, job_state, config, session_dir,
137+ nest_dir):
138+ return None
139+
140+ def get_checkbox_score(self, job):
141+ """
142+ Compute how applicable this controller is for the specified job.
143+
144+ :returns:
145+ 4 for jobs with a user override, zero otherwise
146+ """
147+ if job.user is not None:
148+ try:
149+ conn = rpyc.connect('localhost', 18871)
150+ self._REMOTE_CONN = conn
151+ except OSError:
152+ return -1
153+ return 4
154+ else:
155+ return 0
156+
157+
158 class RootViaSudoExecutionController(
159 CheckBoxDifferentialExecutionController):
160 """
161diff --git a/plainbox/impl/runner.py b/plainbox/impl/runner.py
162index 7807a8f..03fff56 100644
163--- a/plainbox/impl/runner.py
164+++ b/plainbox/impl/runner.py
165@@ -309,28 +309,21 @@ class JobRunner(IJobRunner):
166 self._session_dir = session_dir
167 if execution_ctrl_list is None:
168 logger.debug("execution_ctrl_list not passed to JobRunner")
169- if sys.platform == 'linux' or sys.platform == 'linux2':
170- from plainbox.impl.ctrl import RootViaPkexecExecutionController
171- from plainbox.impl.ctrl import RootViaPTL1ExecutionController
172- from plainbox.impl.ctrl import RootViaSudoExecutionController
173- from plainbox.impl.ctrl import UserJobExecutionController
174- from plainbox.impl.ctrl import QmlJobExecutionController
175- execution_ctrl_list = [
176- RootViaPTL1ExecutionController(provider_list),
177- RootViaPkexecExecutionController(provider_list),
178- # XXX: maybe this one should be only used on command line
179- RootViaSudoExecutionController(provider_list),
180- UserJobExecutionController(provider_list),
181- QmlJobExecutionController(provider_list),
182- ]
183- elif sys.platform == 'win32':
184- from plainbox.impl.ctrl import UserJobExecutionController
185- execution_ctrl_list = [
186- UserJobExecutionController(provider_list)
187- ]
188- else:
189- logger.warning("Unsupported platform: %s", sys.platform)
190- execution_ctrl_list = []
191+ from plainbox.impl.ctrl import RootViaPkexecExecutionController
192+ from plainbox.impl.ctrl import RootViaPTL1ExecutionController
193+ from plainbox.impl.ctrl import RootViaRemoteDeamonController
194+ from plainbox.impl.ctrl import RootViaSudoExecutionController
195+ from plainbox.impl.ctrl import UserJobExecutionController
196+ from plainbox.impl.ctrl import QmlJobExecutionController
197+ execution_ctrl_list = [
198+ RootViaPTL1ExecutionController(provider_list),
199+ RootViaPkexecExecutionController(provider_list),
200+ RootViaRemoteDeamonController(provider_list),
201+ # XXX: maybe this one should be only used on command line
202+ RootViaSudoExecutionController(provider_list),
203+ UserJobExecutionController(provider_list),
204+ QmlJobExecutionController(provider_list),
205+ ]
206 self._jobs_io_log_dir = jobs_io_log_dir
207 # NOTE: deprecated
208 self._command_io_delegate = command_io_delegate
209@@ -787,24 +780,38 @@ class JobRunner(IJobRunner):
210 return JobResultBuilder(
211 outcome=IJobResult.OUTCOME_NOT_SUPPORTED,
212 comments=_('No suitable execution controller is available)'))
213- # Run the embedded command
214- start_time = time.time()
215- return_code, record_path = self._run_command(
216- job, job_state, config, ctrl)
217- execution_duration = time.time() - start_time
218- # Convert the return of the command to the outcome of the job
219- if return_code == 0:
220- outcome = IJobResult.OUTCOME_PASS
221- elif return_code < 0:
222- outcome = IJobResult.OUTCOME_CRASH
223- else:
224- outcome = IJobResult.OUTCOME_FAIL
225- # Create a result object and return it
226- return JobResultBuilder(
227- outcome=outcome,
228- return_code=return_code,
229- io_log_filename=record_path,
230- execution_duration=execution_duration)
231+ try:
232+ result = ctrl.execute_remote_job(job, job_state, config)
233+ params = {}
234+ io_log_filename = getattr(
235+ result, 'io_log_filename', None)
236+ if io_log_filename:
237+ params['io_log_filename'] = io_log_filename
238+ if result.return_code is not None:
239+ params['return_code'] = result.return_code
240+ if result.execution_duration:
241+ params['execution_duration'] = result.execution_duration
242+ params['outcome'] = result.outcome
243+ return JobResultBuilder(**params)
244+ except AttributeError:
245+ # Run the embedded command
246+ start_time = time.time()
247+ return_code, record_path = self._run_command(
248+ job, job_state, config, ctrl)
249+ execution_duration = time.time() - start_time
250+ # Convert the return of the command to the outcome of the job
251+ if return_code == 0:
252+ outcome = IJobResult.OUTCOME_PASS
253+ elif return_code < 0:
254+ outcome = IJobResult.OUTCOME_CRASH
255+ else:
256+ outcome = IJobResult.OUTCOME_FAIL
257+ # Create a result object and return it
258+ return JobResultBuilder(
259+ outcome=outcome,
260+ return_code=return_code,
261+ io_log_filename=record_path,
262+ execution_duration=execution_duration)
263
264 def _prepare_io_handling(self, job, config):
265 ui_io_delegate = self._command_io_delegate
266diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
267index d85c467..2cebb4f 100644
268--- a/plainbox/impl/session/assistant.py
269+++ b/plainbox/impl/session/assistant.py
270@@ -1278,6 +1278,72 @@ class SessionAssistant:
271 if jsm[job.id].result.outcome is None
272 ]
273
274+ #@raises(ValueError, TypeError, UnexpectedMethodCall)
275+ def run_job_command(
276+ self, job_id: str, ui: 'Union[str, IJobRunnerUI]',
277+ native: bool
278+ ) -> 'JobResultBuilder':
279+ """
280+ Run a job with the specific identifier.
281+ """
282+ if isinstance(ui, IJobRunnerUI):
283+ pass
284+ elif isinstance(ui, str):
285+ if ui == 'silent':
286+ ui = _SilentUI()
287+ elif ui == 'piano':
288+ ui = _PianoUI()
289+ else:
290+ raise ValueError("unknown user interface: {!r}".format(ui))
291+ else:
292+ raise TypeError("incorrect UI type")
293+ start_time = time.time()
294+ job_state = self._context.state.job_state_map[job_id]
295+ job = job_state.job
296+ ui.considering_job(job, job_state)
297+ ui.about_to_start_running(job, job_state)
298+ self._context.state.metadata.running_job_name = job.id
299+ self._manager.checkpoint()
300+ autorestart = (self._restart_strategy is not None and
301+ 'autorestart' in job.get_flag_set())
302+ if autorestart:
303+ restart_cmd = ' '.join(
304+ shlex.quote(cmd_part)
305+ for cmd_part in self._restart_cmd_callback(
306+ self._manager.storage.id))
307+ self._restart_strategy.prime_application_restart(
308+ self._app_id, self._manager.storage.id, restart_cmd)
309+ ui.started_running(job, job_state)
310+ if 'noreturn' in job.get_flag_set():
311+ # 'share' the information how to respawn the application
312+ # once all the test actions are performed.
313+ # tests can read this from $PLAINBOX_PROVIDER_SHARE envvar
314+ checkbox_data_dir = os.path.join(
315+ self.get_session_dir(), 'CHECKBOX_DATA')
316+ if not os.path.exists(checkbox_data_dir):
317+ os.mkdir(checkbox_data_dir)
318+ respawn_cmd_file = os.path.join(
319+ checkbox_data_dir, '__respawn_checkbox')
320+ if self._restart_cmd_callback:
321+ with open(respawn_cmd_file, 'wt') as f:
322+ f.writelines(self._restart_cmd_callback(
323+ self.get_session_id()))
324+ if not native:
325+ builder = self._runner.run_job(
326+ job, job_state, self._config, ui
327+ ).get_builder()
328+ else:
329+ builder = JobResultBuilder(
330+ outcome=IJobResult.OUTCOME_UNDECIDED,
331+ )
332+ builder.execution_duration = time.time() - start_time
333+ if autorestart:
334+ self._restart_strategy.diffuse_application_restart(
335+ self._app_id)
336+ self._manager.checkpoint()
337+ ui.finished_running(job, job_state, builder.get_result())
338+ return builder
339+
340 @raises(ValueError, TypeError, UnexpectedMethodCall)
341 def run_job(
342 self, job_id: str, ui: 'Union[str, IJobRunnerUI]',
343@@ -1501,7 +1567,7 @@ class SessionAssistant:
344 # leave the same usage expectations
345 return
346 if SessionMetaData.FLAG_SUBMITTED not in self._metadata.flags:
347- _logger.warning("Finalizing session that hasn't been submitted "
348+ _logger.info("Finalizing session that hasn't been submitted "
349 "anywhere: %s", self._manager.storage.id)
350 self._metadata.flags.remove(SessionMetaData.FLAG_INCOMPLETE)
351 self._manager.checkpoint()
352diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py
353index e1a9930..fa28ae9 100644
354--- a/plainbox/impl/session/remote_assistant.py
355+++ b/plainbox/impl/session/remote_assistant.py
356@@ -42,7 +42,7 @@ from checkbox_ng.launcher.run import SilentUI
357
358 _ = gettext.gettext
359
360-_logger = logging.getLogger("plainbox.session.assistant2")
361+_logger = logging.getLogger("plainbox.session.remote_assistant")
362
363 Interaction = namedtuple('Interaction', ['kind', 'message', 'extra'])
364
365@@ -74,7 +74,8 @@ class BufferedUI(SilentUI):
366 self.clear_buffers()
367
368 def got_program_output(self, stream_name, line):
369- self._queue.put(line.decode(sys.stdout.encoding, 'replace'))
370+ self._queue.put(
371+ (stream_name, line.decode(sys.stdout.encoding, 'replace')))
372 self._whole_queue.put(line)
373
374 def whole_output(self):
375@@ -85,9 +86,9 @@ class BufferedUI(SilentUI):
376
377 def get_output(self):
378 """Returns all the output queued up since previous call."""
379- output = ''
380+ output = []
381 while not self._queue.empty():
382- output += self._queue.get()
383+ output.append(self._queue.get())
384 return output
385
386 def clear_buffers(self):
387@@ -270,6 +271,18 @@ class RemoteSessionAssistant():
388 self._state = TestsSelected
389
390 @allowed_when(TestsSelected)
391+ def run_job_command(self, job_id):
392+ _logger.debug("run_job: %r", job_id)
393+ self._job_index = self._jobs_count - len(
394+ self._sa.get_dynamic_todo_list()) + 1
395+ self._currently_running_job = job_id
396+ job = self._sa.get_job(job_id)
397+ if job.command:
398+ self._state = Running
399+ self._be = BackgroundExecutor(
400+ self, job_id, self._sa.run_job_command)
401+
402+ @allowed_when(TestsSelected)
403 def run_job(self, job_id):
404 """
405 Depending on the type of the job, run_job can yield different number
406@@ -434,6 +447,17 @@ class RemoteSessionAssistant():
407 self._state = Idle
408 else:
409 self._state = TestsSelected
410+
411+ def finish_job_command(self):
412+ # assert the thread completed
413+ self.session_change_lock.acquire(blocking=False)
414+ self._session_change_lock.release()
415+ result = self._be.wait().get_result()
416+ if self._state != Bootstrapping:
417+ if not self._sa.get_dynamic_todo_list():
418+ self._state = Idle
419+ else:
420+ self._state = TestsSelected
421 return result
422
423 def get_jobs_repr(self, job_ids, offset=0):
424diff --git a/plainbox/impl/test_ctrl.py b/plainbox/impl/test_ctrl.py
425index a211b0e..63cd01e 100644
426--- a/plainbox/impl/test_ctrl.py
427+++ b/plainbox/impl/test_ctrl.py
428@@ -613,12 +613,12 @@ class UserJobExecutionControllerTests(CheckBoxExecutionControllerTestsMixIn,
429 @mock.patch('os.getuid')
430 def test_get_checkbox_score_as_root(self, mock_getuid, mock_plat):
431 """
432- verify that score for jobs with an user override is 4 if I am root
433+ verify that score for jobs with an user override is 5 if I am root
434 """
435 mock_plat.return_value = 'linux'
436 mock_getuid.return_value = 0 # Pretend to be root
437 self.job.user = 'root'
438- self.assertEqual(self.ctrl.get_checkbox_score(self.job), 4)
439+ self.assertEqual(self.ctrl.get_checkbox_score(self.job), 5)
440
441 @mock.patch.dict('os.environ', clear=True)
442 def test_get_execution_environment_resets_locales(self):

Subscribers

People subscribed via source and target branches