Merge lp:~gocept/landscape-client/py3-manager-scriptexecution into lp:~landscape/landscape-client/trunk

Proposed by Steffen Allner
Status: Merged
Approved by: Eric Snow
Approved revision: 978
Merged at revision: 983
Proposed branch: lp:~gocept/landscape-client/py3-manager-scriptexecution
Merge into: lp:~landscape/landscape-client/trunk
Prerequisite: lp:~gocept/landscape-client/py3-broker-exchange
Diff against target: 350 lines (+46/-41)
3 files modified
landscape/manager/scriptexecution.py (+23/-8)
landscape/manager/tests/test_scriptexecution.py (+22/-32)
py3_ready_tests (+1/-1)
To merge this branch: bzr merge lp:~gocept/landscape-client/py3-manager-scriptexecution
Reviewer Review Type Date Requested Status
Chad Smith Approve
Eric Snow (community) Approve
🤖 Landscape Builder test results Approve
Gocept Pending
Review via email: mp+321147@code.launchpad.net

Commit message

This is the Py3 port of landscape.manager.scriptexecution.

All the changes are to fix bytes/unicode. Notably, the stdlib pwd module uses decodes its data strictly by the default FS encoding (which cannot be changed from within Python). Since the tests are run with LC_ALL=C, some tests failed with unicode errors. The fix in the code protects us from the problem now and in the future.

Description of the change

This MP considers the landscape.manager.scriptexecution module, which tests were mostly failing because the communication with processes is not done with bytes.

Additionally, a bigger problem occurred, when the system default encoding (e.g. LC_ALL=C) is not able to handle unicode and a unicode containing username is queried by pwd.getpwnam(). The solution to handle this as an UnkownUserError, was chosen (1) because a simple switch of locales via locale.setlocale() did not solve the problem and (2) the edge case of falsely get an entry with unicode content could only happen if the entry was inserted with a less restrictive encoding in contrast to the retrieval encoding, which is not probable.

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: TRIAL_ARGS=-j4 make check
Result: Success
Revno: 977
Branch: lp:~gocept/landscape-client/py3-manager-scriptexecution
Jenkins: https://ci.lscape.net/job/latch-test-xenial/3789/

review: Approve (test results)
Revision history for this message
Eric Snow (ericsnowcurrently) wrote :

Other than few couple minor comments, LGTM.

review: Approve
Revision history for this message
Eric Snow (ericsnowcurrently) :
review: Needs Fixing
978. By Steffen Allner

Improve documentation in comments.

Revision history for this message
Steffen Allner (sallner) wrote :

I updated the comment.

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: TRIAL_ARGS=-j4 make ci-check
Result: Success
Revno: 978
Branch: lp:~gocept/landscape-client/py3-manager-scriptexecution
Jenkins: https://ci.lscape.net/job/latch-test-xenial/3792/

review: Approve (test results)
Revision history for this message
Eric Snow (ericsnowcurrently) wrote :

Thanks for those changes.

review: Approve
Revision history for this message
Chad Smith (chad.smith) wrote :

