Merge ~sylvain-pineau/checkbox-ng:unleash-hell into checkbox-ng:master

Proposed by Sylvain Pineau
Status: Rejected
Rejected by: Sylvain Pineau
Proposed branch: ~sylvain-pineau/checkbox-ng:unleash-hell
Merge into: checkbox-ng:master
Diff against target: 450 lines (+212/-9)
9 files modified
checkbox_ng/launcher/master.py (+7/-1)
checkbox_ng/launcher/subcommands.py (+15/-0)
docs/external-pxu.rst (+65/-0)
docs/index.rst (+1/-0)
docs/side-loading.rst (+1/-1)
plainbox/impl/session/assistant.py (+67/-0)
plainbox/impl/session/remote_assistant.py (+5/-0)
plainbox/impl/session/state.py (+18/-7)
plainbox/impl/session/test_assistant.py (+33/-0)
Reviewer Review Type Date Requested Status
Maciej Kisielewski (community) Disapprove
Jonathan Cave (community) Needs Fixing
Review via email: mp+385750@code.launchpad.net

Description of the change

New feature to second side-loading providers by supplying an external pxu file to help fixing test plans and add new job definitions.

This method can be used when side-loading is not easy to setup (e.g on Ubuntu core).

Units provided by this external pxu must belong to an existing namespace and must name it explicitly in their definitions.

All supported type of units can be loaded via this single pxu file which will be saved in the session dir. Current limit is 10 units max.

Tested with remote and resume scenarios.

To post a comment you must log in.
Revision history for this message
Daniel Manrique (roadmr) wrote :

+1000 on branch name.

Revision history for this message
Jonathan Cave (jocave) wrote :

We have recently tried to create some traceability in our session reports e.g. metadata to indicate if the job selection was modified, list of jobs deselected, discussed supporting a field identifying who ran this session. I feel this undoes a lot of that traceability. At the very least I think there should be an extra flag added to the submission json to indicate that an external pxu was used, possibly it could list the unit IDs that were loaded from it.

Some code comments below too.

"Needs Fixing" for the submission json changes.

review: Needs Fixing
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

I dislike this development very much.

Let's address the needs for this first.

1) lack of git on a target system.
   - how do you access that system? via ssh? so you have scp. Or even sshfs for a smoother development.
2) sideloading blocks submissions. This was done explicitly. Can be removed by removing two lines in checkbox.

The development here introduces much logic to checkbox, including:

- additional cli arg
- expicit namespace field in units, I understand why this is needed, but having a field for external-pxu use only looks insane to me
- broadening of already bloated SA API
- new unit source flavor

I'm personally sorry that you committed your time to this.
If my veto gets overridden and this will land I have to ask for following fixes:
a) the --pxu should be changed to a top-level opt.
b) the filesizes checks for "The check on file size should avoid passing large binary files by mistake." are weird. If the user does something wrong they should expect wrong behavior. If it takes forever before the parser notices it's not a RFC822 record then we should improve the parser (AFAIR there is requirement about encoding, line length and overall structure, so I'm guessing we only need to read 1kB of the stuff before we can safely bail out, which you cover in line 267 of your diff). Next to the filesize checks there are no checks if the file is a file, which makes checkbox burp on non-concrete-files).
c) the all_units loading is expensive and should be avoided at all cost. Kudos for the TODO.

My bottom line is that loading definitions in checkbox is already a frankenstein monster and this branch stitches a lamb chop onto the monster's forehead.

review: Disapprove

Unmerged commits

f98b430... by Sylvain Pineau

docs: Add external-pxu.rst

7126680... by Sylvain Pineau

docs: typo fixed

e0998c9... by Sylvain Pineau

master: Add --pxu to the master command

The external pxu is read and the plain text content saved in remote session
assistant.

The check on file size should avoid passing large binary files over rpyc by
mistake.

b5212c3... by Sylvain Pineau

subcommands: Add --pxu to list-bootstrapped, launcher and run commands

The external pxu is read and the plain text content saved in session
assistant.

The check on file size should avoid passing large binary files by mistake.

