Merge ~sylvain-pineau/checkbox-ng:revert-remote-api-bump into checkbox-ng:master
- Git
- lp:~sylvain-pineau/checkbox-ng
- revert-remote-api-bump
- Merge into master
Proposed by
Sylvain Pineau
Status: | Merged |
---|---|
Approved by: | Sylvain Pineau |
Approved revision: | 31d5d7665e41a275e0d2070c2b2ad7b6372238c2 |
Merged at revision: | 040e4860e11bf84b9a4764f741833c9b60fe4c4d |
Proposed branch: | ~sylvain-pineau/checkbox-ng:revert-remote-api-bump |
Merge into: | checkbox-ng:master |
Diff against target: |
4516 lines (+1663/-718) 37 files modified
checkbox_ng/config.py (+38/-45) checkbox_ng/launcher/check_config.py (+30/-27) checkbox_ng/launcher/checkbox_cli.py (+2/-0) checkbox_ng/launcher/master.py (+28/-16) checkbox_ng/launcher/stages.py (+69/-88) checkbox_ng/launcher/subcommands.py (+39/-42) dev/null (+0/-130) plainbox/impl/applogic.py (+18/-0) plainbox/impl/commands/__init__.py (+11/-0) plainbox/impl/ctrl.py (+1/-0) plainbox/impl/launcher.py (+258/-0) plainbox/impl/runner.py (+1/-0) plainbox/impl/secure/test_config.py (+608/-0) plainbox/impl/session/assistant.py (+32/-11) plainbox/impl/session/remote_assistant.py (+53/-27) plainbox/impl/test_launcher.py (+136/-0) plainbox/impl/unit/unit.py (+2/-1) plainbox/vendor/rpyc/__init__.py (+1/-1) plainbox/vendor/rpyc/core/async_.py (+10/-11) plainbox/vendor/rpyc/core/brine.py (+43/-10) plainbox/vendor/rpyc/core/consts.py (+0/-3) plainbox/vendor/rpyc/core/netref.py (+38/-30) plainbox/vendor/rpyc/core/protocol.py (+22/-21) plainbox/vendor/rpyc/core/service.py (+6/-8) plainbox/vendor/rpyc/core/stream.py (+3/-4) plainbox/vendor/rpyc/core/vinegar.py (+20/-4) plainbox/vendor/rpyc/lib/__init__.py (+4/-4) plainbox/vendor/rpyc/lib/compat.py (+0/-1) plainbox/vendor/rpyc/utils/authenticators.py (+1/-1) plainbox/vendor/rpyc/utils/classic.py (+13/-19) plainbox/vendor/rpyc/utils/factory.py (+32/-46) plainbox/vendor/rpyc/utils/helpers.py (+5/-5) plainbox/vendor/rpyc/utils/registry.py (+32/-82) plainbox/vendor/rpyc/utils/server.py (+34/-34) plainbox/vendor/rpyc/utils/teleportation.py (+69/-32) plainbox/vendor/rpyc/utils/zerodeploy.py (+2/-13) plainbox/vendor/rpyc/version.py (+2/-2) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau (community) | Approve | ||
Maciej Kisielewski | Approve | ||
Review via email:
|
Commit message
Description of the change
Revert the remote api branch
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
self-approved
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/checkbox_ng/config.py b/checkbox_ng/config.py |
2 | index a8e8ad0..5a4ba11 100644 |
3 | --- a/checkbox_ng/config.py |
4 | +++ b/checkbox_ng/config.py |
5 | @@ -23,62 +23,55 @@ |
6 | ===================================================== |
7 | """ |
8 | import gettext |
9 | +import itertools |
10 | import logging |
11 | import os |
12 | |
13 | -from plainbox.impl.config import Configuration |
14 | +from plainbox.impl.launcher import DefaultLauncherDefinition |
15 | +from plainbox.impl.launcher import LauncherDefinition |
16 | |
17 | |
18 | _ = gettext.gettext |
19 | |
20 | _logger = logging.getLogger("config") |
21 | |
22 | - |
23 | -SEARCH_DIRS = [ |
24 | - '$SNAP_DATA', |
25 | - '/etc/xdg/', |
26 | - '~/.config/', |
27 | - ] |
28 | - |
29 | - |
30 | def expand_all(path): |
31 | - """Expand both: envvars and ~ in `path`.""" |
32 | return os.path.expandvars(os.path.expanduser(path)) |
33 | |
34 | - |
35 | -def load_configs(launcher_file=None, cfg=None): |
36 | - """ |
37 | - Read a chain of configs/launchers. |
38 | - |
39 | - In theory there can be a very long list of configs that are linked by |
40 | - specifying config_filename in each. Each time this happen we need to |
41 | - consider the new one and override all the values contained therein. |
42 | - And after this chain is exhausted the values in the launcher should |
43 | - take precedence over the previously read. |
44 | - Warning: some recursion ahead. |
45 | - """ |
46 | - if not cfg: |
47 | - cfg = Configuration() |
48 | - previous_cfg_name = cfg.get_value('config', 'config_filename') |
49 | - if os.path.isabs(expand_all(previous_cfg_name)): |
50 | - cfg.update_from_another( |
51 | - Configuration.from_path(expand_all(previous_cfg_name)), |
52 | - 'config file: {}'.format(previous_cfg_name)) |
53 | +def load_configs(launcher_file=None): |
54 | + # launcher can override the default name of config files to look for |
55 | + # so first we need to establish the filename to look for |
56 | + configs = [] |
57 | + config_filename = 'checkbox.conf' |
58 | + launcher = DefaultLauncherDefinition() |
59 | + if launcher_file: |
60 | + configs.append(launcher_file) |
61 | + generic_launcher = LauncherDefinition() |
62 | + if not os.path.exists(launcher_file): |
63 | + _logger.error(_( |
64 | + "Unable to load launcher '%s'. File not found!"), |
65 | + launcher_file) |
66 | + raise SystemExit(1) |
67 | + generic_launcher.read(launcher_file) |
68 | + config_filename = os.path.expandvars(os.path.expanduser( |
69 | + generic_launcher.config_filename)) |
70 | + launcher = generic_launcher.get_concrete_launcher() |
71 | + if os.path.isabs(config_filename): |
72 | + configs.append(config_filename) |
73 | else: |
74 | - for sdir in SEARCH_DIRS: |
75 | - config = expand_all(os.path.join(sdir, previous_cfg_name)) |
76 | + search_dirs = [ |
77 | + '$SNAP_DATA', |
78 | + '/etc/xdg/', |
79 | + '~/.config/', |
80 | + ] |
81 | + for d in search_dirs: |
82 | + config = expand_all(os.path.join(d, config_filename)) |
83 | if os.path.exists(config): |
84 | - cfg.update_from_another( |
85 | - Configuration.from_path(config), |
86 | - 'config file: {}'.format(config)) |
87 | - else: |
88 | - _logger.info( |
89 | - "Referenced config file doesn't exist: %s", config) |
90 | - new_cfg_filename = cfg.get_value('config', 'config_filename') |
91 | - if new_cfg_filename != previous_cfg_name: |
92 | - load_configs(launcher_file, cfg) |
93 | - if launcher_file: |
94 | - cfg.update_from_another( |
95 | - Configuration.from_path(launcher_file), |
96 | - 'Launcher file: {}'.format(launcher_file)) |
97 | - return cfg |
98 | + configs.append(config) |
99 | + launcher.read(configs) |
100 | + if launcher.problem_list: |
101 | + _logger.error(_("Unable to start launcher because of errors:")) |
102 | + for problem in launcher.problem_list: |
103 | + _logger.error("%s", str(problem)) |
104 | + raise SystemExit(1) |
105 | + return launcher |
106 | diff --git a/checkbox_ng/launcher/check_config.py b/checkbox_ng/launcher/check_config.py |
107 | index 0ac3088..359d3ca 100644 |
108 | --- a/checkbox_ng/launcher/check_config.py |
109 | +++ b/checkbox_ng/launcher/check_config.py |
110 | @@ -1,6 +1,6 @@ |
111 | # This file is part of Checkbox. |
112 | # |
113 | -# Copyright 2018-2021 Canonical Ltd. |
114 | +# Copyright 2018-2019 Canonical Ltd. |
115 | # Written by: |
116 | # Maciej Kisielewski <maciej.kisielewski@canonical.com> |
117 | # |
118 | @@ -15,38 +15,41 @@ |
119 | # |
120 | # You should have received a copy of the GNU General Public License |
121 | # along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
122 | -"""This module contains the implementation of the `check-config` subcmd.""" |
123 | +from plainbox.impl.secure.config import ValidationError |
124 | +from plainbox.i18n import gettext as _ |
125 | |
126 | from checkbox_ng.config import load_configs |
127 | |
128 | |
129 | class CheckConfig(): |
130 | - """Implementation of the `check-config` sub-command.""" |
131 | - @staticmethod |
132 | - def invoked(_): |
133 | - """Function that's run with `check-config` invocation.""" |
134 | + def invoked(self, ctx): |
135 | config = load_configs() |
136 | - print("Configuration files:") |
137 | - for source in config.sources: |
138 | - print(" - {}".format(source)) |
139 | - for sect_name, section in config.sections.items(): |
140 | - print(" [{0}]".format(sect_name)) |
141 | - for var_name in section.keys(): |
142 | - value = config.get_value(sect_name, var_name) |
143 | - if isinstance(value, list): |
144 | - value = ', '.join(value) |
145 | - origin = config.get_origin(sect_name, var_name) |
146 | - origin = "From {}".format(origin) if origin else "(Default)" |
147 | - key_val = "{}={}".format(var_name, value) |
148 | - print(" {0: <34} {1}".format(key_val, origin)) |
149 | - problems = config.get_problems() |
150 | - if not problems: |
151 | - print("No problems with config(s) found!") |
152 | + print(_("Configuration files:")) |
153 | + for filename in config.filename_list: |
154 | + print(" - {}".format(filename)) |
155 | + for variable in config.Meta.variable_list: |
156 | + print(" [{0}]".format(variable.section)) |
157 | + print(" {0}={1}".format( |
158 | + variable.name, |
159 | + variable.__get__(config, config.__class__))) |
160 | + for section in config.Meta.section_list: |
161 | + print(" [{0}]".format(section.name)) |
162 | + section_value = section.__get__(config, config.__class__) |
163 | + if section_value: |
164 | + for key, value in sorted(section_value.items()): |
165 | + print(" {0}={1}".format(key, value)) |
166 | + if config.problem_list: |
167 | + print(_("Problems:")) |
168 | + for problem in config.problem_list: |
169 | + if isinstance(problem, ValidationError): |
170 | + print(_(" - variable {0}: {1}").format( |
171 | + problem.variable.name, problem.message)) |
172 | + else: |
173 | + print(" - {0}".format(problem.message)) |
174 | + return 1 |
175 | + else: |
176 | + print(_("No validation problems found")) |
177 | return 0 |
178 | - print('Problems:') |
179 | - for problem in problems: |
180 | - print('- ', problem) |
181 | - return 1 |
182 | |
183 | def register_arguments(self, parser): |
184 | - """Register extra args for this subcmd. No extra args ATM.""" |
185 | + pass |
186 | diff --git a/checkbox_ng/launcher/checkbox_cli.py b/checkbox_ng/launcher/checkbox_cli.py |
187 | index 9740eb8..b69da17 100644 |
188 | --- a/checkbox_ng/launcher/checkbox_cli.py |
189 | +++ b/checkbox_ng/launcher/checkbox_cli.py |
190 | @@ -27,6 +27,8 @@ import subprocess |
191 | import sys |
192 | |
193 | from plainbox.impl.jobcache import ResourceJobCache |
194 | +from plainbox.impl.launcher import DefaultLauncherDefinition |
195 | +from plainbox.impl.launcher import LauncherDefinition |
196 | from plainbox.impl.session.assistant import SessionAssistant |
197 | |
198 | from checkbox_ng.config import load_configs |
199 | diff --git a/checkbox_ng/launcher/master.py b/checkbox_ng/launcher/master.py |
200 | index 6f28444..730fc02 100644 |
201 | --- a/checkbox_ng/launcher/master.py |
202 | +++ b/checkbox_ng/launcher/master.py |
203 | @@ -37,7 +37,8 @@ from functools import partial |
204 | from tempfile import SpooledTemporaryFile |
205 | |
206 | from plainbox.impl.color import Colorizer |
207 | -from plainbox.impl.config import Configuration |
208 | +from plainbox.impl.launcher import DefaultLauncherDefinition |
209 | +from plainbox.impl.secure.config import Unset |
210 | from plainbox.impl.session.remote_assistant import RemoteSessionAssistant |
211 | from plainbox.vendor import rpyc |
212 | from checkbox_ng.urwid_ui import TestPlanBrowser |
213 | @@ -110,7 +111,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
214 | |
215 | @property |
216 | def is_interactive(self): |
217 | - return (self.launcher.get_value('ui', 'type') == 'interactive' and |
218 | + return (self.launcher.ui_type == 'interactive' and |
219 | sys.stdin.isatty() and sys.stdout.isatty()) |
220 | |
221 | @property |
222 | @@ -128,7 +129,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
223 | self._is_bootstrapping = False |
224 | self._target_host = ctx.args.host |
225 | self._normal_user = '' |
226 | - self.launcher = Configuration() |
227 | + self.launcher = DefaultLauncherDefinition() |
228 | if ctx.args.launcher: |
229 | expanded_path = os.path.expanduser(ctx.args.launcher) |
230 | if not os.path.exists(expanded_path): |
231 | @@ -136,8 +137,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
232 | expanded_path)) |
233 | with open(expanded_path, 'rt') as f: |
234 | self._launcher_text = f.read() |
235 | - self.launcher = Configuration.from_text( |
236 | - self._launcher_text, 'Remote:{}'.format(expanded_path)) |
237 | + self.launcher.read_string(self._launcher_text) |
238 | if ctx.args.user: |
239 | self._normal_user = ctx.args.user |
240 | timeout = 600 |
241 | @@ -185,9 +185,21 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
242 | nonlocal keep_running |
243 | keep_running = False |
244 | server_msg = msg |
245 | - conn.root.register_master_blaster(quitter) |
246 | + with contextlib.suppress(AttributeError): |
247 | + # TODO: REMOTE_API |
248 | + # when bumping the remote api make this bit obligatory |
249 | + # i.e. remove the suppressing |
250 | + conn.root.register_master_blaster(quitter) |
251 | self._sa = conn.root.get_sa() |
252 | self.sa.conn = conn |
253 | + # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump |
254 | + # the check and bailout is not needed if the slave as up to |
255 | + # date as this master, so after bumping RAPI we can assume |
256 | + # that slave is always passwordless |
257 | + if not self.sa.passwordless_sudo: |
258 | + raise SystemExit( |
259 | + _("This version of Checkbox requires the service" |
260 | + " to be run as root")) |
261 | try: |
262 | slave_api_version = self.sa.get_remote_api_version() |
263 | except AttributeError: |
264 | @@ -255,8 +267,8 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
265 | tps = self.sa.start_session(configuration) |
266 | except RuntimeError as exc: |
267 | raise SystemExit(exc.args[0]) from exc |
268 | - if self.launcher.get_value('test plan', 'forced'): |
269 | - self.select_tp(self.launcher.get_value('test plan', 'unit')) |
270 | + if self.launcher.test_plan_forced: |
271 | + self.select_tp(self.launcher.test_plan_default_selection) |
272 | self.select_jobs(self.jobs) |
273 | else: |
274 | self.interactively_choose_tp(tps) |
275 | @@ -299,8 +311,8 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
276 | return val.lower() in ('y', 'yes', 't', 'true', 'on', '1') |
277 | |
278 | def select_jobs(self, all_jobs): |
279 | - if self.launcher.get_value('test selection', 'forced'): |
280 | - if self.launcher.manifest: |
281 | + if self.launcher.test_selection_forced: |
282 | + if self.launcher.manifest is not Unset: |
283 | self.sa.save_manifest( |
284 | {manifest_id: |
285 | self._strtobool(self.launcher.manifest[manifest_id]) for |
286 | @@ -340,7 +352,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
287 | Returns True if the remote should keep running. |
288 | And False if it should quit. |
289 | """ |
290 | - if self.launcher.get_value('ui', 'type') == 'silent': |
291 | + if self.launcher.ui_type == 'silent': |
292 | self._sa.terminate() |
293 | return False |
294 | response = interrupt_dialog(self._target_host) |
295 | @@ -360,7 +372,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
296 | |
297 | def finish_session(self): |
298 | print(self.C.header("Results")) |
299 | - if self.launcher.get_value('launcher', 'local_submission'): |
300 | + if self.launcher.local_submission: |
301 | # Disable SIGINT while we save local results |
302 | with contextlib.ExitStack() as stack: |
303 | tmp_sig = signal.signal(signal.SIGINT, signal.SIG_IGN) |
304 | @@ -378,7 +390,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
305 | self.run_jobs() |
306 | |
307 | def _handle_last_job_after_resume(self, resumed_session_info): |
308 | - if self.launcher.get_value('ui', 'type') == 'silent': |
309 | + if self.launcher.ui_type == 'silent': |
310 | time.sleep(20) |
311 | else: |
312 | resume_dialog(10) |
313 | @@ -408,11 +420,11 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
314 | self._run_jobs(jobs_repr, total_num) |
315 | rerun_candidates = self.sa.get_rerun_candidates('manual') |
316 | if rerun_candidates: |
317 | - if self.launcher.get_value('ui', 'type') == 'interactive': |
318 | + if self.launcher.ui_type == 'interactive': |
319 | while True: |
320 | if not self._maybe_manual_rerun_jobs(): |
321 | break |
322 | - if self.launcher.get_value('ui', 'auto_retry'): |
323 | + if self.launcher.auto_retry: |
324 | while True: |
325 | if not self._maybe_auto_rerun_jobs(): |
326 | break |
327 | @@ -498,7 +510,7 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
328 | if not rerun_candidates: |
329 | return False |
330 | # we wait before retrying |
331 | - delay = self.launcher.get_value('ui', 'delay_before_retry') |
332 | + delay = self.launcher.delay_before_retry |
333 | _logger.info(_("Waiting {} seconds before retrying failed" |
334 | " jobs...".format(delay))) |
335 | time.sleep(delay) |
336 | diff --git a/checkbox_ng/launcher/stages.py b/checkbox_ng/launcher/stages.py |
337 | index 05566be..126324b 100644 |
338 | --- a/checkbox_ng/launcher/stages.py |
339 | +++ b/checkbox_ng/launcher/stages.py |
340 | @@ -26,11 +26,9 @@ import json |
341 | import logging |
342 | import os |
343 | import time |
344 | -import textwrap |
345 | |
346 | from plainbox.abc import IJobResult |
347 | from plainbox.i18n import pgettext as C_ |
348 | -from plainbox.impl.config import Configuration |
349 | from plainbox.impl.result import JobResultBuilder |
350 | from plainbox.impl.result import tr_outcome |
351 | from plainbox.impl.transport import InvalidSecureIDError |
352 | @@ -318,44 +316,42 @@ class ReportsStage(CheckboxUiStage): |
353 | self._export_fn = export_fn |
354 | |
355 | def _prepare_stock_report(self, report): |
356 | - |
357 | - new_origin = 'stock_reports' |
358 | + # this is purposefully not using pythonic dict-keying for better |
359 | + # readability |
360 | + if not self.sa.config.transports: |
361 | + self.sa.config.transports = dict() |
362 | + if not self.sa.config.exporters: |
363 | + self.sa.config.exporters = dict() |
364 | + if not self.sa.config.reports: |
365 | + self.sa.config.reports = dict() |
366 | if report == 'text': |
367 | - additional_config = Configuration.from_text(textwrap.dedent(""" |
368 | - [exporter:text] |
369 | - unit = com.canonical.plainbox::text |
370 | - [transport:stdout] |
371 | - stream = stdout |
372 | - type = stream |
373 | - [report:1_text_to_screen] |
374 | - exporter = text |
375 | - forced = yes |
376 | - transport = stdout |
377 | - """), new_origin) |
378 | - self.sa.config.update_from_another(additional_config, new_origin) |
379 | + self.sa.config.exporters['text'] = { |
380 | + 'unit': 'com.canonical.plainbox::text'} |
381 | + self.sa.config.transports['stdout'] = { |
382 | + 'type': 'stream', 'stream': 'stdout'} |
383 | + # '1_' prefix ensures ordering amongst other stock reports. This |
384 | + # report name does not appear anywhere (because of forced: yes) |
385 | + self.sa.config.reports['1_text_to_screen'] = { |
386 | + 'transport': 'stdout', 'exporter': 'text', 'forced': 'yes'} |
387 | elif report == 'certification': |
388 | - additional_config = Configuration.from_text(textwrap.dedent(""" |
389 | - [exporter:tar] |
390 | - unit = com.canonical.plainbox::tar |
391 | - [transport:c3] |
392 | - type = submission-service |
393 | - [report:upload to certification] |
394 | - exporter = tar |
395 | - transport = c3 |
396 | - """), new_origin) |
397 | - self.sa.config.update_from_another(additional_config, new_origin) |
398 | + self.sa.config.exporters['tar'] = { |
399 | + 'unit': 'com.canonical.plainbox::tar'} |
400 | + self.sa.config.transports['c3'] = { |
401 | + 'type': 'submission-service', |
402 | + 'secure_id': self.sa.config.transports.get('c3', {}).get( |
403 | + 'secure_id', None)} |
404 | + self.sa.config.reports['upload to certification'] = { |
405 | + 'transport': 'c3', 'exporter': 'tar'} |
406 | elif report == 'certification-staging': |
407 | - additional_config = Configuration.from_text(textwrap.dedent(""" |
408 | - [exporter:tar] |
409 | - unit = com.canonical.plainbox::tar |
410 | - [transport:c3] |
411 | - staging = yes |
412 | - type = submission-service |
413 | - [report:upload to certification-staging] |
414 | - exporter = tar |
415 | - transport = c3 |
416 | - """), new_origin) |
417 | - self.sa.config.update_from_another(additional_config, new_origin) |
418 | + self.sa.config.exporters['tar'] = { |
419 | + 'unit': 'com.canonical.plainbox::tar'} |
420 | + self.sa.config.transports['c3-staging'] = { |
421 | + 'type': 'submission-service', |
422 | + 'secure_id': self.sa.config.transports.get('c3', {}).get( |
423 | + 'secure_id', None), |
424 | + 'staging': 'yes'} |
425 | + self.sa.config.reports['upload to certification-staging'] = { |
426 | + 'transport': 'c3-staging', 'exporter': 'tar'} |
427 | elif report == 'submission_files': |
428 | # LP:1585326 maintain isoformat but removing ':' chars that cause |
429 | # issues when copying files. |
430 | @@ -368,21 +364,21 @@ class ReportsStage(CheckboxUiStage): |
431 | ('tar', '.tar.xz')]: |
432 | path = os.path.join(self.base_dir, ''.join( |
433 | ['submission_', timestamp, file_ext])) |
434 | - template = textwrap.dedent(""" |
435 | - [transport:{exporter}_file] |
436 | - path = {path} |
437 | - type = file |
438 | - [exporter:{exporter}] |
439 | - unit = com.canonical.plainbox::{exporter} |
440 | - [report:2_{exporter}_file] |
441 | - exporter = {exporter} |
442 | - forced = yes |
443 | - transport = {exporter}_file |
444 | - """) |
445 | - additional_config = Configuration.from_text( |
446 | - template.format(exporter=exporter, path=path), new_origin) |
447 | - self.sa.config.update_from_another( |
448 | - additional_config, new_origin) |
449 | + self.sa.config.transports['{}_file'.format(exporter)] = { |
450 | + 'type': 'file', |
451 | + 'path': path} |
452 | + if exporter not in self.sa.config.exporters: |
453 | + self.sa.config.exporters[exporter] = { |
454 | + 'unit': 'com.canonical.plainbox::{}'.format( |
455 | + exporter)} |
456 | + if not self.sa.config.exporters[exporter].get('unit'): |
457 | + unit = 'com.canonical.plainbox::{}'.format(exporter) |
458 | + self.sa.config.exporters[exporter]['unit'] = unit |
459 | + self.sa.config.reports['2_{}_file'.format(exporter)] = { |
460 | + 'transport': '{}_file'.format(exporter), |
461 | + 'exporter': '{}'.format(exporter), |
462 | + 'forced': 'yes' |
463 | + } |
464 | |
465 | def _prepare_transports(self): |
466 | self.base_dir = os.path.join( |
467 | @@ -398,9 +394,7 @@ class ReportsStage(CheckboxUiStage): |
468 | # depending on the type of transport we need to pick variable that |
469 | # serves as the 'where' param for the transport. In case of |
470 | # certification site the URL is supplied here |
471 | - transport_cfg = self.sa.config.get_parametric_sections( |
472 | - 'transport')[transport] |
473 | - tr_type = transport_cfg['type'] |
474 | + tr_type = self.sa.config.transports[transport]['type'] |
475 | if tr_type not in self._available_transports: |
476 | _logger.error(_("Unrecognized type '%s' of transport '%s'"), |
477 | tr_type, transport) |
478 | @@ -408,11 +402,14 @@ class ReportsStage(CheckboxUiStage): |
479 | cls = self._available_transports[tr_type] |
480 | if tr_type == 'file': |
481 | self.transports[transport] = cls( |
482 | - os.path.expanduser(transport_cfg['path'])) |
483 | + os.path.expanduser( |
484 | + self.sa.config.transports[transport]['path'])) |
485 | elif tr_type == 'stream': |
486 | - self.transports[transport] = cls(transport_cfg['stream']) |
487 | + self.transports[transport] = cls( |
488 | + self.sa.config.transports[transport]['stream']) |
489 | elif tr_type == 'submission-service': |
490 | - secure_id = transport_cfg.get('secure_id', None) |
491 | + secure_id = self.sa.config.transports[transport].get( |
492 | + 'secure_id', None) |
493 | if self.is_interactive: |
494 | new_description = input(self.C.BLUE(_( |
495 | 'Enter submission description (press Enter to skip): '))) |
496 | @@ -428,7 +425,7 @@ class ReportsStage(CheckboxUiStage): |
497 | options = "secure_id={}".format(secure_id) |
498 | else: |
499 | options = "" |
500 | - if transport_cfg.get('staging', False): |
501 | + if self.sa.config.transports[transport].get('staging', False): |
502 | url = ('https://certification.staging.canonical.com/' |
503 | 'api/v1/submission/{}/'.format(secure_id)) |
504 | elif os.getenv('C3_URL'): |
505 | @@ -440,15 +437,14 @@ class ReportsStage(CheckboxUiStage): |
506 | self.transports[transport] = cls(url, options) |
507 | |
508 | def _export_results(self): |
509 | - stock_reports = self.sa.config.get_value('launcher', 'stock_reports') |
510 | - if 'none' not in stock_reports: |
511 | - for report in stock_reports: |
512 | + if 'none' not in self.sa.config.stock_reports: |
513 | + for report in self.sa.config.stock_reports: |
514 | if report in ['certification', 'certification-staging']: |
515 | # skip stock c3 report if secure_id is not given from |
516 | # config files or launchers, and the UI is non-interactive |
517 | # (silent) |
518 | - if ('transport:c3' not in self.sa.config.sections.keys() |
519 | - and not self.is_interactive): |
520 | + if ('c3' not in self.sa.config.transports and |
521 | + not self.is_interactive): |
522 | continue |
523 | # don't generate stock c3 reports if sideloaded providers |
524 | # were in use, something that should only be done during |
525 | @@ -461,9 +457,7 @@ class ReportsStage(CheckboxUiStage): |
526 | # reports are stored in an ordinary dict(), so sorting them ensures |
527 | # the same order of submitting them between runs, and if they |
528 | # share common prefix, they are next to each other |
529 | - for name, params in sorted( |
530 | - self.sa.config.get_parametric_sections('report').items()): |
531 | - |
532 | + for name, params in sorted(self.sa.config.reports.items()): |
533 | # don't generate stock c3 reports if sideloaded providers |
534 | # were in use, something that should only be done during |
535 | # development |
536 | @@ -482,10 +476,8 @@ class ReportsStage(CheckboxUiStage): |
537 | cmd = 'y' |
538 | if cmd == 'n': |
539 | continue |
540 | - all_exporters = self.sa.config.get_parametric_sections('exporter') |
541 | - exporter_id = self.sa.config.get_parametric_sections('exporter')[ |
542 | - params['exporter']]['unit'] |
543 | - exp_options = self.sa.config.get_parametric_sections('exporter')[ |
544 | + exporter_id = self.sa.config.exporters[params['exporter']]['unit'] |
545 | + exporter_options = self.sa.config.exporters[ |
546 | params['exporter']].get('options', '').split() |
547 | done_sending = False |
548 | while not done_sending: |
549 | @@ -497,7 +489,7 @@ class ReportsStage(CheckboxUiStage): |
550 | else: |
551 | try: |
552 | result = self.sa.export_to_transport( |
553 | - exporter_id, transport, exp_options) |
554 | + exporter_id, transport, exporter_options) |
555 | except ExporterError as exc: |
556 | _logger.warning( |
557 | _("Problem occured when preparing %s report:" |
558 | @@ -525,14 +517,14 @@ class ReportsStage(CheckboxUiStage): |
559 | done_sending = True |
560 | continue |
561 | if self._retry_dialog(): |
562 | - self.sa.config.sections['transports']['c3'].pop( |
563 | - 'secure_id') |
564 | + self.sa.config.transports['c3'].pop('secure_id') |
565 | continue |
566 | - except Exception as exc: |
567 | + except Exception: |
568 | _logger.error( |
569 | _("Problem with a '%s' report using '%s' exporter " |
570 | - "sent to '%s' transport. Reason %s"), |
571 | - name, exporter_id, transport.url, exc) |
572 | + "sent to '%s' transport."), |
573 | + name, exporter_id, transport.url) |
574 | + self._reset_auto_submission_retries() |
575 | done_sending = True |
576 | |
577 | def _retry_dialog(self): |
578 | @@ -554,14 +546,3 @@ class ReportsStage(CheckboxUiStage): |
579 | return True |
580 | |
581 | return False |
582 | - |
583 | -template = textwrap.dedent(""" |
584 | - [transport:{exporter}_file] |
585 | - type = file |
586 | - path = {path} |
587 | - [exporter:{exporter}] |
588 | - unit = com.canonical.plainbox::{exporter} |
589 | - [report:2_{exporter}_file] |
590 | - transport = {exporter}_file |
591 | - exporter = {exporter} |
592 | - forced = yes""") |
593 | diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py |
594 | index 1d3c278..69f4a4a 100644 |
595 | --- a/checkbox_ng/launcher/subcommands.py |
596 | +++ b/checkbox_ng/launcher/subcommands.py |
597 | @@ -42,6 +42,7 @@ from plainbox.impl.execution import UnifiedRunner |
598 | from plainbox.impl.highlevel import Explorer |
599 | from plainbox.impl.result import MemoryJobResult |
600 | from plainbox.impl.runner import slugify |
601 | +from plainbox.impl.secure.config import Unset |
602 | from plainbox.impl.secure.sudo_broker import sudo_password_provider |
603 | from plainbox.impl.session.assistant import SA_RESTARTABLE |
604 | from plainbox.impl.session.restart import detect_restart_strategy |
605 | @@ -167,10 +168,10 @@ class Launcher(MainLoopStage, ReportsStage): |
606 | return self._C |
607 | |
608 | def get_sa_api_version(self): |
609 | - return '0.99' |
610 | + return self.launcher.api_version |
611 | |
612 | def get_sa_api_flags(self): |
613 | - return [SA_RESTARTABLE] |
614 | + return self.launcher.api_flags |
615 | |
616 | def invoked(self, ctx): |
617 | if ctx.args.version: |
618 | @@ -183,12 +184,12 @@ class Launcher(MainLoopStage, ReportsStage): |
619 | # exited by now, so validation passed |
620 | print(_("Launcher seems valid.")) |
621 | return |
622 | - self.configuration = load_configs(ctx.args.launcher) |
623 | + self.launcher = load_configs(ctx.args.launcher) |
624 | logging_level = { |
625 | 'normal': logging.WARNING, |
626 | 'verbose': logging.INFO, |
627 | 'debug': logging.DEBUG, |
628 | - }[self.configuration.get_value('ui', 'verbosity')] |
629 | + }[self.launcher.verbosity] |
630 | if not ctx.args.verbose and not ctx.args.debug: |
631 | # Command line args take precendence |
632 | logging.basicConfig(level=logging_level) |
633 | @@ -199,28 +200,25 @@ class Launcher(MainLoopStage, ReportsStage): |
634 | # replace the previously built SA with the defaults |
635 | self._configure_restart(ctx) |
636 | self._prepare_transports() |
637 | - ctx.sa.use_alternate_configuration(self.configuration) |
638 | + ctx.sa.use_alternate_configuration(self.launcher) |
639 | if not self._maybe_resume_session(): |
640 | self._start_new_session() |
641 | self._pick_jobs_to_run() |
642 | if not self.ctx.sa.get_static_todo_list(): |
643 | return 0 |
644 | - if 'submission_files' in self.configuration.get_value( |
645 | - 'launcher', 'stock_reports'): |
646 | + if 'submission_files' in self.launcher.stock_reports: |
647 | print("Reports will be saved to: {}".format(self.base_dir)) |
648 | # we initialize the nb of attempts for all the selected jobs... |
649 | for job_id in self.ctx.sa.get_dynamic_todo_list(): |
650 | job_state = self.ctx.sa.get_job_state(job_id) |
651 | - job_state.attempts = self.configuration.get_value( |
652 | - 'ui', 'max_attempts') |
653 | + job_state.attempts = self.launcher.max_attempts |
654 | # ... before running them |
655 | self._run_jobs(self.ctx.sa.get_dynamic_todo_list()) |
656 | - if self.is_interactive and not self.configuration.get_value( |
657 | - 'ui', 'auto_retry'): |
658 | + if self.is_interactive and not self.launcher.auto_retry: |
659 | while True: |
660 | if not self._maybe_rerun_jobs(): |
661 | break |
662 | - elif self.configuration.get_value('ui', 'auto_retry'): |
663 | + elif self.launcher.auto_retry: |
664 | while True: |
665 | if not self._maybe_auto_rerun_jobs(): |
666 | break |
667 | @@ -237,18 +235,23 @@ class Launcher(MainLoopStage, ReportsStage): |
668 | |
669 | We can then interact with the user when we encounter OUTCOME_UNDECIDED. |
670 | """ |
671 | - return (self.configuration.get_value('ui', 'type') == 'interactive' |
672 | - and sys.stdin.isatty() and sys.stdout.isatty()) |
673 | + return (self.launcher.ui_type == 'interactive' and |
674 | + sys.stdin.isatty() and sys.stdout.isatty()) |
675 | |
676 | def _configure_restart(self, ctx): |
677 | if SA_RESTARTABLE not in self.get_sa_api_flags(): |
678 | return |
679 | - if self.configuration.get_value('restart', 'strategy'): |
680 | + if self.launcher.restart_strategy: |
681 | try: |
682 | cls = get_strategy_by_name( |
683 | - self.configuration.get_value('restart', 'strategy')) |
684 | - strategy = cls(**self.configuration.get_strategy_kwargs()) |
685 | + self.launcher.restart_strategy) |
686 | + kwargs = copy.deepcopy(self.launcher.restart) |
687 | + # [restart] section has the kwargs for the strategy initializer |
688 | + # and the 'strategy' which is not one, let's pop it |
689 | + kwargs.pop('strategy') |
690 | + strategy = cls(**kwargs) |
691 | ctx.sa.use_alternate_restart_strategy(strategy) |
692 | + |
693 | except KeyError: |
694 | _logger.warning(_('Unknown restart strategy: %s', ( |
695 | self.launcher.restart_strategy))) |
696 | @@ -278,7 +281,7 @@ class Launcher(MainLoopStage, ReportsStage): |
697 | respawn_cmd += os.path.abspath(ctx.args.launcher) + ' ' |
698 | respawn_cmd += '--resume {}' # interpolate with session_id |
699 | ctx.sa.configure_application_restart( |
700 | - lambda session_id: [respawn_cmd.format(session_id)], 'local') |
701 | + lambda session_id: [respawn_cmd.format(session_id)]) |
702 | |
703 | def _maybe_resume_session(self): |
704 | resume_candidates = list(self.ctx.sa.get_resumable_sessions()) |
705 | @@ -335,21 +338,18 @@ class Launcher(MainLoopStage, ReportsStage): |
706 | |
707 | def _start_new_session(self): |
708 | print(_("Preparing...")) |
709 | - title = self.ctx.args.title or self.configuration.get_value( |
710 | - 'launcher', 'session_title') |
711 | - title = title or self.configuration.get_value('launcher', 'app_id') |
712 | - if self.configuration.get_value('launcher', 'app_version'): |
713 | - title += ' {}'.format(self.configuration.get_value( |
714 | - 'launcher', 'app_version')) |
715 | + title = self.ctx.args.title or self.launcher.session_title |
716 | + title = title or self.launcher.app_id |
717 | + if self.launcher.app_version: |
718 | + title += ' {}'.format(self.launcher.app_version) |
719 | runner_kwargs = { |
720 | - 'normal_user_provider': lambda: self.configuration.get_value( |
721 | - 'daemon', 'normal_user'), |
722 | + 'normal_user_provider': lambda: self.launcher.normal_user, |
723 | 'password_provider': sudo_password_provider.get_sudo_password, |
724 | 'stdin': None, |
725 | } |
726 | self.ctx.sa.start_new_session(title, UnifiedRunner, runner_kwargs) |
727 | - if self.configuration.get_value('test plan', 'forced'): |
728 | - tp_id = self.configuration.get_value('test plan', 'unit') |
729 | + if self.launcher.test_plan_forced: |
730 | + tp_id = self.launcher.test_plan_default_selection |
731 | if tp_id not in self.ctx.sa.get_test_plans(): |
732 | _logger.error(_( |
733 | 'The test plan "%s" is not available!'), tp_id) |
734 | @@ -365,8 +365,7 @@ class Launcher(MainLoopStage, ReportsStage): |
735 | if tp_id is None: |
736 | raise SystemExit(_("No test plan selected.")) |
737 | self.ctx.sa.select_test_plan(tp_id) |
738 | - description = self.ctx.args.message or self.configuration.get_value( |
739 | - 'launcher', 'session_desc') |
740 | + description = self.ctx.args.message or self.launcher.session_desc |
741 | self.ctx.sa.update_app_blob(json.dumps( |
742 | {'testplan_id': tp_id, |
743 | 'description': description}).encode("UTF-8")) |
744 | @@ -381,7 +380,7 @@ class Launcher(MainLoopStage, ReportsStage): |
745 | def _interactively_pick_test_plan(self): |
746 | test_plan_ids = self.ctx.sa.get_test_plans() |
747 | filtered_tp_ids = set() |
748 | - for filter in self.configuration.get_value('test plan', 'filter'): |
749 | + for filter in self.launcher.test_plan_filters: |
750 | filtered_tp_ids.update(fnmatch.filter(test_plan_ids, filter)) |
751 | tp_info_list = self._generate_tp_infos(filtered_tp_ids) |
752 | if not tp_info_list: |
753 | @@ -389,20 +388,19 @@ class Launcher(MainLoopStage, ReportsStage): |
754 | return |
755 | selected_tp = TestPlanBrowser( |
756 | _("Select test plan"), tp_info_list, |
757 | - self.configuration.get_value('test plan', 'unit')).run() |
758 | + self.launcher.test_plan_default_selection).run() |
759 | return selected_tp |
760 | |
761 | def _strtobool(self, val): |
762 | return val.lower() in ('y', 'yes', 't', 'true', 'on', '1') |
763 | |
764 | def _pick_jobs_to_run(self): |
765 | - if self.configuration.get_value('test selection', 'forced'): |
766 | - if self.configuration.manifest: |
767 | + if self.launcher.test_selection_forced: |
768 | + if self.launcher.manifest is not Unset: |
769 | self.ctx.sa.save_manifest( |
770 | {manifest_id: |
771 | - self._strtobool( |
772 | - self.configuration.manifest[manifest_id]) for |
773 | - manifest_id in self.configuration.manifest} |
774 | + self._strtobool(self.launcher.manifest[manifest_id]) for |
775 | + manifest_id in self.launcher.manifest} |
776 | ) |
777 | # by default all tests are selected; so we're done here |
778 | return |
779 | @@ -494,7 +492,7 @@ class Launcher(MainLoopStage, ReportsStage): |
780 | if not rerun_candidates: |
781 | return False |
782 | # we wait before retrying |
783 | - delay = self.configuration.get_value('ui', 'delay_before_retry') |
784 | + delay = self.launcher.delay_before_retry |
785 | _logger.info(_("Waiting {} seconds before retrying failed" |
786 | " jobs...".format(delay))) |
787 | time.sleep(delay) |
788 | @@ -527,11 +525,10 @@ class Launcher(MainLoopStage, ReportsStage): |
789 | def considering_job(self, job, job_state): |
790 | pass |
791 | show_out = True |
792 | - output = self.configuration.get_value('ui', 'output') |
793 | - if output == 'hide-resource-and-attachment': |
794 | + if self.launcher.output == 'hide-resource-and-attachment': |
795 | if job.plugin in ('local', 'resource', 'attachment'): |
796 | show_out = False |
797 | - elif output in ['hide', 'hide-automated']: |
798 | + elif self.launcher.output in ['hide', 'hide-automated']: |
799 | if job.plugin in ('shell', 'local', 'resource', 'attachment'): |
800 | show_out = False |
801 | if 'suppress-output' in job.get_flag_set(): |
802 | @@ -751,7 +748,7 @@ class Run(MainLoopStage): |
803 | respawn_cmd = sys.argv[0] # entry-point to checkbox |
804 | respawn_cmd += ' --resume {}' # interpolate with session_id |
805 | self.sa.configure_application_restart( |
806 | - lambda session_id: [respawn_cmd.format(session_id)], 'local') |
807 | + lambda session_id: [respawn_cmd.format(session_id)]) |
808 | |
809 | |
810 | class List(): |
811 | diff --git a/plainbox/impl/applogic.py b/plainbox/impl/applogic.py |
812 | index 84f9906..1d90dfd 100644 |
813 | --- a/plainbox/impl/applogic.py |
814 | +++ b/plainbox/impl/applogic.py |
815 | @@ -31,6 +31,7 @@ import os |
816 | from plainbox.abc import IJobResult |
817 | from plainbox.i18n import gettext as _ |
818 | from plainbox.impl.result import MemoryJobResult |
819 | +from plainbox.impl.secure import config |
820 | from plainbox.impl.secure.qualifiers import select_jobs |
821 | from plainbox.impl.session import SessionManager |
822 | from plainbox.impl.session.jobs import InhibitionCause |
823 | @@ -79,6 +80,23 @@ def run_job_if_possible(session, runner, config, job, update=True, ui=None): |
824 | return job_state, job_result |
825 | |
826 | |
827 | +class PlainBoxConfig(config.Config): |
828 | + """ |
829 | + Configuration for PlainBox itself |
830 | + """ |
831 | + |
832 | + environment = config.Section( |
833 | + help_text=_("Environment variables for scripts and jobs")) |
834 | + |
835 | + class Meta: |
836 | + |
837 | + # TODO: properly depend on xdg and use real code that also handles |
838 | + # XDG_CONFIG_HOME. |
839 | + filename_list = [ |
840 | + '/etc/xdg/plainbox.conf', |
841 | + os.path.expanduser('~/.config/plainbox.conf')] |
842 | + |
843 | + |
844 | def get_all_exporter_names(): |
845 | """ |
846 | Get the identifiers (names) of all the supported session state exporters. |
847 | diff --git a/plainbox/impl/commands/__init__.py b/plainbox/impl/commands/__init__.py |
848 | index efdb1f6..5a26853 100644 |
849 | --- a/plainbox/impl/commands/__init__.py |
850 | +++ b/plainbox/impl/commands/__init__.py |
851 | @@ -47,6 +47,7 @@ class PlainBoxToolBase(ToolBase): |
852 | 1. :meth:`get_exec_name()` -- to know how the command will be called |
853 | 2. :meth:`get_exec_version()` -- to know how the version of the tool |
854 | 3. :meth:`add_subcommands()` -- to add some actual commands to execute |
855 | + 4. :meth:`get_config_cls()` -- to know which config to use |
856 | |
857 | This class has some complex control flow to support important and |
858 | interesting use cases. There are some concerns to people that subclass this |
859 | @@ -68,6 +69,16 @@ class PlainBoxToolBase(ToolBase): |
860 | known yet. |
861 | """ |
862 | |
863 | + @classmethod |
864 | + @abc.abstractmethod |
865 | + def get_config_cls(cls): |
866 | + """ |
867 | + Get the Config class that is used by this implementation. |
868 | + |
869 | + This can be overridden by subclasses to use a different config class |
870 | + that is suitable for the particular application. |
871 | + """ |
872 | + |
873 | def _load_config(self): |
874 | return self.get_config_cls().get() |
875 | |
876 | diff --git a/plainbox/impl/config.py b/plainbox/impl/config.py |
877 | deleted file mode 100644 |
878 | index 044862d..0000000 |
879 | --- a/plainbox/impl/config.py |
880 | +++ /dev/null |
881 | @@ -1,398 +0,0 @@ |
882 | -# This file is part of Checkbox. |
883 | -# |
884 | -# Copyright 2021-2022 Canonical Ltd. |
885 | -# Written by: |
886 | -# Maciej Kisielewski <maciej.kisielewski@canonical.com> |
887 | -# |
888 | -# Checkbox is free software: you can redistribute it and/or modify |
889 | -# it under the terms of the GNU General Public License version 3, |
890 | -# as published by the Free Software Foundation. |
891 | -# |
892 | -# Checkbox is distributed in the hope that it will be useful, |
893 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
894 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
895 | -# GNU General Public License for more details. |
896 | -# |
897 | -# You should have received a copy of the GNU General Public License |
898 | -# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
899 | -""" |
900 | -This module defines class for handling Checkbox configs. |
901 | - |
902 | -If we ever need to add validators to config variables, the addition should be |
903 | -done in VarSpec (the fourth 'field'). |
904 | -""" |
905 | -import copy |
906 | -import io |
907 | -import logging |
908 | -import os |
909 | -import shlex |
910 | - |
911 | -from configparser import ConfigParser |
912 | -from collections import namedtuple, OrderedDict |
913 | - |
914 | -logger = logging.getLogger(__name__) |
915 | - |
916 | - |
917 | -class Configuration: |
918 | - """ |
919 | - Checkbox configuration storing objects. |
920 | - |
921 | - Checkbox configs store various information on how to run the Checkbox. |
922 | - For instance what reports to generate, should the session be interactive, |
923 | - and many others. Look at CONFIG_SPEC for details. |
924 | - """ |
925 | - def __init__(self, source=None): |
926 | - """Create a new configuration object filled with default values.""" |
927 | - self.sections = OrderedDict() |
928 | - self._origins = dict() |
929 | - self._problems = [] |
930 | - # sources is similar to origins, but instead of keeping an info on |
931 | - # each variable, we note what configs got read in general |
932 | - self._sources = [source] if source else [] |
933 | - for section, contents in CONFIG_SPEC: |
934 | - if isinstance(contents, ParametricSection): |
935 | - # we don't know what the actual section name will be, |
936 | - # so let's wait with the creation until we know the full name |
937 | - continue |
938 | - if isinstance(contents, DynamicSection): |
939 | - self.sections[section] = DynamicSection() |
940 | - else: |
941 | - self.sections[section] = OrderedDict() |
942 | - self._origins[section] = dict() |
943 | - for name, spec in sorted(contents.items()): |
944 | - self.sections[section][name] = spec.default |
945 | - self._origins[section][name] = '' |
946 | - |
947 | - @property |
948 | - def environment(self): |
949 | - """Return contents of the environment section.""" |
950 | - return self.sections['environment'] |
951 | - |
952 | - @property |
953 | - def manifest(self): |
954 | - """Return contents of the manifest section.""" |
955 | - return self.sections['manifest'] |
956 | - |
957 | - @property |
958 | - def sources(self): |
959 | - """Return list of sources for this configuration.""" |
960 | - return self._sources |
961 | - |
962 | - def get_strategy_kwargs(self): |
963 | - """Return custom restart strategy parameters.""" |
964 | - kwargs = copy.deepcopy(self.sections['restart']) |
965 | - # [restart] section has the kwargs for the strategy initializer |
966 | - # and the 'strategy' which is not one, let's pop it |
967 | - kwargs.pop('strategy') |
968 | - return kwargs |
969 | - |
970 | - def notice_problem(self, problem): |
971 | - """ Record and log problem encountered when building configuration.""" |
972 | - self._problems.append(problem) |
973 | - logger.warning(problem) |
974 | - |
975 | - def get_problems(self): |
976 | - """Return a list of problem as strings.""" |
977 | - return self._problems |
978 | - |
979 | - def get_value(self, section, name): |
980 | - """Return a value of given `name` from given `section`,""" |
981 | - return self.sections[section][name] |
982 | - |
983 | - def get_origin(self, section, name): |
984 | - """Return origin of the value.""" |
985 | - return self._origins[section][name] |
986 | - |
987 | - def update_from_another(self, configuration, origin): |
988 | - """ |
989 | - Update this configuration with values from `configuration`. |
990 | - |
991 | - Only the values that are not defaults from 'configuration` are taken |
992 | - into account. |
993 | - """ |
994 | - for section, variables in configuration.sections.items(): |
995 | - for name in variables.keys(): |
996 | - new_origin = configuration.get_origin(section, name) |
997 | - if new_origin: |
998 | - if ':' in section and section not in self.sections.keys(): |
999 | - self.sections[section] = OrderedDict() |
1000 | - self._origins[section] = dict() |
1001 | - self.sections[section][name] = configuration.get_value( |
1002 | - section, name) |
1003 | - self._origins[section][name] = origin or new_origin |
1004 | - self._sources += configuration.sources |
1005 | - self._problems += configuration.get_problems() |
1006 | - |
1007 | - def dyn_set_value(self, section, name, value, origin): |
1008 | - """Set a value of a var from a dynamic section.""" |
1009 | - if section == 'environment': |
1010 | - name = name.upper() |
1011 | - self.sections[section][name] = value |
1012 | - self._origins[section][name] = origin |
1013 | - |
1014 | - def set_value(self, section, name, value, origin, parser): |
1015 | - """Set a new value for variable and update its origin.""" |
1016 | - # we are kind off guaranteed that section will be found in the spec |
1017 | - # but let's make linters happy |
1018 | - if section in self._DYNAMIC_SECTIONS: |
1019 | - self.dyn_set_value(section, name, value, origin) |
1020 | - return |
1021 | - parametrized = False |
1022 | - if ':' in section: |
1023 | - parametrized = True |
1024 | - prefix, _ = section.split(':') |
1025 | - if parametrized: |
1026 | - # TODO: do the check here for typing |
1027 | - pass |
1028 | - |
1029 | - index = -1 |
1030 | - for i, (sect_name, spec) in enumerate(CONFIG_SPEC): |
1031 | - if sect_name == section: |
1032 | - index = i |
1033 | - if isinstance(spec, ParametricSection): |
1034 | - if parametrized and sect_name == prefix: |
1035 | - if name not in spec: |
1036 | - problem = ( |
1037 | - "Unexpected variable '{}' in section [{}] " |
1038 | - "Origin: {}").format(name, section, origin) |
1039 | - self.notice_problem(problem) |
1040 | - return |
1041 | - index = i |
1042 | - if index == -1: |
1043 | - # this should happen only for parametric sections |
1044 | - problem = "Unexpected section [{}]. Origin: {}".format( |
1045 | - section, origin) |
1046 | - self.notice_problem(problem) |
1047 | - return |
1048 | - |
1049 | - assert index > -1 |
1050 | - kind = CONFIG_SPEC[index][1][name].kind |
1051 | - try: |
1052 | - if kind == list: |
1053 | - value = shlex.split(value.replace(',', ' ')) |
1054 | - elif kind == bool: |
1055 | - value = parser.getboolean(section, name) |
1056 | - elif kind == float: |
1057 | - value = parser.getfloat(section, name) |
1058 | - elif kind == int: |
1059 | - value = parser.getint(section, name) |
1060 | - else: |
1061 | - value = kind(value) |
1062 | - if parametrized: |
1063 | - # we couldn't have known the param names eariler (in __init__) |
1064 | - # but now we do know them, so let's create the dict to hold |
1065 | - # the values |
1066 | - if section not in self.sections.keys(): |
1067 | - self.sections[section] = OrderedDict() |
1068 | - self._origins[section] = dict() |
1069 | - self.sections[section][name] = value |
1070 | - self._origins[section][name] = origin |
1071 | - except TypeError: |
1072 | - problem = ( |
1073 | - "Problem with setting field {} in section [{}] " |
1074 | - "'{}' cannot be used as {}. Origin: {}").format( |
1075 | - name, section, value, kind, origin) |
1076 | - self.notice_problem(problem) |
1077 | - |
1078 | - def get_parametric_sections(self, prefix): |
1079 | - """ |
1080 | - Return a dict of parametrised section that share the same prefix. |
1081 | - |
1082 | - The resulting dict is keyed by the parameter, the values are dicts |
1083 | - with the declared variables. |
1084 | - |
1085 | - E.g. |
1086 | - If there's two sections: [report:myrep] and [report:other] |
1087 | - The resulting dict will have two keys: myrep and other. |
1088 | - """ |
1089 | - result = dict() |
1090 | - # check if there is such section declared in the SPEC |
1091 | - for sect_name, section in CONFIG_SPEC: |
1092 | - if not isinstance(section, ParametricSection): |
1093 | - continue |
1094 | - if sect_name == prefix: |
1095 | - break |
1096 | - else: |
1097 | - raise ValueError("No such section in the spec ({}".format(prefix)) |
1098 | - for sect_name, section in self.sections.items(): |
1099 | - sect_prefix, _, sect_param = sect_name.partition(':') |
1100 | - if sect_prefix == prefix: |
1101 | - result[sect_param] = section |
1102 | - return result |
1103 | - |
1104 | - @classmethod |
1105 | - def from_text(cls, text, origin): |
1106 | - """ |
1107 | - Create a new configuration with values from the text. |
1108 | - |
1109 | - Behaves just the same as the from_ini_file method, but accepts string |
1110 | - as the param. |
1111 | - """ |
1112 | - return cls.from_ini_file(io.StringIO(text), origin) |
1113 | - |
1114 | - @classmethod |
1115 | - def from_path(cls, path): |
1116 | - """Create a new configuration with values stored in a file at path.""" |
1117 | - cfg = Configuration() |
1118 | - if not os.path.isfile(path): |
1119 | - cfg.notice_problem("{} file not found".format(path)) |
1120 | - return cfg |
1121 | - with open(path, 'rt') as ini_file: |
1122 | - return cls.from_ini_file(ini_file, path) |
1123 | - |
1124 | - @classmethod |
1125 | - def from_ini_file(cls, ini_file, origin): |
1126 | - """ |
1127 | - Create a new configuration with values from the ini file. |
1128 | - |
1129 | - ini_file should be a file object. |
1130 | - |
1131 | - This function is designed not to fail (raise), so if some entry in the |
1132 | - ini file is misdefined then it should be ignored and the default value |
1133 | - should be kept. Each such problem is kept in the self._problems list. |
1134 | - """ |
1135 | - cfg = Configuration(origin) |
1136 | - parser = ConfigParser(delimiters='=') |
1137 | - parser.read_string(ini_file.read()) |
1138 | - for sect_name, section in parser.items(): |
1139 | - if sect_name == 'DEFAULT': |
1140 | - for var_name in section: |
1141 | - problem = "[DEFAULT] section is not supported" |
1142 | - cfg.notice_problem(problem) |
1143 | - continue |
1144 | - if ':' in sect_name: |
1145 | - for var_name, var in section.items(): |
1146 | - cfg.set_value(sect_name, var_name, var, origin, parser) |
1147 | - continue |
1148 | - if sect_name not in cfg.sections: |
1149 | - problem = "Unexpected section [{}]. Origin: {}".format( |
1150 | - sect_name, origin) |
1151 | - cfg.notice_problem(problem) |
1152 | - continue |
1153 | - for var_name, var in section.items(): |
1154 | - is_dyn = sect_name in cls._DYNAMIC_SECTIONS |
1155 | - if var_name not in cfg.sections[sect_name] and not is_dyn: |
1156 | - problem = ( |
1157 | - "Unexpected variable '{}' in section [{}] " |
1158 | - "Origin: {}").format(var_name, sect_name, origin) |
1159 | - cfg.notice_problem(problem) |
1160 | - continue |
1161 | - cfg.set_value(sect_name, var_name, var, origin, parser) |
1162 | - return cfg |
1163 | - |
1164 | - _DYNAMIC_SECTIONS = ('environment', 'manifest') |
1165 | - |
1166 | - |
1167 | -VarSpec = namedtuple('VarSpec', ['kind', 'default', 'help']) |
1168 | - |
1169 | - |
1170 | -class ParametricSection(dict): |
1171 | - """ Dict for storing parametric section's contents.""" |
1172 | - |
1173 | - |
1174 | -class DynamicSection(dict): |
1175 | - """ |
1176 | - Dict for storing dynamic section's contents. |
1177 | - |
1178 | - This is an extra type to record the fact that this is a different section |
1179 | - compared to the predefined ones. It works and isn't very complex, but |
1180 | - a different way of storing this information might be more elegant. |
1181 | - """ |
1182 | - |
1183 | - |
1184 | -# in order to maintain the section order the CONFIG_SPEC is a list of pairs, |
1185 | -# where the first value is the name of the section and the other is a dict |
1186 | -# of variable specs. |
1187 | -CONFIG_SPEC = [ |
1188 | - ('config', { |
1189 | - 'config_filename': VarSpec( |
1190 | - str, 'checkbox.conf', |
1191 | - 'Name of the configuration file to look for.'), |
1192 | - }), |
1193 | - ('launcher', { |
1194 | - 'launcher_version': VarSpec( |
1195 | - int, 1, "Version of launcher to use"), |
1196 | - 'app_id': VarSpec( |
1197 | - str, 'checkbox-cli', "Identifier of the application"), |
1198 | - 'app_version': VarSpec( |
1199 | - str, '', "Version of the application"), |
1200 | - 'stock_reports': VarSpec( |
1201 | - list, ['text', 'certification', 'submission_files'], |
1202 | - "List of stock reports to use"), |
1203 | - 'local_submission': VarSpec( |
1204 | - bool, True, ("Send/generate submission report locally when using " |
1205 | - "checkbox remote")), |
1206 | - 'session_title': VarSpec( |
1207 | - str, 'session title', |
1208 | - ("A title to be applied to the sessions created using this " |
1209 | - "launcher that can be used in report generation")), |
1210 | - 'session_desc': VarSpec( |
1211 | - str, '', ("A string that can be applied to sessions created using " |
1212 | - "this launcher. Useful for storing some contextual " |
1213 | - "infomation about the session")), |
1214 | - }), |
1215 | - ('test plan', { |
1216 | - 'filter': VarSpec( |
1217 | - list, ['*'], |
1218 | - "Constrain interactive choice to test plans matching this glob"), |
1219 | - 'unit': VarSpec(str, '', "Select this test plan by default."), |
1220 | - 'forced': VarSpec( |
1221 | - bool, False, "Don't allow the user to change test plan."), |
1222 | - }), |
1223 | - ('test selection', { |
1224 | - 'forced': VarSpec( |
1225 | - bool, False, "Don't allow the user to alter test selection."), |
1226 | - 'exclude': VarSpec( |
1227 | - list, [], "Exclude test matching patterns from running."), |
1228 | - }), |
1229 | - ('ui', { |
1230 | - 'type': VarSpec(str, 'interactive', "Type of user interface to use."), |
1231 | - 'output': VarSpec(str, 'show', "Silence or restrict command output."), |
1232 | - 'dont_suppress_output': VarSpec( |
1233 | - bool, False, |
1234 | - "Don't suppress the output of certain job plugin types."), |
1235 | - 'verbosity': VarSpec(str, 'normal', "Verbosity level."), |
1236 | - 'auto_retry': VarSpec( |
1237 | - bool, False, |
1238 | - "Automatically retry failed jobs at the end of the session."), |
1239 | - 'max_attempts': VarSpec( |
1240 | - int, 3, |
1241 | - "Number of attempts to run a job when in auto-retry mode."), |
1242 | - 'delay_before_retry': VarSpec( |
1243 | - int, 1, ("Delay (in seconds) before " |
1244 | - "retrying failed jobs in auto-retry mode.")), |
1245 | - }), |
1246 | - ('daemon', { |
1247 | - 'normal_user': VarSpec( |
1248 | - str, '', "Username to use for jobs that don't specify user."), |
1249 | - }), |
1250 | - ('restart', { |
1251 | - 'strategy': VarSpec(str, '', "Use alternative restart strategy."), |
1252 | - }), |
1253 | - ('report', ParametricSection({ |
1254 | - 'exporter': VarSpec( |
1255 | - str, '', "Name of the exporter to use"), |
1256 | - 'transport': VarSpec( |
1257 | - str, '', "Name of the transport to use"), |
1258 | - 'forced': VarSpec( |
1259 | - bool, False, "Don't ask the user if they want the report."), |
1260 | - })), |
1261 | - ('transport', ParametricSection({ |
1262 | - 'type': VarSpec( |
1263 | - str, '', "Type of transport to use."), |
1264 | - 'stream': VarSpec( |
1265 | - str, 'stdout', "Stream to use - stdout or stderr."), |
1266 | - 'path': VarSpec( |
1267 | - str, '', "Path to where the report should be saved to."), |
1268 | - 'secure_id': VarSpec( |
1269 | - str, '', "Secure ID to use."), |
1270 | - 'staging': VarSpec( |
1271 | - bool, False, "Pushes to staging C3 instead of normal C3."), |
1272 | - })), |
1273 | - ('exporter', ParametricSection({ |
1274 | - 'unit': VarSpec(str, '', "ID of the exporter to use."), |
1275 | - 'options': VarSpec(list, [], "Flags to forward to the exporter."), |
1276 | - })), |
1277 | - ('environment', DynamicSection()), |
1278 | - ('manifest', DynamicSection()), |
1279 | -] |
1280 | diff --git a/plainbox/impl/ctrl.py b/plainbox/impl/ctrl.py |
1281 | index 2242114..5facd64 100644 |
1282 | --- a/plainbox/impl/ctrl.py |
1283 | +++ b/plainbox/impl/ctrl.py |
1284 | @@ -60,6 +60,7 @@ from plainbox.impl.resource import ExpressionCannotEvaluateError |
1285 | from plainbox.impl.resource import ExpressionFailedError |
1286 | from plainbox.impl.resource import ResourceProgramError |
1287 | from plainbox.impl.resource import Resource |
1288 | +from plainbox.impl.secure.config import Unset |
1289 | from plainbox.impl.secure.origin import JobOutputTextSource |
1290 | from plainbox.impl.secure.providers.v1 import Provider1 |
1291 | from plainbox.impl.secure.rfc822 import RFC822SyntaxError |
1292 | diff --git a/plainbox/impl/launcher.py b/plainbox/impl/launcher.py |
1293 | new file mode 100644 |
1294 | index 0000000..16b3059 |
1295 | --- /dev/null |
1296 | +++ b/plainbox/impl/launcher.py |
1297 | @@ -0,0 +1,258 @@ |
1298 | +# This file is part of Checkbox. |
1299 | +# |
1300 | +# Copyright 2014-2016 Canonical Ltd. |
1301 | +# Written by: |
1302 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
1303 | +# Maciej Kisielewski <maciej.kisielewski@canonical.com> |
1304 | +# |
1305 | +# Checkbox is free software: you can redistribute it and/or modify |
1306 | +# it under the terms of the GNU General Public License version 3, |
1307 | +# as published by the Free Software Foundation. |
1308 | +# |
1309 | +# Checkbox is distributed in the hope that it will be useful, |
1310 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1311 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1312 | +# GNU General Public License for more details. |
1313 | +# |
1314 | +# You should have received a copy of the GNU General Public License |
1315 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
1316 | + |
1317 | +""" |
1318 | +:mod:`plainbox.impl.launcher` -- launcher definition |
1319 | +================================================== |
1320 | +""" |
1321 | + |
1322 | +from gettext import gettext as _ |
1323 | +import logging |
1324 | + |
1325 | +from plainbox.impl.applogic import PlainBoxConfig |
1326 | +from plainbox.impl.secure import config |
1327 | +from plainbox.impl.session.assistant import SA_RESTARTABLE |
1328 | +from plainbox.impl.session.assistant import get_all_sa_flags |
1329 | +from plainbox.impl.session.assistant import get_known_sa_api_versions |
1330 | +from plainbox.impl.transport import get_all_transports |
1331 | +from plainbox.impl.transport import SECURE_ID_PATTERN |
1332 | + |
1333 | + |
1334 | +logger = logging.getLogger("plainbox.launcher") |
1335 | + |
1336 | + |
1337 | +class LauncherDefinition(PlainBoxConfig): |
1338 | + """ |
1339 | + Launcher definition. |
1340 | + |
1341 | + Launchers are small executables using one of the available user interfaces |
1342 | + as the interpreter. This class contains all the available options that can |
1343 | + be set inside the launcher, that will affect the user interface at runtime. |
1344 | + This generic launcher definition class helps to pick concrete version of |
1345 | + the launcher definition. |
1346 | + """ |
1347 | + launcher_version = config.Variable( |
1348 | + section="launcher", |
1349 | + help_text=_("Version of launcher to use")) |
1350 | + |
1351 | + config_filename = config.Variable( |
1352 | + section="config", |
1353 | + default="checkbox.conf", |
1354 | + help_text=_("Name of custom configuration file")) |
1355 | + |
1356 | + def get_concrete_launcher(self): |
1357 | + """Create appropriate LauncherDefinition instance. |
1358 | + |
1359 | + Depending on the value of launcher_version variable appropriate |
1360 | + LauncherDefinition class is chosen and its instance returned. |
1361 | + |
1362 | + :returns: LauncherDefinition instance |
1363 | + :raises KeyError: for unknown launcher_version values |
1364 | + """ |
1365 | + return {'1': LauncherDefinition1}[self.launcher_version]() |
1366 | + |
1367 | + |
1368 | +class LauncherDefinition1(LauncherDefinition): |
1369 | + """ |
1370 | + Definition for launchers version 1. |
1371 | + |
1372 | + As specced in https://goo.gl/qJYtPX |
1373 | + """ |
1374 | + |
1375 | + def __init__(self): |
1376 | + super().__init__() |
1377 | + |
1378 | + launcher_version = config.Variable( |
1379 | + section="launcher", |
1380 | + default='1', |
1381 | + help_text=_("Version of launcher to use")) |
1382 | + |
1383 | + app_id = config.Variable( |
1384 | + section='launcher', |
1385 | + default='checkbox-cli', |
1386 | + help_text=_('Identifier of the application')) |
1387 | + |
1388 | + app_version = config.Variable( |
1389 | + section='launcher', |
1390 | + help_text=_('Version of the application')) |
1391 | + |
1392 | + api_flags = config.Variable( |
1393 | + section='launcher', |
1394 | + kind=list, |
1395 | + default=[SA_RESTARTABLE], |
1396 | + validator_list=[config.SubsetValidator(get_all_sa_flags())], |
1397 | + help_text=_('List of feature-flags the application requires')) |
1398 | + |
1399 | + api_version = config.Variable( |
1400 | + section='launcher', |
1401 | + default='0.99', |
1402 | + validator_list=[config.ChoiceValidator( |
1403 | + get_known_sa_api_versions())], |
1404 | + help_text=_('Version of API the launcher uses')) |
1405 | + |
1406 | + stock_reports = config.Variable( |
1407 | + section='launcher', |
1408 | + kind=list, |
1409 | + validator_list=[ |
1410 | + config.SubsetValidator({ |
1411 | + 'text', 'certification', 'certification-staging', |
1412 | + 'submission_files', 'none'}), |
1413 | + config.OneOrTheOtherValidator( |
1414 | + {'none'}, {'text', 'certification', 'certification-staging', |
1415 | + 'submission_files'}), |
1416 | + ], |
1417 | + default=['text', 'certification', 'submission_files'], |
1418 | + help_text=_('List of stock reports to use')) |
1419 | + |
1420 | + local_submission = config.Variable( |
1421 | + section='launcher', |
1422 | + kind=bool, |
1423 | + default=True, |
1424 | + help_text=_("Send/generate submission report locally when using " |
1425 | + "checkbox remote")) |
1426 | + |
1427 | + session_title = config.Variable( |
1428 | + section='launcher', |
1429 | + default='session title', |
1430 | + help_text=_("A title to be applied to the sessions created using " |
1431 | + "this launcher that can be used in report generation")) |
1432 | + |
1433 | + session_desc = config.Variable( |
1434 | + section='launcher', |
1435 | + default='', |
1436 | + help_text=_("A string that can be applied to sessions created using " |
1437 | + "this launcher. Useful for storing some contextual " |
1438 | + "infomation about the session")) |
1439 | + |
1440 | + test_plan_filters = config.Variable( |
1441 | + section='test plan', |
1442 | + name='filter', |
1443 | + default=['*'], |
1444 | + kind=list, |
1445 | + help_text=_('Constrain interactive choice to test plans matching this' |
1446 | + 'glob')) |
1447 | + |
1448 | + test_plan_default_selection = config.Variable( |
1449 | + section='test plan', |
1450 | + name='unit', |
1451 | + help_text=_('Select this test plan by default.')) |
1452 | + |
1453 | + test_plan_forced = config.Variable( |
1454 | + section='test plan', |
1455 | + name='forced', |
1456 | + kind=bool, |
1457 | + default=False, |
1458 | + help_text=_("Don't allow the user to change test plan.")) |
1459 | + |
1460 | + test_selection_forced = config.Variable( |
1461 | + section='test selection', |
1462 | + name='forced', |
1463 | + kind=bool, |
1464 | + default=False, |
1465 | + help_text=_("Don't allow the user to alter test selection.")) |
1466 | + |
1467 | + test_exclude = config.Variable( |
1468 | + section='test selection', |
1469 | + name='exclude', |
1470 | + default=[], |
1471 | + kind=list, |
1472 | + help_text=_("Exclude test matching the patterns from running")) |
1473 | + |
1474 | + ui_type = config.Variable( |
1475 | + section='ui', |
1476 | + name='type', |
1477 | + default='interactive', |
1478 | + validator_list=[config.ChoiceValidator( |
1479 | + ['interactive', 'silent'])], |
1480 | + help_text=_('Type of stock user interface to use.')) |
1481 | + |
1482 | + output = config.Variable( |
1483 | + section='ui', |
1484 | + default='show', |
1485 | + validator_list=[config.ChoiceValidator( |
1486 | + ['show', 'hide', 'hide-resource-and-attachment', |
1487 | + 'hide-automated'])], |
1488 | + help_text=_('Silence or restrict command output')) |
1489 | + |
1490 | + dont_suppress_output = config.Variable( |
1491 | + section="ui", kind=bool, default=False, |
1492 | + help_text=_("Don't suppress the output of certain job plugin types.")) |
1493 | + |
1494 | + verbosity = config.Variable( |
1495 | + section="ui", validator_list=[config.ChoiceValidator( |
1496 | + ['normal', 'verbose', 'debug'])], help_text=_('Verbosity level'), |
1497 | + default='normal') |
1498 | + |
1499 | + auto_retry = config.Variable( |
1500 | + section='ui', |
1501 | + kind=bool, |
1502 | + default=False, |
1503 | + help_text=_("Automatically retry failed jobs at the end" |
1504 | + " of the session.")) |
1505 | + |
1506 | + max_attempts = config.Variable( |
1507 | + section='ui', |
1508 | + kind=int, |
1509 | + default=3, |
1510 | + help_text=_("Number of attempts to run a job when in auto-retry mode.")) |
1511 | + |
1512 | + delay_before_retry = config.Variable( |
1513 | + section='ui', |
1514 | + kind=int, |
1515 | + default=1, |
1516 | + help_text=_("Delay (in seconds) before retrying failed jobs in" |
1517 | + " auto-retry mode.")) |
1518 | + |
1519 | + normal_user = config.Variable( |
1520 | + section='daemon', |
1521 | + kind=str, |
1522 | + default='', |
1523 | + help_text=_("Username to use for jobs that don't specify user")) |
1524 | + |
1525 | + restart_strategy = config.Variable( |
1526 | + section='restart', |
1527 | + name='strategy', |
1528 | + help_text=_('Use alternative restart strategy')) |
1529 | + |
1530 | + restart = config.Section( |
1531 | + help_text=_('Restart strategy parameters')) |
1532 | + |
1533 | + reports = config.ParametricSection( |
1534 | + name='report', |
1535 | + help_text=_('Report declaration')) |
1536 | + |
1537 | + exporters = config.ParametricSection( |
1538 | + name='exporter', |
1539 | + help_text=_('Exporter declaration')) |
1540 | + |
1541 | + transports = config.ParametricSection( |
1542 | + name='transport', |
1543 | + help_text=_('Transport declaration')) |
1544 | + |
1545 | + environment = config.Section( |
1546 | + help_text=_('Environment variables to use')) |
1547 | + |
1548 | + daemon = config.Section( |
1549 | + name='daemon', |
1550 | + help_text=_('Daemon-specific configuration')) |
1551 | + |
1552 | + manifest = config.Section( |
1553 | + help_text=_('Manifest entries to use')) |
1554 | + |
1555 | +DefaultLauncherDefinition = LauncherDefinition1 |
1556 | diff --git a/plainbox/impl/runner.py b/plainbox/impl/runner.py |
1557 | index fae15fb..f46a99f 100644 |
1558 | --- a/plainbox/impl/runner.py |
1559 | +++ b/plainbox/impl/runner.py |
1560 | @@ -50,6 +50,7 @@ from plainbox.i18n import gettext as _ |
1561 | from plainbox.impl.result import IOLogRecord |
1562 | from plainbox.impl.result import IOLogRecordWriter |
1563 | from plainbox.impl.result import JobResultBuilder |
1564 | +from plainbox.impl.secure.config import Unset |
1565 | from plainbox.vendor import extcmd |
1566 | from plainbox.vendor import morris |
1567 | |
1568 | diff --git a/plainbox/impl/secure/test_config.py b/plainbox/impl/secure/test_config.py |
1569 | new file mode 100644 |
1570 | index 0000000..1efab12 |
1571 | --- /dev/null |
1572 | +++ b/plainbox/impl/secure/test_config.py |
1573 | @@ -0,0 +1,608 @@ |
1574 | +# This file is part of Checkbox. |
1575 | +# |
1576 | +# Copyright 2013, 2014 Canonical Ltd. |
1577 | +# Written by: |
1578 | +# Zygmunt Krynicki <zygmunt.krynicki@canonical.com> |
1579 | +# |
1580 | +# Checkbox is free software: you can redistribute it and/or modify |
1581 | +# it under the terms of the GNU General Public License version 3, |
1582 | +# as published by the Free Software Foundation. |
1583 | + |
1584 | +# |
1585 | +# Checkbox is distributed in the hope that it will be useful, |
1586 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1587 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1588 | +# GNU General Public License for more details. |
1589 | +# |
1590 | +# You should have received a copy of the GNU General Public License |
1591 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
1592 | + |
1593 | +""" |
1594 | +plainbox.impl.secure.test_config |
1595 | +================================ |
1596 | + |
1597 | +Test definitions for plainbox.impl.secure.config module |
1598 | +""" |
1599 | +from io import StringIO |
1600 | +from unittest import TestCase |
1601 | +import configparser |
1602 | + |
1603 | +from plainbox.impl.secure.config import ChoiceValidator |
1604 | +from plainbox.impl.secure.config import ConfigMetaData |
1605 | +from plainbox.impl.secure.config import KindValidator |
1606 | +from plainbox.impl.secure.config import NotEmptyValidator |
1607 | +from plainbox.impl.secure.config import NotUnsetValidator |
1608 | +from plainbox.impl.secure.config import OneOrTheOtherValidator |
1609 | +from plainbox.impl.secure.config import PatternValidator |
1610 | +from plainbox.impl.secure.config import ParametricSection |
1611 | +from plainbox.impl.secure.config import PlainBoxConfigParser, Config |
1612 | +from plainbox.impl.secure.config import ValidationError |
1613 | +from plainbox.impl.secure.config import Variable, Section, Unset |
1614 | +from plainbox.impl.secure.config import understands_Unset |
1615 | +from plainbox.vendor import mock |
1616 | + |
1617 | + |
1618 | +class UnsetTests(TestCase): |
1619 | + |
1620 | + def test_str(self): |
1621 | + self.assertEqual(str(Unset), "unset") |
1622 | + |
1623 | + def test_repr(self): |
1624 | + self.assertEqual(repr(Unset), "Unset") |
1625 | + |
1626 | + def test_bool(self): |
1627 | + self.assertEqual(bool(Unset), False) |
1628 | + |
1629 | + |
1630 | +class understands_Unset_Tests(TestCase): |
1631 | + |
1632 | + def test_func(self): |
1633 | + @understands_Unset |
1634 | + def func(): |
1635 | + pass |
1636 | + |
1637 | + self.assertTrue(hasattr(func, 'understands_Unset')) |
1638 | + self.assertTrue(getattr(func, 'understands_Unset')) |
1639 | + |
1640 | + def test_cls(self): |
1641 | + @understands_Unset |
1642 | + class cls: |
1643 | + pass |
1644 | + |
1645 | + self.assertTrue(hasattr(cls, 'understands_Unset')) |
1646 | + self.assertTrue(getattr(cls, 'understands_Unset')) |
1647 | + |
1648 | + |
1649 | +class VariableTests(TestCase): |
1650 | + |
1651 | + def test_name(self): |
1652 | + v1 = Variable() |
1653 | + self.assertIsNone(v1.name) |
1654 | + v2 = Variable('var') |
1655 | + self.assertEqual(v2.name, 'var') |
1656 | + v3 = Variable(name='var') |
1657 | + self.assertEqual(v3.name, 'var') |
1658 | + |
1659 | + def test_section(self): |
1660 | + v1 = Variable() |
1661 | + self.assertEqual(v1.section, 'DEFAULT') |
1662 | + v2 = Variable(section='foo') |
1663 | + self.assertEqual(v2.section, 'foo') |
1664 | + |
1665 | + def test_kind(self): |
1666 | + v1 = Variable(kind=bool) |
1667 | + self.assertIs(v1.kind, bool) |
1668 | + v2 = Variable(kind=int) |
1669 | + self.assertIs(v2.kind, int) |
1670 | + v3 = Variable(kind=float) |
1671 | + self.assertIs(v3.kind, float) |
1672 | + v4 = Variable(kind=str) |
1673 | + self.assertIs(v4.kind, str) |
1674 | + v5 = Variable() |
1675 | + self.assertIs(v5.kind, str) |
1676 | + v6 = Variable(kind=list) |
1677 | + self.assertIs(v6.kind, list) |
1678 | + with self.assertRaises(ValueError): |
1679 | + Variable(kind=dict) |
1680 | + |
1681 | + def test_validator_list__default(self): |
1682 | + """ |
1683 | + verify that each Variable has a validator_list and that by default, |
1684 | + that list contains a KindValidator as the first element |
1685 | + """ |
1686 | + self.assertEqual(Variable().validator_list, [KindValidator]) |
1687 | + |
1688 | + def test_validator_list__explicit(self): |
1689 | + """ |
1690 | + verify that each Variable has a validator_list and that, if |
1691 | + customized, the list contains the custom validators, preceded by |
1692 | + the implicit KindValidator object |
1693 | + """ |
1694 | + def DummyValidator(variable, new_value): |
1695 | + """ Dummy validator for the test below""" |
1696 | + pass |
1697 | + var = Variable(validator_list=[DummyValidator]) |
1698 | + self.assertEqual(var.validator_list, [KindValidator, DummyValidator]) |
1699 | + |
1700 | + def test_validator_list__with_NotUnsetValidator(self): |
1701 | + """ |
1702 | + verify that each Variable has a validator_list and that, if |
1703 | + customized, and if using NotUnsetValidator it will take precedence |
1704 | + over all other validators, including the implicit KindValidator |
1705 | + """ |
1706 | + var = Variable(validator_list=[NotUnsetValidator()]) |
1707 | + self.assertEqual( |
1708 | + var.validator_list, [NotUnsetValidator(), KindValidator]) |
1709 | + |
1710 | + |
1711 | +class SectionTests(TestCase): |
1712 | + |
1713 | + def test_name(self): |
1714 | + s1 = Section() |
1715 | + self.assertIsNone(s1.name) |
1716 | + s2 = Section('sec') |
1717 | + self.assertEqual(s2.name, 'sec') |
1718 | + s3 = Variable(name='sec') |
1719 | + self.assertEqual(s3.name, 'sec') |
1720 | + |
1721 | + |
1722 | +class ConfigTests(TestCase): |
1723 | + |
1724 | + def test_Meta_present(self): |
1725 | + class TestConfig(Config): |
1726 | + pass |
1727 | + self.assertTrue(hasattr(TestConfig, 'Meta')) |
1728 | + |
1729 | + def test_Meta_base_cls(self): |
1730 | + class TestConfig(Config): |
1731 | + pass |
1732 | + self.assertTrue(issubclass(TestConfig.Meta, ConfigMetaData)) |
1733 | + |
1734 | + class HelperMeta: |
1735 | + pass |
1736 | + |
1737 | + class TestConfigWMeta(Config): |
1738 | + Meta = HelperMeta |
1739 | + self.assertTrue(issubclass(TestConfigWMeta.Meta, ConfigMetaData)) |
1740 | + self.assertTrue(issubclass(TestConfigWMeta.Meta, HelperMeta)) |
1741 | + |
1742 | + def test_Meta_variable_list(self): |
1743 | + class TestConfig(Config): |
1744 | + v1 = Variable() |
1745 | + v2 = Variable() |
1746 | + self.assertEqual( |
1747 | + TestConfig.Meta.variable_list, |
1748 | + [TestConfig.v1, TestConfig.v2]) |
1749 | + |
1750 | + def test_variable_smoke(self): |
1751 | + class TestConfig(Config): |
1752 | + v = Variable() |
1753 | + conf = TestConfig() |
1754 | + self.assertIs(conf.v, Unset) |
1755 | + conf.v = "value" |
1756 | + self.assertEqual(conf.v, "value") |
1757 | + del conf.v |
1758 | + self.assertIs(conf.v, Unset) |
1759 | + |
1760 | + def _get_featureful_config(self): |
1761 | + # define a featureful config class |
1762 | + class TestConfig(Config): |
1763 | + v1 = Variable() |
1764 | + v2 = Variable(section="v23_section") |
1765 | + v3 = Variable(section="v23_section") |
1766 | + v_unset = Variable() |
1767 | + v_bool = Variable(section="type_section", kind=bool) |
1768 | + v_int = Variable(section="type_section", kind=int) |
1769 | + v_float = Variable(section="type_section", kind=float) |
1770 | + v_list = Variable(section="type_section", kind=list) |
1771 | + v_str = Variable(section="type_section", kind=str) |
1772 | + s = Section() |
1773 | + ps = ParametricSection() |
1774 | + conf = TestConfig() |
1775 | + # assign value to each variable, except v3_unset |
1776 | + conf.v1 = "v1 value" |
1777 | + conf.v2 = "v2 value" |
1778 | + conf.v3 = "v3 value" |
1779 | + conf.v_bool = True |
1780 | + conf.v_int = -7 |
1781 | + conf.v_float = 1.5 |
1782 | + conf.v_str = "hi" |
1783 | + conf.v_list = ['foo', 'bar'] |
1784 | + # assign value to the section |
1785 | + conf.s = {"a": 1, "b": 2} |
1786 | + conf.ps = {"foo": {"c": 3, "d": 4}} |
1787 | + return conf |
1788 | + |
1789 | + def test_get_parser_obj(self): |
1790 | + """ |
1791 | + verify that Config.get_parser_obj() properly writes all the data to the |
1792 | + ConfigParser object. |
1793 | + """ |
1794 | + conf = self._get_featureful_config() |
1795 | + parser = conf.get_parser_obj() |
1796 | + # verify that section and section-less variables work |
1797 | + self.assertEqual(parser.get("DEFAULT", "v1"), "v1 value") |
1798 | + self.assertEqual(parser.get("v23_section", "v2"), "v2 value") |
1799 | + self.assertEqual(parser.get("v23_section", "v3"), "v3 value") |
1800 | + # verify that unset variable is not getting set to anything |
1801 | + with self.assertRaises(configparser.Error): |
1802 | + parser.get("DEFAULT", "v_unset") |
1803 | + # verify that various types got converted correctly and still resolve |
1804 | + # to correct typed values |
1805 | + self.assertEqual(parser.get("type_section", "v_bool"), "True") |
1806 | + self.assertEqual(parser.getboolean("type_section", "v_bool"), True) |
1807 | + self.assertEqual(parser.get("type_section", "v_int"), "-7") |
1808 | + self.assertEqual(parser.getint("type_section", "v_int"), -7) |
1809 | + self.assertEqual(parser.get("type_section", "v_float"), "1.5") |
1810 | + self.assertEqual(parser.getfloat("type_section", "v_float"), 1.5) |
1811 | + self.assertEqual(parser.get("type_section", "v_str"), "hi") |
1812 | + # verify that section work okay |
1813 | + self.assertEqual(parser.get("s", "a"), "1") |
1814 | + self.assertEqual(parser.get("s", "b"), "2") |
1815 | + # verify that parametric section works okay |
1816 | + self.assertEqual(parser.get("ps:foo", "c"), "3") |
1817 | + self.assertEqual(parser.get("ps:foo", "d"), "4") |
1818 | + |
1819 | + def test_write(self): |
1820 | + """ |
1821 | + verify that Config.write() works |
1822 | + """ |
1823 | + conf = self._get_featureful_config() |
1824 | + with StringIO() as stream: |
1825 | + conf.write(stream) |
1826 | + self.assertEqual(stream.getvalue(), ( |
1827 | + "[DEFAULT]\n" |
1828 | + "v1 = v1 value\n" |
1829 | + "\n" |
1830 | + "[v23_section]\n" |
1831 | + "v2 = v2 value\n" |
1832 | + "v3 = v3 value\n" |
1833 | + "\n" |
1834 | + "[type_section]\n" |
1835 | + "v_bool = True\n" |
1836 | + "v_float = 1.5\n" |
1837 | + "v_int = -7\n" |
1838 | + "v_list = foo, bar\n" |
1839 | + "v_str = hi\n" |
1840 | + "\n" |
1841 | + "[s]\n" |
1842 | + "a = 1\n" |
1843 | + "b = 2\n" |
1844 | + "\n" |
1845 | + "[ps:foo]\n" |
1846 | + "c = 3\n" |
1847 | + "d = 4\n" |
1848 | + "\n")) |
1849 | + |
1850 | + def test_section_smoke(self): |
1851 | + class TestConfig(Config): |
1852 | + s = Section() |
1853 | + conf = TestConfig() |
1854 | + self.assertIs(conf.s, Unset) |
1855 | + with self.assertRaises(TypeError): |
1856 | + conf.s['key'] = "key-value" |
1857 | + conf.s = {} |
1858 | + self.assertEqual(conf.s, {}) |
1859 | + conf.s['key'] = "key-value" |
1860 | + self.assertEqual(conf.s['key'], "key-value") |
1861 | + del conf.s |
1862 | + self.assertIs(conf.s, Unset) |
1863 | + |
1864 | + def test_read_string(self): |
1865 | + class TestConfig(Config): |
1866 | + v = Variable() |
1867 | + conf = TestConfig() |
1868 | + conf.read_string( |
1869 | + "[DEFAULT]\n" |
1870 | + "v = 1") |
1871 | + self.assertEqual(conf.v, "1") |
1872 | + self.assertEqual(len(conf.problem_list), 0) |
1873 | + |
1874 | + def test_read_list_with_spaces(self): |
1875 | + class TestConfig(Config): |
1876 | + l = Variable(kind=list) |
1877 | + conf = TestConfig() |
1878 | + conf.read_string('[DEFAULT]\nl = foo bar') |
1879 | + self.assertEqual(conf.l, ['foo', 'bar']) |
1880 | + self.assertEqual(len(conf.problem_list), 0) |
1881 | + |
1882 | + def test_read_list_with_commas(self): |
1883 | + class TestConfig(Config): |
1884 | + l = Variable(kind=list) |
1885 | + conf = TestConfig() |
1886 | + conf.read_string('[DEFAULT]\nl = foo,bar') |
1887 | + self.assertEqual(conf.l, ['foo', 'bar']) |
1888 | + self.assertEqual(len(conf.problem_list), 0) |
1889 | + |
1890 | + def test_read_list_quoted_strings(self): |
1891 | + class TestConfig(Config): |
1892 | + l = Variable(kind=list) |
1893 | + conf = TestConfig() |
1894 | + conf.read_string('[DEFAULT]\nl = foo "bar baz"') |
1895 | + self.assertEqual(conf.l, ['foo', 'bar baz']) |
1896 | + self.assertEqual(len(conf.problem_list), 0) |
1897 | + |
1898 | + def test_read_string_calls_validate_whole(self): |
1899 | + """ |
1900 | + verify that Config.read_string() calls validate_whole()" |
1901 | + """ |
1902 | + conf = Config() |
1903 | + with mock.patch.object(conf, 'validate_whole') as mocked_validate: |
1904 | + conf.read_string('') |
1905 | + mocked_validate.assert_called_once_with() |
1906 | + |
1907 | + def test_read_calls_validate_whole(self): |
1908 | + """ |
1909 | + verify that Config.read() calls validate_whole()" |
1910 | + """ |
1911 | + conf = Config() |
1912 | + with mock.patch.object(conf, 'validate_whole') as mocked_validate: |
1913 | + conf.read([]) |
1914 | + mocked_validate.assert_called_once_with() |
1915 | + |
1916 | + def test_read__handles_errors_from_validate_whole(self): |
1917 | + """ |
1918 | + verify that Config.read() collects errors from validate_whole()". |
1919 | + """ |
1920 | + class TestConfig(Config): |
1921 | + v = Variable() |
1922 | + |
1923 | + def validate_whole(self): |
1924 | + raise ValidationError(TestConfig.v, self.v, "v is evil") |
1925 | + conf = TestConfig() |
1926 | + conf.read([]) |
1927 | + self.assertEqual(len(conf.problem_list), 1) |
1928 | + self.assertEqual(conf.problem_list[0].variable, TestConfig.v) |
1929 | + self.assertEqual(conf.problem_list[0].new_value, Unset) |
1930 | + self.assertEqual(conf.problem_list[0].message, "v is evil") |
1931 | + |
1932 | + def test_read_string__does_not_ignore_nonmentioned_variables(self): |
1933 | + class TestConfig(Config): |
1934 | + v = Variable(validator_list=[NotUnsetValidator()]) |
1935 | + conf = TestConfig() |
1936 | + conf.read_string("") |
1937 | + # Because Unset is the default, sadly |
1938 | + self.assertEqual(conf.v, Unset) |
1939 | + # But there was a problem noticed |
1940 | + self.assertEqual(len(conf.problem_list), 1) |
1941 | + self.assertEqual(conf.problem_list[0].variable, TestConfig.v) |
1942 | + self.assertEqual(conf.problem_list[0].new_value, Unset) |
1943 | + self.assertEqual(conf.problem_list[0].message, |
1944 | + "must be set to something") |
1945 | + |
1946 | + def test_read_string__handles_errors_from_validate_whole(self): |
1947 | + """ |
1948 | + verify that Config.read_strig() collects errors from validate_whole()". |
1949 | + """ |
1950 | + class TestConfig(Config): |
1951 | + v = Variable() |
1952 | + |
1953 | + def validate_whole(self): |
1954 | + raise ValidationError(TestConfig.v, self.v, "v is evil") |
1955 | + conf = TestConfig() |
1956 | + conf.read_string("") |
1957 | + self.assertEqual(len(conf.problem_list), 1) |
1958 | + self.assertEqual(conf.problem_list[0].variable, TestConfig.v) |
1959 | + self.assertEqual(conf.problem_list[0].new_value, Unset) |
1960 | + self.assertEqual(conf.problem_list[0].message, "v is evil") |
1961 | + |
1962 | + |
1963 | +class ConfigMetaDataTests(TestCase): |
1964 | + |
1965 | + def test_filename_list(self): |
1966 | + self.assertEqual(ConfigMetaData.filename_list, []) |
1967 | + |
1968 | + def test_variable_list(self): |
1969 | + self.assertEqual(ConfigMetaData.variable_list, []) |
1970 | + |
1971 | + def test_section_list(self): |
1972 | + self.assertEqual(ConfigMetaData.section_list, []) |
1973 | + |
1974 | + def test_parametric_section_list(self): |
1975 | + self.assertEqual(ConfigMetaData.parametric_section_list, []) |
1976 | + |
1977 | + |
1978 | +class PlainBoxConfigParserTest(TestCase): |
1979 | + |
1980 | + def test_parser(self): |
1981 | + conf_file = StringIO("[testsection]\nlower = low\nUPPER = up") |
1982 | + config = PlainBoxConfigParser() |
1983 | + config.read_file(conf_file) |
1984 | + |
1985 | + self.assertEqual(['testsection'], config.sections()) |
1986 | + all_keys = list(config['testsection'].keys()) |
1987 | + self.assertTrue('lower' in all_keys) |
1988 | + self.assertTrue('UPPER' in all_keys) |
1989 | + self.assertFalse('upper' in all_keys) |
1990 | + |
1991 | + def test_parametric_sections_parsing(self): |
1992 | + class TestConfig(Config): |
1993 | + ps = ParametricSection() |
1994 | + conf_str = "[ps:foo]\nval = baz\n[ps:bar]\nvar = biz" |
1995 | + config = TestConfig() |
1996 | + config.read_string(conf_str) |
1997 | + self.assertEqual( |
1998 | + config.ps, |
1999 | + {'foo': {'val': 'baz'}, 'bar': {'var': 'biz'}}) |
2000 | + |
2001 | + |
2002 | +class KindValidatorTests(TestCase): |
2003 | + |
2004 | + class _Config(Config): |
2005 | + var_bool = Variable(kind=bool) |
2006 | + var_int = Variable(kind=int) |
2007 | + var_float = Variable(kind=float) |
2008 | + var_str = Variable(kind=str) |
2009 | + |
2010 | + def test_error_msg(self): |
2011 | + """ |
2012 | + verify that KindValidator() has correct error message for each type |
2013 | + """ |
2014 | + bad_value = object() |
2015 | + self.assertEqual( |
2016 | + KindValidator(self._Config.var_bool, bad_value), |
2017 | + "expected a boolean") |
2018 | + self.assertEqual( |
2019 | + KindValidator(self._Config.var_int, bad_value), |
2020 | + "expected an integer") |
2021 | + self.assertEqual( |
2022 | + KindValidator(self._Config.var_float, bad_value), |
2023 | + "expected a floating point number") |
2024 | + self.assertEqual( |
2025 | + KindValidator(self._Config.var_str, bad_value), |
2026 | + "expected a string") |
2027 | + |
2028 | + |
2029 | +class PatternValidatorTests(TestCase): |
2030 | + |
2031 | + class _Config(Config): |
2032 | + var = Variable() |
2033 | + |
2034 | + def test_smoke(self): |
2035 | + """ |
2036 | + verify that PatternValidator works as intended |
2037 | + """ |
2038 | + validator = PatternValidator("foo.+") |
2039 | + self.assertEqual(validator(self._Config.var, "foobar"), None) |
2040 | + self.assertEqual( |
2041 | + validator(self._Config.var, "foo"), |
2042 | + "does not match pattern: 'foo.+'") |
2043 | + |
2044 | + def test_comparison_works(self): |
2045 | + self.assertTrue(PatternValidator('foo') == PatternValidator('foo')) |
2046 | + self.assertTrue(PatternValidator('foo') != PatternValidator('bar')) |
2047 | + self.assertTrue(PatternValidator('foo') != object()) |
2048 | + |
2049 | + |
2050 | +class ChoiceValidatorTests(TestCase): |
2051 | + |
2052 | + class _Config(Config): |
2053 | + var = Variable() |
2054 | + |
2055 | + def test_smoke(self): |
2056 | + """ |
2057 | + verify that ChoiceValidator works as intended |
2058 | + """ |
2059 | + validator = ChoiceValidator(["foo", "bar"]) |
2060 | + self.assertEqual(validator(self._Config.var, "foo"), None) |
2061 | + self.assertEqual( |
2062 | + validator(self._Config.var, "omg"), |
2063 | + "var must be one of foo, bar. Got 'omg'") |
2064 | + |
2065 | + def test_comparison_works(self): |
2066 | + self.assertTrue(ChoiceValidator(["a"]) == ChoiceValidator(["a"])) |
2067 | + self.assertTrue(ChoiceValidator(["a"]) != ChoiceValidator(["b"])) |
2068 | + self.assertTrue(ChoiceValidator(["a"]) != object()) |
2069 | + |
2070 | + |
2071 | +class NotUnsetValidatorTests(TestCase): |
2072 | + """ |
2073 | + Tests for the NotUnsetValidator class |
2074 | + """ |
2075 | + |
2076 | + class _Config(Config): |
2077 | + var = Variable() |
2078 | + |
2079 | + def test_understands_Unset(self): |
2080 | + """ |
2081 | + verify that Unset can be handled at all |
2082 | + """ |
2083 | + self.assertTrue(getattr(NotUnsetValidator, "understands_Unset")) |
2084 | + |
2085 | + def test_rejects_unset_values(self): |
2086 | + """ |
2087 | + verify that Unset variables are rejected |
2088 | + """ |
2089 | + validator = NotUnsetValidator() |
2090 | + self.assertEqual( |
2091 | + validator(self._Config.var, Unset), "must be set to something") |
2092 | + |
2093 | + def test_accepts_other_values(self): |
2094 | + """ |
2095 | + verify that other values are accepted |
2096 | + """ |
2097 | + validator = NotUnsetValidator() |
2098 | + self.assertIsNone(validator(self._Config.var, None)) |
2099 | + self.assertIsNone(validator(self._Config.var, "string")) |
2100 | + self.assertIsNone(validator(self._Config.var, 15)) |
2101 | + |
2102 | + def test_supports_custom_message(self): |
2103 | + """ |
2104 | + verify that custom message is used |
2105 | + """ |
2106 | + validator = NotUnsetValidator("value required!") |
2107 | + self.assertEqual( |
2108 | + validator(self._Config.var, Unset), "value required!") |
2109 | + |
2110 | + def test_comparison_works(self): |
2111 | + """ |
2112 | + verify that comparison works as expected |
2113 | + """ |
2114 | + self.assertTrue(NotUnsetValidator() == NotUnsetValidator()) |
2115 | + self.assertTrue(NotUnsetValidator("?") == NotUnsetValidator("?")) |
2116 | + self.assertTrue(NotUnsetValidator() != NotUnsetValidator("?")) |
2117 | + self.assertTrue(NotUnsetValidator() != object()) |
2118 | + |
2119 | + |
2120 | +class NotEmptyValidatorTests(TestCase): |
2121 | + |
2122 | + class _Config(Config): |
2123 | + var = Variable() |
2124 | + |
2125 | + def test_rejects_empty_values(self): |
2126 | + validator = NotEmptyValidator() |
2127 | + self.assertEqual(validator(self._Config.var, ""), "cannot be empty") |
2128 | + |
2129 | + def test_supports_custom_message(self): |
2130 | + validator = NotEmptyValidator("name required!") |
2131 | + self.assertEqual(validator(self._Config.var, ""), "name required!") |
2132 | + |
2133 | + def test_isnt_broken(self): |
2134 | + validator = NotEmptyValidator() |
2135 | + self.assertEqual(validator(self._Config.var, "some value"), None) |
2136 | + |
2137 | + def test_comparison_works(self): |
2138 | + self.assertTrue(NotEmptyValidator() == NotEmptyValidator()) |
2139 | + self.assertTrue(NotEmptyValidator("?") == NotEmptyValidator("?")) |
2140 | + self.assertTrue(NotEmptyValidator() != NotEmptyValidator("?")) |
2141 | + self.assertTrue(NotEmptyValidator() != object()) |
2142 | + |
2143 | + |
2144 | +class OneOrTheOtherValidatorTests(TestCase): |
2145 | + |
2146 | + class _Config(Config): |
2147 | + var = Variable("The Name", kind=list) |
2148 | + |
2149 | + def test_pass_validation(self): |
2150 | + validator = OneOrTheOtherValidator({'foo'}, {'bar'}) |
2151 | + value = ['foo'] |
2152 | + self.assertIsNone(validator(self._Config.var, value)) |
2153 | + value = ['bar'] |
2154 | + self.assertIsNone(validator(self._Config.var, value)) |
2155 | + |
2156 | + def test_fail_validation(self): |
2157 | + validator = OneOrTheOtherValidator({'foo'}, {'bar'}) |
2158 | + value = ['foo', 'bar'] |
2159 | + self.assertEquals( |
2160 | + validator(self._Config.var, value), |
2161 | + "The Name can only use values from {'foo'} or from {'bar'}") |
2162 | + |
2163 | + def test_pass_empty(self): |
2164 | + validator = OneOrTheOtherValidator({'foo'}, {'bar'}) |
2165 | + value = [] |
2166 | + self.assertIsNone(validator(self._Config.var, value)) |
2167 | + |
2168 | + def test_comparison_works(self): |
2169 | + self.assertEqual( |
2170 | + OneOrTheOtherValidator({'foo'}, {'bar'}), |
2171 | + OneOrTheOtherValidator({'foo'}, {'bar'}) |
2172 | + ) |
2173 | + self.assertEqual( |
2174 | + OneOrTheOtherValidator({1, 2}, {3, 4}), |
2175 | + OneOrTheOtherValidator({2, 1}, {4, 3}) |
2176 | + ) |
2177 | + self.assertNotEqual( |
2178 | + OneOrTheOtherValidator({1}, {2}), |
2179 | + OneOrTheOtherValidator({1}, {'foo'}) |
2180 | + ) |
2181 | + self.assertNotEqual(OneOrTheOtherValidator({1}, {2}), object()) |
2182 | diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py |
2183 | index fa852c8..41ec411 100644 |
2184 | --- a/plainbox/impl/session/assistant.py |
2185 | +++ b/plainbox/impl/session/assistant.py |
2186 | @@ -38,15 +38,16 @@ from plainbox.abc import IJobResult |
2187 | from plainbox.abc import IJobRunnerUI |
2188 | from plainbox.abc import ISessionStateTransport |
2189 | from plainbox.i18n import gettext as _ |
2190 | +from plainbox.impl.applogic import PlainBoxConfig |
2191 | from plainbox.impl.decorators import raises |
2192 | from plainbox.impl.developer import UnexpectedMethodCall |
2193 | from plainbox.impl.developer import UsageExpectation |
2194 | from plainbox.impl.execution import UnifiedRunner |
2195 | -from plainbox.impl.config import Configuration |
2196 | from plainbox.impl.providers import get_providers |
2197 | from plainbox.impl.result import JobResultBuilder |
2198 | from plainbox.impl.result import MemoryJobResult |
2199 | from plainbox.impl.runner import JobRunnerUIDelegate |
2200 | +from plainbox.impl.secure.config import Unset |
2201 | from plainbox.impl.secure.origin import Origin |
2202 | from plainbox.impl.secure.qualifiers import select_jobs |
2203 | from plainbox.impl.secure.qualifiers import FieldQualifier |
2204 | @@ -161,7 +162,7 @@ class SessionAssistant: |
2205 | self._app_version = app_version |
2206 | self._api_version = api_version |
2207 | self._api_flags = api_flags |
2208 | - self._config = Configuration() |
2209 | + self._config = PlainBoxConfig().get() |
2210 | Unit.config = self._config |
2211 | self._execution_ctrl_list = None # None is "default" |
2212 | self._ctrl_setup_list = [] |
2213 | @@ -210,8 +211,7 @@ class SessionAssistant: |
2214 | |
2215 | @raises(UnexpectedMethodCall, LookupError) |
2216 | def configure_application_restart( |
2217 | - self, cmd_callback, session_type: |
2218 | - 'Callable[[str], List[str]], str') -> None: |
2219 | + self, cmd_callback: 'Callable[[str], List[str]]') -> None: |
2220 | """ |
2221 | Configure automatic restart capability. |
2222 | |
2223 | @@ -219,8 +219,6 @@ class SessionAssistant: |
2224 | A callable (function or lambda) that when called with a single |
2225 | string argument, session_id, returns a list of strings describing |
2226 | how to execute the tool in order to restart a particular session. |
2227 | - :param session_type: |
2228 | - Kind of the session we're running. Either 'local' or 'remote' |
2229 | :raises UnexpectedMethodCall: |
2230 | If the call is made at an unexpected time. Do not catch this error. |
2231 | It is a bug in your program. The error message will indicate what |
2232 | @@ -247,6 +245,25 @@ class SessionAssistant: |
2233 | """ |
2234 | UsageExpectation.of(self).enforce() |
2235 | if self._restart_strategy is None: |
2236 | + # 'checkbox-slave' is deprecated, it's here so people can resume |
2237 | + # old session, the next if statement can be changed to just checking |
2238 | + # for 'remote' type |
2239 | + # session_type = 'remote' if self._metadata.title == 'remote' |
2240 | + # else 'local' |
2241 | + # with the next release or when we do inclusive naming refactor |
2242 | + # or roughly after April of 2022 |
2243 | + # TODO: REMOTE API RAPI: |
2244 | + # this heuristic of guessing session type from the title |
2245 | + # should be changed to a proper arg/flag with the Remote API bump |
2246 | + remote_types = ('remote', 'checkbox-slave') |
2247 | + session_type = 'local' |
2248 | + try: |
2249 | + app_blob = json.loads(self._metadata.app_blob.decode("UTF-8")) |
2250 | + session_type = app_blob['type'] |
2251 | + if session_type in remote_types: |
2252 | + session_type = 'remote' |
2253 | + except (AttributeError, ValueError, KeyError): |
2254 | + session_type = 'local' |
2255 | self._restart_strategy = detect_restart_strategy( |
2256 | self, session_type=session_type) |
2257 | self._restart_cmd_callback = cmd_callback |
2258 | @@ -300,7 +317,8 @@ class SessionAssistant: |
2259 | Use alternate configuration object. |
2260 | |
2261 | :param config: |
2262 | - A Checkbox configuration object. |
2263 | + A configuration object that implements a superset of the plainbox |
2264 | + configuration. |
2265 | :raises UnexpectedMethodCall: |
2266 | If the call is made at an unexpected time. Do not catch this error. |
2267 | It is a bug in your program. The error message will indicate what |
2268 | @@ -313,7 +331,7 @@ class SessionAssistant: |
2269 | UsageExpectation.of(self).enforce() |
2270 | self._config = config |
2271 | self._exclude_qualifiers = [] |
2272 | - for pattern in self._config.get_value('test selection', 'exclude'): |
2273 | + for pattern in self._config.test_exclude: |
2274 | self._exclude_qualifiers.append( |
2275 | RegExpJobQualifier(pattern, None, False)) |
2276 | Unit.config = config |
2277 | @@ -1201,7 +1219,7 @@ class SessionAssistant: |
2278 | if os.path.isfile(manifest): |
2279 | with open(manifest, 'rt', encoding='UTF-8') as stream: |
2280 | manifest_cache = json.load(stream) |
2281 | - if self._config is not None and self._config.manifest: |
2282 | + if self._config is not None and self._config.manifest is not Unset: |
2283 | for manifest_id in self._config.manifest: |
2284 | manifest_cache.update( |
2285 | {manifest_id: self._config.manifest[manifest_id]}) |
2286 | @@ -1364,8 +1382,11 @@ class SessionAssistant: |
2287 | f.writelines(self._restart_cmd_callback( |
2288 | self.get_session_id())) |
2289 | if not native: |
2290 | - result = self._runner.run_job(job, job_state, |
2291 | - self._config.environment, ui) |
2292 | + if self._config.environment is Unset: |
2293 | + result = self._runner.run_job(job, job_state, ui=ui) |
2294 | + else: |
2295 | + result = self._runner.run_job(job, job_state, |
2296 | + self._config.environment, ui) |
2297 | builder = result.get_builder() |
2298 | else: |
2299 | builder = JobResultBuilder( |
2300 | diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py |
2301 | index e15961b..e3c6c11 100644 |
2302 | --- a/plainbox/impl/session/remote_assistant.py |
2303 | +++ b/plainbox/impl/session/remote_assistant.py |
2304 | @@ -28,7 +28,6 @@ from contextlib import suppress |
2305 | from tempfile import SpooledTemporaryFile |
2306 | from threading import Thread, Lock |
2307 | |
2308 | -from plainbox.impl.config import Configuration |
2309 | from plainbox.impl.execution import UnifiedRunner |
2310 | from plainbox.impl.session.assistant import SessionAssistant |
2311 | from plainbox.impl.session.assistant import SA_RESTARTABLE |
2312 | @@ -141,7 +140,7 @@ class BackgroundExecutor(Thread): |
2313 | class RemoteSessionAssistant(): |
2314 | """Remote execution enabling wrapper for the SessionAssistant""" |
2315 | |
2316 | - REMOTE_API_VERSION = 12 |
2317 | + REMOTE_API_VERSION = 11 |
2318 | |
2319 | def __init__(self, cmd_callback): |
2320 | _logger.debug("__init__()") |
2321 | @@ -274,20 +273,20 @@ class RemoteSessionAssistant(): |
2322 | |
2323 | self._launcher = load_configs() |
2324 | if configuration['launcher']: |
2325 | - self._launcher = Configuration.from_text( |
2326 | - configuration['launcher'], 'Remote launcher') |
2327 | - session_title = self._launcher.get_value( |
2328 | - 'launcher', 'session_title') or session_title |
2329 | - session_desc = self._launcher.get_value( |
2330 | - 'launcher', 'session_desc') or session_desc |
2331 | + self._launcher.read_string(configuration['launcher'], False) |
2332 | + if self._launcher.session_title: |
2333 | + session_title = self._launcher.session_title |
2334 | + if self._launcher.session_desc: |
2335 | + session_desc = self._launcher.session_desc |
2336 | |
2337 | self._sa.use_alternate_configuration(self._launcher) |
2338 | |
2339 | if configuration['normal_user']: |
2340 | self._normal_user = configuration['normal_user'] |
2341 | else: |
2342 | - self._normal_user = self._launcher.get_value( |
2343 | - 'daemon', 'normal_user') or _guess_normal_user() |
2344 | + self._normal_user = self._launcher.normal_user |
2345 | + if not self._normal_user: |
2346 | + self._normal_user = _guess_normal_user() |
2347 | runner_kwargs = { |
2348 | 'normal_user_provider': lambda: self._normal_user, |
2349 | 'stdin': self._pipe_to_subproc, |
2350 | @@ -301,13 +300,12 @@ class RemoteSessionAssistant(): |
2351 | 'effective_normal_user': self._normal_user, |
2352 | }).encode("UTF-8") |
2353 | self._sa.update_app_blob(new_blob) |
2354 | - self._sa.configure_application_restart( |
2355 | - self._cmd_callback, session_type='remote') |
2356 | + self._sa.configure_application_restart(self._cmd_callback) |
2357 | |
2358 | self._session_id = self._sa.get_session_id() |
2359 | tps = self._sa.get_test_plans() |
2360 | filtered_tps = set() |
2361 | - for filter in self._launcher.get_value('test plan', 'filter'): |
2362 | + for filter in self._launcher.test_plan_filters: |
2363 | filtered_tps.update(fnmatch.filter(tps, filter)) |
2364 | filtered_tps = list(filtered_tps) |
2365 | response = zip(filtered_tps, [self._sa.get_test_plan( |
2366 | @@ -324,6 +322,11 @@ class RemoteSessionAssistant(): |
2367 | self._sa.update_app_blob(json.dumps( |
2368 | {'testplan_id': test_plan_id, }).encode("UTF-8")) |
2369 | self._sa.select_test_plan(test_plan_id) |
2370 | + # TODO: REMOTE API RAPI: Change this API on the next RAPI bump |
2371 | + # previously the function returned bool signifying the need for sudo |
2372 | + # password. With slave being guaranteed to never need it anymor |
2373 | + # we can make this funciton return nothing |
2374 | + return False |
2375 | |
2376 | @allowed_when(Started) |
2377 | def get_bootstrapping_todo_list(self): |
2378 | @@ -332,11 +335,10 @@ class RemoteSessionAssistant(): |
2379 | def finish_bootstrap(self): |
2380 | self._sa.finish_bootstrap() |
2381 | self._state = Bootstrapped |
2382 | - if self._launcher.get_value('ui', 'auto_retry'): |
2383 | + if self._launcher.auto_retry: |
2384 | for job_id in self._sa.get_static_todo_list(): |
2385 | job_state = self._sa.get_job_state(job_id) |
2386 | - job_state.attempts = self._launcher.get_value( |
2387 | - 'ui', 'max_attempts') |
2388 | + job_state.attempts = self._launcher.max_attempts |
2389 | return self._sa.get_static_todo_list() |
2390 | |
2391 | def get_manifest_repr(self): |
2392 | @@ -361,12 +363,10 @@ class RemoteSessionAssistant(): |
2393 | |
2394 | def _get_ui_for_job(self, job): |
2395 | show_out = True |
2396 | - if self._launcher.get_value( |
2397 | - 'ui', 'output') == 'hide-resource-and-attachment': |
2398 | + if self._launcher.output == 'hide-resource-and-attachment': |
2399 | if job.plugin in ('local', 'resource', 'attachment'): |
2400 | show_out = False |
2401 | - elif self._launcher.get_value( |
2402 | - 'ui', 'output') in ['hide', 'hide-automated']: |
2403 | + elif self._launcher.output in ['hide', 'hide-automated']: |
2404 | if job.plugin in ('shell', 'local', 'resource', 'attachment'): |
2405 | show_out = False |
2406 | if 'suppress-output' in job.get_flag_set(): |
2407 | @@ -522,6 +522,27 @@ class RemoteSessionAssistant(): |
2408 | "todo": self._sa.get_dynamic_todo_list(), |
2409 | } |
2410 | |
2411 | + def get_master_public_key(self): |
2412 | + # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump |
2413 | + # this key is only for RAPI compliance. It will never be used as |
2414 | + # this master requires slave to be completely sudoless |
2415 | + return ( |
2416 | + b'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMII' |
2417 | + b'BCgKCAQEA5r0bjOA+IH5lDKkW3OYb\nDuEjf5VKgUlDSJJuyBlfLTBIXZ8j3s98' |
2418 | + b'6AbV0zB62rAcgiFrBOzx51IzBDBmHI8V\nYYpEa+q4OP4yprYpSg6xzX6LRQapC' |
2419 | + b'Iv9BAqN4MWrKBukGMzJyemIVEPv4BSHL5L/\nLY98Mwh4dAXxj5ZdsoVPqgeMo8' |
2420 | + b'dxfYEOwVRJvSkseIhxRL6tvgP37c48ApUyjdUO\n3C2YgqJRx7mKKDyLOvhDVEl' |
2421 | + b'MqkAfp6qS/8xcGBTEqn08dDQIgPl8KofpC9GXMGbK\nV9FGP+c1bpA3vMOfnpsE' |
2422 | + b'WCju2qDoTSKJTm3VMZj88mqH7nOpbk7JI/Yz0EmtNXOM\n6QIDAQAB\n-----EN' |
2423 | + b'D PUBLIC KEY-----') |
2424 | + |
2425 | + def save_password(self, password): |
2426 | + """Store sudo password""" |
2427 | + # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump |
2428 | + # if the slave is running it means we don't need password |
2429 | + # so we can consider call to this function as passing |
2430 | + return True |
2431 | + |
2432 | def finish_job(self, result=None): |
2433 | # assert the thread completed |
2434 | self.session_change_lock.acquire(blocking=False) |
2435 | @@ -537,7 +558,7 @@ class RemoteSessionAssistant(): |
2436 | if self._state != Bootstrapping: |
2437 | if not self._sa.get_dynamic_todo_list(): |
2438 | if ( |
2439 | - self._launcher.get_value('ui', 'auto_retry') and |
2440 | + self._launcher.auto_retry and |
2441 | self.get_rerun_candidates('auto') |
2442 | ): |
2443 | self._state = TestsSelected |
2444 | @@ -623,12 +644,11 @@ class RemoteSessionAssistant(): |
2445 | meta = self._sa.resume_session(session_id, runner_kwargs=runner_kwargs) |
2446 | app_blob = json.loads(meta.app_blob.decode("UTF-8")) |
2447 | launcher = app_blob['launcher'] |
2448 | - self._launcher = Configuration.from_text(launcher, 'Remote launcher') |
2449 | + self._launcher.read_string(launcher, False) |
2450 | self._sa.use_alternate_configuration(self._launcher) |
2451 | |
2452 | self._normal_user = app_blob.get( |
2453 | - 'effective_normal_user', self._launcher.get_value( |
2454 | - 'daemon', 'normal_user')) |
2455 | + 'effective_normal_user', self._launcher.normal_user) |
2456 | _logger.info( |
2457 | "normal_user after loading metadata: %r", self._normal_user) |
2458 | test_plan_id = app_blob['testplan_id'] |
2459 | @@ -664,12 +684,12 @@ class RemoteSessionAssistant(): |
2460 | |
2461 | # some jobs have already been run, so we need to update the attempts |
2462 | # count for future auto-rerunning |
2463 | - if self._launcher.get_value('ui', 'auto_retry'): |
2464 | + if self._launcher.auto_retry: |
2465 | for job_id in [ |
2466 | job.id for job in self.get_rerun_candidates('auto')]: |
2467 | job_state = self._sa.get_job_state(job_id) |
2468 | - job_state.attempts = self._launcher.get_value( |
2469 | - 'ui', 'max_attempts') - len(job_state.result_history) |
2470 | + job_state.attempts = self._launcher.max_attempts - len( |
2471 | + job_state.result_history) |
2472 | |
2473 | self._state = TestsSelected |
2474 | |
2475 | @@ -695,6 +715,12 @@ class RemoteSessionAssistant(): |
2476 | return self._sa._manager |
2477 | |
2478 | @property |
2479 | + def passwordless_sudo(self): |
2480 | + # TODO: REMOTE API RAPI: Remove this API on the next RAPI bump |
2481 | + # if the slave is still running it means it's very passwordless |
2482 | + return True |
2483 | + |
2484 | + @property |
2485 | def sideloaded_providers(self): |
2486 | return self._sa.sideloaded_providers |
2487 | |
2488 | diff --git a/plainbox/impl/test_config.py b/plainbox/impl/test_config.py |
2489 | deleted file mode 100644 |
2490 | index fce0700..0000000 |
2491 | --- a/plainbox/impl/test_config.py |
2492 | +++ /dev/null |
2493 | @@ -1,130 +0,0 @@ |
2494 | -# This file is part of Checkbox. |
2495 | -# |
2496 | -# Copyright 2020 Canonical Ltd. |
2497 | -# Written by: |
2498 | -# Maciej Kisielewski <maciej.kisielewski@canonical.com> |
2499 | -# |
2500 | -# Checkbox is free software: you can redistribute it and/or modify |
2501 | -# it under the terms of the GNU General Public License version 3, |
2502 | -# as published by the Free Software Foundation. |
2503 | -# |
2504 | -# Checkbox is distributed in the hope that it will be useful, |
2505 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2506 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2507 | -# GNU General Public License for more details. |
2508 | -# |
2509 | -# You should have received a copy of the GNU General Public License |
2510 | -# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
2511 | -""" |
2512 | -This module contains tests for the new Checkbox Config module |
2513 | -""" |
2514 | -from contextlib import contextmanager |
2515 | -import logging |
2516 | -from unittest import TestCase |
2517 | -from unittest.mock import mock_open, patch |
2518 | - |
2519 | -from plainbox.impl.config import Configuration |
2520 | - |
2521 | - |
2522 | -@contextmanager |
2523 | -def muted_logging(): |
2524 | - """Disable logging so the test that use this have no output.""" |
2525 | - saved_level = logging.root.getEffectiveLevel() |
2526 | - logging.root.setLevel(logging.CRITICAL) |
2527 | - yield |
2528 | - logging.root.setLevel(saved_level) |
2529 | - |
2530 | - |
2531 | -class ConfigurationTests(TestCase): |
2532 | - """Tests for the Configuration class.""" |
2533 | - |
2534 | - def test_empty_file_yields_defaults(self): |
2535 | - """A default configuration instance should have default values.""" |
2536 | - # let's check a few values from random sections |
2537 | - cfg = Configuration() |
2538 | - self.assertEqual(cfg.get_value('test plan', 'filter'), ['*']) |
2539 | - self.assertTrue(cfg.get_value('launcher', 'local_submission')) |
2540 | - self.assertEqual(cfg.get_value('daemon', 'normal_user'), '') |
2541 | - |
2542 | - @patch('os.path.isfile', return_value=True) |
2543 | - def test_one_var_overwrites(self, _): |
2544 | - """ |
2545 | - One variable properly shadows defaults. |
2546 | - |
2547 | - Having one (good) value in config should yield a config with |
2548 | - defaults except the one var placed in the config file. |
2549 | - """ |
2550 | - ini_data = """ |
2551 | - [launcher] |
2552 | - stock_reports = text |
2553 | - """ |
2554 | - with patch('builtins.open', mock_open(read_data=ini_data)): |
2555 | - cfg = Configuration.from_path('unit test') |
2556 | - self.assertEqual(cfg.get_value('test plan', 'filter'), ['*']) |
2557 | - self.assertTrue(cfg.get_value('launcher', 'local_submission')) |
2558 | - self.assertEqual(cfg.get_value('daemon', 'normal_user'), '') |
2559 | - self.assertEqual(cfg.get_origin('daemon', 'normal_user'), '') |
2560 | - self.assertEqual(cfg.get_value('launcher', 'stock_reports'), ['text']) |
2561 | - self.assertEqual( |
2562 | - cfg.get_origin('launcher', 'stock_reports'), |
2563 | - 'unit test') |
2564 | - |
2565 | - @patch('os.path.isfile', return_value=True) |
2566 | - def test_string_list_distinction(self, _): |
2567 | - """ |
2568 | - Parsing of lists and multi-word strings. |
2569 | - |
2570 | - Depending on the config spec the field can be considered a string |
2571 | - (with spaces) or a list. |
2572 | - """ |
2573 | - ini_data = """ |
2574 | - [launcher] |
2575 | - launcher_version = 1 |
2576 | - stock_reports = submission_files, text |
2577 | - session_title = A session title |
2578 | - """ |
2579 | - with patch('builtins.open', mock_open(read_data=ini_data)): |
2580 | - cfg = Configuration.from_path('unit test') |
2581 | - self.assertEqual( |
2582 | - cfg.get_value('launcher', 'stock_reports'), |
2583 | - ['submission_files', 'text']) |
2584 | - self.assertEqual( |
2585 | - cfg.get_value('launcher', 'session_title'), |
2586 | - 'A session title') |
2587 | - |
2588 | - @patch('os.path.isfile', return_value=True) |
2589 | - def test_unexpected_content(self, _): |
2590 | - """ |
2591 | - Yield problems with extra data in configs. |
2592 | - """ |
2593 | - ini_data = """ |
2594 | - [launcher] |
2595 | - barfoo = 5 |
2596 | - [foobar] |
2597 | - """ |
2598 | - with muted_logging(): |
2599 | - with patch('builtins.open', mock_open(read_data=ini_data)): |
2600 | - cfg = Configuration.from_path('unit test') |
2601 | - self.assertEqual(len(cfg.get_problems()), 2) |
2602 | - |
2603 | - def test_default_vars_are_not_supported(self): |
2604 | - """ |
2605 | - Yield a problem when variable is defined in the [DEFAULT] section. |
2606 | - """ |
2607 | - ini_data = """ |
2608 | - [DEFAULT] |
2609 | - badvar = 4 |
2610 | - """ |
2611 | - with muted_logging(): |
2612 | - with patch('builtins.open', mock_open(read_data=ini_data)): |
2613 | - cfg = Configuration.from_path('unit test') |
2614 | - self.assertEqual(len(cfg.get_problems()), 1) |
2615 | - |
2616 | - @patch('os.path.isfile', return_value=False) |
2617 | - def test_ini_not_found(self, _): |
2618 | - """ |
2619 | - Yield a problem when an ini file cannot be opened. |
2620 | - """ |
2621 | - with muted_logging(): |
2622 | - cfg = Configuration.from_path('invalid path') |
2623 | - self.assertEqual(len(cfg.get_problems()), 1) |
2624 | diff --git a/plainbox/impl/test_launcher.py b/plainbox/impl/test_launcher.py |
2625 | new file mode 100644 |
2626 | index 0000000..47b47d1 |
2627 | --- /dev/null |
2628 | +++ b/plainbox/impl/test_launcher.py |
2629 | @@ -0,0 +1,136 @@ |
2630 | +# This file is part of Checkbox. |
2631 | +# |
2632 | +# Copyright 2016 Canonical Ltd. |
2633 | +# Written by: |
2634 | +# Maciej Kisielewski <maciej.kisielewski@canonical.com> |
2635 | +# |
2636 | +# Checkbox is free software: you can redistribute it and/or modify |
2637 | +# it under the terms of the GNU General Public License version 3, |
2638 | +# as published by the Free Software Foundation. |
2639 | +# |
2640 | +# Checkbox is distributed in the hope that it will be useful, |
2641 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2642 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2643 | +# GNU General Public License for more details. |
2644 | +# |
2645 | +# You should have received a copy of the GNU General Public License |
2646 | +# along with Checkbox. If not, see <http://www.gnu.org/licenses/>. |
2647 | + |
2648 | +""" |
2649 | +plainbox.impl.test_launcher |
2650 | +========================= |
2651 | + |
2652 | +Test definitions for plainbox.imlp.launcher module |
2653 | +""" |
2654 | + |
2655 | +from unittest import TestCase |
2656 | +from textwrap import dedent |
2657 | + |
2658 | +from plainbox.impl.secure.config import Unset |
2659 | + |
2660 | +from plainbox.impl.launcher import LauncherDefinition |
2661 | +from plainbox.impl.launcher import LauncherDefinition1 |
2662 | + |
2663 | + |
2664 | +class LauncherDefinitionTests(TestCase): |
2665 | + launcher_version_legacy = dedent(""" |
2666 | + [launcher] |
2667 | + """) |
2668 | + launcher_version_1 = dedent(""" |
2669 | + [launcher] |
2670 | + launcher_version = 1 |
2671 | + """) |
2672 | + launcher_version_future = dedent(""" |
2673 | + [launcher] |
2674 | + launcher_version = 2 |
2675 | + """) |
2676 | + |
2677 | + def test_get_concrete_launcher_legacy(self): |
2678 | + l = LauncherDefinition() |
2679 | + l.read_string(self.launcher_version_legacy) |
2680 | + with self.assertRaises(KeyError): |
2681 | + l.get_concrete_launcher() |
2682 | + |
2683 | + def test_get_concrete_launcher_launcher1(self): |
2684 | + l = LauncherDefinition() |
2685 | + l.read_string(self.launcher_version_1) |
2686 | + cls = l.get_concrete_launcher().__class__ |
2687 | + self.assertIs(cls, LauncherDefinition1) |
2688 | + |
2689 | + def test_get_concrete_launcher_future_raises(self): |
2690 | + l = LauncherDefinition() |
2691 | + l.read_string(self.launcher_version_future) |
2692 | + with self.assertRaises(KeyError): |
2693 | + l.get_concrete_launcher() |
2694 | + |
2695 | + |
2696 | +class LauncherDefinition1Tests(TestCase): |
2697 | + |
2698 | + def test_defaults(self): |
2699 | + empty_launcher = dedent(""" |
2700 | + [launcher] |
2701 | + launcher_version = 1 |
2702 | + """) |
2703 | + l = LauncherDefinition1() |
2704 | + l.read_string(empty_launcher) |
2705 | + self.assertEqual(l.api_version, '0.99') |
2706 | + self.assertEqual(l.app_id, 'checkbox-cli') |
2707 | + self.assertEqual(l.api_flags, ['restartable']) |
2708 | + self.assertEqual(l.test_plan_filters, ['*']) |
2709 | + self.assertEqual(l.test_plan_default_selection, Unset) |
2710 | + self.assertEqual(l.test_plan_forced, False) |
2711 | + self.assertEqual(l.test_selection_forced, False) |
2712 | + self.assertEqual(l.ui_type, 'interactive') |
2713 | + self.assertEqual(l.auto_retry, False) |
2714 | + self.assertEqual(l.max_attempts, 3) |
2715 | + self.assertEqual(l.delay_before_retry, 1) |
2716 | + self.assertEqual(l.restart_strategy, Unset) |
2717 | + |
2718 | + def test_smoke(self): |
2719 | + definition = dedent(""" |
2720 | + [launcher] |
2721 | + launcher_version = 1 |
2722 | + api_version = 0.99 |
2723 | + api_flags = restartable |
2724 | + app_id = FOOBAR |
2725 | + [test plan] |
2726 | + unit = 2000.the.chosen.one |
2727 | + filter = 2000*, 3000* tp_foo* |
2728 | + forced = yes |
2729 | + [test selection] |
2730 | + forced = yes |
2731 | + [ui] |
2732 | + type = silent |
2733 | + auto_retry = yes |
2734 | + max_attempts = 5 |
2735 | + delay_before_retry = 60 |
2736 | + [restart] |
2737 | + strategy = magic |
2738 | + [report:foo_report] |
2739 | + exporter = bar_exporter |
2740 | + transport = file |
2741 | + [exporter:bar_exporter] |
2742 | + unit = bar_exporter_unit |
2743 | + [transport:file] |
2744 | + path = /tmp/path |
2745 | + """) |
2746 | + l = LauncherDefinition1() |
2747 | + l.read_string(definition) |
2748 | + self.assertEqual(l.api_version, '0.99') |
2749 | + self.assertEqual(l.app_id, 'FOOBAR') |
2750 | + self.assertEqual(l.api_flags, ['restartable']) |
2751 | + self.assertEqual(l.test_plan_filters, ['2000*', '3000*', 'tp_foo*']) |
2752 | + self.assertEqual(l.test_plan_default_selection, '2000.the.chosen.one') |
2753 | + self.assertEqual(l.test_plan_forced, True) |
2754 | + self.assertEqual(l.test_selection_forced, True) |
2755 | + self.assertEqual(l.ui_type, 'silent') |
2756 | + self.assertEqual(l.auto_retry, True) |
2757 | + self.assertEqual(l.max_attempts, 5) |
2758 | + self.assertEqual(l.delay_before_retry, 60) |
2759 | + self.assertEqual(l.restart_strategy, 'magic') |
2760 | + self.assertEqual(l.reports, { |
2761 | + 'foo_report': {'exporter': 'bar_exporter', 'transport': 'file'}}) |
2762 | + self.assertEqual(l.exporters, { |
2763 | + 'bar_exporter': {'unit': 'bar_exporter_unit'}}) |
2764 | + self.assertEqual(l.transports, { |
2765 | + 'file': {'path': '/tmp/path'}}) |
2766 | diff --git a/plainbox/impl/unit/unit.py b/plainbox/impl/unit/unit.py |
2767 | index 8041683..cd7b365 100644 |
2768 | --- a/plainbox/impl/unit/unit.py |
2769 | +++ b/plainbox/impl/unit/unit.py |
2770 | @@ -36,6 +36,7 @@ from jinja2 import Template |
2771 | from plainbox.i18n import gettext as _ |
2772 | from plainbox.impl.decorators import cached_property |
2773 | from plainbox.impl.decorators import instance_method_lru_cache |
2774 | +from plainbox.impl.secure.config import Unset |
2775 | from plainbox.impl.secure.origin import Origin |
2776 | from plainbox.impl.secure.rfc822 import normalize_rfc822_value |
2777 | from plainbox.impl.symbol import Symbol |
2778 | @@ -608,7 +609,7 @@ class Unit(metaclass=UnitType): |
2779 | |
2780 | @instance_method_lru_cache(maxsize=None) |
2781 | def _checkbox_env(self): |
2782 | - if self.config is not None and self.config.environment: |
2783 | + if self.config is not None and self.config.environment is not Unset: |
2784 | return self.config.environment |
2785 | else: |
2786 | return {} |
2787 | diff --git a/plainbox/vendor/rpyc/__init__.py b/plainbox/vendor/rpyc/__init__.py |
2788 | index 4ad98f7..9cd71d5 100644 |
2789 | --- a/plainbox/vendor/rpyc/__init__.py |
2790 | +++ b/plainbox/vendor/rpyc/__init__.py |
2791 | @@ -46,7 +46,7 @@ from plainbox.vendor.rpyc.core import (SocketStream, TunneledSocketStream, PipeS |
2792 | Connection, Service, BaseNetref, AsyncResult, GenericException, |
2793 | AsyncResultTimeout, VoidService, SlaveService, MasterService, ClassicService) |
2794 | from plainbox.vendor.rpyc.utils.factory import (connect_stream, connect_channel, connect_pipes, |
2795 | - connect_stdpipes, connect, ssl_connect, list_services, discover, connect_by_service, connect_subproc, |
2796 | + connect_stdpipes, connect, ssl_connect, discover, connect_by_service, connect_subproc, |
2797 | connect_thread, ssh_connect) |
2798 | from plainbox.vendor.rpyc.utils.helpers import async_, timed, buffiter, BgServingThread, restricted |
2799 | from plainbox.vendor.rpyc.utils import classic |
2800 | diff --git a/plainbox/vendor/rpyc/core/async_.py b/plainbox/vendor/rpyc/core/async_.py |
2801 | index b46f4d0..fd69027 100644 |
2802 | --- a/plainbox/vendor/rpyc/core/async_.py |
2803 | +++ b/plainbox/vendor/rpyc/core/async_.py |
2804 | @@ -1,5 +1,4 @@ |
2805 | import time # noqa: F401 |
2806 | -from threading import Event |
2807 | from plainbox.vendor.rpyc.lib import Timeout |
2808 | from plainbox.vendor.rpyc.lib.compat import TimeoutError as AsyncResultTimeout |
2809 | |
2810 | @@ -13,14 +12,14 @@ class AsyncResult(object): |
2811 | |
2812 | def __init__(self, conn): |
2813 | self._conn = conn |
2814 | - self._is_ready = Event() |
2815 | + self._is_ready = False |
2816 | self._is_exc = None |
2817 | self._obj = None |
2818 | self._callbacks = [] |
2819 | self._ttl = Timeout(None) |
2820 | |
2821 | def __repr__(self): |
2822 | - if self._is_ready.is_set(): |
2823 | + if self._is_ready: |
2824 | state = "ready" |
2825 | elif self._is_exc: |
2826 | state = "error" |
2827 | @@ -28,14 +27,14 @@ class AsyncResult(object): |
2828 | state = "expired" |
2829 | else: |
2830 | state = "pending" |
2831 | - return "<AsyncResult object ({}) at 0x{:08x}>".format((state), (id(self))) |
2832 | + return "<AsyncResult object (%s) at 0x%08x>" % (state, id(self)) |
2833 | |
2834 | def __call__(self, is_exc, obj): |
2835 | if self.expired: |
2836 | return |
2837 | self._is_exc = is_exc |
2838 | self._obj = obj |
2839 | - self._is_ready.set() |
2840 | + self._is_ready = True |
2841 | for cb in self._callbacks: |
2842 | cb(self) |
2843 | del self._callbacks[:] |
2844 | @@ -44,9 +43,9 @@ class AsyncResult(object): |
2845 | """Waits for the result to arrive. If the AsyncResult object has an |
2846 | expiry set, and the result did not arrive within that timeout, |
2847 | an :class:`AsyncResultTimeout` exception is raised""" |
2848 | - while not self._is_ready.is_set() and not self._ttl.expired(): |
2849 | + while not self._is_ready and not self._ttl.expired(): |
2850 | self._conn.serve(self._ttl) |
2851 | - if not self._is_ready.is_set(): |
2852 | + if not self._is_ready: |
2853 | raise AsyncResultTimeout("result expired") |
2854 | |
2855 | def add_callback(self, func): |
2856 | @@ -57,7 +56,7 @@ class AsyncResult(object): |
2857 | |
2858 | :param func: the callback function to add |
2859 | """ |
2860 | - if self._is_ready.is_set(): |
2861 | + if self._is_ready: |
2862 | func(self) |
2863 | else: |
2864 | self._callbacks.append(func) |
2865 | @@ -73,12 +72,12 @@ class AsyncResult(object): |
2866 | @property |
2867 | def ready(self): |
2868 | """Indicates whether the result has arrived""" |
2869 | - if self._is_ready.is_set(): |
2870 | + if self._is_ready: |
2871 | return True |
2872 | if self._ttl.expired(): |
2873 | return False |
2874 | self._conn.poll_all() |
2875 | - return self._is_ready.is_set() |
2876 | + return self._is_ready |
2877 | |
2878 | @property |
2879 | def error(self): |
2880 | @@ -88,7 +87,7 @@ class AsyncResult(object): |
2881 | @property |
2882 | def expired(self): |
2883 | """Indicates whether the AsyncResult has expired""" |
2884 | - return not self._is_ready.is_set() and self._ttl.expired() |
2885 | + return not self._is_ready and self._ttl.expired() |
2886 | |
2887 | @property |
2888 | def value(self): |
2889 | diff --git a/plainbox/vendor/rpyc/core/brine.py b/plainbox/vendor/rpyc/core/brine.py |
2890 | index 20fa5c2..0545cb3 100644 |
2891 | --- a/plainbox/vendor/rpyc/core/brine.py |
2892 | +++ b/plainbox/vendor/rpyc/core/brine.py |
2893 | @@ -1,6 +1,6 @@ |
2894 | """*Brine* is a simple, fast and secure object serializer for **immutable** objects. |
2895 | |
2896 | -The following types are supported: ``int``, ``bool``, ``str``, ``float``, |
2897 | +The following types are supported: ``int``, ``long``, ``bool``, ``str``, ``float``, |
2898 | ``unicode``, ``bytes``, ``slice``, ``complex``, ``tuple`` (of simple types), |
2899 | ``frozenset`` (of simple types) as well as the following singletons: ``None``, |
2900 | ``NotImplemented``, and ``Ellipsis``. |
2901 | @@ -17,7 +17,7 @@ Example:: |
2902 | >>> x == z |
2903 | True |
2904 | """ |
2905 | -from plainbox.vendor.rpyc.lib.compat import Struct, BytesIO, BYTES_LITERAL |
2906 | +from plainbox.vendor.rpyc.lib.compat import Struct, BytesIO, is_py_3k, BYTES_LITERAL |
2907 | |
2908 | |
2909 | # singletons |
2910 | @@ -30,7 +30,7 @@ TAG_NOT_IMPLEMENTED = b"\x05" |
2911 | TAG_ELLIPSIS = b"\x06" |
2912 | # types |
2913 | TAG_UNICODE = b"\x08" |
2914 | -# deprecated w/ py2 support TAG_LONG = b"\x09" |
2915 | +TAG_LONG = b"\x09" |
2916 | TAG_STR1 = b"\x0a" |
2917 | TAG_STR2 = b"\x0b" |
2918 | TAG_STR3 = b"\x0c" |
2919 | @@ -49,7 +49,10 @@ TAG_FLOAT = b"\x18" |
2920 | TAG_SLICE = b"\x19" |
2921 | TAG_FSET = b"\x1a" |
2922 | TAG_COMPLEX = b"\x1b" |
2923 | -IMM_INTS = dict((i, bytes([i + 0x50])) for i in range(-0x30, 0xa0)) |
2924 | +if is_py_3k: |
2925 | + IMM_INTS = dict((i, bytes([i + 0x50])) for i in range(-0x30, 0xa0)) |
2926 | +else: |
2927 | + IMM_INTS = dict((i, chr(i + 0x50)) for i in range(-0x30, 0xa0)) |
2928 | |
2929 | I1 = Struct("!B") |
2930 | I4 = Struct("!L") |
2931 | @@ -153,6 +156,13 @@ def _dump_str(obj, stream): |
2932 | _dump_bytes(obj.encode("utf8"), stream) |
2933 | |
2934 | |
2935 | +if not is_py_3k: |
2936 | + @register(_dump_registry, long) # noqa: F821 |
2937 | + def _dump_long(obj, stream): |
2938 | + stream.append(TAG_LONG) |
2939 | + _dump_int(obj, stream) |
2940 | + |
2941 | + |
2942 | @register(_dump_registry, tuple) |
2943 | def _dump_tuple(obj, stream): |
2944 | lenobj = len(obj) |
2945 | @@ -175,7 +185,7 @@ def _dump_tuple(obj, stream): |
2946 | |
2947 | |
2948 | def _undumpable(obj, stream): |
2949 | - raise TypeError("cannot dump {}".format((obj))) |
2950 | + raise TypeError("cannot dump %r" % (obj,)) |
2951 | |
2952 | |
2953 | def _dump(obj, stream): |
2954 | @@ -219,6 +229,18 @@ def _load_empty_str(stream): |
2955 | return b"" |
2956 | |
2957 | |
2958 | +if is_py_3k: |
2959 | + @register(_load_registry, TAG_LONG) |
2960 | + def _load_long(stream): |
2961 | + obj = _load(stream) |
2962 | + return int(obj) |
2963 | +else: |
2964 | + @register(_load_registry, TAG_LONG) |
2965 | + def _load_long(stream): |
2966 | + obj = _load(stream) |
2967 | + return long(obj) # noqa: F821 |
2968 | + |
2969 | + |
2970 | @register(_load_registry, TAG_FLOAT) |
2971 | def _load_float(stream): |
2972 | return F8.unpack(stream.read(8))[0] |
2973 | @@ -294,10 +316,16 @@ def _load_tup_l1(stream): |
2974 | return tuple(_load(stream) for i in range(l)) |
2975 | |
2976 | |
2977 | -@register(_load_registry, TAG_TUP_L4) |
2978 | -def _load_tup_l4(stream): |
2979 | - l, = I4.unpack(stream.read(4)) |
2980 | - return tuple(_load(stream) for i in range(l)) |
2981 | +if is_py_3k: |
2982 | + @register(_load_registry, TAG_TUP_L4) |
2983 | + def _load_tup_l4(stream): |
2984 | + l, = I4.unpack(stream.read(4)) |
2985 | + return tuple(_load(stream) for i in range(l)) |
2986 | +else: |
2987 | + @register(_load_registry, TAG_TUP_L4) |
2988 | + def _load_tup_l4(stream): |
2989 | + l, = I4.unpack(stream.read(4)) |
2990 | + return tuple(_load(stream) for i in xrange(l)) # noqa |
2991 | |
2992 | |
2993 | @register(_load_registry, TAG_SLICE) |
2994 | @@ -357,7 +385,12 @@ def load(data): |
2995 | return _load(stream) |
2996 | |
2997 | |
2998 | -simple_types = frozenset([type(None), int, bool, float, bytes, str, complex, type(NotImplemented), type(Ellipsis)]) |
2999 | +if is_py_3k: |
3000 | + simple_types = frozenset([type(None), int, bool, float, bytes, str, complex, |
3001 | + type(NotImplemented), type(Ellipsis)]) |
3002 | +else: |
3003 | + simple_types = frozenset([type(None), int, long, bool, float, bytes, unicode, complex, # noqa: F821 |
3004 | + type(NotImplemented), type(Ellipsis)]) |
3005 | |
3006 | |
3007 | def dumpable(obj): |
3008 | diff --git a/plainbox/vendor/rpyc/core/consts.py b/plainbox/vendor/rpyc/core/consts.py |
3009 | index 300186b..31a532d 100644 |
3010 | --- a/plainbox/vendor/rpyc/core/consts.py |
3011 | +++ b/plainbox/vendor/rpyc/core/consts.py |
3012 | @@ -37,9 +37,6 @@ HANDLE_INSTANCECHECK = 20 |
3013 | # optimized exceptions |
3014 | EXC_STOP_ITERATION = 1 |
3015 | |
3016 | -# IO values |
3017 | -STREAM_CHUNK = 64000 # read/write chunk is 64KB, too large of a value will degrade response for other clients |
3018 | - |
3019 | # DEBUG |
3020 | # for k in globals().keys(): |
3021 | # globals()[k] = k |
3022 | diff --git a/plainbox/vendor/rpyc/core/netref.py b/plainbox/vendor/rpyc/core/netref.py |
3023 | index 9287906..fd15ef1 100644 |
3024 | --- a/plainbox/vendor/rpyc/core/netref.py |
3025 | +++ b/plainbox/vendor/rpyc/core/netref.py |
3026 | @@ -4,7 +4,7 @@ of *magic*, so beware. |
3027 | import sys |
3028 | import types |
3029 | from plainbox.vendor.rpyc.lib import get_methods, get_id_pack |
3030 | -from plainbox.vendor.rpyc.lib.compat import pickle, maxint, with_metaclass |
3031 | +from plainbox.vendor.rpyc.lib.compat import pickle, is_py_3k, maxint, with_metaclass |
3032 | from plainbox.vendor.rpyc.core import consts |
3033 | |
3034 | |
3035 | @@ -16,7 +16,6 @@ DELETED_ATTRS = frozenset([ |
3036 | '__array_struct__', '__array_interface__', |
3037 | ]) |
3038 | |
3039 | -"""the set of attributes that are local to the netref object""" |
3040 | LOCAL_ATTRS = frozenset([ |
3041 | '____conn__', '____id_pack__', '____refcount__', '__class__', '__cmp__', '__del__', '__delattr__', |
3042 | '__dir__', '__doc__', '__getattr__', '__getattribute__', '__hash__', '__instancecheck__', |
3043 | @@ -25,25 +24,39 @@ LOCAL_ATTRS = frozenset([ |
3044 | '__weakref__', '__dict__', '__methods__', '__exit__', |
3045 | '__eq__', '__ne__', '__lt__', '__gt__', '__le__', '__ge__', |
3046 | ]) | DELETED_ATTRS |
3047 | +"""the set of attributes that are local to the netref object""" |
3048 | |
3049 | -"""a list of types considered built-in (shared between connections) |
3050 | -this is needed because iterating the members of the builtins module is not enough, |
3051 | -some types (e.g NoneType) are not members of the builtins module. |
3052 | -TODO: this list is not complete. |
3053 | -""" |
3054 | _builtin_types = [ |
3055 | type, object, bool, complex, dict, float, int, list, slice, str, tuple, set, |
3056 | - frozenset, BaseException, Exception, type(None), types.BuiltinFunctionType, types.GeneratorType, |
3057 | + frozenset, Exception, type(None), types.BuiltinFunctionType, types.GeneratorType, |
3058 | types.MethodType, types.CodeType, types.FrameType, types.TracebackType, |
3059 | - types.ModuleType, types.FunctionType, types.MappingProxyType, |
3060 | + types.ModuleType, types.FunctionType, |
3061 | |
3062 | type(int.__add__), # wrapper_descriptor |
3063 | type((1).__add__), # method-wrapper |
3064 | type(iter([])), # listiterator |
3065 | type(iter(())), # tupleiterator |
3066 | type(iter(set())), # setiterator |
3067 | - bytes, bytearray, type(iter(range(10))), memoryview |
3068 | ] |
3069 | +"""a list of types considered built-in (shared between connections)""" |
3070 | + |
3071 | +try: |
3072 | + BaseException |
3073 | +except NameError: |
3074 | + pass |
3075 | +else: |
3076 | + _builtin_types.append(BaseException) |
3077 | + |
3078 | +if is_py_3k: |
3079 | + _builtin_types.extend([ |
3080 | + bytes, bytearray, type(iter(range(10))), memoryview, |
3081 | + ]) |
3082 | + xrange = range |
3083 | +else: |
3084 | + _builtin_types.extend([ |
3085 | + basestring, unicode, long, xrange, type(iter(xrange(10))), file, # noqa |
3086 | + types.InstanceType, types.ClassType, types.DictProxyType, |
3087 | + ]) |
3088 | _normalized_builtin_types = {} |
3089 | |
3090 | |
3091 | @@ -88,9 +101,9 @@ class NetrefMetaclass(type): |
3092 | |
3093 | def __repr__(self): |
3094 | if self.__module__: |
3095 | - return "<netref class '{}.{}'>".format((self.__module__), (self.__name__)) |
3096 | + return "<netref class '%s.%s'>" % (self.__module__, self.__name__) |
3097 | else: |
3098 | - return "<netref class '{}'>".format((self.__name__)) |
3099 | + return "<netref class '%s'>" % (self.__name__,) |
3100 | |
3101 | |
3102 | class BaseNetref(with_metaclass(NetrefMetaclass, object)): |
3103 | @@ -303,24 +316,19 @@ def class_factory(id_pack, methods): |
3104 | name_pack = id_pack[0] |
3105 | class_descriptor = None |
3106 | if name_pack is not None: |
3107 | - # attempt to resolve __class__ using normalized builtins first |
3108 | - _builtin_class = _normalized_builtin_types.get(name_pack) |
3109 | - if _builtin_class is not None: |
3110 | - class_descriptor = NetrefClass(_builtin_class) |
3111 | - # then by imported modules (this also tries all builtins under "builtins") |
3112 | - else: |
3113 | - _module = None |
3114 | - cursor = len(name_pack) |
3115 | - while cursor != -1: |
3116 | - _module = sys.modules.get(name_pack[:cursor]) |
3117 | - if _module is None: |
3118 | - cursor = name_pack[:cursor].rfind('.') |
3119 | - continue |
3120 | - _class_name = name_pack[cursor + 1:] |
3121 | - _class = getattr(_module, _class_name, None) |
3122 | - if _class is not None and hasattr(_class, '__class__'): |
3123 | - class_descriptor = NetrefClass(_class) |
3124 | - break |
3125 | + # attempt to resolve __class__ using sys.modules (i.e. builtins and imported modules) |
3126 | + _module = None |
3127 | + cursor = len(name_pack) |
3128 | + while cursor != -1: |
3129 | + _module = sys.modules.get(name_pack[:cursor]) |
3130 | + if _module is None: |
3131 | + cursor = name_pack[:cursor].rfind('.') |
3132 | + continue |
3133 | + _class_name = name_pack[cursor + 1:] |
3134 | + _class = getattr(_module, _class_name, None) |
3135 | + if _class is not None and hasattr(_class, '__class__'): |
3136 | + class_descriptor = NetrefClass(_class) |
3137 | + break |
3138 | ns['__class__'] = class_descriptor |
3139 | netref_name = class_descriptor.owner.__name__ if class_descriptor is not None else name_pack |
3140 | # create methods that must perform a syncreq |
3141 | diff --git a/plainbox/vendor/rpyc/core/protocol.py b/plainbox/vendor/rpyc/core/protocol.py |
3142 | index 703d499..56cd9a9 100644 |
3143 | --- a/plainbox/vendor/rpyc/core/protocol.py |
3144 | +++ b/plainbox/vendor/rpyc/core/protocol.py |
3145 | @@ -8,7 +8,7 @@ import gc # noqa: F401 |
3146 | |
3147 | from threading import Lock, Condition |
3148 | from plainbox.vendor.rpyc.lib import spawn, Timeout, get_methods, get_id_pack |
3149 | -from plainbox.vendor.rpyc.lib.compat import pickle, next, maxint, select_error, acquire_lock # noqa: F401 |
3150 | +from plainbox.vendor.rpyc.lib.compat import pickle, next, is_py_3k, maxint, select_error, acquire_lock # noqa: F401 |
3151 | from plainbox.vendor.rpyc.lib.colls import WeakValueDict, RefCountingColl |
3152 | from plainbox.vendor.rpyc.core import consts, brine, vinegar, netref |
3153 | from plainbox.vendor.rpyc.core.async_ import AsyncResult |
3154 | @@ -39,7 +39,7 @@ DEFAULT_CONFIG = dict( |
3155 | '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', |
3156 | '__rxor__', '__setitem__', '__setslice__', '__str__', '__sub__', |
3157 | '__truediv__', '__xor__', 'next', '__length_hint__', '__enter__', |
3158 | - '__exit__', '__next__', '__format__']), |
3159 | + '__exit__', '__next__', ]), |
3160 | exposed_prefix="exposed_", |
3161 | allow_getattr=True, |
3162 | allow_setattr=False, |
3163 | @@ -60,8 +60,6 @@ DEFAULT_CONFIG = dict( |
3164 | endpoints=None, |
3165 | logger=None, |
3166 | sync_request_timeout=30, |
3167 | - before_closed=None, |
3168 | - close_catchall=False, |
3169 | ) |
3170 | """ |
3171 | The default configuration dictionary of the protocol. You can override these parameters |
3172 | @@ -140,7 +138,7 @@ class Connection(object): |
3173 | self._config = DEFAULT_CONFIG.copy() |
3174 | self._config.update(config) |
3175 | if self._config["connid"] is None: |
3176 | - self._config["connid"] = "conn{}".format((next(_connection_id_generator))) |
3177 | + self._config["connid"] = "conn%d" % (next(_connection_id_generator),) |
3178 | |
3179 | self._HANDLERS = self._request_handlers() |
3180 | self._channel = channel |
3181 | @@ -169,7 +167,7 @@ class Connection(object): |
3182 | |
3183 | def __repr__(self): |
3184 | a, b = object.__repr__(self).split(" object ") |
3185 | - return "{} {!r} object {}".format((a), (self._config['connid']), (b)) |
3186 | + return "%s %r object %s" % (a, self._config["connid"], b) |
3187 | |
3188 | def _cleanup(self, _anyway=True): # IO |
3189 | if self._closed and not _anyway: |
3190 | @@ -188,19 +186,17 @@ class Connection(object): |
3191 | # self._config.clear() |
3192 | del self._HANDLERS |
3193 | |
3194 | - def close(self): # IO |
3195 | + def close(self, _catchall=True): # IO |
3196 | """closes the connection, releasing all held resources""" |
3197 | if self._closed: |
3198 | return |
3199 | + self._closed = True |
3200 | try: |
3201 | - self._closed = True |
3202 | - if self._config.get("before_closed"): |
3203 | - self._config["before_closed"](self.root) |
3204 | self._async_request(consts.HANDLE_CLOSE) |
3205 | except EOFError: |
3206 | pass |
3207 | except Exception: |
3208 | - if not self._config["close_catchall"]: |
3209 | + if not _catchall: |
3210 | raise |
3211 | finally: |
3212 | self._cleanup(_anyway=True) |
3213 | @@ -298,7 +294,7 @@ class Connection(object): |
3214 | proxy = self._netref_factory(id_pack) |
3215 | self._proxy_cache[id_pack] = proxy |
3216 | return proxy |
3217 | - raise ValueError("invalid label {!r}".format((label))) |
3218 | + raise ValueError("invalid label %r" % (label,)) |
3219 | |
3220 | def _netref_factory(self, id_pack): # boxing |
3221 | """id_pack is for remote, so when class id fails to directly match """ |
3222 | @@ -367,7 +363,7 @@ class Connection(object): |
3223 | obj = self._unbox_exc(args) |
3224 | self._seq_request_callback(msg, seq, True, obj) |
3225 | else: |
3226 | - raise ValueError("invalid message type: {!r}".format((msg))) |
3227 | + raise ValueError("invalid message type: %r" % (msg,)) |
3228 | |
3229 | def serve(self, timeout=1, wait_for_lock=True): # serving |
3230 | """Serves a single request or reply that arrives within the given |
3231 | @@ -492,7 +488,7 @@ class Connection(object): |
3232 | """ |
3233 | timeout = kwargs.pop("timeout", None) |
3234 | if kwargs: |
3235 | - raise TypeError("got unexpected keyword argument(s) {list(kwargs.keys()}") |
3236 | + raise TypeError("got unexpected keyword argument(s) %s" % (list(kwargs.keys()),)) |
3237 | res = AsyncResult(self) |
3238 | self._async_request(handler, args, res) |
3239 | if timeout is not None: |
3240 | @@ -509,7 +505,7 @@ class Connection(object): |
3241 | def _check_attr(self, obj, name, perm): # attribute access |
3242 | config = self._config |
3243 | if not config[perm]: |
3244 | - raise AttributeError("cannot access {!r}".format((name))) |
3245 | + raise AttributeError("cannot access %r" % (name,)) |
3246 | prefix = config["allow_exposed_attrs"] and config["exposed_prefix"] |
3247 | plain = config["allow_all_attrs"] |
3248 | plain |= config["allow_exposed_attrs"] and name.startswith(prefix) |
3249 | @@ -522,13 +518,18 @@ class Connection(object): |
3250 | return prefix + name |
3251 | if plain: |
3252 | return name # chance for better traceback |
3253 | - raise AttributeError("cannot access {!r}".format((name))) |
3254 | + raise AttributeError("cannot access %r" % (name,)) |
3255 | |
3256 | def _access_attr(self, obj, name, args, overrider, param, default): # attribute access |
3257 | - if type(name) is bytes: |
3258 | - name = str(name, "utf8") |
3259 | - elif type(name) is not str: |
3260 | - raise TypeError("name must be a string") |
3261 | + if is_py_3k: |
3262 | + if type(name) is bytes: |
3263 | + name = str(name, "utf8") |
3264 | + elif type(name) is not str: |
3265 | + raise TypeError("name must be a string") |
3266 | + else: |
3267 | + if type(name) not in (str, unicode): # noqa |
3268 | + raise TypeError("name must be a string") |
3269 | + name = str(name) # IronPython issue #10 + py3k issue |
3270 | accessor = getattr(type(obj), overrider, None) |
3271 | if accessor is None: |
3272 | accessor = default |
3273 | @@ -637,7 +638,7 @@ class Connection(object): |
3274 | # since __mro__ is not a safe attribute the request is forwarded using the proxy connection |
3275 | # relates to issue #346 or tests.test_netref_hierachy.Test_Netref_Hierarchy.test_StandardError |
3276 | conn = obj.____conn__ |
3277 | - return conn.sync_request(consts.HANDLE_INSPECT, other_id_pack) |
3278 | + return conn.sync_request(consts.HANDLE_INSPECT, id_pack) |
3279 | # Create a name pack which would be familiar here and see if there is a hit |
3280 | other_id_pack2 = (other_id_pack[0], other_id_pack[1], 0) |
3281 | if other_id_pack[0] in netref.builtin_classes_cache: |
3282 | diff --git a/plainbox/vendor/rpyc/core/service.py b/plainbox/vendor/rpyc/core/service.py |
3283 | index 6159b75..55526be 100644 |
3284 | --- a/plainbox/vendor/rpyc/core/service.py |
3285 | +++ b/plainbox/vendor/rpyc/core/service.py |
3286 | @@ -9,7 +9,7 @@ can interoperate, you're good to go. |
3287 | from functools import partial |
3288 | |
3289 | from plainbox.vendor.rpyc.lib import hybridmethod |
3290 | -from plainbox.vendor.rpyc.lib.compat import execute |
3291 | +from plainbox.vendor.rpyc.lib.compat import execute, is_py_3k |
3292 | from plainbox.vendor.rpyc.core.protocol import Connection |
3293 | |
3294 | |
3295 | @@ -137,10 +137,7 @@ class ModuleNamespace(object): |
3296 | return self.__cache[name] |
3297 | |
3298 | def __getattr__(self, name): |
3299 | - try: |
3300 | - return self[name] |
3301 | - except ImportError: |
3302 | - raise AttributeError(name) |
3303 | + return self[name] |
3304 | |
3305 | |
3306 | class Slave(object): |
3307 | @@ -218,13 +215,14 @@ class MasterService(Service): |
3308 | @staticmethod |
3309 | def _install(conn, slave): |
3310 | modules = ModuleNamespace(slave.getmodule) |
3311 | + builtin = modules.builtins if is_py_3k else modules.__builtin__ |
3312 | conn.modules = modules |
3313 | conn.eval = slave.eval |
3314 | conn.execute = slave.execute |
3315 | conn.namespace = slave.namespace |
3316 | - conn.builtins = modules.builtins |
3317 | - conn.builtin = modules.builtins # TODO: cruft from py2 that requires cleanup elsewhere and CHANGELOG note |
3318 | - from rpyc.utils.classic import teleport_function |
3319 | + conn.builtin = builtin |
3320 | + conn.builtins = builtin |
3321 | + from plainbox.vendor.rpyc.utils.classic import teleport_function |
3322 | conn.teleport = partial(teleport_function, conn) |
3323 | |
3324 | |
3325 | diff --git a/plainbox/vendor/rpyc/core/stream.py b/plainbox/vendor/rpyc/core/stream.py |
3326 | index 7d3cab7..6c8d790 100644 |
3327 | --- a/plainbox/vendor/rpyc/core/stream.py |
3328 | +++ b/plainbox/vendor/rpyc/core/stream.py |
3329 | @@ -8,7 +8,6 @@ import socket |
3330 | import errno |
3331 | from plainbox.vendor.rpyc.lib import safe_import, Timeout, socket_backoff_connect |
3332 | from plainbox.vendor.rpyc.lib.compat import poll, select_error, BYTES_LITERAL, get_exc_errno, maxint # noqa: F401 |
3333 | -from plainbox.vendor.rpyc.core.consts import STREAM_CHUNK |
3334 | win32file = safe_import("win32file") |
3335 | win32pipe = safe_import("win32pipe") |
3336 | win32event = safe_import("win32event") |
3337 | @@ -112,7 +111,7 @@ class SocketStream(Stream): |
3338 | """A stream over a socket""" |
3339 | |
3340 | __slots__ = ("sock",) |
3341 | - MAX_IO_CHUNK = STREAM_CHUNK |
3342 | + MAX_IO_CHUNK = 64000 # read/write chunk is 64KB, too large of a value will degrade response for other clients |
3343 | |
3344 | def __init__(self, sock): |
3345 | self.sock = sock |
3346 | @@ -292,7 +291,7 @@ class PipeStream(Stream): |
3347 | """A stream over two simplex pipes (one used to input, another for output)""" |
3348 | |
3349 | __slots__ = ("incoming", "outgoing") |
3350 | - MAX_IO_CHUNK = STREAM_CHUNK |
3351 | + MAX_IO_CHUNK = 32000 |
3352 | |
3353 | def __init__(self, incoming, outgoing): |
3354 | outgoing.flush() |
3355 | @@ -370,7 +369,7 @@ class Win32PipeStream(Stream): |
3356 | |
3357 | __slots__ = ("incoming", "outgoing", "_fileno", "_keepalive") |
3358 | PIPE_BUFFER_SIZE = 130000 |
3359 | - MAX_IO_CHUNK = STREAM_CHUNK |
3360 | + MAX_IO_CHUNK = 32000 |
3361 | |
3362 | def __init__(self, incoming, outgoing): |
3363 | import msvcrt |
3364 | diff --git a/plainbox/vendor/rpyc/core/vinegar.py b/plainbox/vendor/rpyc/core/vinegar.py |
3365 | index db1f3e1..b766b58 100644 |
3366 | --- a/plainbox/vendor/rpyc/core/vinegar.py |
3367 | +++ b/plainbox/vendor/rpyc/core/vinegar.py |
3368 | @@ -22,6 +22,7 @@ except ImportError: |
3369 | from plainbox.vendor.rpyc.core import brine |
3370 | from plainbox.vendor.rpyc.core import consts |
3371 | from plainbox.vendor.rpyc import version |
3372 | +from plainbox.vendor.rpyc.lib.compat import is_py_3k |
3373 | |
3374 | |
3375 | REMOTE_LINE_START = "\n\n========= Remote Traceback " |
3376 | @@ -29,6 +30,13 @@ REMOTE_LINE_END = " =========\n" |
3377 | REMOTE_LINE = "{0}({{}}){1}".format(REMOTE_LINE_START, REMOTE_LINE_END) |
3378 | |
3379 | |
3380 | +try: |
3381 | + BaseException |
3382 | +except NameError: |
3383 | + # python 2.4 compatible |
3384 | + BaseException = Exception |
3385 | + |
3386 | + |
3387 | def dump(typ, val, tb, include_local_traceback, include_local_version): |
3388 | """Dumps the given exceptions info, as returned by ``sys.exc_info()`` |
3389 | |
3390 | @@ -127,15 +135,23 @@ def load(val, import_custom_exceptions, instantiate_custom_exceptions, instantia |
3391 | else: |
3392 | cls = None |
3393 | |
3394 | - if not isinstance(cls, type) or not issubclass(cls, BaseException): |
3395 | - cls = None |
3396 | + if is_py_3k: |
3397 | + if not isinstance(cls, type) or not issubclass(cls, BaseException): |
3398 | + cls = None |
3399 | + else: |
3400 | + if not isinstance(cls, (type, ClassType)): |
3401 | + cls = None |
3402 | + elif issubclass(cls, ClassType) and not instantiate_oldstyle_exceptions: |
3403 | + cls = None |
3404 | + elif not issubclass(cls, BaseException): |
3405 | + cls = None |
3406 | |
3407 | if cls is None: |
3408 | - fullname = "{}.{}".format((modname), (clsname)) |
3409 | + fullname = "%s.%s" % (modname, clsname) |
3410 | # py2: `type()` expects `str` not `unicode`! |
3411 | fullname = str(fullname) |
3412 | if fullname not in _generic_exceptions_cache: |
3413 | - fakemodule = {"__module__": "{}/{}".format((__name__), (modname))} |
3414 | + fakemodule = {"__module__": "%s/%s" % (__name__, modname)} |
3415 | if isinstance(GenericException, ClassType): |
3416 | _generic_exceptions_cache[fullname] = ClassType(fullname, (GenericException,), fakemodule) |
3417 | else: |
3418 | diff --git a/plainbox/vendor/rpyc/lib/__init__.py b/plainbox/vendor/rpyc/lib/__init__.py |
3419 | index 6462d36..d96656b 100644 |
3420 | --- a/plainbox/vendor/rpyc/lib/__init__.py |
3421 | +++ b/plainbox/vendor/rpyc/lib/__init__.py |
3422 | @@ -19,8 +19,8 @@ class MissingModule(object): |
3423 | |
3424 | def __getattr__(self, name): |
3425 | if name.startswith("__"): # issue 71 |
3426 | - raise AttributeError("module {!r} not found".format((self.__name))) |
3427 | - raise ImportError("module {!r} not found".format((self.__name))) |
3428 | + raise AttributeError("module %r not found" % (self.__name,)) |
3429 | + raise ImportError("module %r not found" % (self.__name,)) |
3430 | |
3431 | def __bool__(self): |
3432 | return False |
3433 | @@ -96,7 +96,7 @@ def spawn_waitready(init, main): |
3434 | return thread, stack.pop() |
3435 | |
3436 | |
3437 | -class Timeout(object): |
3438 | +class Timeout: |
3439 | |
3440 | def __init__(self, timeout): |
3441 | if isinstance(timeout, Timeout): |
3442 | @@ -175,7 +175,7 @@ def get_id_pack(obj): |
3443 | else: |
3444 | name_pack = '{0}.{1}'.format(obj.__class__.__module__, obj.__name__) |
3445 | elif inspect.ismodule(obj): |
3446 | - name_pack = '{0}.{1}'.format(obj.__module__, obj.__name__) |
3447 | + name_pack = '{0}.{1}'.format(obj__module__, obj.__name__) |
3448 | print(name_pack) |
3449 | elif hasattr(obj, '__module__'): |
3450 | name_pack = '{0}.{1}'.format(obj.__module__, obj.__name__) |
3451 | diff --git a/plainbox/vendor/rpyc/lib/compat.py b/plainbox/vendor/rpyc/lib/compat.py |
3452 | index 63c48fe..35be8fd 100644 |
3453 | --- a/plainbox/vendor/rpyc/lib/compat.py |
3454 | +++ b/plainbox/vendor/rpyc/lib/compat.py |
3455 | @@ -7,7 +7,6 @@ import time |
3456 | |
3457 | is_py_3k = (sys.version_info[0] >= 3) |
3458 | is_py_gte38 = is_py_3k and (sys.version_info[1] >= 8) |
3459 | -is_py_gte37 = is_py_3k and (sys.version_info[1] >= 7) |
3460 | |
3461 | |
3462 | if is_py_3k: |
3463 | diff --git a/plainbox/vendor/rpyc/utils/authenticators.py b/plainbox/vendor/rpyc/utils/authenticators.py |
3464 | index 63ee828..0d97882 100644 |
3465 | --- a/plainbox/vendor/rpyc/utils/authenticators.py |
3466 | +++ b/plainbox/vendor/rpyc/utils/authenticators.py |
3467 | @@ -71,7 +71,7 @@ class SSLAuthenticator(object): |
3468 | else: |
3469 | self.cert_reqs = cert_reqs |
3470 | if ssl_version is None: |
3471 | - self.ssl_version = ssl.PROTOCOL_TLS |
3472 | + self.ssl_version = ssl.PROTOCOL_TLSv1 |
3473 | else: |
3474 | self.ssl_version = ssl_version |
3475 | |
3476 | diff --git a/plainbox/vendor/rpyc/utils/classic.py b/plainbox/vendor/rpyc/utils/classic.py |
3477 | index 3903a79..2f97dcf 100644 |
3478 | --- a/plainbox/vendor/rpyc/utils/classic.py |
3479 | +++ b/plainbox/vendor/rpyc/utils/classic.py |
3480 | @@ -2,11 +2,10 @@ from __future__ import with_statement |
3481 | import sys |
3482 | import os |
3483 | import inspect |
3484 | -from plainbox.vendor.rpyc.lib.compat import pickle, execute |
3485 | +from plainbox.vendor.rpyc.lib.compat import pickle, execute, is_py_3k # noqa: F401 |
3486 | from plainbox.vendor.rpyc.core.service import ClassicService, Slave |
3487 | from plainbox.vendor.rpyc.utils import factory |
3488 | from plainbox.vendor.rpyc.core.service import ModuleNamespace # noqa: F401 |
3489 | -from plainbox.vendor.rpyc.core.consts import STREAM_CHUNK |
3490 | from contextlib import contextmanager |
3491 | |
3492 | |
3493 | @@ -172,7 +171,7 @@ def connect_multiprocess(args={}): |
3494 | # remoting utilities |
3495 | # =============================================================================== |
3496 | |
3497 | -def upload(conn, localpath, remotepath, filter=None, ignore_invalid=False, chunk_size=STREAM_CHUNK): |
3498 | +def upload(conn, localpath, remotepath, filter=None, ignore_invalid=False, chunk_size=16000): |
3499 | """uploads a file or a directory to the given remote path |
3500 | |
3501 | :param localpath: the local file or directory |
3502 | @@ -187,10 +186,10 @@ def upload(conn, localpath, remotepath, filter=None, ignore_invalid=False, chunk |
3503 | upload_file(conn, localpath, remotepath, chunk_size) |
3504 | else: |
3505 | if not ignore_invalid: |
3506 | - raise ValueError("cannot upload {!r}".format((localpath))) |
3507 | + raise ValueError("cannot upload %r" % (localpath,)) |
3508 | |
3509 | |
3510 | -def upload_file(conn, localpath, remotepath, chunk_size=STREAM_CHUNK): |
3511 | +def upload_file(conn, localpath, remotepath, chunk_size=16000): |
3512 | with open(localpath, "rb") as lf: |
3513 | with conn.builtin.open(remotepath, "wb") as rf: |
3514 | while True: |
3515 | @@ -200,7 +199,7 @@ def upload_file(conn, localpath, remotepath, chunk_size=STREAM_CHUNK): |
3516 | rf.write(buf) |
3517 | |
3518 | |
3519 | -def upload_dir(conn, localpath, remotepath, filter=None, chunk_size=STREAM_CHUNK): |
3520 | +def upload_dir(conn, localpath, remotepath, filter=None, chunk_size=16000): |
3521 | if not conn.modules.os.path.isdir(remotepath): |
3522 | conn.modules.os.makedirs(remotepath) |
3523 | for fn in os.listdir(localpath): |
3524 | @@ -210,7 +209,7 @@ def upload_dir(conn, localpath, remotepath, filter=None, chunk_size=STREAM_CHUNK |
3525 | upload(conn, lfn, rfn, filter=filter, ignore_invalid=True, chunk_size=chunk_size) |
3526 | |
3527 | |
3528 | -def download(conn, remotepath, localpath, filter=None, ignore_invalid=False, chunk_size=STREAM_CHUNK): |
3529 | +def download(conn, remotepath, localpath, filter=None, ignore_invalid=False, chunk_size=16000): |
3530 | """ |
3531 | download a file or a directory to the given remote path |
3532 | |
3533 | @@ -221,15 +220,15 @@ def download(conn, remotepath, localpath, filter=None, ignore_invalid=False, chu |
3534 | :param chunk_size: the IO chunk size |
3535 | """ |
3536 | if conn.modules.os.path.isdir(remotepath): |
3537 | - download_dir(conn, remotepath, localpath, filter, chunk_size) |
3538 | + download_dir(conn, remotepath, localpath, filter) |
3539 | elif conn.modules.os.path.isfile(remotepath): |
3540 | download_file(conn, remotepath, localpath, chunk_size) |
3541 | else: |
3542 | if not ignore_invalid: |
3543 | - raise ValueError("cannot download {!r}".format((remotepath))) |
3544 | + raise ValueError("cannot download %r" % (remotepath,)) |
3545 | |
3546 | |
3547 | -def download_file(conn, remotepath, localpath, chunk_size=STREAM_CHUNK): |
3548 | +def download_file(conn, remotepath, localpath, chunk_size=16000): |
3549 | with conn.builtin.open(remotepath, "rb") as rf: |
3550 | with open(localpath, "wb") as lf: |
3551 | while True: |
3552 | @@ -239,17 +238,17 @@ def download_file(conn, remotepath, localpath, chunk_size=STREAM_CHUNK): |
3553 | lf.write(buf) |
3554 | |
3555 | |
3556 | -def download_dir(conn, remotepath, localpath, filter=None, chunk_size=STREAM_CHUNK): |
3557 | +def download_dir(conn, remotepath, localpath, filter=None, chunk_size=16000): |
3558 | if not os.path.isdir(localpath): |
3559 | os.makedirs(localpath) |
3560 | for fn in conn.modules.os.listdir(remotepath): |
3561 | if not filter or filter(fn): |
3562 | rfn = conn.modules.os.path.join(remotepath, fn) |
3563 | lfn = os.path.join(localpath, fn) |
3564 | - download(conn, rfn, lfn, filter=filter, ignore_invalid=True, chunk_size=chunk_size) |
3565 | + download(conn, rfn, lfn, filter=filter, ignore_invalid=True) |
3566 | |
3567 | |
3568 | -def upload_package(conn, module, remotepath=None, chunk_size=STREAM_CHUNK): |
3569 | +def upload_package(conn, module, remotepath=None, chunk_size=16000): |
3570 | """ |
3571 | uploads a module or a package to the remote party |
3572 | |
3573 | @@ -385,17 +384,12 @@ def teleport_function(conn, func, globals=None, def_=True): |
3574 | import os |
3575 | return (os.getpid() + y) * x |
3576 | |
3577 | - .. note:: While it is not forbidden to "teleport" functions across different Python |
3578 | - versions, it *may* result in errors due to Python bytecode differences. It is |
3579 | - recommended to ensure both the client and the server are of the same Python |
3580 | - version when using this function. |
3581 | - |
3582 | :param conn: the RPyC connection |
3583 | :param func: the function object to be delivered to the other party |
3584 | """ |
3585 | if globals is None: |
3586 | globals = conn.namespace |
3587 | - from rpyc.utils.teleportation import export_function |
3588 | + from plainbox.vendor.rpyc.utils.teleportation import export_function |
3589 | exported = export_function(func) |
3590 | return conn.modules["rpyc.utils.teleportation"].import_function( |
3591 | exported, globals, def_) |
3592 | diff --git a/plainbox/vendor/rpyc/utils/factory.py b/plainbox/vendor/rpyc/utils/factory.py |
3593 | index aa59a7e..23acc1f 100644 |
3594 | --- a/plainbox/vendor/rpyc/utils/factory.py |
3595 | +++ b/plainbox/vendor/rpyc/utils/factory.py |
3596 | @@ -5,7 +5,7 @@ cases) |
3597 | from __future__ import with_statement |
3598 | import socket |
3599 | from contextlib import closing |
3600 | -from functools import partial |
3601 | + |
3602 | import threading |
3603 | try: |
3604 | from thread import interrupt_main |
3605 | @@ -19,7 +19,7 @@ except ImportError: |
3606 | |
3607 | from plainbox.vendor.rpyc.core.channel import Channel |
3608 | from plainbox.vendor.rpyc.core.stream import SocketStream, TunneledSocketStream, PipeStream |
3609 | -from plainbox.vendor.rpyc.core.service import VoidService, MasterService, SlaveService |
3610 | +from plainbox.vendor.rpyc.core.service import VoidService |
3611 | from plainbox.vendor.rpyc.utils.registry import UDPRegistryClient |
3612 | from plainbox.vendor.rpyc.lib import safe_import, spawn |
3613 | ssl = safe_import("ssl") |
3614 | @@ -29,10 +29,6 @@ class DiscoveryError(Exception): |
3615 | pass |
3616 | |
3617 | |
3618 | -class ForbiddenError(Exception): |
3619 | - pass |
3620 | - |
3621 | - |
3622 | # ------------------------------------------------------------------------------ |
3623 | # API |
3624 | # ------------------------------------------------------------------------------ |
3625 | @@ -220,26 +216,16 @@ def discover(service_name, host=None, registrar=None, timeout=2): |
3626 | registrar = UDPRegistryClient(timeout=timeout) |
3627 | addrs = registrar.discover(service_name) |
3628 | if not addrs: |
3629 | - raise DiscoveryError("no servers exposing {!r} were found".format((service_name))) |
3630 | + raise DiscoveryError("no servers exposing %r were found" % (service_name,)) |
3631 | if host: |
3632 | ips = socket.gethostbyname_ex(host)[2] |
3633 | addrs = [(h, p) for h, p in addrs if h in ips] |
3634 | if not addrs: |
3635 | - raise DiscoveryError("no servers exposing {} were found on {}".format((service_name), (host))) |
3636 | + raise DiscoveryError("no servers exposing %r were found on %r" % (service_name, host)) |
3637 | return addrs |
3638 | |
3639 | |
3640 | -def list_services(registrar=None, timeout=2): |
3641 | - services = () |
3642 | - if registrar is None: |
3643 | - registrar = UDPRegistryClient(timeout=timeout) |
3644 | - services = registrar.list() |
3645 | - if services is None: |
3646 | - raise ForbiddenError("Registry doesn't allow listing") |
3647 | - return services |
3648 | - |
3649 | - |
3650 | -def connect_by_service(service_name, host=None, registrar=None, timeout=2, service=VoidService, config={}): |
3651 | +def connect_by_service(service_name, host=None, service=VoidService, config={}): |
3652 | """create a connection to an arbitrary server that exposes the requested service |
3653 | |
3654 | :param service_name: the service to discover |
3655 | @@ -254,13 +240,13 @@ def connect_by_service(service_name, host=None, registrar=None, timeout=2, servi |
3656 | # some of which could be dead. We iterate over the list returned and return the first |
3657 | # one we could connect to. If none of the registered servers is responsive we re-throw |
3658 | # the exception |
3659 | - addrs = discover(service_name, host=host, registrar=registrar, timeout=timeout) |
3660 | + addrs = discover(service_name, host=host) |
3661 | for host, port in addrs: |
3662 | try: |
3663 | return connect(host, port, service, config=config) |
3664 | except socket.error: |
3665 | pass |
3666 | - raise DiscoveryError("All services are down: {}".format((addrs))) |
3667 | + raise DiscoveryError("All services are down: %s" % (addrs,)) |
3668 | |
3669 | |
3670 | def connect_subproc(args, service=VoidService, config={}): |
3671 | @@ -278,27 +264,6 @@ def connect_subproc(args, service=VoidService, config={}): |
3672 | return conn |
3673 | |
3674 | |
3675 | -def _server(listener, remote_service, remote_config, args=None): |
3676 | - try: |
3677 | - with closing(listener): |
3678 | - client = listener.accept()[0] |
3679 | - conn = connect_stream(SocketStream(client), service=remote_service, config=remote_config) |
3680 | - if isinstance(args, dict): |
3681 | - _oldstyle = (MasterService, SlaveService) |
3682 | - is_newstyle = isinstance(remote_service, type) and not issubclass(remote_service, _oldstyle) |
3683 | - is_newstyle |= not isinstance(remote_service, type) and not isinstance(remote_service, _oldstyle) |
3684 | - is_voidservice = isinstance(remote_service, type) and issubclass(remote_service, VoidService) |
3685 | - is_voidservice |= not isinstance(remote_service, type) and isinstance(remote_service, VoidService) |
3686 | - if is_newstyle and not is_voidservice: |
3687 | - conn._local_root.exposed_namespace.update(args) |
3688 | - elif not is_voidservice: |
3689 | - conn._local_root.namespace.update(args) |
3690 | - |
3691 | - conn.serve_all() |
3692 | - except KeyboardInterrupt: |
3693 | - interrupt_main() |
3694 | - |
3695 | - |
3696 | def connect_thread(service=VoidService, config={}, remote_service=VoidService, remote_config={}): |
3697 | """starts an rpyc server on a new thread, bound to an arbitrary port, |
3698 | and connects to it over a socket. |
3699 | @@ -311,8 +276,18 @@ def connect_thread(service=VoidService, config={}, remote_service=VoidService, r |
3700 | listener = socket.socket() |
3701 | listener.bind(("localhost", 0)) |
3702 | listener.listen(1) |
3703 | - remote_server = partial(_server, listener, remote_service, remote_config) |
3704 | - spawn(remote_server) |
3705 | + |
3706 | + def server(listener=listener): |
3707 | + with closing(listener): |
3708 | + client = listener.accept()[0] |
3709 | + conn = connect_stream(SocketStream(client), service=remote_service, |
3710 | + config=remote_config) |
3711 | + try: |
3712 | + conn.serve_all() |
3713 | + except KeyboardInterrupt: |
3714 | + interrupt_main() |
3715 | + |
3716 | + spawn(server) |
3717 | host, port = listener.getsockname() |
3718 | return connect(host, port, service=service, config=config) |
3719 | |
3720 | @@ -336,8 +311,19 @@ def connect_multiprocess(service=VoidService, config={}, remote_service=VoidServ |
3721 | listener = socket.socket() |
3722 | listener.bind(("localhost", 0)) |
3723 | listener.listen(1) |
3724 | - remote_server = partial(_server, listener, remote_service, remote_config, args) |
3725 | - t = Process(target=remote_server) |
3726 | + |
3727 | + def server(listener=listener, args=args): |
3728 | + with closing(listener): |
3729 | + client = listener.accept()[0] |
3730 | + conn = connect_stream(SocketStream(client), service=remote_service, config=remote_config) |
3731 | + try: |
3732 | + for k in args: |
3733 | + conn._local_root.exposed_namespace[k] = args[k] |
3734 | + conn.serve_all() |
3735 | + except KeyboardInterrupt: |
3736 | + interrupt_main() |
3737 | + |
3738 | + t = Process(target=server) |
3739 | t.start() |
3740 | host, port = listener.getsockname() |
3741 | return connect(host, port, service=service, config=config) |
3742 | diff --git a/plainbox/vendor/rpyc/utils/helpers.py b/plainbox/vendor/rpyc/utils/helpers.py |
3743 | index 67be083..521e226 100644 |
3744 | --- a/plainbox/vendor/rpyc/utils/helpers.py |
3745 | +++ b/plainbox/vendor/rpyc/utils/helpers.py |
3746 | @@ -34,7 +34,7 @@ def buffiter(obj, chunk=10, max_chunk=1000, factor=2): |
3747 | print id, name, dob |
3748 | """ |
3749 | if factor < 1: |
3750 | - raise ValueError("factor must be >= 1, got {!r}".format((factor))) |
3751 | + raise ValueError("factor must be >= 1, got %r" % (factor,)) |
3752 | it = iter(obj) |
3753 | count = chunk |
3754 | while True: |
3755 | @@ -102,7 +102,7 @@ class _Async(object): |
3756 | return asyncreq(self.proxy, HANDLE_CALL, args, tuple(kwargs.items())) |
3757 | |
3758 | def __repr__(self): |
3759 | - return "async_({!r})".format((self.proxy)) |
3760 | + return "async_(%r)" % (self.proxy,) |
3761 | |
3762 | |
3763 | _async_proxies_cache = WeakValueDict() |
3764 | @@ -145,9 +145,9 @@ def async_(proxy): |
3765 | if pid in _async_proxies_cache: |
3766 | return _async_proxies_cache[pid] |
3767 | if not hasattr(proxy, "____conn__") or not hasattr(proxy, "____id_pack__"): |
3768 | - raise TypeError("'proxy' must be a Netref: {!r}".format((proxy))) |
3769 | + raise TypeError("'proxy' must be a Netref: %r", (proxy,)) |
3770 | if not callable(proxy): |
3771 | - raise TypeError("'proxy' must be callable: {!r}".format((proxy))) |
3772 | + raise TypeError("'proxy' must be callable: %r" % (proxy,)) |
3773 | caller = _Async(proxy) |
3774 | _async_proxies_cache[id(caller)] = _async_proxies_cache[pid] = caller |
3775 | return caller |
3776 | @@ -186,7 +186,7 @@ class timed(object): |
3777 | return res |
3778 | |
3779 | def __repr__(self): |
3780 | - return "timed({!r}, {!r})".format((self.proxy.proxy), (self.timeout)) |
3781 | + return "timed(%r, %r)" % (self.proxy.proxy, self.timeout) |
3782 | |
3783 | |
3784 | class BgServingThread(object): |
3785 | diff --git a/plainbox/vendor/rpyc/utils/registry.py b/plainbox/vendor/rpyc/utils/registry.py |
3786 | index b9a58ff..315d9f3 100644 |
3787 | --- a/plainbox/vendor/rpyc/utils/registry.py |
3788 | +++ b/plainbox/vendor/rpyc/utils/registry.py |
3789 | @@ -1,8 +1,13 @@ |
3790 | """ |
3791 | -RPyC Registry Server maintains service information on RPyC services for *Service Registry and Discovery patterns*. Service Registry and Discovery patterns solve the connectivity problem for communication between services and external consumers. RPyC services will register with the server when :code:`auto_register` is :code:`True`. |
3792 | +RPyC **registry server** implementation. The registry is much like |
3793 | +`Avahi <http://en.wikipedia.org/wiki/Avahi_(software)>`_ or |
3794 | +`Bonjour <http://en.wikipedia.org/wiki/Bonjour_(software)>`_, but tailored to |
3795 | +the needs of RPyC. Also, neither of them supports (or supported) Windows, |
3796 | +and Bonjour has a restrictive license. Moreover, they are too "powerful" for |
3797 | +what RPyC needed and required too complex a setup. |
3798 | |
3799 | -Service registries such as `Avahi <http://en.wikipedia.org/wiki/Avahi_(software)>`_ and |
3800 | -`Bonjour <http://en.wikipedia.org/wiki/Bonjour_(software)>`_ are alternatives to the RPyC Registry Server. These alternatives do no support Windows and have more restrictive licensing. |
3801 | +If anyone wants to implement the RPyC registry using Avahi, Bonjour, or any |
3802 | +other zeroconf implementation -- I'll be happy to include them. |
3803 | |
3804 | Refer to :file:`rpyc/scripts/rpyc_registry.py` for more info. |
3805 | """ |
3806 | @@ -26,7 +31,7 @@ REGISTRY_PORT = 18811 |
3807 | class RegistryServer(object): |
3808 | """Base registry server""" |
3809 | |
3810 | - def __init__(self, listenersock, pruning_timeout=None, logger=None, allow_listing=False): |
3811 | + def __init__(self, listenersock, pruning_timeout=None, logger=None): |
3812 | self.sock = listenersock |
3813 | self.port = self.sock.getsockname()[1] |
3814 | self.active = False |
3815 | @@ -36,7 +41,6 @@ class RegistryServer(object): |
3816 | self.pruning_timeout = pruning_timeout |
3817 | if logger is None: |
3818 | logger = self._get_logger() |
3819 | - self.allow_listing = allow_listing |
3820 | self.logger = logger |
3821 | |
3822 | def _get_logger(self): |
3823 | @@ -75,7 +79,7 @@ class RegistryServer(object): |
3824 | def cmd_query(self, host, name): |
3825 | """implementation of the ``query`` command""" |
3826 | name = name.upper() |
3827 | - self.logger.debug("querying for {!r}".format((name))) |
3828 | + self.logger.debug("querying for %r", name) |
3829 | if name not in self.services: |
3830 | self.logger.debug("no such service") |
3831 | return () |
3832 | @@ -85,35 +89,24 @@ class RegistryServer(object): |
3833 | servers = [] |
3834 | for addrinfo, t in all_servers: |
3835 | if t < oldest: |
3836 | - self.logger.debug("discarding stale {}:{}".format((addrinfo[0]), (addrinfo[1]))) |
3837 | + self.logger.debug("discarding stale %s:%s", *addrinfo) |
3838 | self._remove_service(name, addrinfo) |
3839 | else: |
3840 | servers.append(addrinfo) |
3841 | |
3842 | - self.logger.debug("replying with {!r}".format((servers))) |
3843 | + self.logger.debug("replying with %r", servers) |
3844 | return tuple(servers) |
3845 | |
3846 | - def cmd_list(self, host): |
3847 | - """implementation for the ``list`` command""" |
3848 | - self.logger.debug("querying for services list:") |
3849 | - if not self.allow_listing: |
3850 | - self.logger.debug("listing is disabled") |
3851 | - return None |
3852 | - services = tuple(self.services.keys()) |
3853 | - self.logger.debug("replying with {}".format((services))) |
3854 | - |
3855 | - return services |
3856 | - |
3857 | def cmd_register(self, host, names, port): |
3858 | """implementation of the ``register`` command""" |
3859 | - self.logger.debug("registering {}:{} as {}".format((host), (port), (', '.join(names)))) |
3860 | + self.logger.debug("registering %s:%s as %s", host, port, ", ".join(names)) |
3861 | for name in names: |
3862 | self._add_service(name.upper(), (host, port)) |
3863 | return "OK" |
3864 | |
3865 | def cmd_unregister(self, host, port): |
3866 | """implementation of the ``unregister`` command""" |
3867 | - self.logger.debug("unregistering {}:{}".format((host), (port))) |
3868 | + self.logger.debug("unregistering %s:%s", host, port) |
3869 | for name in list(self.services.keys()): |
3870 | self._remove_service(name, (host, port)) |
3871 | return "OK" |
3872 | @@ -135,11 +128,11 @@ class RegistryServer(object): |
3873 | except Exception: |
3874 | continue |
3875 | if magic != "RPYC": |
3876 | - self.logger.warn("invalid magic: {!r}".format((magic))) |
3877 | + self.logger.warn("invalid magic: %r", magic) |
3878 | continue |
3879 | - cmdfunc = getattr(self, "cmd_{}".format((cmd.lower())), None) |
3880 | + cmdfunc = getattr(self, "cmd_%s" % (cmd.lower(),), None) |
3881 | if not cmdfunc: |
3882 | - self.logger.warn("unknown command: {!r}".format((cmd))) |
3883 | + self.logger.warn("unknown command: %r", cmd) |
3884 | continue |
3885 | |
3886 | try: |
3887 | @@ -155,8 +148,7 @@ class RegistryServer(object): |
3888 | raise ValueError("server is already running") |
3889 | if self.sock is None: |
3890 | raise ValueError("object disposed") |
3891 | - addrinfo = self.sock.getsockname()[:2] |
3892 | - self.logger.debug("server started on {}:{}".format((addrinfo[0]), (addrinfo[1]))) |
3893 | + self.logger.debug("server started on %s:%s", *self.sock.getsockname()[:2]) |
3894 | try: |
3895 | self.active = True |
3896 | self._work() |
3897 | @@ -182,17 +174,17 @@ class UDPRegistryServer(RegistryServer): |
3898 | |
3899 | TIMEOUT = 1.0 |
3900 | |
3901 | - def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None, logger=None, allow_listing=False): |
3902 | + def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None, logger=None): |
3903 | family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0, |
3904 | socket.SOCK_DGRAM)[0] |
3905 | sock = socket.socket(family, socktype, proto) |
3906 | sock.bind(sockaddr) |
3907 | sock.settimeout(self.TIMEOUT) |
3908 | RegistryServer.__init__(self, sock, pruning_timeout=pruning_timeout, |
3909 | - logger=logger, allow_listing=allow_listing) |
3910 | + logger=logger) |
3911 | |
3912 | def _get_logger(self): |
3913 | - return logging.getLogger("REGSRV/UDP/{}".format((self.port))) |
3914 | + return logging.getLogger("REGSRV/UDP/%d" % (self.port,)) |
3915 | |
3916 | def _recv(self): |
3917 | return self.sock.recvfrom(MAX_DGRAM_SIZE) |
3918 | @@ -212,9 +204,10 @@ class TCPRegistryServer(RegistryServer): |
3919 | TIMEOUT = 3.0 |
3920 | |
3921 | def __init__(self, host="0.0.0.0", port=REGISTRY_PORT, pruning_timeout=None, |
3922 | - logger=None, reuse_addr=True, allow_listing=False): |
3923 | + logger=None, reuse_addr=True): |
3924 | |
3925 | - family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0] |
3926 | + family, socktype, proto, _, sockaddr = socket.getaddrinfo(host, port, 0, |
3927 | + socket.SOCK_STREAM)[0] |
3928 | sock = socket.socket(family, socktype, proto) |
3929 | if reuse_addr and sys.platform != "win32": |
3930 | # warning: reuseaddr is not what you expect on windows! |
3931 | @@ -223,11 +216,11 @@ class TCPRegistryServer(RegistryServer): |
3932 | sock.listen(10) |
3933 | sock.settimeout(self.TIMEOUT) |
3934 | RegistryServer.__init__(self, sock, pruning_timeout=pruning_timeout, |
3935 | - logger=logger, allow_listing=allow_listing) |
3936 | + logger=logger) |
3937 | self._connected_sockets = {} |
3938 | |
3939 | def _get_logger(self): |
3940 | - return logging.getLogger("REGSRV/TCP/{}".format((self.port))) |
3941 | + return logging.getLogger("REGSRV/TCP/%d" % (self.port,)) |
3942 | |
3943 | def _recv(self): |
3944 | sock2, _ = self.sock.accept() |
3945 | @@ -274,13 +267,6 @@ class RegistryClient(object): |
3946 | """ |
3947 | raise NotImplementedError() |
3948 | |
3949 | - def list(self): |
3950 | - """ |
3951 | - Send a query for the full lists of exposed servers |
3952 | - :returns: a list of `` service_name `` |
3953 | - """ |
3954 | - raise NotImplementedError() |
3955 | - |
3956 | def register(self, aliases, port): |
3957 | """Registers the given service aliases with the given TCP port. This |
3958 | API is intended to be called only by an RPyC server. |
3959 | @@ -307,7 +293,6 @@ class UDPRegistryClient(RegistryClient): |
3960 | Example:: |
3961 | |
3962 | registrar = UDPRegistryClient() |
3963 | - list_of_services = registrar.list() |
3964 | list_of_servers = registrar.discover("foo") |
3965 | |
3966 | .. note:: |
3967 | @@ -349,26 +334,8 @@ class UDPRegistryClient(RegistryClient): |
3968 | servers = brine.load(data) |
3969 | return servers |
3970 | |
3971 | - def list(self): |
3972 | - sock = socket.socket(self.sock_family, socket.SOCK_DGRAM) |
3973 | - |
3974 | - with closing(sock): |
3975 | - if self.bcast: |
3976 | - sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True) |
3977 | - data = brine.dump(("RPYC", "LIST", ())) |
3978 | - sock.sendto(data, (self.ip, self.port)) |
3979 | - sock.settimeout(self.timeout) |
3980 | - |
3981 | - try: |
3982 | - data, _ = sock.recvfrom(MAX_DGRAM_SIZE) |
3983 | - except (socket.error, socket.timeout): |
3984 | - services = () |
3985 | - else: |
3986 | - services = brine.load(data) |
3987 | - return services |
3988 | - |
3989 | def register(self, aliases, port, interface=""): |
3990 | - self.logger.info("registering on {}:{}".format((self.ip), (self.port))) |
3991 | + self.logger.info("registering on %s:%s", self.ip, self.port) |
3992 | sock = socket.socket(self.sock_family, socket.SOCK_DGRAM) |
3993 | with closing(sock): |
3994 | sock.bind((interface, 0)) |
3995 | @@ -393,14 +360,14 @@ class UDPRegistryClient(RegistryClient): |
3996 | except Exception: |
3997 | continue |
3998 | if reply == "OK": |
3999 | - self.logger.info("registry {}:{} acknowledged".format((rip), (rport))) |
4000 | + self.logger.info("registry %s:%s acknowledged", rip, rport) |
4001 | return True |
4002 | else: |
4003 | self.logger.warn("no registry acknowledged") |
4004 | return False |
4005 | |
4006 | def unregister(self, port): |
4007 | - self.logger.info("unregistering from {}:{}".format((self.ip), (self.port))) |
4008 | + self.logger.info("unregistering from %s:%s", self.ip, self.port) |
4009 | sock = socket.socket(self.sock_family, socket.SOCK_DGRAM) |
4010 | with closing(sock): |
4011 | if self.bcast: |
4012 | @@ -416,7 +383,6 @@ class TCPRegistryClient(RegistryClient): |
4013 | Example:: |
4014 | |
4015 | registrar = TCPRegistryClient("localhost") |
4016 | - list_of_services = registrar.list() |
4017 | list_of_servers = registrar.discover("foo") |
4018 | |
4019 | .. note:: |
4020 | @@ -446,24 +412,8 @@ class TCPRegistryClient(RegistryClient): |
4021 | servers = brine.load(data) |
4022 | return servers |
4023 | |
4024 | - def list(self): |
4025 | - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
4026 | - with closing(sock): |
4027 | - sock.settimeout(self.timeout) |
4028 | - data = brine.dump(("RPYC", "LIST", ())) |
4029 | - sock.connect((self.ip, self.port)) |
4030 | - sock.send(data) |
4031 | - |
4032 | - try: |
4033 | - data = sock.recv(MAX_DGRAM_SIZE) |
4034 | - except (socket.error, socket.timeout): |
4035 | - servers = () |
4036 | - else: |
4037 | - servers = brine.load(data) |
4038 | - return servers |
4039 | - |
4040 | def register(self, aliases, port, interface=""): |
4041 | - self.logger.info("registering on {}:{}".format((self.ip), (self.port))) |
4042 | + self.logger.info("registering on %s:%s", self.ip, self.port) |
4043 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
4044 | with closing(sock): |
4045 | sock.bind((interface, 0)) |
4046 | @@ -486,12 +436,12 @@ class TCPRegistryClient(RegistryClient): |
4047 | self.logger.warn("received corrupted data from registry") |
4048 | return False |
4049 | if reply == "OK": |
4050 | - self.logger.info("registry {}:{} acknowledged".format((self.ip), (self.port))) |
4051 | + self.logger.info("registry %s:%s acknowledged", self.ip, self.port) |
4052 | |
4053 | return True |
4054 | |
4055 | def unregister(self, port): |
4056 | - self.logger.info("unregistering from {}:{}".format((self.ip), (self.port))) |
4057 | + self.logger.info("unregistering from %s:%s", self.ip, self.port) |
4058 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
4059 | with closing(sock): |
4060 | sock.settimeout(self.timeout) |
4061 | diff --git a/plainbox/vendor/rpyc/utils/server.py b/plainbox/vendor/rpyc/utils/server.py |
4062 | index 2e73ecb..d50e118 100644 |
4063 | --- a/plainbox/vendor/rpyc/utils/server.py |
4064 | +++ b/plainbox/vendor/rpyc/utils/server.py |
4065 | @@ -26,8 +26,8 @@ class Server(object): |
4066 | """Base server implementation |
4067 | |
4068 | :param service: the :class:`~rpyc.core.service.Service` to expose |
4069 | - :param hostname: the host to bind to. By default, the 'wildcard address' is used to listen on all interfaces. |
4070 | - if not properly secured, the server can receive traffic from unintended or even malicious sources. |
4071 | + :param hostname: the host to bind to. Default is IPADDR_ANY, but you may |
4072 | + want to restrict it only to ``localhost`` in some setups |
4073 | :param ipv6: whether to create an IPv6 or IPv4 socket. The default is IPv4 |
4074 | :param port: the TCP port to bind to |
4075 | :param backlog: the socket's backlog (passed to ``listen()``) |
4076 | @@ -47,9 +47,9 @@ class Server(object): |
4077 | on embedded platforms with limited battery) |
4078 | """ |
4079 | |
4080 | - def __init__(self, service, hostname=None, ipv6=False, port=0, |
4081 | + def __init__(self, service, hostname="", ipv6=False, port=0, |
4082 | backlog=socket.SOMAXCONN, reuse_addr=True, authenticator=None, registrar=None, |
4083 | - auto_register=None, protocol_config=None, logger=None, listener_timeout=0.5, |
4084 | + auto_register=None, protocol_config={}, logger=None, listener_timeout=0.5, |
4085 | socket_path=None): |
4086 | self.active = False |
4087 | self._closed = False |
4088 | @@ -60,15 +60,11 @@ class Server(object): |
4089 | self.auto_register = bool(registrar) |
4090 | else: |
4091 | self.auto_register = auto_register |
4092 | - |
4093 | - if protocol_config is None: |
4094 | - protocol_config = {} |
4095 | - |
4096 | self.protocol_config = protocol_config |
4097 | self.clients = set() |
4098 | |
4099 | if socket_path is not None: |
4100 | - if hostname is not None or port != 0 or ipv6 is not False: |
4101 | + if hostname != "" or port != 0 or ipv6 is not False: |
4102 | raise ValueError("socket_path is mutually exclusive with: hostname, port, ipv6") |
4103 | self.listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
4104 | self.listener.bind(socket_path) |
4105 | @@ -76,18 +72,20 @@ class Server(object): |
4106 | self.host, self.port = "", socket_path |
4107 | else: |
4108 | if ipv6: |
4109 | - family = socket.AF_INET6 |
4110 | + if hostname == "localhost" and sys.platform != "win32": |
4111 | + # on windows, you should bind to localhost even for ipv6 |
4112 | + hostname = "localhost6" |
4113 | + self.listener = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) |
4114 | else: |
4115 | - family = socket.AF_INET |
4116 | - self.listener = socket.socket(family, socket.SOCK_STREAM) |
4117 | - address = socket.getaddrinfo(hostname, port, family=family, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP, flags=socket.AI_PASSIVE)[0][-1] |
4118 | + self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
4119 | |
4120 | if reuse_addr and sys.platform != "win32": |
4121 | # warning: reuseaddr is not what you'd expect on windows! |
4122 | # it allows you to bind an already bound port, resulting in |
4123 | # "unexpected behavior" (quoting MSDN) |
4124 | self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
4125 | - self.listener.bind(address) |
4126 | + |
4127 | + self.listener.bind((hostname, port)) |
4128 | self.listener.settimeout(listener_timeout) |
4129 | |
4130 | # hack for IPv6 (the tuple can be longer than 2) |
4131 | @@ -95,7 +93,7 @@ class Server(object): |
4132 | self.host, self.port = sockname[0], sockname[1] |
4133 | |
4134 | if logger is None: |
4135 | - logger = logging.getLogger("{}/{}".format((self.service.get_service_name()), (self.port))) |
4136 | + logger = logging.getLogger("%s/%s" % (self.service.get_service_name(), self.port)) |
4137 | self.logger = logger |
4138 | if "logger" not in self.protocol_config: |
4139 | self.protocol_config["logger"] = self.logger |
4140 | @@ -153,7 +151,7 @@ class Server(object): |
4141 | return |
4142 | |
4143 | sock.setblocking(True) |
4144 | - self.logger.info("accepted {} with fd {}".format((addrinfo), (sock.fileno()))) |
4145 | + self.logger.info("accepted %s with fd %s", addrinfo, sock.fileno()) |
4146 | self.clients.add(sock) |
4147 | self._accept_method(sock) |
4148 | |
4149 | @@ -171,10 +169,10 @@ class Server(object): |
4150 | try: |
4151 | sock2, credentials = self.authenticator(sock) |
4152 | except AuthenticationError: |
4153 | - self.logger.info("{} failed to authenticate... rejecting connection".format((addrinfo))) |
4154 | + self.logger.info("%s failed to authenticate, rejecting connection", addrinfo) |
4155 | return |
4156 | else: |
4157 | - self.logger.info("{} authenticated successfully".format((addrinfo))) |
4158 | + self.logger.info("%s authenticated successfully", addrinfo) |
4159 | else: |
4160 | credentials = None |
4161 | sock2 = sock |
4162 | @@ -194,16 +192,16 @@ class Server(object): |
4163 | def _serve_client(self, sock, credentials): |
4164 | addrinfo = sock.getpeername() |
4165 | if credentials: |
4166 | - self.logger.info("welcome {} ({!r})".format((addrinfo), (credentials))) |
4167 | + self.logger.info("welcome %s (%r)", addrinfo, credentials) |
4168 | else: |
4169 | - self.logger.info("welcome {}".format((addrinfo))) |
4170 | + self.logger.info("welcome %s", addrinfo) |
4171 | try: |
4172 | config = dict(self.protocol_config, credentials=credentials, |
4173 | endpoints=(sock.getsockname(), addrinfo), logger=self.logger) |
4174 | conn = self.service._connect(Channel(SocketStream(sock)), config) |
4175 | self._handle_connection(conn) |
4176 | finally: |
4177 | - self.logger.info("goodbye {}".format((addrinfo))) |
4178 | + self.logger.info("goodbye %s", addrinfo) |
4179 | |
4180 | def _handle_connection(self, conn): |
4181 | """This methoed should implement the server's logic.""" |
4182 | @@ -212,7 +210,7 @@ class Server(object): |
4183 | def _bg_register(self): |
4184 | interval = self.registrar.REREGISTER_INTERVAL |
4185 | self.logger.info("started background auto-register thread " |
4186 | - "(interval = {})".format((interval))) |
4187 | + "(interval = %s)", interval) |
4188 | tnext = 0 |
4189 | try: |
4190 | while self.active: |
4191 | @@ -247,11 +245,12 @@ class Server(object): |
4192 | # Note that for AF_UNIX the following won't work (but we are safe |
4193 | # since we already saved the socket_path into self.port): |
4194 | self.port = self.listener.getsockname()[1] |
4195 | - self.logger.info("server started on [{}]:{}".format((self.host), (self.port))) |
4196 | + self.logger.info("server started on [%s]:%s", self.host, self.port) |
4197 | self.active = True |
4198 | |
4199 | def _register(self): |
4200 | if self.auto_register: |
4201 | + self.auto_register = False |
4202 | spawn(self._bg_register) |
4203 | |
4204 | def start(self): |
4205 | @@ -341,7 +340,7 @@ class ThreadPoolServer(Server): |
4206 | self.workers = [] |
4207 | for i in range(self.nbthreads): |
4208 | t = spawn(self._serve_clients) |
4209 | - t.setName("Worker{}".format((i))) |
4210 | + t.setName('Worker%i' % i) |
4211 | self.workers.append(t) |
4212 | # setup a thread for polling inactive connections |
4213 | self.polling_thread = spawn(self._poll_inactive_clients) |
4214 | @@ -382,7 +381,7 @@ class ThreadPoolServer(Server): |
4215 | pass |
4216 | |
4217 | # close connection |
4218 | - self.logger.info("Closing connection for fd {}".format((fd))) |
4219 | + self.logger.info("Closing connection for fd %d", fd) |
4220 | if conn: |
4221 | conn.close() |
4222 | |
4223 | @@ -420,7 +419,7 @@ class ThreadPoolServer(Server): |
4224 | except Exception: |
4225 | ex = sys.exc_info()[1] |
4226 | # "Caught exception in Worker thread" message |
4227 | - self.logger.warning("Failed to poll clients, caught exception : {}".format((ex))) |
4228 | + self.logger.warning("Failed to poll clients, caught exception : %s", str(ex)) |
4229 | # wait a bit so that we do not loop too fast in case of error |
4230 | time.sleep(0.2) |
4231 | |
4232 | @@ -468,7 +467,7 @@ class ThreadPoolServer(Server): |
4233 | time.sleep(0.2) |
4234 | |
4235 | def _authenticate_and_build_connection(self, sock): |
4236 | - '''Authenticate a client and if it succeeds, wraps the socket in a connection object. |
4237 | + '''Authenticate a client and if it succees, wraps the socket in a connection object. |
4238 | Note that this code is cut and paste from the rpyc internals and may have to be |
4239 | changed if rpyc evolves''' |
4240 | # authenticate |
4241 | @@ -477,27 +476,27 @@ class ThreadPoolServer(Server): |
4242 | else: |
4243 | credentials = None |
4244 | # build a connection |
4245 | - addrinfo = sock.getpeername() |
4246 | - config = dict(self.protocol_config, credentials=credentials, connid="{}".format(addrinfo), |
4247 | - endpoints=(sock.getsockname(), addrinfo)) |
4248 | + h, p = sock.getpeername() |
4249 | + config = dict(self.protocol_config, credentials=credentials, connid="%s:%d" % (h, p), |
4250 | + endpoints=(sock.getsockname(), (h, p))) |
4251 | return sock, self.service._connect(Channel(SocketStream(sock)), config) |
4252 | |
4253 | def _accept_method(self, sock): |
4254 | '''Implementation of the accept method : only pushes the work to the internal queue. |
4255 | In case the queue is full, raises an AsynResultTimeout error''' |
4256 | try: |
4257 | - addrinfo = None |
4258 | + h, p = None, None |
4259 | # authenticate and build connection object |
4260 | sock, conn = self._authenticate_and_build_connection(sock) |
4261 | # put the connection in the active queue |
4262 | - addrinfo = sock.getpeername() |
4263 | + h, p = sock.getpeername() |
4264 | fd = conn.fileno() |
4265 | - self.logger.debug("Created connection to {addrinfo} with fd {fd}") |
4266 | + self.logger.debug("Created connection to %s:%d with fd %d", h, p, fd) |
4267 | self.fd_to_conn[fd] = conn |
4268 | self._add_inactive_connection(fd) |
4269 | self.clients.clear() |
4270 | except Exception: |
4271 | - err_msg = "Failed to serve client for {}, caught exception".format(addrinfo) |
4272 | + err_msg = "Failed to serve client for {}:{}, caught exception".format(h, p) |
4273 | self.logger.exception(err_msg) |
4274 | sock.close() |
4275 | |
4276 | @@ -564,6 +563,7 @@ class GeventServer(Server): |
4277 | |
4278 | def _register(self): |
4279 | if self.auto_register: |
4280 | + self.auto_register = False |
4281 | gevent.spawn(self._bg_register) |
4282 | |
4283 | def _accept_method(self, sock): |
4284 | diff --git a/plainbox/vendor/rpyc/utils/teleportation.py b/plainbox/vendor/rpyc/utils/teleportation.py |
4285 | index a06af70..451c93d 100644 |
4286 | --- a/plainbox/vendor/rpyc/utils/teleportation.py |
4287 | +++ b/plainbox/vendor/rpyc/utils/teleportation.py |
4288 | @@ -1,21 +1,48 @@ |
4289 | import opcode |
4290 | - |
4291 | -from plainbox.vendor.rpyc.lib.compat import is_py_gte38 |
4292 | +import sys |
4293 | +try: |
4294 | + import __builtin__ |
4295 | +except ImportError: |
4296 | + import builtins as __builtin__ # noqa: F401 |
4297 | +from plainbox.vendor.rpyc.lib.compat import is_py_3k, is_py_gte38 |
4298 | from types import CodeType, FunctionType |
4299 | -from plainbox.vendor.rpyc.core import brine, netref |
4300 | -from dis import _unpack_opargs |
4301 | +from plainbox.vendor.rpyc.core import brine |
4302 | +from plainbox.vendor.rpyc.core import netref |
4303 | |
4304 | CODEOBJ_MAGIC = "MAg1c J0hNNzo0hn ZqhuBP17LQk8" |
4305 | |
4306 | |
4307 | # NOTE: dislike this kind of hacking on the level of implementation details, |
4308 | # should search for a more reliable/future-proof way: |
4309 | -CODE_HAVEARG_SIZE = 3 |
4310 | +CODE_HAVEARG_SIZE = 2 if sys.version_info >= (3, 6) else 3 |
4311 | +try: |
4312 | + from dis import _unpack_opargs |
4313 | +except ImportError: |
4314 | + # COPIED from 3.5's `dis.py`, this should hopefully be correct for <=3.5: |
4315 | + def _unpack_opargs(code): |
4316 | + extended_arg = 0 |
4317 | + n = len(code) |
4318 | + i = 0 |
4319 | + while i < n: |
4320 | + op = code[i] |
4321 | + offset = i |
4322 | + i = i + 1 |
4323 | + arg = None |
4324 | + if op >= opcode.HAVE_ARGUMENT: |
4325 | + arg = code[i] + code[i + 1] * 256 + extended_arg |
4326 | + extended_arg = 0 |
4327 | + i = i + 2 |
4328 | + if op == opcode.EXTENDED_ARG: |
4329 | + extended_arg = arg * 65536 |
4330 | + yield (offset, op, arg) |
4331 | |
4332 | |
4333 | def decode_codeobj(codeobj): |
4334 | # adapted from dis.dis |
4335 | - codestr = codeobj.co_code |
4336 | + if is_py_3k: |
4337 | + codestr = codeobj.co_code |
4338 | + else: |
4339 | + codestr = [ord(ch) for ch in codeobj.co_code] |
4340 | free = None |
4341 | for i, op, oparg in _unpack_opargs(codestr): |
4342 | opname = opcode.opname[op] |
4343 | @@ -46,7 +73,7 @@ def _export_codeobj(cobj): |
4344 | elif isinstance(const, CodeType): |
4345 | consts2.append(_export_codeobj(const)) |
4346 | else: |
4347 | - raise TypeError(f"Cannot export a function with non-brinable constants: {const!r}") |
4348 | + raise TypeError("Cannot export a function with non-brinable constants: %r" % (const,)) |
4349 | |
4350 | if is_py_gte38: |
4351 | # Constructor was changed in 3.8 to support "advanced" programming styles |
4352 | @@ -54,41 +81,50 @@ def _export_codeobj(cobj): |
4353 | cobj.co_stacksize, cobj.co_flags, cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, |
4354 | cobj.co_filename, cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, |
4355 | cobj.co_cellvars) |
4356 | - else: |
4357 | + elif is_py_3k: |
4358 | exported = (cobj.co_argcount, cobj.co_kwonlyargcount, cobj.co_nlocals, cobj.co_stacksize, cobj.co_flags, |
4359 | cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, cobj.co_filename, |
4360 | cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, cobj.co_cellvars) |
4361 | + else: |
4362 | + exported = (cobj.co_argcount, cobj.co_nlocals, cobj.co_stacksize, cobj.co_flags, |
4363 | + cobj.co_code, tuple(consts2), cobj.co_names, cobj.co_varnames, cobj.co_filename, |
4364 | + cobj.co_name, cobj.co_firstlineno, cobj.co_lnotab, cobj.co_freevars, cobj.co_cellvars) |
4365 | + |
4366 | assert brine.dumpable(exported) |
4367 | return (CODEOBJ_MAGIC, exported) |
4368 | |
4369 | |
4370 | def export_function(func): |
4371 | - closure = func.__closure__ |
4372 | - code = func.__code__ |
4373 | - defaults = func.__defaults__ |
4374 | - kwdefaults = func.__kwdefaults__ |
4375 | - if kwdefaults is not None: |
4376 | - kwdefaults = tuple(kwdefaults.items()) |
4377 | - |
4378 | - if closure: |
4379 | + if is_py_3k: |
4380 | + func_closure = func.__closure__ |
4381 | + func_code = func.__code__ |
4382 | + func_defaults = func.__defaults__ |
4383 | + else: |
4384 | + func_closure = func.func_closure |
4385 | + func_code = func.func_code |
4386 | + func_defaults = func.func_defaults |
4387 | + |
4388 | + if func_closure: |
4389 | raise TypeError("Cannot export a function closure") |
4390 | - if not brine.dumpable(defaults): |
4391 | - raise TypeError("Cannot export a function with non-brinable defaults (__defaults__)") |
4392 | - if not brine.dumpable(kwdefaults): |
4393 | - raise TypeError("Cannot export a function with non-brinable defaults (__kwdefaults__)") |
4394 | + if not brine.dumpable(func_defaults): |
4395 | + raise TypeError("Cannot export a function with non-brinable defaults (func_defaults)") |
4396 | |
4397 | - return func.__name__, func.__module__, defaults, kwdefaults, _export_codeobj(code)[1] |
4398 | + return func.__name__, func.__module__, func_defaults, _export_codeobj(func_code)[1] |
4399 | |
4400 | |
4401 | def _import_codetup(codetup): |
4402 | - # Handle tuples sent from 3.8 as well as 3 < version < 3.8. |
4403 | - if len(codetup) == 16: |
4404 | - (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, |
4405 | - filename, name, firstlineno, lnotab, freevars, cellvars) = codetup |
4406 | + if is_py_3k: |
4407 | + # Handle tuples sent from 3.8 as well as 3 < version < 3.8. |
4408 | + if len(codetup) == 16: |
4409 | + (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, |
4410 | + filename, name, firstlineno, lnotab, freevars, cellvars) = codetup |
4411 | + else: |
4412 | + (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, |
4413 | + filename, name, firstlineno, lnotab, freevars, cellvars) = codetup |
4414 | + posonlyargcount = 0 |
4415 | else: |
4416 | - (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, |
4417 | + (argcount, nlocals, stacksize, flags, code, consts, names, varnames, |
4418 | filename, name, firstlineno, lnotab, freevars, cellvars) = codetup |
4419 | - posonlyargcount = 0 |
4420 | |
4421 | consts2 = [] |
4422 | for const in consts: |
4423 | @@ -100,14 +136,17 @@ def _import_codetup(codetup): |
4424 | if is_py_gte38: |
4425 | codetup = (argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, |
4426 | filename, name, firstlineno, lnotab, freevars, cellvars) |
4427 | - else: |
4428 | + elif is_py_3k: |
4429 | codetup = (argcount, kwonlyargcount, nlocals, stacksize, flags, code, consts, names, varnames, filename, name, |
4430 | firstlineno, lnotab, freevars, cellvars) |
4431 | + else: |
4432 | + codetup = (argcount, nlocals, stacksize, flags, code, consts, names, varnames, filename, name, firstlineno, |
4433 | + lnotab, freevars, cellvars) |
4434 | return CodeType(*codetup) |
4435 | |
4436 | |
4437 | def import_function(functup, globals=None, def_=True): |
4438 | - name, modname, defaults, kwdefaults, codetup = functup |
4439 | + name, modname, defaults, codetup = functup |
4440 | if globals is None: |
4441 | try: |
4442 | mod = __import__(modname, None, None, "*") |
4443 | @@ -116,13 +155,11 @@ def import_function(functup, globals=None, def_=True): |
4444 | globals = mod.__dict__ |
4445 | # function globals must be real dicts, sadly: |
4446 | if isinstance(globals, netref.BaseNetref): |
4447 | - from rpyc.utils.classic import obtain |
4448 | + from plainbox.vendor.rpyc.utils.classic import obtain |
4449 | globals = obtain(globals) |
4450 | globals.setdefault('__builtins__', __builtins__) |
4451 | codeobj = _import_codetup(codetup) |
4452 | funcobj = FunctionType(codeobj, globals, name, defaults) |
4453 | - if kwdefaults is not None: |
4454 | - funcobj.__kwdefaults__ = {t[0]: t[1] for t in kwdefaults} |
4455 | if def_: |
4456 | globals[name] = funcobj |
4457 | return funcobj |
4458 | diff --git a/plainbox/vendor/rpyc/utils/zerodeploy.py b/plainbox/vendor/rpyc/utils/zerodeploy.py |
4459 | index a246e64..bbc4922 100644 |
4460 | --- a/plainbox/vendor/rpyc/utils/zerodeploy.py |
4461 | +++ b/plainbox/vendor/rpyc/utils/zerodeploy.py |
4462 | @@ -55,7 +55,7 @@ $EXTRA_SETUP$ |
4463 | t = ServerCls(SlaveService, hostname = "localhost", port = 0, reuse_addr = True, logger = logger) |
4464 | thd = t._start_in_thread() |
4465 | |
4466 | -sys.stdout.write(f"{t.port}\n") |
4467 | +sys.stdout.write("%s\n" % (t.port,)) |
4468 | sys.stdout.flush() |
4469 | |
4470 | try: |
4471 | @@ -111,7 +111,7 @@ class DeployedServer(object): |
4472 | major = sys.version_info[0] |
4473 | minor = sys.version_info[1] |
4474 | cmd = None |
4475 | - for opt in [f"python{major}.{minor}", f"python{major}"]: |
4476 | + for opt in ["python%s.%s" % (major, minor), "python%s" % (major,)]: |
4477 | try: |
4478 | cmd = remote_machine[opt] |
4479 | except CommandNotFound: |
4480 | @@ -155,26 +155,15 @@ class DeployedServer(object): |
4481 | if self.proc is not None: |
4482 | try: |
4483 | self.proc.terminate() |
4484 | - self.proc.communicate() |
4485 | except Exception: |
4486 | pass |
4487 | self.proc = None |
4488 | if self.tun is not None: |
4489 | try: |
4490 | - self.tun._session.proc.terminate() |
4491 | - self.tun._session.proc.communicate() |
4492 | self.tun.close() |
4493 | except Exception: |
4494 | pass |
4495 | self.tun = None |
4496 | - if self.remote_machine is not None: |
4497 | - try: |
4498 | - self.remote_machine._session.proc.terminate() |
4499 | - self.remote_machine._session.proc.communicate() |
4500 | - self.remote_machine.close() |
4501 | - except Exception: |
4502 | - pass |
4503 | - self.remote_machine = None |
4504 | if self._tmpdir_ctx is not None: |
4505 | try: |
4506 | self._tmpdir_ctx.__exit__(None, None, None) |
4507 | diff --git a/plainbox/vendor/rpyc/version.py b/plainbox/vendor/rpyc/version.py |
4508 | index 3028d79..d7618e4 100644 |
4509 | --- a/plainbox/vendor/rpyc/version.py |
4510 | +++ b/plainbox/vendor/rpyc/version.py |
4511 | @@ -1,3 +1,3 @@ |
4512 | -version = (5, 1, 0) |
4513 | +version = (4, 1, 4) |
4514 | version_string = ".".join(map(str, version)) |
4515 | -release_date = "2022-02-26" |
4516 | +release_date = "2020.1.30" |
Yeah, +1