+1 on the changeset. Did a little experimenting with script execution w/ and w/out unicode content using this build and didn't run into any unexpected problems.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'landscape/manager/scriptexecution.py'
2--- landscape/manager/scriptexecution.py 2017-03-10 12:10:24 +0000
3+++ landscape/manager/scriptexecution.py 2017-03-28 16:22:54 +0000
4@@ -38,10 +38,23 @@
5 gid = None
6 path = None
7 if username is not None:
8- username_str = encode_if_needed(username)
9+ if _PY3:
10+ username_str = username
11+ else:
12+ username_str = encode_if_needed(username)
13 try:
14+ # XXX: We have a situation with the system default FS encoding with
15+ # Python 3 here: We have to pass a string to pwd.getpwnam(), but if
16+ # the default does not support unicode characters, a
17+ # UnicodeEncodeError will be thrown. This edge case can be harmful,
18+ # if the user was added with a less restrictive encoding active,
19+ # and is now retrieved with LC_ALL=C for example, as it is during
20+ # automatic test runs. This should not be a problem under normal
21+ # circumstances. Alternatively, a different way of parsing
22+ # /etc/passwd would have to be implemented. A simple
23+ # locale.setlocale() to use UTF-8 was not successful.
24 info = pwd.getpwnam(username_str)
25- except KeyError:
26+ except (KeyError, UnicodeEncodeError):
27 raise UnknownUserError(u"Unknown user '%s'" % username)
28 uid = info.pw_uid
29 gid = info.pw_gid
30@@ -117,8 +130,7 @@
31 os.chown(filename, uid, gid)
32
33 script = build_script(shell, code)
34- if not _PY3:
35- script = script.encode('utf-8')
36+ script = script.encode("utf-8")
37 script_file.write(script)
38 script_file.close()
39
40@@ -220,7 +232,7 @@
41 for filename, attachment_id in attachments.items():
42 if isinstance(attachment_id, str):
43 # Backward compatible behavior
44- data = attachment_id
45+ data = attachment_id.encode("utf-8")
46 yield succeed(None)
47 else:
48 data = yield fetch_async(
49@@ -267,7 +279,7 @@
50 uid, gid, path = get_user_info(user)
51
52 fd, filename = tempfile.mkstemp()
53- script_file = os.fdopen(fd, "w")
54+ script_file = os.fdopen(fd, "wb")
55 self.write_script_file(
56 script_file, filename, shell, code, uid, gid)
57
58@@ -320,7 +332,7 @@
59 self.result_deferred = Deferred()
60 self._cancelled = False
61 self.size_limit = size_limit
62- self._truncation_indicator = truncation_indicator
63+ self._truncation_indicator = truncation_indicator.encode("utf-8")
64 self._truncation_offset = len(self._truncation_indicator)
65 self._truncated_size_limit = self.size_limit - self._truncation_offset
66 self.reactor = reactor
67@@ -355,7 +367,10 @@
68 far.
69 """
70 exit_code = reason.value.exitCode
71- data = "".join(self.data)
72+ # We get bytes with self.data, but want unicode with replace
73+ # characters. This is again attempted in
74+ # ScriptExecutionPlugin._respond, but it is not called in all cases.
75+ data = b"".join(self.data).decode("utf-8", "replace")
76 if self._cancelled:
77 self.result_deferred.errback(ProcessTimeLimitReachedError(data))
78 else:
79
80=== modified file 'landscape/manager/tests/test_scriptexecution.py'
81--- landscape/manager/tests/test_scriptexecution.py 2017-03-06 10:38:07 +0000
82+++ landscape/manager/tests/test_scriptexecution.py 2017-03-28 16:22:54 +0000
83@@ -43,7 +43,6 @@
84 self.plugin = ScriptExecutionPlugin()
85 self.manager.add(self.plugin)
86
87- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
88 def test_basic_run(self):
89 """
90 The plugin returns a Deferred resulting in the output of basic
91@@ -53,21 +52,18 @@
92 result.addCallback(self.assertEqual, "hi\n")
93 return result
94
95- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
96 def test_snap_path(self):
97 """The bin path for snaps is included in the PATH."""
98 deferred = self.plugin.run_script("/bin/sh", "echo $PATH")
99 return deferred.addCallback(
100 lambda result: self.assertIn("/snap/bin", result))
101
102- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
103 def test_other_interpreter(self):
104 """Non-shell interpreters can be specified."""
105 result = self.plugin.run_script("/usr/bin/python", "print 'hi'")
106 result.addCallback(self.assertEqual, "hi\n")
107 return result
108
109- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
110 def test_other_interpreter_env(self):
111 """
112 Non-shell interpreters don't have their paths set by the shell, so we
113@@ -75,7 +71,7 @@
114 """
115 result = self.plugin.run_script(
116 sys.executable,
117- "import os\nprint os.environ")
118+ "import os\nprint(os.environ)")
119
120 def check_environment(results):
121 for string in get_default_environment():
122@@ -84,7 +80,6 @@
123 result.addCallback(check_environment)
124 return result
125
126- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
127 def test_server_supplied_env(self):
128 """
129 Server-supplied environment variables are merged with default
130@@ -93,7 +88,7 @@
131 server_supplied_env = {"DOG": "Woof", "CAT": "Meow"}
132 result = self.plugin.run_script(
133 sys.executable,
134- "import os\nprint os.environ",
135+ "import os\nprint(os.environ)",
136 server_supplied_env=server_supplied_env)
137
138 def check_environment(results):
139@@ -106,7 +101,6 @@
140 result.addCallback(check_environment)
141 return result
142
143- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
144 def test_server_supplied_env_overrides_client(self):
145 """
146 Server-supplied environment variables override client default
147@@ -116,7 +110,7 @@
148 "HOME": "server-home"}
149 result = self.plugin.run_script(
150 sys.executable,
151- "import os\nprint os.environ",
152+ "import os\nprint(os.environ)",
153 server_supplied_env=server_supplied_env)
154
155 def check_environment(results):
156@@ -127,7 +121,6 @@
157 result.addCallback(check_environment)
158 return result
159
160- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
161 def test_concurrent(self):
162 """
163 Scripts run with the ScriptExecutionPlugin plugin are run concurrently.
164@@ -144,7 +137,6 @@
165 d2.addCallback(self.assertEqual, "")
166 return gatherResults([d1, d2])
167
168- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
169 def test_accented_run_in_code(self):
170 """
171 Scripts can contain accented data both in the code and in the
172@@ -153,11 +145,12 @@
173 accented_content = u"\N{LATIN SMALL LETTER E WITH ACUTE}"
174 result = self.plugin.run_script(
175 u"/bin/sh", u"echo %s" % (accented_content,))
176+ # self.assertEqual gets the result as first argument and that's what we
177+ # compare against.
178 result.addCallback(
179- self.assertEqual, "%s\n" % (accented_content.encode("utf-8"),))
180+ self.assertEqual, "%s\n" % (accented_content,))
181 return result
182
183- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
184 def test_accented_run_in_interpreter(self):
185 """
186 Scripts can also contain accents in the interpreter.
187@@ -168,12 +161,11 @@
188
189 def check(result):
190 self.assertTrue(
191- "%s " % (accented_content.encode("utf-8"),) in result)
192+ "%s " % (accented_content,) in result)
193
194 result.addCallback(check)
195 return result
196
197- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
198 def test_set_umask_appropriately(self):
199 """
200 We should be setting the umask to 0o022 before executing a script, and
201@@ -196,7 +188,6 @@
202 result.addCallback(check)
203 return result.addCallback(lambda _: patch_umask.stop())
204
205- @skipIf(_PY3, "mock does not get cleaned up, poisoning all other tests.")
206 def test_restore_umask_in_event_of_error(self):
207 """
208 We set the umask before executing the script, in the event that there's
209@@ -312,7 +303,6 @@
210
211 return result.addCallback(check).addBoth(cleanup)
212
213- @skipIf(_PY3, 'Takes long with Python3, probably not an unclean Reactor')
214 def test_self_remove_script(self):
215 """
216 If a script removes itself, it doesn't create an error when the script
217@@ -358,7 +348,7 @@
218 self.assertEqual(spawn[6], expected_gid)
219
220 protocol = spawn[0]
221- protocol.childDataReceived(1, "foobar")
222+ protocol.childDataReceived(1, b"foobar")
223 for fd in (0, 1, 2):
224 protocol.childConnectionLost(fd)
225 protocol.processEnded(Failure(ProcessDone(0)))
226@@ -436,7 +426,7 @@
227 self.assertEqual(stat.S_IMODE(os.stat(filename).st_mode), 0o600)
228
229 protocol = spawn[0]
230- protocol.childDataReceived(1, "foobar")
231+ protocol.childDataReceived(1, b"foobar")
232 for fd in (0, 1, 2):
233 protocol.childConnectionLost(fd)
234 protocol.processEnded(Failure(ProcessDone(0)))
235@@ -467,7 +457,7 @@
236 protocol = factory.spawns[0][0]
237
238 # Push 200 bytes of output, so we trigger truncation.
239- protocol.childDataReceived(1, "x" * 200)
240+ protocol.childDataReceived(1, b"x" * 200)
241
242 for fd in (0, 1, 2):
243 protocol.childConnectionLost(fd)
244@@ -489,9 +479,9 @@
245 protocol = factory.spawns[0][0]
246
247 # Push 200 bytes of output, so we trigger truncation.
248- protocol.childDataReceived(1, "x" * 200)
249+ protocol.childDataReceived(1, b"x" * 200)
250 # Push 200 bytes more
251- protocol.childDataReceived(1, "x" * 200)
252+ protocol.childDataReceived(1, b"x" * 200)
253
254 for fd in (0, 1, 2):
255 protocol.childConnectionLost(fd)
256@@ -518,7 +508,7 @@
257 result = self.plugin.run_script("/bin/sh", "", time_limit=500)
258 protocol = factory.spawns[0][0]
259 protocol.makeConnection(DummyProcess())
260- protocol.childDataReceived(1, "hi\n")
261+ protocol.childDataReceived(1, b"hi\n")
262 self.manager.reactor.advance(501)
263 protocol.processEnded(Failure(ProcessDone(0)))
264
265@@ -539,7 +529,7 @@
266 protocol = factory.spawns[0][0]
267 transport = DummyProcess()
268 protocol.makeConnection(transport)
269- protocol.childDataReceived(1, "hi\n")
270+ protocol.childDataReceived(1, b"hi\n")
271 protocol.processEnded(Failure(ProcessDone(0)))
272 self.manager.reactor.advance(501)
273 self.assertEqual(transport.signals, [])
274@@ -556,7 +546,7 @@
275 result = self.plugin.run_script("/bin/sh", "", time_limit=500)
276 protocol = factory.spawns[0][0]
277 protocol.makeConnection(DummyProcess())
278- protocol.childDataReceived(1, "hi")
279+ protocol.childDataReceived(1, b"hi")
280 protocol.processEnded(Failure(ProcessDone(0)))
281 self.manager.reactor.advance(501)
282
283@@ -614,10 +604,10 @@
284 user=pwd.getpwuid(uid)[0])
285
286 def check(_):
287- mock_fdopen.assert_called_with(99, "w")
288+ mock_fdopen.assert_called_with(99, "wb")
289 mock_chmod.assert_called_with("tempo!", 0o700)
290 mock_chown.assert_called_with("tempo!", uid, gid)
291- script_file.write.assert_called_with("#!/bin/sh\ncode")
292+ script_file.write.assert_called_with(b"#!/bin/sh\ncode")
293 script_file.close.assert_called_with()
294 self.assertEqual(
295 [mock_mkstemp, mock_fdopen, mock_chmod, mock_chown],
296@@ -706,7 +696,7 @@
297 self.broker_service.message_store.get_pending_messages(), [])
298
299 # Now let's simulate the completion of the process
300- factory.spawns[0][0].childDataReceived(1, "hi!\n")
301+ factory.spawns[0][0].childDataReceived(1, b"hi!\n")
302 factory.spawns[0][0].processEnded(Failure(ProcessDone(0)))
303
304 def got_result(r):
305@@ -748,7 +738,7 @@
306 self.broker_service.message_store.get_pending_messages(), [])
307
308 # Now let's simulate the completion of the process
309- factory.spawns[0][0].childDataReceived(1, "Woof\n")
310+ factory.spawns[0][0].childDataReceived(1, b"Woof\n")
311 factory.spawns[0][0].processEnded(Failure(ProcessDone(0)))
312
313 def got_result(r):
314@@ -821,7 +811,7 @@
315
316 protocol = factory.spawns[0][0]
317 protocol.makeConnection(DummyProcess())
318- protocol.childDataReceived(2, "ONOEZ")
319+ protocol.childDataReceived(2, b"ONOEZ")
320 self.manager.reactor.advance(31)
321 protocol.processEnded(Failure(ProcessDone(0)))
322
323@@ -861,7 +851,7 @@
324 """Responses to script execution messages are urgent."""
325
326 def spawnProcess(protocol, filename, uid, gid, path, env):
327- protocol.childDataReceived(1, "hi!\n")
328+ protocol.childDataReceived(1, b"hi!\n")
329 protocol.processEnded(Failure(ProcessDone(0)))
330 self._verify_script(filename, sys.executable, "print 'hi'")
331
332@@ -893,7 +883,7 @@
333 """
334 def spawnProcess(protocol, filename, uid, gid, path, env):
335 protocol.childDataReceived(
336- 1, "\x7fELF\x01\x01\x01\x00\x00\x00\x95\x01")
337+ 1, b"\x7fELF\x01\x01\x01\x00\x00\x00\x95\x01")
338 protocol.processEnded(Failure(ProcessDone(0)))
339 self._verify_script(filename, sys.executable, "print 'hi'")
340
341
342=== modified file 'py3_ready_tests'
343--- py3_ready_tests 2017-03-27 06:58:52 +0000
344+++ py3_ready_tests 2017-03-28 16:22:54 +0000
345@@ -5,7 +5,7 @@
346
347
348 landscape.tests.test_schema
349-
350+landscape.manager.tests.test_scriptexecution
351
352
353

Subscribers

People subscribed via source and target branches

to all changes: