Merge ~jslarraz/qa-regression-testing:urllib3_tests into qa-regression-testing:master
- Git
- lp:~jslarraz/qa-regression-testing
- urllib3_tests
- Merge into master
Status: | Merged |
---|---|
Merged at revision: | 73d56ddfc4f8db91989836367923a62081a699f2 |
Proposed branch: | ~jslarraz/qa-regression-testing:urllib3_tests |
Merge into: | qa-regression-testing:master |
Diff against target: |
476 lines (+404/-3) 5 files modified
.launchpad.yaml (+14/-0) scripts/python-urllib3/CVE-2023-45803.patch (+23/-0) scripts/python-urllib3/fix_HTTPHeaderDict_not_json_serializable.patch (+13/-0) scripts/test-python-urllib3.py (+282/-3) scripts/testlib.py (+72/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alex Murray | Approve | ||
Review via email: mp+455698@code.launchpad.net |
Commit message
Description of the change
This commit expands the existent python-urllib3 test suite with tests for CVE-2020-26137, CVE-2018-25091, CVE-2023-43804, CVE-2023-45803, and some additional tests designed to detect potential regressions related to the fixes for those CVEs. Those tests rely on dummyserver, which is obtained in run-time via `apt source python-urllib3` from -release pocket and patched (patchs in $QRT/scripts/
It has also been included one test "test_run_
This commit also include some common functionality in testlib.py required by test-python-
Alex Murray (alexmurray) wrote : | # |
Jorge Sancho Larraz (jslarraz) wrote : | # |
Thanks for the review Alex!
-----
Docstring comments added.
pip_install_r function rename to from_requirements as suggested.
test-python-urllib3 calls to pip_install_r updated accordingly.
Preview Diff
1 | diff --git a/.launchpad.yaml b/.launchpad.yaml |
2 | index 242b9b8..9e75672 100644 |
3 | --- a/.launchpad.yaml |
4 | +++ b/.launchpad.yaml |
5 | @@ -11,6 +11,7 @@ pipeline: |
6 | - busybox |
7 | - coreutils |
8 | - util-linux |
9 | + - python-urllib3 |
10 | |
11 | jobs: |
12 | imagemagick: |
13 | @@ -155,3 +156,16 @@ jobs: |
14 | - sudo |
15 | run: | |
16 | ./lpcraft-runner util-linux |
17 | + |
18 | + python-urllib3: |
19 | + matrix: |
20 | + - series: jammy |
21 | + architectures: amd64 |
22 | + - series: focal |
23 | + architectures: amd64 |
24 | + - series: bionic |
25 | + architectures: amd64 |
26 | + packages: |
27 | + - sudo |
28 | + run: | |
29 | + ./lpcraft-runner python-urllib3 |
30 | diff --git a/scripts/python-urllib3/CVE-2023-45803.patch b/scripts/python-urllib3/CVE-2023-45803.patch |
31 | new file mode 100644 |
32 | index 0000000..b66a445 |
33 | --- /dev/null |
34 | +++ b/scripts/python-urllib3/CVE-2023-45803.patch |
35 | @@ -0,0 +1,23 @@ |
36 | +diff --git a/dummyserver/handlers.py b/dummyserver/handlers.py |
37 | +index c90c2fce20..acd181d2db 100644 |
38 | +--- a/dummyserver/handlers.py |
39 | ++++ b/dummyserver/handlers.py |
40 | +@@ -186,6 +186,8 @@ def redirect(self, request): |
41 | + status = request.params.get("status", "303 See Other") |
42 | + if len(status) == 3: |
43 | + status = "%s Redirect" % status.decode("latin-1") |
44 | ++ elif isinstance(status, bytes): |
45 | ++ status = status.decode("latin-1") |
46 | + |
47 | + headers = [("Location", target)] |
48 | + return Response(status=status, headers=headers) |
49 | +@@ -264,4 +266,9 @@ def encodingrequest(self, request): |
50 | + def headers(self, request): |
51 | + return Response(json.dumps(dict(request.headers))) |
52 | + |
53 | ++ def headers_and_params(self, request): |
54 | ++ return Response( |
55 | ++ json.dumps({"headers": dict(request.headers), "params": request.params}) |
56 | ++ ) |
57 | ++ |
58 | + def successful_retry(self, request): |
59 | diff --git a/scripts/python-urllib3/fix_HTTPHeaderDict_not_json_serializable.patch b/scripts/python-urllib3/fix_HTTPHeaderDict_not_json_serializable.patch |
60 | new file mode 100644 |
61 | index 0000000..fd5d20d |
62 | --- /dev/null |
63 | +++ b/scripts/python-urllib3/fix_HTTPHeaderDict_not_json_serializable.patch |
64 | @@ -0,0 +1,13 @@ |
65 | +diff --git a/dummyserver/handlers.py b/dummyserver/handlers.py |
66 | +index ddf5c49f..83c28a68 100644 |
67 | +--- a/dummyserver/handlers.py |
68 | ++++ b/dummyserver/handlers.py |
69 | +@@ -227,7 +227,7 @@ class TestingApp(RequestHandler): |
70 | + return Response(data, headers=headers) |
71 | + |
72 | + def headers(self, request): |
73 | +- return Response(json.dumps(request.headers)) |
74 | ++ return Response(json.dumps(dict(request.headers))) |
75 | + |
76 | + def successful_retry(self, request): |
77 | + """ Handler which will return an error and then success |
78 | diff --git a/scripts/test-python-urllib3.py b/scripts/test-python-urllib3.py |
79 | index 6a4d779..c664c0c 100755 |
80 | --- a/scripts/test-python-urllib3.py |
81 | +++ b/scripts/test-python-urllib3.py |
82 | @@ -24,9 +24,9 @@ |
83 | |
84 | ''' |
85 | |
86 | -# QRT-Depends: testlib_httpd.py testlib_ssl.py ssl |
87 | -# QRT-Packages: python3-urllib3 ssl-cert openssl lsb-release |
88 | -# QRT-Alternates: apache2:!precise apache2-mpm-prefork:precise |
89 | +# QRT-Depends: testlib_httpd.py testlib_ssl.py ssl python-urllib3 |
90 | +# QRT-Packages: python3-urllib3 ssl-cert openssl lsb-release apache2 git python3-tornado python3-pytest |
91 | +# QRT-Alternates: python3-trustme:!xenial python3-trustme:!bionic |
92 | # QRT-Privilege: root |
93 | |
94 | import unittest, subprocess |
95 | @@ -38,6 +38,8 @@ import testlib_ssl |
96 | import tempfile |
97 | import urllib3 |
98 | import sys |
99 | +import json |
100 | +import tarfile |
101 | |
102 | class BasicTest(testlib_httpd.HttpdCommon): |
103 | '''Test basic functionality''' |
104 | @@ -328,11 +330,288 @@ SSLCACertificateFile /etc/ssl/certs/ca.crt |
105 | cert_file = clientcert_pem, |
106 | key_file = clientkey_pem) |
107 | |
108 | +class PackageTest(testlib.TestlibCase): |
109 | + |
110 | + @classmethod |
111 | + def setUpClass(cls): |
112 | + # Check that patches exists |
113 | + if not os.path.isdir('python-urllib3'): |
114 | + print("python-urllib3 folder was not copied to target. Ensure it is added to QRT-Depends"); exit(-1) |
115 | + |
116 | + # Download sources |
117 | + testlib._ensure_deb_src_entries() |
118 | + testlib._run_apt_command(["python-urllib3"], "source") |
119 | + |
120 | + # Unzip sources |
121 | + for file in os.scandir('.'): |
122 | + if file.name.startswith("python-urllib3") and file.name.endswith("orig.tar.gz"): |
123 | + source_package = tarfile.open(file.name) |
124 | + source_package.extractall('.') |
125 | + source_package.close() |
126 | + |
127 | + # Change directory |
128 | + for folder in os.scandir('.'): |
129 | + if folder.name.startswith("urllib3"): |
130 | + os.chdir(folder.name) |
131 | + |
132 | + # Remove urllib3 sources to ensure we are not using them accidentally |
133 | + for file in os.scandir('.'): |
134 | + if not file.name in ["test", "dummyserver", "dev-requirements.txt", "test-requirements.txt"]: |
135 | + if file.is_file(): |
136 | + os.remove(file.name) |
137 | + elif file.is_dir(): |
138 | + shutil.rmtree(file.name) |
139 | + |
140 | + # Six is no longer embedded in urllib3 version packaged by debian. We need to use system wide six module instead. |
141 | + # This lines substitude patch 01_do-not-use-embedded-python-six.patch but are version independent. |
142 | + os.system("grep -r -l 'from urllib3.packages.six' * | xargs sed -i 's/from urllib3.packages.six/from six/g'") |
143 | + os.system("grep -r -l 'from urllib3.packages import six' * | xargs sed -i 's/from urllib3.packages import six/import six/g'") |
144 | + |
145 | + # Apply patches |
146 | + patches = [] |
147 | + if testlib.TestlibManager().lsb_release['Release'] <= 18.04: |
148 | + patches += ["fix_HTTPHeaderDict_not_json_serializable.patch"] |
149 | + os.system("sed -i s/\\\"/\\\'/g ../python-urllib3/CVE-2023-45803.patch") |
150 | + patches += ["CVE-2023-45803.patch"] |
151 | + |
152 | + for patch in patches: |
153 | + rc, report = testlib.cmd(["git", "apply", "--reject", '../python-urllib3/' + patch.strip()]) |
154 | + if not rc != 0: |
155 | + print(report) |
156 | + |
157 | + # Add current directory to path so that dummyserver can be imported and start a dummyserver instance |
158 | + sys.path += [os.getcwd()] |
159 | + |
160 | + from tornado import ioloop, web |
161 | + from dummyserver.handlers import TestingApp |
162 | + from dummyserver.server import ( |
163 | + DEFAULT_CERTS, |
164 | + run_loop_in_thread, |
165 | + run_tornado_app, |
166 | + ) |
167 | + |
168 | + # Start test server |
169 | + cls.io_loop = ioloop.IOLoop.current() |
170 | + app = web.Application([(r".*", TestingApp)]) |
171 | + cls.server, cls.port = run_tornado_app( |
172 | + app, cls.io_loop, DEFAULT_CERTS, "http", "localhost" |
173 | + ) |
174 | + cls.server_thread = run_loop_in_thread(cls.io_loop) |
175 | + |
176 | + cls.base_url = "http://%s:%d" % ("localhost", cls.port) |
177 | + cls.base_url_alt = "http://%s:%d" % ("127.0.0.1", cls.port) |
178 | + |
179 | + @classmethod |
180 | + def tearDownClass(cls): |
181 | + # Stop test server |
182 | + cls.io_loop.add_callback(cls.server.stop) |
183 | + cls.io_loop.add_callback(cls.io_loop.stop) |
184 | + cls.server_thread.join() |
185 | + |
186 | + def test_invalid_method_not_allowed(self): |
187 | + ''' CVE-2020-26137 ''' |
188 | + for char in [" ", "\r", "\n", "\x00"]: |
189 | + exception_caught = False |
190 | + with urllib3.HTTPConnectionPool("localhost", self.port) as pool: |
191 | + try: |
192 | + pool.request("GET" + char, "/") |
193 | + except ValueError: |
194 | + exception_caught = True |
195 | + self.assertTrue(exception_caught, "Invalid method with character " + char + ". It may be caused by CVE-2020-26137") |
196 | + |
197 | + def test_retry_default_remove_headers_on_redirect(self): |
198 | + ''' CVE-2018-25091 or CVE-2023-43804 ''' |
199 | + retry = urllib3.util.retry.Retry() |
200 | + |
201 | + self.assertEqual(retry.remove_headers_on_redirect, {"authorization", "cookie"}, "Wrong default value of remove_headers_on_redirect. It is probably caused by CVE-2018-25091 or CVE-2023-43804") |
202 | + |
203 | + def test_retry_set_remove_headers_on_redirect(self): |
204 | + ''' Functional test related to potential CVE-2018-25091 and CVE-2023-43804 regressions. It is not expected to fail even if the package is vulnerable. ''' |
205 | + retry = urllib3.util.retry.Retry(remove_headers_on_redirect=["X-API-Secret"]) |
206 | + |
207 | + self.assertEqual(retry.remove_headers_on_redirect, {"x-api-secret"}, "urllib3 didn't honored the value of the remove_headers_on_redirect passed to the retry function. This is probably caused by a regression.") |
208 | + |
209 | + def test_redirect_cross_host_remove_headers(self): |
210 | + ''' CVE-2018-25091 or CVE-2023-43804 ''' |
211 | + with urllib3.poolmanager.PoolManager() as http: |
212 | + r = http.request( |
213 | + "GET", |
214 | + "%s/redirect" % self.base_url, |
215 | + fields={"target": "%s/headers" % self.base_url_alt}, |
216 | + headers={"Authorization": "foo", "Cookie": "foo=bar"}, |
217 | + ) |
218 | + |
219 | + self.assertEqual(r.status, 200, "Something went wrong with dummyserver, it returned wrong status code. It is probably a problem in the test and not in the library") |
220 | + |
221 | + data = json.loads(r.data.decode("utf-8")) |
222 | + |
223 | + self.assertNotIn("Authorization", data, "Authorization header was not properly removed when redirected to a different origin. It is probably caused by CVE-2018-25091") |
224 | + self.assertNotIn("Cookie", data, "Cookie header was not properly removed when redirected to a different origin. It is probably caused by CVE-2023-43804") |
225 | + |
226 | + r = http.request( |
227 | + "GET", |
228 | + "%s/redirect" % self.base_url, |
229 | + fields={"target": "%s/headers" % self.base_url_alt}, |
230 | + headers={"authorization": "foo", "cookie": "foo=bar"}, |
231 | + ) |
232 | + |
233 | + self.assertEqual(r.status, 200, "Something went wrong with dummyserver, it returned wrong status code. It is probably a problem in the test and not in the library") |
234 | + |
235 | + data = json.loads(r.data.decode("utf-8")) |
236 | + |
237 | + self.assertNotIn("authorization", data, "authorization header was not properly removed when redirected to a different origin. It is probably caused by CVE-2018-20060.") |
238 | + self.assertNotIn("Authorization", data, "Authorization header was not properly removed when redirected to a different origin. It is probably caused by CVE-2018-25091") |
239 | + self.assertNotIn("cookie", data, "cookie header was not properly removed when redirected to a different origin. It is probably caused by CVE-2023-43804") |
240 | + self.assertNotIn("Cookie", data, "Cookie header was not properly removed when redirected to a different origin. It is probably caused by CVE-2023-43804") |
241 | + |
242 | + def test_redirect_cross_host_no_remove_headers(self): |
243 | + ''' Functional test related to potential CVE-2018-25091 and CVE-2023-43804 regressions. It is not expected to fail even if the package is vulnerable. ''' |
244 | + with urllib3.poolmanager.PoolManager() as http: |
245 | + r = http.request( |
246 | + "GET", |
247 | + "%s/redirect" % self.base_url, |
248 | + fields={"target": "%s/headers" % self.base_url_alt}, |
249 | + headers={"Authorization": "foo", "Cookie": "foo=bar"}, |
250 | + retries=urllib3.util.retry.Retry(remove_headers_on_redirect=[]), |
251 | + ) |
252 | + |
253 | + self.assertEqual(r.status, 200, "Something went wrong with dummyserver, it returned wrong status code. It is probably a problem in the test and not in the library") |
254 | + |
255 | + data = json.loads(r.data.decode("utf-8")) |
256 | + |
257 | + self.assertEqual(data["Authorization"], "foo", "The Authorization header value changed or was removed when explicitly stating the contraty. This is probably caused by a regression") |
258 | + self.assertEqual(data["Cookie"], "foo=bar", "The Cookie header value changed or was removed when explicitly stating the contraty. This is probably caused by a regression") |
259 | + |
260 | + def test_redirect_cross_host_set_removed_headers(self): |
261 | + ''' Functional test related to potential CVE-2018-25091 and CVE-2023-43804 regressions. It is not expected to fail even if the package is vulnerable. ''' |
262 | + with urllib3.poolmanager.PoolManager() as http: |
263 | + r = http.request( |
264 | + "GET", |
265 | + "%s/redirect" % self.base_url, |
266 | + fields={"target": "%s/headers" % self.base_url_alt}, |
267 | + headers={ |
268 | + "X-API-Secret": "foo", |
269 | + "Authorization": "bar", |
270 | + "Cookie": "foo=bar", |
271 | + }, |
272 | + retries=urllib3.util.retry.Retry(remove_headers_on_redirect=["X-API-Secret"]), |
273 | + ) |
274 | + |
275 | + self.assertEqual(r.status, 200, "Something went wrong with dummyserver, it returned wrong status code. It is probably a problem in the test and not in the library") |
276 | + |
277 | + data = json.loads(r.data.decode("utf-8")) |
278 | + |
279 | + self.assertNotIn("X-API-Secret", data, "X-API-Secret header was not properly removed when explicitly requested. It is probably caused by a regression.") |
280 | + self.assertEqual(data["Authorization"], "bar", "The Authorization header value changed or was removed when explicitly stating the contraty. This is probably caused by a regression") |
281 | + self.assertEqual(data["Cookie"], "foo=bar", "The Cookie header value changed or was removed when explicitly stating the contraty. This is probably caused by a regression") |
282 | + |
283 | + r = http.request( |
284 | + "GET", |
285 | + "%s/redirect" % self.base_url, |
286 | + fields={"target": "%s/headers" % self.base_url_alt}, |
287 | + headers={ |
288 | + "x-api-secret": "foo", |
289 | + "authorization": "bar", |
290 | + "cookie": "foo=bar", |
291 | + }, |
292 | + retries=urllib3.util.retry.Retry(remove_headers_on_redirect=["X-API-Secret"]), |
293 | + ) |
294 | + |
295 | + self.assertEqual(r.status, 200, "Something went wrong with dummyserver, it returned wrong status code. It is probably a problem in the test and not in the library") |
296 | + |
297 | + data = json.loads(r.data.decode("utf-8")) |
298 | + |
299 | + self.assertNotIn("x-api-secret", data, "x-api-secret header was not properly removed when explicitly requested. It is probably caused by a regression.") |
300 | + self.assertNotIn("X-API-Secret", data, "X-API-Secret header was not properly removed when explicitly requested. It is probably caused by a regression.") |
301 | + self.assertEqual(data["Authorization"], "bar", "The Authorization header value changed or was removed when explicitly stating the contraty. This is probably caused by a regression") |
302 | + self.assertEqual(data["Cookie"], "foo=bar", "The Cookie header value changed or was removed when explicitly stating the contraty. This is probably caused by a regression") |
303 | + |
304 | + def test_303_redirect_makes_request_lose_body(self): |
305 | + ''' CVE-2023-45803 ''' |
306 | + with urllib3.poolmanager.PoolManager() as http: |
307 | + response = http.request( |
308 | + "POST", |
309 | + "%s/redirect" % self.base_url, |
310 | + fields={ |
311 | + "target": "%s/headers_and_params" % self.base_url, |
312 | + "status": "303 See Other", |
313 | + }, |
314 | + ) |
315 | + data = json.loads(response.data.decode("utf-8")) |
316 | + self.assertEqual(data["params"], {}, "Request body was not properly removed when 303 redirected to a different origin. It is probably caused by CVE-2023-45803") |
317 | + self.assertNotIn("Content-Type", urllib3._collections.HTTPHeaderDict(data["headers"]), "Request body was not properly removed when 303 redirected to a different origin. It is probably caused by CVE-2023-45803") |
318 | + |
319 | + @unittest.skipIf( |
320 | + 'LPCRAFT_QRT_RUNNER' in os.environ, |
321 | + 'External URLs not allowed in lpci environ' |
322 | + ) |
323 | + def test_run_package_tests(self): |
324 | + ''' Run urllib3 package test for the version included in the release and search for the expected results. ''' |
325 | + |
326 | + # Install dependencies |
327 | + testlib.get_pip() |
328 | + |
329 | + release = testlib.TestlibManager().lsb_release['Release'] |
330 | + if release == 18.04: |
331 | + # Required to build psutils, which is only required in bionic |
332 | + testlib.install_packages(['build-essential', 'python3-dev']) |
333 | + |
334 | + if release == 14.04: |
335 | + testlib.pip_install_from_requirements('test-requirements.txt') |
336 | + else: |
337 | + testlib.pip_install_from_requirements('dev-requirements.txt') |
338 | + |
339 | + # Remove pip, it is needed to test the urllib3 module bundled inside pip |
340 | + testlib.pip_uninstall('pip') |
341 | + |
342 | + # Fix broken 'six' dependencies caused by patch 01_do-not-use-embedded-python-six.patch. ssltransport seems to only be used by old tests. |
343 | + os.system("sudo sed -i 's/from ..packages import six/import six/g' /usr/lib/python3/dist-packages/urllib3/util/ssltransport.py") |
344 | + os.system("sudo sed -i 's/from urllib3.packages import six/import six/g' /usr/lib/python3/dist-packages/urllib3/util/ssltransport.py") |
345 | + |
346 | + |
347 | + # Run package test |
348 | + command = [sys.executable, "-m", "pytest", "test", "-k", "not test_retry_default_remove_headers_on_redirect and " |
349 | + "not test_retry_set_remove_headers_on_redirect and " |
350 | + "not test_redirect_cross_host_remove_headers and " |
351 | + "not test_redirect_cross_host_no_remove_headers and " |
352 | + "not test_redirect_cross_host_no_remove_headers and " |
353 | + "not test_redirect_cross_host_set_removed_headers and " |
354 | + "not test_303_redirect_makes_request_lose_body", |
355 | + "--ignore=test/test_retry_deprecated.py"] |
356 | + if release == 20.04: # There is an error in the version used in focal. Test using dummyserver don't work properly |
357 | + command += ['--ignore=test/with_dummyserver'] |
358 | + rc, report = testlib.cmd(command) |
359 | + |
360 | + # Check expected results |
361 | + if release == 16.04: |
362 | + self.assertIn("29 failed, 264 passed, 7 skipped", |
363 | + report, "Packet test execution result different than expected: " + report) |
364 | + elif release == 18.04: |
365 | + self.assertIn("35 failed, 763 passed, 52 skipped", |
366 | + report, "Packet test execution result different than expected: " + report) |
367 | + elif release == 20.04: |
368 | + self.assertIn("3 failed, 585 passed, 378 skipped, 2 deselected", |
369 | + report, "Packet test execution result different than expected: " + report) |
370 | + elif release == 22.04: |
371 | + self.assertIn("4 failed, 1226 passed, 588 skipped, 5 deselected", |
372 | + report, "Packet test execution result different than expected: " + report) |
373 | + elif release == 23.04: |
374 | + self.assertIn("3 failed, 1276 passed, 611 skipped, 5 deselected", |
375 | + report, "Packet test execution result different than expected: " + report) |
376 | + elif release == 23.10: |
377 | + self.assertIn("3 failed, 1289 passed, 611 skipped, 5 deselected", |
378 | + report, "Packet test execution result different than expected: " + report) |
379 | + else: |
380 | + print("Expected outcome is not defined for release: " + str(release)) |
381 | |
382 | if __name__ == '__main__': |
383 | testlib.require_root() |
384 | suite = unittest.TestSuite() |
385 | suite.addTest(unittest.TestLoader().loadTestsFromTestCase(BasicTest)) |
386 | + # Fixes for CVE-2018-25091, CVE-2023-43804 and CVE-2023-45803 are only available in esm for xenial and bionic, |
387 | + # skip those tests when run in LPCI |
388 | + if not ('LPCRAFT_QRT_RUNNER' in os.environ) or (testlib.TestlibManager().lsb_release['Release'] >= 20.04): |
389 | + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(PackageTest)) |
390 | |
391 | rc = unittest.TextTestRunner(verbosity=2).run(suite) |
392 | if not rc.wasSuccessful(): |
393 | diff --git a/scripts/testlib.py b/scripts/testlib.py |
394 | index 8ed2353..9191a4a 100644 |
395 | --- a/scripts/testlib.py |
396 | +++ b/scripts/testlib.py |
397 | @@ -915,7 +915,79 @@ def _run_snap_remove(snapname): |
398 | rc, report = cmd(command) |
399 | return rc, report |
400 | |
401 | +def get_pip(): |
402 | + '''Install the latest compatible version of pip via the get-pip.py scirpt.''' |
403 | + tempdir = tempfile.mkdtemp(dir='/tmp', prefix="testlib-") |
404 | + |
405 | + # Download boostrap script, get-pip.py |
406 | + if TestlibManager().lsb_release['Release'] == 14.04: |
407 | + get_pip_url = 'https://bootstrap.pypa.io/pip/3.4/get-pip.py' |
408 | + elif TestlibManager().lsb_release['Release'] == 16.04: |
409 | + get_pip_url = 'https://bootstrap.pypa.io/pip/3.5/get-pip.py' |
410 | + elif TestlibManager().lsb_release['Release'] == 18.04: |
411 | + install_package('python3-distutils') |
412 | + get_pip_url = 'https://bootstrap.pypa.io/pip/3.6/get-pip.py' |
413 | + else: |
414 | + install_package('python3-distutils') |
415 | + get_pip_url = 'https://bootstrap.pypa.io/pip/get-pip.py' |
416 | + command = ['wget', get_pip_url, '-O', tempdir+'/get-pip.py'] |
417 | + rc, report = cmd(command) |
418 | + if rc != 0: |
419 | + return rc, report |
420 | + |
421 | + # Run the script to install the latest supported version of pip |
422 | + command = [sys.executable, tempdir+'/get-pip.py'] |
423 | + if TestlibManager().lsb_release['Release'] == 14.04: |
424 | + command += ['--user'] |
425 | + sys.path = [os.path.expanduser("~/.local/bin")] + sys.path |
426 | + if TestlibManager().lsb_release['Release'] >= 23.04: |
427 | + command += ['--break-system-packages'] |
428 | + rc, report = cmd(command) |
429 | + |
430 | + shutil.rmtree(tempdir) |
431 | + return rc, report |
432 | + |
433 | +def pip_install(pkg): |
434 | + '''Installs a python package or a list of packages using pip''' |
435 | + command = [sys.executable, '-m', 'pip', 'install'] |
436 | + if isinstance(pkg, str): |
437 | + command += [pkg] |
438 | + elif isinstance(pkg, list): |
439 | + command += pkg |
440 | + |
441 | + if TestlibManager().lsb_release['Release'] == 14.04: |
442 | + command += ['--user'] |
443 | + sys.path = [os.path.expanduser("~/.local/bin")] + sys.path |
444 | + if TestlibManager().lsb_release['Release'] >= 23.04: |
445 | + command += ['--break-system-packages'] |
446 | + rc, report = cmd(command) |
447 | + return rc, report |
448 | |
449 | +def pip_uninstall(pkg): |
450 | + '''Uninstalls a python package or a list of packages using pip''' |
451 | + command = [sys.executable, '-m', 'pip', 'uninstall', '--yes'] |
452 | + if isinstance(pkg, str): |
453 | + command += [pkg] |
454 | + elif isinstance(pkg, list): |
455 | + command += pkg |
456 | + |
457 | + if TestlibManager().lsb_release['Release'] == 14.04: |
458 | + command += ['--user'] |
459 | + if TestlibManager().lsb_release['Release'] >= 23.04: |
460 | + command += ['--break-system-packages'] |
461 | + rc, report = cmd(command) |
462 | + return rc, report |
463 | + |
464 | +def pip_install_from_requirements(requirements): |
465 | + '''Install a set of python packages from a requirements file using pip''' |
466 | + command = [sys.executable, '-m', 'pip', 'install', '-r', requirements] |
467 | + if TestlibManager().lsb_release['Release'] == 14.04: |
468 | + command += ['--user'] |
469 | + sys.path = [os.path.expanduser("~/.local/bin")] + sys.path |
470 | + if TestlibManager().lsb_release['Release'] >= 23.04: |
471 | + command += ['--break-system-packages'] |
472 | + rc, report = cmd(command) |
473 | + return rc, report |
474 | |
475 | class TestDaemon: |
476 | '''Helper class to manage daemons consistently''' |
LGTM but could you please add a docstring comment for each of the new functions added to testlib.py to explain what they do (particularly `get_pip()` and `pip_install_r()` - and I wonder if perhaps `pip_install_r()` should be renamed `pip_install_ from_requiremen ts()` to make it clearer what this function does just from its name?