Merge ~sylvain-pineau/checkbox-ng:remote-deb-restart-strategy into checkbox-ng:master

Proposed by Sylvain Pineau
Status: Merged
Approved by: Sylvain Pineau
Approved revision: 3e48104d8cc1b000a7b1d4ac8f02ff1334989ef6
Merged at revision: 4ce0831a45a72cf1c36a41f4fdad49f34e944ead
Proposed branch: ~sylvain-pineau/checkbox-ng:remote-deb-restart-strategy
Merge into: checkbox-ng:master
Diff against target: 318 lines (+105/-25)
6 files modified
checkbox_ng/launcher/slave.py (+14/-2)
plainbox/impl/ctrl.py (+1/-1)
plainbox/impl/execution.py (+5/-2)
plainbox/impl/session/assistant.py (+29/-11)
plainbox/impl/session/remote_assistant.py (+18/-6)
plainbox/impl/session/restart.py (+38/-3)
Reviewer Review Type Date Requested Status
Jonathan Cave (community) Approve
Review via email: mp+381568@code.launchpad.net

Description of the change

I'd like to abuse the current API bump already in master to propose support for a new kind of restart strategy, RemoteDebRestartStrategy.

This patchset will allow jobs mainly based on p-p-c pm_test script to run via remote. To replicate the pure local invocation, the slave service is disabled when a noreturn flag is set in the job definition and reactivated via the restart command callback written in the __respawn magic file.

In order to work such jobs running as root over remote require more than the DISPLAY env var, they do need access to the running X server via XAUTHORITY. The extra env is now including this var and sent to the runner.
The same env also contains the NORMAL_USER set in launcher as scripts like pm_test which run `sudo --user` but now miss vars like SUDO_ID.

Tested using the following launcher on two 18.04 desktops:

[launcher]
app_id = com.canonical.certification:checkbox-test
launcher_version = 1
stock_reports = text, submission_files

[test plan]
#unit = com.canonical.certification::warm-boot-stress-test
unit = com.canonical.certification::power-management-reboot-poweroff-cert-automated
#unit = com.canonical.certification::stress-suspend-30-cycles-with-reboots-automated
forced = yes

[test selection]
forced = no

[ui]
output = hide-resource-and-attachment

[daemon]
normal_user = u

[environment]
STRESS_BOOT_ITERATIONS=2
STRESS_BOOT_WAIT_DELAY=10
STRESS_BOOT_WAKEUP_DELAY=15

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