716baa8... by Sylvain Pineau

session:remote_assisant: Add use_external_pxu to remote

a60285c... by Sylvain Pineau

session:test_assistant: Check units from external pxu are overriding units

ed9fe60... by Sylvain Pineau

session:state: External pxu units always override existing units

a9368d8... by Sylvain Pineau

session:assistant: Reload the external pxu file from the session dir

When resuming sessions, re-populate the unit list from the saved pxu file.

c25c153... by Sylvain Pineau

session:assistant: Add support for the external pxu file override

In addition to selected providers, units can be loaded from a single
external pxu file.

Units must belong to an existing namespace and such namespace added to the
job definition.

The external pxu file is saved in the session directory.

Warning: 10 units maximum are loadable via the external pxu file.

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 5f5af9c..562361b 100644
3--- a/checkbox_ng/launcher/master.py
4+++ b/checkbox_ng/launcher/master.py
5@@ -129,6 +129,7 @@ class RemoteMaster(ReportsStage, MainLoopStage):
6 self._is_bootstrapping = False
7 self._target_host = ctx.args.host
8 self._normal_user = ''
9+ self._external_pxu = None
10 self.launcher = DefaultLauncherDefinition()
11 if ctx.args.launcher:
12 expanded_path = os.path.expanduser(ctx.args.launcher)
13@@ -144,6 +145,10 @@ class RemoteMaster(ReportsStage, MainLoopStage):
14 self._normal_user = getpass.getuser()
15 if ctx.args.user:
16 self._normal_user = ctx.args.user
17+ if ctx.args.pxu:
18+ if os.path.getsize(ctx.args.pxu) < 1000000:
19+ with open(ctx.args.pxu) as pxu:
20+ self._external_pxu = pxu.read()
21 timeout = 600
22 deadline = time.time() + timeout
23 port = ctx.args.port
24@@ -265,7 +270,7 @@ class RemoteMaster(ReportsStage, MainLoopStage):
25 configuration = dict()
26 configuration['launcher'] = self._launcher_text
27 configuration['normal_user'] = self._normal_user
28-
29+ self.sa.use_external_pxu(self._external_pxu)
30 tps = self.sa.start_session(configuration)
31 if self.launcher.test_plan_forced:
32 self.select_tp(self.launcher.test_plan_default_selection)
33@@ -346,6 +351,7 @@ class RemoteMaster(ReportsStage, MainLoopStage):
34 "port to connect to"))
35 parser.add_argument('-u', '--user', help=_(
36 "normal user to run non-root jobs"))
37+ parser.add_argument("--pxu", help=_("external PXU file"))
38
39 def _handle_interrupt(self):
40 """
41diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py
42index 0432cdb..6ff56cf 100644
43--- a/checkbox_ng/launcher/subcommands.py
44+++ b/checkbox_ng/launcher/subcommands.py
45@@ -208,6 +208,10 @@ class Launcher(MainLoopStage, ReportsStage):
46 self._configure_restart(ctx)
47 self._prepare_transports()
48 ctx.sa.use_alternate_configuration(self.launcher)
49+ if ctx.args.pxu and os.path.getsize(ctx.args.pxu) < 1000000:
50+ with open(ctx.args.pxu) as pxu:
51+ external_pxu = pxu.read()
52+ self.sa.use_external_pxu(external_pxu)
53 if not self._maybe_resume_session():
54 self._start_new_session()
55 self._pick_jobs_to_run()
56@@ -579,6 +583,7 @@ class Launcher(MainLoopStage, ReportsStage):
57 "remove previous sessions' data"))
58 parser.add_argument('--version', action='store_true', help=_(
59 "show program's version information and exit"))
60+ parser.add_argument("--pxu", help=_("external PXU file"))
61
62
63 class CheckboxUI(NormalUI):
64@@ -632,6 +637,7 @@ class Run(MainLoopStage):
65 parser.add_argument(
66 "-m", "--message",
67 help=_("submission description"))
68+ parser.add_argument("--pxu", help=_("external PXU file"))
69
70 @property
71 def C(self):
72@@ -659,6 +665,10 @@ class Run(MainLoopStage):
73 self._configure_restart()
74 config = load_configs()
75 self.sa.use_alternate_configuration(config)
76+ if ctx.args.pxu and os.path.getsize(ctx.args.pxu) < 1000000:
77+ with open(ctx.args.pxu) as pxu:
78+ external_pxu = pxu.read()
79+ self.sa.use_external_pxu(external_pxu)
80 self.sa.start_new_session(
81 self.ctx.args.title or 'checkbox-run',
82 UnifiedRunner)
83@@ -827,9 +837,14 @@ class ListBootstrapped():
84 '-f', '--format', type=str, default="{full_id}\n",
85 help=_(("output format, as passed to print function. "
86 "Use '?' to list possible values")))
87+ parser.add_argument("--pxu", help=_("external PXU file"))
88
89 def invoked(self, ctx):
90 self.ctx = ctx
91+ if ctx.args.pxu and os.path.getsize(ctx.args.pxu) < 1000000:
92+ with open(ctx.args.pxu) as pxu:
93+ external_pxu = pxu.read()
94+ self.sa.use_external_pxu(external_pxu)
95 self.sa.start_new_session('checkbox-listing-ephemeral')
96 tps = self.sa.get_test_plans()
97 if ctx.args.TEST_PLAN not in tps:
98diff --git a/docs/external-pxu.rst b/docs/external-pxu.rst
99new file mode 100644
100index 0000000..403df4c
101--- /dev/null
102+++ b/docs/external-pxu.rst
103@@ -0,0 +1,65 @@
104+.. _external-pxu:
105+
106+Supplying an external pxu file
107+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
108+
109+:doc:`side-loading` is convenient but only meant for developers to quickly
110+iterate on new tests writing without snapping their checkbox applications.
111+
112+If the test environment is Ubuntu core, git is not available and manual copying
113+of a provider directory is required. Moreover using side-loaded providers
114+prevents the submission tarball to be sent to the certification website.
115+
116+An alternate solution to side-loading is the external pxu, it's not as flexible
117+as side-loading but it allows testers to edit their test plan, add a few more
118+jobs definition and still keep the ability to submit their test reports to the
119+certification website.
120+
121+The supplied external pxu is stored in the session directory. There's no
122+caching, if subsequent invocations of checkbox don't specify an external pxu,
123+the new sessions won't run with it.
124+
125+.. warning::
126+ - **limited to 10 units**
127+ - all units must define their targeted **namespace**
128+ - unit namespaces must already be available via installed/developed
129+ providers
130+ - only one external pxu file
131+
132+.. note::
133+ Supported commands:
134+
135+ - ``checkbox-cli run``
136+ - ``checkbox-cli list-bootstrapped``
137+ - ``checkbox-cli master``
138+ - ``checkbox-cli launcher``
139+
140+Example scenario
141+================
142+
143+Using this `ext.pxu` file::
144+
145+ plugin: shell
146+ namespace: com.canonical.certification
147+ category_id: com.canonical.plainbox::disk
148+ id: disk/detect
149+ _summary: foo
150+ _description: bar
151+ command: echo "at my signal"
152+
153+Invoke checkbox using the ``--pxu`` command line option::
154+
155+ checkbox-cli run com.canonical.certification::disk/detect --pxu ext.pxu
156+ Loading units from '[...].session/_external.pxu'...
157+ =========================[ Running Selected Jobs ]=======================
158+ ------------[ Running job 1 / 1. Estimated time left: unknown ]----------
159+ ----------------------------------[ foo ]--------------------------------
160+ ID: com.canonical.certification::disk/detect
161+ Category: com.canonical.plainbox::disk
162+ ... 8< -----------------------------------------------------------------
163+ at my signal
164+ ----------------------------------------------------------------- >8 ---
165+ Outcome: job passed
166+ ================================[ Results ]=============================
167+ ☑ : foo
168+
169diff --git a/docs/index.rst b/docs/index.rst
170index 5f76977..1dd2c04 100644
171--- a/docs/index.rst
172+++ b/docs/index.rst
173@@ -67,6 +67,7 @@ Table of contents
174 stack.rst
175 launcher-tutorial.rst
176 side-loading.rst
177+ external-pxu.rst
178 configs.rst
179 nested-test-plan.rst
180 snappy.rst
181diff --git a/docs/side-loading.rst b/docs/side-loading.rst
182index 35323ee..beab587 100644
183--- a/docs/side-loading.rst
184+++ b/docs/side-loading.rst
185@@ -19,7 +19,7 @@ on the number of new providers supplied with side-loading.
186 Don't use it *in production*. Also remember to empty (or delete) the
187 `~/provider` directory once you're done developing, so you don't get nasty
188 surprises down the line.
189- Checkbox will not submit any reports to Certificaiton website if
190+ Checkbox will not submit any reports to Certification website if
191 side-loaded providers have been used.
192
193 Example scenario
194diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py
195index c584513..a8b7f2d 100644
196--- a/plainbox/impl/session/assistant.py
197+++ b/plainbox/impl/session/assistant.py
198@@ -49,11 +49,15 @@ from plainbox.impl.result import MemoryJobResult
199 from plainbox.impl.runner import JobRunnerUIDelegate
200 from plainbox.impl.secure.config import Unset
201 from plainbox.impl.secure.origin import Origin
202+from plainbox.impl.secure.plugins import PlugInError
203 from plainbox.impl.secure.qualifiers import select_jobs
204 from plainbox.impl.secure.qualifiers import FieldQualifier
205 from plainbox.impl.secure.qualifiers import JobIdQualifier
206 from plainbox.impl.secure.qualifiers import PatternMatcher
207 from plainbox.impl.secure.qualifiers import RegExpJobQualifier
208+from plainbox.impl.secure.rfc822 import FileTextSource
209+from plainbox.impl.secure.rfc822 import RFC822SyntaxError
210+from plainbox.impl.secure.rfc822 import load_rfc822_records
211 from plainbox.impl.session import SessionMetaData
212 from plainbox.impl.session import SessionPeekHelper
213 from plainbox.impl.session import SessionResumeError
214@@ -67,6 +71,7 @@ from plainbox.impl.transport import OAuthTransport
215 from plainbox.impl.transport import TransportError
216 from plainbox.impl.unit.exporter import ExporterError
217 from plainbox.impl.unit.unit import Unit
218+from plainbox.impl.validation import ValidationError
219 from plainbox.vendor import morris
220
221 _logger = logging.getLogger("plainbox.session.assistant")
222@@ -184,6 +189,7 @@ class SessionAssistant:
223 self._metadata = None
224 self._runner = None
225 self._job_start_time = None
226+ self._external_pxu = None
227 # Keep a record of jobs run during bootstrap phase
228 self._bootstrap_done_list = []
229 self._load_providers()
230@@ -390,6 +396,62 @@ class SessionAssistant:
231 del UsageExpectation.of(self).allowed_calls[
232 self.use_alternate_execution_controllers]
233
234+ @staticmethod
235+ def _get_unit_cls(unit_name):
236+ """
237+ Get a class that implements the specified unit
238+ """
239+ # TODO: transition to lazy plugin collection
240+ from plainbox.impl.unit import all_units
241+ all_units.load()
242+ return all_units.get_by_name(unit_name).plugin_object
243+
244+ def use_external_pxu(self, pxu) -> None:
245+ self._external_pxu = pxu
246+
247+ def _save_external_pxu(self) -> None:
248+ pxu = os.path.join(self._manager.storage.location, '_external.pxu')
249+ with open(pxu, 'w') as pxu:
250+ pxu.write(self._external_pxu)
251+
252+ def _load_external_pxu(self, session_dir=None) -> None:
253+ if not session_dir:
254+ session_dir = self._manager.storage.location
255+ external_pxu = os.path.join(session_dir, '_external.pxu')
256+ if not os.path.exists(external_pxu):
257+ return
258+ with open(external_pxu) as pxu:
259+ try:
260+ records = load_rfc822_records(
261+ pxu, source='External')
262+ if records > records[:0o1310-0b1010111110]:
263+ _logger.warning(
264+ _("External pxu size exceeds the allowable limit"))
265+ return
266+ _logger.warning(_("Loading units from %r..."), pxu.name)
267+ except RFC822SyntaxError as exc:
268+ raise PlugInError(
269+ _("Cannot load job definitions from {!r}: {}").format(
270+ pxu.name, exc))
271+ for record in records:
272+ unit_name = record.data.get('unit', 'job')
273+ try:
274+ unit_cls = self._get_unit_cls(unit_name)
275+ except KeyError:
276+ raise PlugInError(
277+ _("Unknown unit type: {!r}").format(unit_name))
278+ try:
279+ provider = [
280+ p for p in self._selected_providers
281+ if p.namespace == record.data['namespace']].pop()
282+ unit = unit_cls.from_rfc822_record(record, provider)
283+ except (ValueError, IndexError, KeyError) as exc:
284+ raise PlugInError(
285+ _("Cannot define unit from record {!r}: {}").format(
286+ record, exc))
287+ _logger.debug(_("Loaded %r"), unit)
288+ unit.provider.unit_list.append(unit)
289+
290 def _load_providers(self) -> None:
291 """Load all Checkbox providers."""
292 self._selected_providers = get_providers()
293@@ -499,6 +561,9 @@ class SessionAssistant:
294 """
295 UsageExpectation.of(self).enforce()
296 self._manager = SessionManager.create(self._repo, prefix=title + '-')
297+ if self._external_pxu:
298+ self._save_external_pxu()
299+ self._load_external_pxu()
300 self._context = self._manager.add_local_device_context()
301 for provider in self._selected_providers:
302 if provider.problem_list:
303@@ -556,6 +621,8 @@ class SessionAssistant:
304 runs bootstrapping, updates app blob, etc.)
305 """
306 UsageExpectation.of(self).enforce()
307+ self._load_external_pxu(
308+ self._resume_candidates[session_id][0].location)
309 all_units = list(itertools.chain(
310 *[p.unit_list for p in self._selected_providers]))
311 self._manager = SessionManager.load_session(
312diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py
313index 55e8226..16f449c 100644
314--- a/plainbox/impl/session/remote_assistant.py
315+++ b/plainbox/impl/session/remote_assistant.py
316@@ -152,6 +152,7 @@ class RemoteSessionAssistant():
317 self._pipe_to_subproc = open(self._input_piping[0])
318 self._reset_sa()
319 self._currently_running_job = None
320+ self._external_pxu = None
321
322 def _reset_sa(self):
323 _logger.info("Resetting RSA")
324@@ -284,6 +285,7 @@ class RemoteSessionAssistant():
325 'stdin': self._pipe_to_subproc,
326 'extra_env': self.prepare_extra_env(),
327 }
328+ self._sa.use_external_pxu(self._external_pxu)
329 self._sa.start_new_session(session_title, UnifiedRunner, runner_kwargs)
330 self._sa.update_app_blob(json.dumps(
331 {'description': session_desc, }).encode("UTF-8"))
332@@ -332,6 +334,9 @@ class RemoteSessionAssistant():
333 job_state.attempts = self._launcher.max_attempts
334 return self._sa.get_static_todo_list()
335
336+ def use_external_pxu(self, pxu):
337+ self._external_pxu = pxu
338+
339 def get_manifest_repr(self):
340 return self._sa.get_manifest_repr()
341
342diff --git a/plainbox/impl/session/state.py b/plainbox/impl/session/state.py
343index 38c1dfe..6d10b25 100644
344--- a/plainbox/impl/session/state.py
345+++ b/plainbox/impl/session/state.py
346@@ -731,8 +731,12 @@ class SessionState:
347 job_list.remove(exc.duplicate_job)
348 continue
349 else:
350- # If the jobs differ report this back to the caller
351- raise
352+ if exc.duplicate_job.origin.source == 'External':
353+ job_list.remove(exc.job)
354+ continue
355+ else:
356+ # If the jobs differ report this back to the caller
357+ raise
358 else:
359 # If there are no problems then break the loop
360 break
361@@ -1030,10 +1034,7 @@ class SessionState:
362 return new_unit
363
364 def _add_job_unit(self, new_job, recompute, via):
365- # See if we have a job with the same id already
366- try:
367- existing_job = self.job_state_map[new_job.id].job
368- except KeyError:
369+ def register_new_job(new_job):
370 # Register the new job in our state
371 self.job_state_map[new_job.id] = JobState(new_job)
372 self.job_list.append(new_job)
373@@ -1043,11 +1044,21 @@ class SessionState:
374 self.on_job_added(new_job)
375 self._add_job_siblings_unit(new_job, recompute, via)
376 return new_job
377+ # See if we have a job with the same id already
378+ try:
379+ existing_job = self.job_state_map[new_job.id].job
380+ except KeyError:
381+ register_new_job(new_job)
382 else:
383 # If there is a clash report DependencyDuplicateError only when the
384 # hashes are different.
385 if new_job != existing_job and not self._fake_resources:
386- raise DependencyDuplicateError(existing_job, new_job)
387+ if new_job.origin.source == 'External':
388+ register_new_job(new_job)
389+ elif existing_job.origin.source == 'External':
390+ pass
391+ else:
392+ raise DependencyDuplicateError(existing_job, new_job)
393 self._add_job_siblings_unit(new_job, recompute, via)
394 return existing_job
395 finally:
396diff --git a/plainbox/impl/session/test_assistant.py b/plainbox/impl/session/test_assistant.py
397index b170499..1347edd 100644
398--- a/plainbox/impl/session/test_assistant.py
399+++ b/plainbox/impl/session/test_assistant.py
400@@ -25,6 +25,7 @@ from plainbox.impl.providers.special import get_stubbox
401 from plainbox.impl.secure.providers.v1 import Provider1
402 from plainbox.impl.session.assistant import SessionAssistant
403 from plainbox.impl.session.assistant import UsageExpectation
404+from plainbox.impl.unit.job import JobDefinition
405 from plainbox.vendor import mock
406 from plainbox.vendor import morris
407
408@@ -39,6 +40,24 @@ class SessionAssistantTests(morris.SignalTestCase):
409 API_VERSION = '0.99'
410 API_FLAGS = []
411
412+ _pxu = """
413+id: stub/true
414+namespace: com.canonical.plainbox
415+_summary: Passing shell job
416+_description:
417+ Check success result from shell test case
418+plugin: shell
419+flags: preserve-locale
420+command: false; echo 'external pxu test 01'
421+estimated_duration: 0.1
422+category_id: plugin-representative
423+
424+id: new/external/job
425+namespace: com.canonical.plainbox
426+flags: simple
427+command: echo 'external pxu test 02'
428+"""
429+
430 def setUp(self):
431 """Common set-up code."""
432 self.sa = SessionAssistant(
433@@ -99,3 +118,17 @@ class SessionAssistantTests(morris.SignalTestCase):
434 # SessionAssistant.select_test_plan() must now be allowed
435 self.assertIn(self.sa.select_test_plan,
436 UsageExpectation.of(self.sa).allowed_calls)
437+
438+ def test_external_pxu(self, mock_get_providers):
439+ """Check units from external pxu are overriding loaded units."""
440+ mock_get_providers.return_value = self._get_test_providers()
441+ self.setUp()
442+ self.sa.use_external_pxu(self._pxu)
443+ self.sa.start_new_session("just for testing")
444+ jsm = self.sa._manager.state.job_state_map
445+ self.assertEqual(
446+ jsm['com.canonical.plainbox::stub/true'].job.command,
447+ "false; echo 'external pxu test 01'")
448+ self.assertEqual(
449+ jsm['com.canonical.plainbox::new/external/job'].job.command,
450+ "echo 'external pxu test 02'")

Subscribers

People subscribed via source and target branches