Merge ~jslarraz/qa-regression-testing:urllib3_tests into qa-regression-testing:master

Proposed by Jorge Sancho Larraz
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)
Reviewer Review Type Date Requested Status
Alex Murray Approve
Review via email: mp+455698@code.launchpad.net

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/python-urllib3) to ensure that requirements introduced in newer versions of the package by CVE patches are meet. Those test have been included in .launchpad and it has been tested that they run smoothly in a local lpci installation based on snap.

    It has also been included one test "test_run_package_tests" that runs urllib3 package tests (obtained via `apt source python-urllib3`) and compared the test result (# of tests failed, passed, skipped and deselected) with the expected outcome for each release. This test has been included more as a way to detect potential regressions between versions (caused by a patch) than as a representative overview of the package status. This test is not run on lpci as it requires connection to pip to install dependencies and it is more prone to cause false positives in the future.

    This commit also include some common functionality in testlib.py required by test-python-urllib3.py to install latest pip version (specially helpful in older releases as the pip version bundled in python3-pip does not work properly) and manage python packages (install and uninstall) via pip. All this functionality is provided with with a common interface across releases, which is really handy to hide per-release specialties from the tests.

To post a comment you must log in.
Revision history for this message
Alex Murray (alexmurray) wrote :

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_requirements()` to make it clearer what this function does just from its name?

Revision history for this message
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.

Revision history for this message
Alex Murray (alexmurray) wrote :

LGTM! Thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.launchpad.yaml b/.launchpad.yaml
2index 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
30diff --git a/scripts/python-urllib3/CVE-2023-45803.patch b/scripts/python-urllib3/CVE-2023-45803.patch
31new file mode 100644
32index 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):
59diff --git a/scripts/python-urllib3/fix_HTTPHeaderDict_not_json_serializable.patch b/scripts/python-urllib3/fix_HTTPHeaderDict_not_json_serializable.patch
60new file mode 100644
61index 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
78diff --git a/scripts/test-python-urllib3.py b/scripts/test-python-urllib3.py
79index 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():
393diff --git a/scripts/testlib.py b/scripts/testlib.py
394index 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'''

Subscribers

People subscribed via source and target branches