LGTM

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/slave.py b/checkbox_ng/launcher/slave.py
2index b568620..96e2519 100644
3--- a/checkbox_ng/launcher/slave.py
4+++ b/checkbox_ng/launcher/slave.py
5@@ -26,12 +26,11 @@ import socket
6 import sys
7
8 from plainbox.impl.session.remote_assistant import RemoteSessionAssistant
9+from plainbox.impl.session.restart import RemoteDebRestartStrategy
10 from plainbox.impl.session.restart import RemoteSnappyRestartStrategy
11 from plainbox.vendor import rpyc
12 from plainbox.vendor.rpyc.utils.server import ThreadedServer
13
14-from checkbox_ng.config import load_configs
15-
16 _ = gettext.gettext
17 _logger = logging.getLogger("slave")
18
19@@ -88,6 +87,19 @@ class RemoteSlave():
20 # XXX: explicitly passing None to not have to bump Remote API
21 # TODO: remove on the next Remote API bump
22 SessionAssistantSlave.session_assistant.resume_by_id(None)
23+ else:
24+ _logger.info("RemoteDebRestartStrategy")
25+ if remote_restart_strategy_debug:
26+ strategy = RemoteDebRestartStrategy(debug=True)
27+ else:
28+ strategy = RemoteDebRestartStrategy()
29+ if os.path.exists(strategy.session_resume_filename):
30+ with open(strategy.session_resume_filename, 'rt') as f:
31+ session_id = f.readline()
32+ _logger.info(
33+ "RemoteDebRestartStrategy resume_by_id %r", session_id)
34+ SessionAssistantSlave.session_assistant.resume_by_id(
35+ session_id)
36 self._server = ThreadedServer(
37 SessionAssistantSlave,
38 port=slave_port,
39diff --git a/plainbox/impl/ctrl.py b/plainbox/impl/ctrl.py
40index 9056054..801eccd 100644
41--- a/plainbox/impl/ctrl.py
42+++ b/plainbox/impl/ctrl.py
43@@ -290,7 +290,7 @@ class CheckBoxSessionStateController(ISessionStateController):
44 # XXX: Consider forwarding the origin object here. I guess we
45 # should have from_frc822_record as with JobDefinition
46 resource = Resource(record.data)
47- logger.info(
48+ logger.debug(
49 _("Storing resource record %r: %s"), job.id, resource)
50 new_resource_list.append(resource)
51 # Create an empty resource object to properly fail __getattr__ calls
52diff --git a/plainbox/impl/execution.py b/plainbox/impl/execution.py
53index ef59210..e311c7c 100644
54--- a/plainbox/impl/execution.py
55+++ b/plainbox/impl/execution.py
56@@ -277,9 +277,10 @@ class UnifiedRunner(IJobRunner):
57 self._extra_env)
58 env = get_execution_environment(
59 job, environ, self._session_dir, nest_dir)
60+ if self._user_provider():
61+ env['NORMAL_USER'] = self._user_provider()
62 # run the command
63- logger.debug(_("job[%(ID)s] executing %(CMD)r with env %(ENV)r"
64- " in cwd %(DIR)r"),
65+ logger.debug(_("job[%(ID)s] executing %(CMD)r with env %(ENV)r"),
66 {"ID": job.id, "CMD": cmd,
67 "ENV": env})
68 if 'preserve-cwd' in job.get_flag_set() or os.getenv("SNAP"):
69@@ -568,6 +569,8 @@ def get_execution_command(job, environ, session_dir,
70 job, environ, session_dir, nest_dir, extra_env)
71 else:
72 env = get_execution_environment(job, environ, session_dir, nest_dir)
73+ if extra_env:
74+ env.update(extra_env)
75 cmd += ["{key}={value}".format(key=key, value=value)
76 for key, value in sorted(env.items())]
77 cmd += [job.shell, '-c', job.command]
78diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
79index 6e8921e..1800fd7 100644
80--- a/plainbox/impl/session/assistant.py
81+++ b/plainbox/impl/session/assistant.py
82@@ -61,6 +61,7 @@ from plainbox.impl.session.jobs import InhibitionCause
83 from plainbox.impl.session.manager import SessionManager
84 from plainbox.impl.session.restart import IRestartStrategy
85 from plainbox.impl.session.restart import detect_restart_strategy
86+from plainbox.impl.session.restart import RemoteDebRestartStrategy
87 from plainbox.impl.session.storage import SessionStorageRepository
88 from plainbox.impl.transport import OAuthTransport
89 from plainbox.impl.transport import TransportError
90@@ -513,7 +514,7 @@ class SessionAssistant:
91 self._command_io_delegate = JobRunnerUIDelegate(_SilentUI())
92 self._init_runner(runner_cls, runner_kwargs)
93 self.session_available(self._manager.storage.id)
94- _logger.debug("New session created: %s", title)
95+ _logger.info("New session created: %s", title)
96 UsageExpectation.of(self).allowed_calls = {
97 self.get_test_plans: "to get the list of available test plans",
98 self.get_test_plan: "to get particular test plan object",
99@@ -531,7 +532,6 @@ class SessionAssistant:
100 allowed_calls[self.use_alternate_restart_strategy] = (
101 "configure automatic restart capability")
102
103-
104 @raises(KeyError, UnexpectedMethodCall)
105 def resume_session(self, session_id: str,
106 runner_cls=UnifiedRunner,
107@@ -578,10 +578,12 @@ class SessionAssistant:
108 ).get_result()
109 self._context.state.update_job_result(job, result)
110 self._manager.checkpoint()
111+ self._restart_strategy = detect_restart_strategy(self)
112+ _logger.info("Session strategy: %r", self._restart_strategy)
113 if self._restart_strategy is not None:
114 self._restart_strategy.diffuse_application_restart(self._app_id)
115 self.session_available(self._manager.storage.id)
116- _logger.debug("Session resumed: %s", session_id)
117+ _logger.info("Session resumed: %s", session_id)
118 if SessionMetaData.FLAG_TESTPLANLESS in self._metadata.flags:
119 UsageExpectation.of(self).allowed_calls = self._get_allowed_calls_in_normal_state()
120 else:
121@@ -622,7 +624,7 @@ class SessionAssistant:
122 try:
123 metadata = SessionPeekHelper().peek(data)
124 except SessionResumeError:
125- _logger.info("Exception raised when trying to resume"
126+ _logger.info("Exception raised when trying to resume "
127 "session: %s", str(storage.id))
128 else:
129 if (metadata.app_id == self._app_id and
130@@ -921,7 +923,6 @@ class SessionAssistant:
131 # during bootstrap phase
132 self._bootstrap_done_list = self.get_dynamic_done_list()
133
134-
135 @raises(KeyError, UnexpectedMethodCall)
136 def use_alternate_selection(self, selection: 'Iterable[str]'):
137 """
138@@ -1387,12 +1388,21 @@ class SessionAssistant:
139 autorestart = (self._restart_strategy is not None and
140 'autorestart' in job.get_flag_set())
141 if autorestart:
142- restart_cmd = ' '.join(
143- shlex.quote(cmd_part)
144- for cmd_part in self._restart_cmd_callback(
145- self._manager.storage.id))
146+ restart_cmd = ''
147+ if self._restart_cmd_callback:
148+ restart_cmd = ' '.join(
149+ shlex.quote(cmd_part)
150+ for cmd_part in self._restart_cmd_callback(
151+ self._manager.storage.id))
152 self._restart_strategy.prime_application_restart(
153 self._app_id, self._manager.storage.id, restart_cmd)
154+ elif (
155+ isinstance(self._restart_strategy, RemoteDebRestartStrategy)
156+ and 'noreturn' in job.get_flag_set()
157+ ):
158+ self._restart_strategy.prime_application_restart(
159+ self._app_id, self._manager.storage.id,
160+ RemoteDebRestartStrategy.service_name)
161 ui.started_running(job, job_state)
162 if 'noreturn' in job.get_flag_set():
163 # 'share' the information how to respawn the application
164@@ -1408,8 +1418,16 @@ class SessionAssistant:
165 checkbox_data_dir, '__respawn_checkbox')
166 if self._restart_cmd_callback:
167 with open(respawn_cmd_file, 'wt') as f:
168- f.writelines(self._restart_cmd_callback(
169- self.get_session_id()))
170+ if isinstance(self._restart_strategy,
171+ RemoteDebRestartStrategy):
172+ service = RemoteDebRestartStrategy.service_name
173+ f.writelines([
174+ 'sudo systemctl enable {}\n'.format(service),
175+ 'sudo systemctl start {}'.format(service),
176+ ])
177+ else:
178+ f.writelines(self._restart_cmd_callback(
179+ self.get_session_id()))
180 if not native:
181 if self._config.environment is Unset:
182 result = self._runner.run_job(job, job_state, ui=ui)
183diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py
184index 1307bb1..4c0a379 100644
185--- a/plainbox/impl/session/remote_assistant.py
186+++ b/plainbox/impl/session/remote_assistant.py
187@@ -210,11 +210,15 @@ class RemoteSessionAssistant():
188
189 def _prepare_display_without_psutil(self):
190 try:
191- value = check_output(
192+ display_value = check_output(
193 'strings /proc/*/environ 2>/dev/null | '
194 'grep -m 1 -oP "(?<=DISPLAY=).*"',
195 shell=True, universal_newlines=True).rstrip()
196- return {'DISPLAY': value}
197+ xauth_value = check_output(
198+ 'strings /proc/*/environ 2>/dev/null | '
199+ 'grep -m 1 -oP "(?<=XAUTHORITY=).*"',
200+ shell=True, universal_newlines=True).rstrip()
201+ return {'DISPLAY': display_value, 'XAUTHORITY': xauth_value}
202 except CalledProcessError:
203 return None
204
205@@ -234,13 +238,20 @@ class RemoteSessionAssistant():
206 # quietly ignore the process that died before we had a chance
207 # to read the environment from them
208 continue
209- if ("DISPLAY" in p_environ and p_user != 'gdm'): # gdm uses :1024
210- return {'DISPLAY': p_environ['DISPLAY']}
211+ if (
212+ "DISPLAY" in p_environ and
213+ "XAUTHORITY" in p_environ and
214+ p_user != 'gdm'
215+ ): # gdm uses :1024
216+ return {
217+ 'DISPLAY': p_environ['DISPLAY'],
218+ 'XAUTHORITY': p_environ['XAUTHORITY']
219+ }
220
221 @allowed_when(Idle)
222 def start_session(self, configuration):
223 self._reset_sa()
224- _logger.debug("start_session: %r", configuration)
225+ _logger.info("start_session: %r", configuration)
226 session_title = 'checkbox-slave'
227 session_desc = 'checkbox-slave session'
228 session_type = 'checkbox-slave'
229@@ -459,7 +470,7 @@ class RemoteSessionAssistant():
230 :returns:
231 (state, payload) tuple.
232 """
233- _logger.debug("whats_up()")
234+ _logger.debug("whats_up() -> %r", self._state)
235 payload = None
236 if self._state == Running:
237 payload = (
238@@ -587,6 +598,7 @@ class RemoteSessionAssistant():
239 return test_info_list
240
241 def resume_by_id(self, session_id=None):
242+ _logger.info("resume_by_id: %r", session_id)
243 self._launcher = load_configs()
244 resume_candidates = list(self._sa.get_resumable_sessions())
245 if not session_id:
246diff --git a/plainbox/impl/session/restart.py b/plainbox/impl/session/restart.py
247index bd6a735..053f040 100644
248--- a/plainbox/impl/session/restart.py
249+++ b/plainbox/impl/session/restart.py
250@@ -233,6 +233,29 @@ class RemoteSnappyRestartStrategy(IRestartStrategy):
251 raise
252
253
254+class RemoteDebRestartStrategy(RemoteSnappyRestartStrategy):
255+
256+ """
257+ Remote Restart strategy for checkbox installed from deb packages.
258+ """
259+
260+ service_name = "checkbox-ng.service"
261+
262+ def get_session_resume_filename(self) -> str:
263+ if self.debug:
264+ return '/tmp/session_resume'
265+ cache_dir = os.getenv('XDG_CACHE_HOME')
266+ return os.path.join(cache_dir, 'session_resume')
267+
268+ def prime_application_restart(self, app_id: str,
269+ session_id: str, cmd: str) -> None:
270+ with open(self.session_resume_filename, 'wt') as f:
271+ f.write(session_id)
272+ os.fsync(f.fileno())
273+ if cmd == self.service_name:
274+ subprocess.call(['systemctl', 'disable', self.service_name])
275+
276+
277 def detect_restart_strategy(session=None) -> IRestartStrategy:
278 """
279 Detect the restart strategy for the current environment.
280@@ -260,16 +283,18 @@ def detect_restart_strategy(session=None) -> IRestartStrategy:
281 except subprocess.CalledProcessError:
282 return SnappyRestartStrategy()
283
284- # Classic snaps
285- snap_data = os.getenv('SNAP_DATA')
286 try:
287 if session:
288- app_blob = json.loads(session._context.state.metadata.app_blob.decode('UTF-8'))
289+ app_blob = json.loads(
290+ session._context.state.metadata.app_blob.decode('UTF-8'))
291 session_type = app_blob.get("type")
292 else:
293 session_type = None
294 except AttributeError:
295 session_type = None
296+
297+ # Classic snaps
298+ snap_data = os.getenv('SNAP_DATA')
299 if snap_data:
300 # Classic snaps w/ remote service enabled and in use
301 if session_type == "checkbox-slave":
302@@ -285,6 +310,16 @@ def detect_restart_strategy(session=None) -> IRestartStrategy:
303 else:
304 return SnappyRestartStrategy()
305
306+ # debian checkbox-ng.service
307+ if session_type == "checkbox-slave":
308+ try:
309+ subprocess.run(
310+ ['systemctl', 'is-active', '--quiet', 'checkbox-ng.service'],
311+ check=True)
312+ return RemoteDebRestartStrategy()
313+ except subprocess.CalledProcessError:
314+ pass
315+
316 if os.path.isdir('/etc/xdg/autostart'):
317 # NOTE: Assume this is a terminal application
318 return XDGRestartStrategy(app_terminal=True)

Subscribers

People subscribed via source and target branches