Merge ~paelzer/ubuntu/+source/open-iscsi:lp-1919461-test-python3-compat into ubuntu/+source/open-iscsi:ubuntu/hirsute-devel

Proposed by Christian Ehrhardt 
Status: Merged
Approved by: Christian Ehrhardt 
Approved revision: 01ee4f90decdc5f7c8387b09bb904bcba34477ea
Merge reported by: Bryce Harrington
Merged at revision: 01ee4f90decdc5f7c8387b09bb904bcba34477ea
Proposed branch: ~paelzer/ubuntu/+source/open-iscsi:lp-1919461-test-python3-compat
Merge into: ubuntu/+source/open-iscsi:ubuntu/hirsute-devel
Diff against target: 1887 lines (+853/-236)
9 files modified
debian/changelog (+18/-0)
debian/patches/series (+3/-0)
debian/patches/upstream/0001-iscsiadm-Fix-memory-leak-in-iscsiadm.patch (+156/-0)
debian/patches/upstream/0002-Fix-iscsiadm-segfault-when-exiting.patch (+47/-0)
debian/patches/upstream/0003-Fix-iscsistart-login-issue-when-target-is-delayed.patch (+58/-0)
debian/tests/control (+1/-1)
debian/tests/test-open-iscsi.py (+14/-13)
debian/tests/testlib.py (+555/-220)
debian/tests/testsuite (+1/-2)
Reviewer Review Type Date Requested Status
Christian Ehrhardt  (community) Approve
Canonical Server packageset reviewers Pending
Canonical Server Pending
Review via email: mp+399963@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Christian Ehrhardt  (paelzer) wrote :
01ee4f9... by Christian Ehrhardt 

changelog: Add patches from upstream [from Debian 2.1.3-2]

Signed-off-by: Christian Ehrhardt <email address hidden>

Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

Hmm, I've just seen open-iscsi in the merge opportunities.
I wonder if we should add this to my upload ...

   * [efb5512] Add patches from upstream:
     Fix memory leak in iscsiadm, Fix iscsiadm segfault when exiting, and
     Fix iscsistart login issue when target is delayed. The last one should
     fix #980085. (Closes: #980085)

I think we should, .... adding ...
Uploaded 2.1.3-1ubuntu5~ppa3 to the PPA and pushed to the MP

Revision history for this message
Dimitri John Ledkov (xnox) wrote :

I would not have bothered with keeping python2 compat, and simply did python3-only. Even to the point of SRUing python3 autopkgtest back.

I.e. no fallbacks to old python api, no imports from __future__.

But I guess it does help with testing to ensure that 2&3 still work on i.e. focal.

Revision history for this message
Utkarsh Gupta (utkarsh) wrote :

* Changelog:
  - [√] changelog entry correct version and targeted codename
  - [?] changelog entries correct

I get:
W: libopeniscsiusr0.2.0: debian-changelog-line-too-long line 8
...maybe move the link to the next line?

Another minor/trivial comment here:
The last line of the d/ch entry says "fix #980085. (Closes: #980085)" which doesn't really help me understand what the change was in the first glance. I understand that this was exactly the commit message/header in Debian, but maybe worth adding a one-liner about what this is about?
This would help others to quickly see what changed or fix from just reading the d/ch entry. Anyway, this is just me so please feel free to ignore this.

  - [√] update-maintainer has been run

* Actual changes:
  - [-] no further upstream version to consider
  - [√] debian changes look safe

* New Delta:
  - [√] patches match what was proposed upstream
  - [√] patches correctly included in debian/patches/series
  - [√] patches have correct DEP3 metadata

No "Origin" field but that's okay since these changes are actually a part of Debian so this part of the delta can be very easily dropped. So all good here.

* Build/Test:
  - [√] build is ok
  - [√] verified PPA package installs/uninstalls
  - [√] autopkgtest against the PPA package passes

Ran autopkgtest against what's in the archive, which failed, as the bug report correctly highlights and then ran autopkgtest against this branch, which passed, as intended, which correctly fixes the issue.
cf: autopkgtest [17:32:52]: @@@@@@@@@@@@@@@@@@@@ summary
install PASS
testsuite PASS
nested PASS

So besides the trivial comments above, this looks good! \o/

Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

Thanks for the catch ont he CL, auto-log at fail.
Fixed

I added a hint what #980085 is - that was worth to ask for

For the patches yeah, since they came in the "upstream" subdir and I mentioned in the CL where they came from I thought that I can keep them as-is. Then it also will be easy to compare when dropping them on the next merge.

Thank you Utkarsh, combined with my tests that is enough for me to upload

review: Approve
Revision history for this message
Christian Ehrhardt  (paelzer) wrote :

To ssh://git.launchpad.net/~usd-import-team/ubuntu/+source/open-iscsi
 * [new tag] upload/2.1.3-1ubuntu5 -> upload/2.1.3-1ubuntu5

Uploading to ubuntu (via ftp to upload.ubuntu.com):
  Uploading open-iscsi_2.1.3-1ubuntu5.dsc: done.
  Uploading open-iscsi_2.1.3-1ubuntu5.debian.tar.xz: done.
  Uploading open-iscsi_2.1.3-1ubuntu5_source.buildinfo: done.
  Uploading open-iscsi_2.1.3-1ubuntu5_source.changes: done.
Successfully uploaded packages.

Revision history for this message
Bryce Harrington (bryce) wrote :

Migrated
  - Current Version: 2.1.3-1ubuntu5
  - Proposed Version: None
  - Debian Version: 2.1.3-1ubuntu5
  - New Version: 2.1.3-1ubuntu5

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/changelog b/debian/changelog
2index 0b9f498..827c0eb 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -1,3 +1,21 @@
6+open-iscsi (2.1.3-1ubuntu5) hirsute; urgency=medium
7+
8+ * make d/t/testuite python3 compatible (LP: #1919461)
9+ - backport changes from qa-regression-testing since we diverged
10+ - various tests: make py3 compliant, fix formatting a bit
11+ - tests: convert deprecated assertEquals() to assertEqual()
12+ - Remove all references to python-utils
13+ - d/t/testlib.py: update to last state from git+ssh://git.launchpad.net/qa-regression-testing
14+ - d/t/{control,testsuite}: bump to use python3
15+ - d/t/test-open-iscsi.py: switch shebang to python3
16+ - d/t/test-open-iscsi.py: adapt to updated testlib
17+ * Add patches from upstream [this is from Debian upload 2.1.3-2]:
18+ - Fix memory leak in iscsiadm, Fix iscsiadm segfault when exiting, and
19+ - Fix iscsistart login issue when target is delayed. The last one should
20+ - fix #980085. (Closes: #980085)
21+
22+ -- Christian Ehrhardt <christian.ehrhardt@canonical.com> Thu, 18 Mar 2021 09:44:07 +0100
23+
24 open-iscsi (2.1.3-1ubuntu4) hirsute; urgency=medium
25
26 * Rebuild again so s390x pick up the right debhelper
27diff --git a/debian/patches/series b/debian/patches/series
28index 909a8f3..8260850 100644
29--- a/debian/patches/series
30+++ b/debian/patches/series
31@@ -1 +1,4 @@
32+upstream/0001-iscsiadm-Fix-memory-leak-in-iscsiadm.patch
33+upstream/0002-Fix-iscsiadm-segfault-when-exiting.patch
34+upstream/0003-Fix-iscsistart-login-issue-when-target-is-delayed.patch
35 lp1755858-default-iscsid_conf-to-iscsid_socket.patch
36diff --git a/debian/patches/upstream/0001-iscsiadm-Fix-memory-leak-in-iscsiadm.patch b/debian/patches/upstream/0001-iscsiadm-Fix-memory-leak-in-iscsiadm.patch
37new file mode 100644
38index 0000000..dda9a00
39--- /dev/null
40+++ b/debian/patches/upstream/0001-iscsiadm-Fix-memory-leak-in-iscsiadm.patch
41@@ -0,0 +1,156 @@
42+From: Wenchao Hao <haowenchao@huawei.com>
43+Date: Tue, 29 Dec 2020 20:30:25 +0800
44+Subject: iscsiadm: Fix memory leak in iscsiadm
45+
46+Memory allocated by iscsi_context_new() would not be freed if
47+error occurred during parameters parser stage and goto free_ifaces
48+is used to jump to resource clean.
49+
50+Since all resource clean is performed after verified, so change
51+all goto free_ifaces to goto out where handles resource better.
52+
53+Signed-off-by: Wenchao Hao <haowenchao@huawei.com>
54+---
55+ libopeniscsiusr/context.c | 6 +++++-
56+ usr/iscsiadm.c | 27 +++++++++++++--------------
57+ 2 files changed, 18 insertions(+), 15 deletions(-)
58+
59+diff --git a/libopeniscsiusr/context.c b/libopeniscsiusr/context.c
60+index fe92155..c5e869f 100644
61+--- a/libopeniscsiusr/context.c
62++++ b/libopeniscsiusr/context.c
63+@@ -55,8 +55,12 @@ struct iscsi_context *iscsi_context_new(void)
64+
65+ void iscsi_context_free(struct iscsi_context *ctx)
66+ {
67+- if (ctx != NULL)
68++ if (ctx == NULL)
69++ return;
70++
71++ if (ctx->db)
72+ _idbm_free(ctx->db);
73++
74+ free(ctx);
75+ }
76+
77+diff --git a/usr/iscsiadm.c b/usr/iscsiadm.c
78+index ea1643b..3987168 100644
79+--- a/usr/iscsiadm.c
80++++ b/usr/iscsiadm.c
81+@@ -3627,7 +3627,7 @@ main(int argc, char **argv)
82+ "Priority must be greater than or "
83+ "equal to zero.", killiscsid);
84+ rc = ISCSI_ERR_INVAL;
85+- goto free_ifaces;
86++ goto out;
87+ }
88+ break;
89+ case 't':
90+@@ -3639,7 +3639,7 @@ main(int argc, char **argv)
91+ log_error("can not recognize operation: '%s'",
92+ optarg);
93+ rc = ISCSI_ERR_INVAL;
94+- goto free_ifaces;
95++ goto out;
96+ }
97+ break;
98+ case 'n':
99+@@ -3651,7 +3651,7 @@ main(int argc, char **argv)
100+ case 'H':
101+ host_no = parse_host_info(optarg, &rc);
102+ if (rc)
103+- goto free_ifaces;
104++ goto out;
105+ break;
106+ case 'r':
107+ sid = iscsi_sysfs_get_sid_from_path(optarg);
108+@@ -3659,7 +3659,7 @@ main(int argc, char **argv)
109+ log_error("invalid sid '%s'",
110+ optarg);
111+ rc = ISCSI_ERR_INVAL;
112+- goto free_ifaces;
113++ goto out;
114+ }
115+ break;
116+ case 'R':
117+@@ -3710,7 +3710,7 @@ main(int argc, char **argv)
118+ mode = str_to_mode(optarg);
119+ rc = verify_mode_params(argc, argv, mode);
120+ if (ISCSI_SUCCESS != rc)
121+- goto free_ifaces;
122++ goto out;
123+ break;
124+ case 'C':
125+ sub_mode = str_to_submode(optarg);
126+@@ -3739,11 +3739,11 @@ main(int argc, char **argv)
127+ printf("Invalid iface name %s. Must be from "
128+ "1 to %d characters.\n",
129+ optarg, ISCSI_MAX_IFACE_LEN - 1);
130+- goto free_ifaces;
131++ goto out;
132+ } else if (!iface || rc) {
133+ printf("Could not add iface %s.", optarg);
134+ rc = ISCSI_ERR_INVAL;
135+- goto free_ifaces;
136++ goto out;
137+ }
138+
139+ list_add_tail(&iface->list, &ifaces);
140+@@ -3760,7 +3760,7 @@ main(int argc, char **argv)
141+ log_error("Invalid index %s. %s.",
142+ optarg, strerror(errno));
143+ rc = ISCSI_ERR_INVAL;
144+- goto free_ifaces;
145++ goto out;
146+ }
147+ break;
148+ case 'A':
149+@@ -3778,7 +3778,7 @@ main(int argc, char **argv)
150+ if (!param) {
151+ log_error("Cannot allocate memory for params.");
152+ rc = ISCSI_ERR_NOMEM;
153+- goto free_ifaces;
154++ goto out;
155+ }
156+ list_add_tail(&param->list, &params);
157+ name = NULL;
158+@@ -3789,12 +3789,12 @@ main(int argc, char **argv)
159+ if (optopt) {
160+ log_error("unrecognized character '%c'", optopt);
161+ rc = ISCSI_ERR_INVAL;
162+- goto free_ifaces;
163++ goto out;
164+ }
165+
166+ if (killiscsid >= 0) {
167+ kill_iscsid(killiscsid, timeout);
168+- goto free_ifaces;
169++ goto out;
170+ }
171+
172+ if (mode < 0)
173+@@ -3802,14 +3802,14 @@ main(int argc, char **argv)
174+
175+ if (mode == MODE_FW) {
176+ rc = exec_fw_op(NULL, NULL, info_level, do_login, op);
177+- goto free_ifaces;
178++ goto out;
179+ }
180+
181+ increase_max_files();
182+ if (idbm_init(get_config_file)) {
183+ log_warning("exiting due to idbm configuration error");
184+ rc = ISCSI_ERR_IDBM;
185+- goto free_ifaces;
186++ goto out;
187+ }
188+
189+ switch (mode) {
190+@@ -4070,7 +4070,6 @@ out:
191+ free(rec);
192+ iscsi_sessions_free(ses, se_count);
193+ idbm_terminate();
194+-free_ifaces:
195+ list_for_each_entry_safe(iface, tmp, &ifaces, list) {
196+ list_del(&iface->list);
197+ free(iface);
198diff --git a/debian/patches/upstream/0002-Fix-iscsiadm-segfault-when-exiting.patch b/debian/patches/upstream/0002-Fix-iscsiadm-segfault-when-exiting.patch
199new file mode 100644
200index 0000000..183bdaa
201--- /dev/null
202+++ b/debian/patches/upstream/0002-Fix-iscsiadm-segfault-when-exiting.patch
203@@ -0,0 +1,47 @@
204+From: Lee Duncan <lduncan@suse.com>
205+Date: Tue, 26 Jan 2021 11:48:32 -0800
206+Subject: Fix iscsiadm segfault when exiting
207+
208+Commit b532ad67d495d added some cleanup code
209+to iscsiadm right before it exits, but it
210+used a list_for_each_entry() to iterate through
211+a list was being deleted, when it should use
212+list_for_each_entry_safe().
213+
214+Fixes: b532ad67d495d
215+---
216+ usr/iscsiadm.c | 8 ++++----
217+ 1 file changed, 4 insertions(+), 4 deletions(-)
218+
219+diff --git a/usr/iscsiadm.c b/usr/iscsiadm.c
220+index 3987168..bb6821d 100644
221+--- a/usr/iscsiadm.c
222++++ b/usr/iscsiadm.c
223+@@ -3582,11 +3582,11 @@ main(int argc, char **argv)
224+ struct sigaction sa_old;
225+ struct sigaction sa_new;
226+ LIST_HEAD(ifaces);
227+- struct iface_rec *iface = NULL, *tmp;
228++ struct iface_rec *iface = NULL, *tmp_iface;
229+ struct node_rec *rec = NULL;
230+ uint32_t host_no = MAX_HOST_NO + 1;
231+ uint64_t index = ULLONG_MAX;
232+- struct user_param *param;
233++ struct user_param *param, *tmp_param;
234+ LIST_HEAD(params);
235+ struct iscsi_context *ctx = NULL;
236+ int librc = LIBISCSI_OK;
237+@@ -4070,11 +4070,11 @@ out:
238+ free(rec);
239+ iscsi_sessions_free(ses, se_count);
240+ idbm_terminate();
241+- list_for_each_entry_safe(iface, tmp, &ifaces, list) {
242++ list_for_each_entry_safe(iface, tmp_iface, &ifaces, list) {
243+ list_del(&iface->list);
244+ free(iface);
245+ }
246+- list_for_each_entry(param, &params, list) {
247++ list_for_each_entry_safe(param, tmp_param, &params, list) {
248+ list_del(&param->list);
249+ idbm_free_user_param(param);
250+ }
251diff --git a/debian/patches/upstream/0003-Fix-iscsistart-login-issue-when-target-is-delayed.patch b/debian/patches/upstream/0003-Fix-iscsistart-login-issue-when-target-is-delayed.patch
252new file mode 100644
253index 0000000..6602f3a
254--- /dev/null
255+++ b/debian/patches/upstream/0003-Fix-iscsistart-login-issue-when-target-is-delayed.patch
256@@ -0,0 +1,58 @@
257+From: Lee Duncan <lduncan@suse.com>
258+Date: Thu, 4 Feb 2021 11:45:00 -0800
259+Subject: Fix iscsistart login issue when target is delayed.
260+
261+Earlier commit 9258c8eae046 changed the return value fron
262+iscsid_response() from ISCSI_ERR_ISCSID_NOTCONN to
263+ISCSI_ERR_SESSION_NOT_CONNECTED in the case where no iscsi response is
264+received when expected.
265+
266+This effected the login code in iscsistart when the target is not
267+completely ready at iscsi login time. This commit updates iscsistart
268+to expect the new error code, but fixing this uncovered another issue,
269+causing iscsistart logins to continue to fail if the target returned its
270+login response too slowly.
271+
272+This commit ups the timeout time for iscsistart logins from 1 second per
273+try to 10 seconds per try. This is perhaps excessive, and a shorter
274+delay would be more appropriate, but the retry/nanosleep logic in
275+iscsistart meant to retry the login in such cases seems problematic in
276+this case, since retrying the 2nd time returns "session already exists",
277+and most iscsistart clients aren't prepared for the command to return a
278+non-zero return value.
279+---
280+ usr/iscsistart.c | 17 +++++++++++++++--
281+ 1 file changed, 15 insertions(+), 2 deletions(-)
282+
283+diff --git a/usr/iscsistart.c b/usr/iscsistart.c
284+index 73991b3..755489f 100644
285+--- a/usr/iscsistart.c
286++++ b/usr/iscsistart.c
287+@@ -241,12 +241,25 @@ static int login_session(struct node_rec *rec)
288+ /*
289+ * Need to handle race where iscsid proc is starting up while we are
290+ * trying to connect. Retry with exponential backoff, start from 50 ms.
291++ *
292++ * NOTE: another race condition can occur if the system is just coming
293++ * up, where our login request will be sent, but the login response
294++ * takes a while. In such a case, if we timeout and give up, the
295++ * login response may still show up once we give up, in which case
296++ * it seems to get iscsistart into a state where it cannot try to
297++ * login again because it thinks there's already a session. So make
298++ * sure our timeout is long enough, on each try, to give the response
299++ * a chance to show up. The old value of 1 second was not enough,
300++ * so we multiply that by 10, which seems reasonable for initial
301++ * login.
302+ */
303+ for (msec = 50; msec <= 15000; msec <<= 1) {
304+- rc = iscsid_exec_req(&req, &rsp, 0, ISCSID_REQ_TIMEOUT);
305++ int tmo = ISCSID_REQ_TIMEOUT * 10;
306++
307++ rc = iscsid_exec_req(&req, &rsp, 0, tmo);
308+ if (rc == 0) {
309+ return rc;
310+- } else if (rc == ISCSI_ERR_ISCSID_NOTCONN) {
311++ } else if (rc == ISCSI_ERR_SESSION_NOT_CONNECTED) {
312+ ts.tv_sec = msec / 1000;
313+ ts.tv_nsec = (msec % 1000) * 1000000L;
314+
315diff --git a/debian/tests/control b/debian/tests/control
316index 026cfc9..0fcfd11 100644
317--- a/debian/tests/control
318+++ b/debian/tests/control
319@@ -3,7 +3,7 @@ Restrictions: needs-root, isolation-machine, breaks-testbed
320
321 Tests: testsuite
322 Restrictions: needs-root isolation-machine breaks-testbed
323-Depends: open-iscsi, python2, tgt, qemu-system, ubuntu-cloudimage-keyring, simplestreams, python-netifaces, distro-info, cloud-image-utils, dctrl-tools, rsync
324+Depends: open-iscsi, python3, tgt, qemu-system, ubuntu-cloudimage-keyring, simplestreams, python3-netifaces, distro-info, cloud-image-utils, dctrl-tools, rsync
325
326 Tests: nested
327 Restrictions: needs-root, isolation-machine, breaks-testbed, allow-stderr
328diff --git a/debian/tests/test-open-iscsi.py b/debian/tests/test-open-iscsi.py
329index cce02fb..1ad8b4f 100644
330--- a/debian/tests/test-open-iscsi.py
331+++ b/debian/tests/test-open-iscsi.py
332@@ -1,4 +1,4 @@
333-#!/usr/bin/python2
334+#!/usr/bin/python3
335 #
336 # test-open-iscsi.py quality assurance test script for open-iscsi
337 # Copyright (C) 2011 Canonical Ltd.
338@@ -32,16 +32,17 @@
339 alter the machine. You have been warned.
340
341 How to run in a clean VM:
342- $ sudo apt-get -y install python-unit <QRT-Packages> && sudo ./test-PKG.py -v'
343+ $ sudo apt-get -y install <QRT-Packages> && sudo ./test-PKG.py -v'
344
345 How to run in a clean schroot named 'lucid':
346- $ schroot -c lucid -u root -- sh -c 'apt-get -y install python-unit <QRT-Packages> && ./test-PKG.py -v'
347+ $ schroot -c lucid -u root -- sh -c 'apt-get -y install <QRT-Packages> && ./test-PKG.py -v'
348
349
350 NOTES:
351 - currently only tested on Ubuntu 8.04
352 '''
353
354+from __future__ import print_function
355
356 from netifaces import gateways, AF_INET
357 import unittest, subprocess, sys, os, glob
358@@ -120,7 +121,7 @@ try:
359 except ImportError:
360 class PrivateOpenIscsiTest(object):
361 '''Empty class'''
362- print >>sys.stdout, "Skipping private tests"
363+ print("Skipping private tests", file=sys.stdout)
364
365 class OpenIscsiTest(testlib.TestlibCase, PrivateOpenIscsiTest):
366 '''Test my thing.'''
367@@ -129,7 +130,7 @@ class OpenIscsiTest(testlib.TestlibCase, PrivateOpenIscsiTest):
368 '''Set up prior to each test_* function'''
369 self.pidfile = "/var/run/iscsid.pid"
370 self.exe = "/sbin/iscsid"
371- self.daemon = testlib.TestDaemon(["service", "open-iscsi"])
372+ self.daemon = testlib.TestDaemon("open-iscsi")
373 self.initiatorname_iscsi = '/etc/iscsi/initiatorname.iscsi'
374 self.iscsid_conf = '/etc/iscsi/iscsid.conf'
375
376@@ -189,7 +190,7 @@ discovery.sendtargets.auth.password_in = %s
377 rc, report = testlib.cmd(["/sbin/iscsi_discovery", remote_server])
378 expected = 0
379 result = 'Got exit code %d, expected %d\n' % (rc, expected)
380- self.assertEquals(expected, rc, result + report)
381+ self.assertEqual(expected, rc, result + report)
382 for i in ['starting discovery to %s' % remote_server,
383 'Testing iser-login to target %s portal %s' % (initiatorname, remote_server),
384 'starting to test tcp-login to target %s portal %s' % (initiatorname, remote_server),
385@@ -409,13 +410,13 @@ if __name__ == '__main__':
386 password_in = options.password_in
387 if options.initiatorname:
388 initiatorname = options.initiatorname
389- print "Connecting to remote server with:"
390- print " host = %s " % remote_server
391- print ' initiatorname = %s' % initiatorname
392- print ' username = %s' % username
393- print ' password = %s' % password
394- print ' username_in = %s' % username_in
395- print ' password_in = %s' % password_in
396+ print("Connecting to remote server with:")
397+ print(" host = %s " % remote_server)
398+ print(' initiatorname = %s' % initiatorname)
399+ print(' username = %s' % username)
400+ print(' password = %s' % password)
401+ print(' username_in = %s' % username_in)
402+ print(' password_in = %s' % password_in)
403
404 suite = unittest.TestSuite()
405 suite.addTest(unittest.TestLoader().loadTestsFromTestCase(OpenIscsiTest))
406diff --git a/debian/tests/testlib.py b/debian/tests/testlib.py
407index b17c3fc..dce4252 100644
408--- a/debian/tests/testlib.py
409+++ b/debian/tests/testlib.py
410@@ -1,6 +1,7 @@
411+from __future__ import print_function
412 #
413 # testlib.py quality assurance test script
414-# Copyright (C) 2008-2011 Canonical Ltd.
415+# Copyright (C) 2008-2016 Canonical Ltd.
416 #
417 # This library is free software; you can redistribute it and/or
418 # modify it under the terms of the GNU Library General Public
419@@ -19,31 +20,71 @@
420
421 '''Common classes and functions for package tests.'''
422
423-import string, random, crypt, subprocess, pwd, grp, signal, time, unittest, tempfile, shutil, os, os.path, re, glob
424-import sys, socket, gzip
425-from stat import *
426-from encodings import string_escape
427+import crypt
428+import glob
429+import grp
430+import gzip
431+import os
432+import os.path
433+import platform
434+import pwd
435+import random
436+import re
437+import shutil
438+import socket
439+import string
440+import subprocess
441+import sys
442+import tempfile
443+import time
444+import unittest
445+
446+from stat import ST_SIZE
447+
448+# Don't make python-pexpect mandatory
449+try:
450+ import pexpect
451+except ImportError:
452+ pass
453
454 import warnings
455 warnings.filterwarnings('ignore', message=r'.*apt_pkg\.TagFile.*', category=DeprecationWarning)
456 try:
457 import apt_pkg
458- apt_pkg.InitSystem();
459+ # cope with apt_pkg api changes.
460+ if 'init_system' in dir(apt_pkg):
461+ apt_pkg.init_system()
462+ else:
463+ apt_pkg.InitSystem()
464 except:
465 # On non-Debian system, fall back to simple comparison without debianisms
466 class apt_pkg(object):
467- def VersionCompare(one, two):
468+ @staticmethod
469+ def version_compare(one, two):
470 list_one = one.split('.')
471 list_two = two.split('.')
472- while len(list_one)>0 and len(list_two)>0:
473- if list_one[0] > list_two[0]:
474- return 1
475- if list_one[0] < list_two[0]:
476- return -1
477+ while len(list_one) > 0 and len(list_two) > 0:
478+ try:
479+ if int(list_one[0]) > int(list_two[0]):
480+ return 1
481+ if int(list_one[0]) < int(list_two[0]):
482+ return -1
483+ except:
484+ # ugh, non-numerics in the version, fall back to
485+ # string comparison, which will be wrong for e.g.
486+ # 3.2 vs 3.16rc1
487+ if list_one[0] > list_two[0]:
488+ return 1
489+ if list_one[0] < list_two[0]:
490+ return -1
491 list_one.pop(0)
492 list_two.pop(0)
493 return 0
494
495+ @staticmethod
496+ def VersionCompare(one, two):
497+ return apt_pkg.version_compare(one, two)
498+
499 bogus_nxdomain = "208.69.32.132"
500
501 # http://www.chiark.greenend.org.uk/ucgi/~cjwatson/blosxom/2009-07-02-python-sigpipe.html
502@@ -55,17 +96,21 @@ def subprocess_setup():
503 # non-Python subprocesses expect.
504 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
505
506+
507 class TimedOutException(Exception):
508- def __init__(self, value = "Timed Out"):
509+ def __init__(self, value="Timed Out"):
510 self.value = value
511+
512 def __str__(self):
513 return repr(self.value)
514
515+
516 def _restore_backup(path):
517 pathbackup = path + '.autotest'
518 if os.path.exists(pathbackup):
519 shutil.move(pathbackup, path)
520
521+
522 def _save_backup(path):
523 pathbackup = path + '.autotest'
524 if os.path.exists(path) and not os.path.exists(pathbackup):
525@@ -75,53 +120,64 @@ def _save_backup(path):
526 a = os.stat(path)
527 os.chown(pathbackup, a[4], a[5])
528
529+
530 def config_copydir(path):
531 if os.path.exists(path) and not os.path.isdir(path):
532- raise OSError, "'%s' is not a directory" % (path)
533+ raise OSError("'%s' is not a directory" % (path))
534 _restore_backup(path)
535
536 pathbackup = path + '.autotest'
537 if os.path.exists(path):
538 shutil.copytree(path, pathbackup, symlinks=True)
539
540-def config_replace(path,contents,append=False):
541+
542+def config_replace(path, contents, append=False):
543 '''Replace (or append) to a config file'''
544 _restore_backup(path)
545 if os.path.exists(path):
546 _save_backup(path)
547 if append:
548- contents = file(path).read() + contents
549- open(path, 'w').write(contents)
550+ with open(path) as fh:
551+ contents = fh.read() + contents
552+ with open(path, 'w') as fh:
553+ fh.write(contents)
554+
555
556 def config_comment(path, field):
557 _save_backup(path)
558 contents = ""
559- for line in file(path):
560- if re.search("^\s*%s\s*=" % (field), line):
561- line = "#" + line
562- contents += line
563+ with open(path) as fh:
564+ for line in fh:
565+ if re.search("^\s*%s\s*=" % (field), line):
566+ line = "#" + line
567+ contents += line
568+
569+ with open(path + '.new', 'w') as new_fh:
570+ new_fh.write(contents)
571+ os.rename(path + '.new', path)
572
573- open(path+'.new', 'w').write(contents)
574- os.rename(path+'.new', path)
575
576 def config_set(path, field, value, spaces=True):
577 _save_backup(path)
578 contents = ""
579- if spaces==True:
580+ if spaces:
581 setting = '%s = %s\n' % (field, value)
582 else:
583 setting = '%s=%s\n' % (field, value)
584 found = False
585- for line in file(path):
586- if re.search("^\s*%s\s*=" % (field), line):
587- found = True
588- line = setting
589- contents += line
590+ with open(path) as fh:
591+ for line in fh:
592+ if re.search("^\s*%s\s*=" % (field), line):
593+ found = True
594+ line = setting
595+ contents += line
596 if not found:
597 contents += setting
598
599- open(path+'.new', 'w').write(contents)
600- os.rename(path+'.new', path)
601+ with open(path + '.new', 'w') as new_config:
602+ new_config.write(contents)
603+ os.rename(path + '.new', path)
604+
605
606 def config_patch(path, patch, depth=1):
607 '''Patch a config file'''
608@@ -129,15 +185,17 @@ def config_patch(path, patch, depth=1):
609 _save_backup(path)
610
611 handle, name = mkstemp_fill(patch)
612- rc = subprocess.call(['/usr/bin/patch', '-p%s' %(depth), path], stdin=handle, stdout=subprocess.PIPE)
613+ rc = subprocess.call(['/usr/bin/patch', '-p%s' % depth, path], stdin=handle, stdout=subprocess.PIPE)
614 os.unlink(name)
615 if rc != 0:
616 raise Exception("Patch failed")
617
618+
619 def config_restore(path):
620 '''Rename a replaced config file back to its initial state'''
621 _restore_backup(path)
622
623+
624 def timeout(secs, f, *args):
625 def handler(signum, frame):
626 raise TimedOutException()
627@@ -153,51 +211,57 @@ def timeout(secs, f, *args):
628
629 return result
630
631+
632 def require_nonroot():
633 if os.geteuid() == 0:
634- print >>sys.stderr, "This series of tests should be run as a regular user with sudo access, not as root."
635+ print("This series of tests should be run as a regular user with sudo access, not as root.", file=sys.stderr)
636 sys.exit(1)
637
638+
639 def require_root():
640 if os.geteuid() != 0:
641- print >>sys.stderr, "This series of tests should be run with root privileges (e.g. via sudo)."
642+ print("This series of tests should be run with root privileges (e.g. via sudo).", file=sys.stderr)
643 sys.exit(1)
644
645+
646 def require_sudo():
647- if os.geteuid() != 0 or os.environ.get('SUDO_USER', None) == None:
648- print >>sys.stderr, "This series of tests must be run under sudo."
649+ if os.geteuid() != 0 or os.environ.get('SUDO_USER', None) is None:
650+ print("This series of tests must be run under sudo.", file=sys.stderr)
651 sys.exit(1)
652 if os.environ['SUDO_USER'] == 'root':
653- print >>sys.stderr, 'Please run this test using sudo from a regular user. (You ran sudo from root.)'
654+ print('Please run this test using sudo from a regular user. (You ran sudo from root.)', file=sys.stderr)
655 sys.exit(1)
656
657-def random_string(length,lower=False):
658+
659+def random_string(length, lower=False):
660 '''Return a random string, consisting of ASCII letters, with given
661 length.'''
662
663 s = ''
664- selection = string.letters
665+ selection = string.ascii_letters
666 if lower:
667- selection = string.lowercase
668- maxind = len(selection)-1
669+ selection = string.ascii_lowercase
670+ maxind = len(selection) - 1
671 for l in range(length):
672 s += selection[random.randint(0, maxind)]
673 return s
674
675-def mkstemp_fill(contents,suffix='',prefix='testlib-',dir=None):
676+
677+def mkstemp_fill(contents, suffix='', prefix='testlib-', dir=None):
678 '''As tempfile.mkstemp does, return a (file, name) pair, but with
679 prefilled contents.'''
680
681- handle, name = tempfile.mkstemp(suffix=suffix,prefix=prefix,dir=dir)
682+ handle, name = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir)
683 os.close(handle)
684- handle = file(name,"w+")
685+ handle = open(name, "w+")
686 handle.write(contents)
687 handle.flush()
688 handle.seek(0)
689
690 return handle, name
691
692-def create_fill(path, contents, mode=0644):
693+
694+def create_fill(path, contents, mode=0o644):
695 '''Safely create a page'''
696 # make the temp file in the same dir as the destination file so we
697 # don't get invalid cross-device link errors when we rename
698@@ -206,6 +270,7 @@ def create_fill(path, contents, mode=0644):
699 os.rename(name, path)
700 os.chmod(path, mode)
701
702+
703 def login_exists(login):
704 '''Checks whether the given login exists on the system.'''
705
706@@ -215,6 +280,7 @@ def login_exists(login):
707 except KeyError:
708 return False
709
710+
711 def group_exists(group):
712 '''Checks whether the given login exists on the system.'''
713
714@@ -224,6 +290,13 @@ def group_exists(group):
715 except KeyError:
716 return False
717
718+
719+def is_empty_file(path):
720+ '''test if file is empty, returns True if so'''
721+ with open(path) as fh:
722+ return (len(fh.read()) == 0)
723+
724+
725 def recursive_rm(dirPath, contents_only=False):
726 '''recursively remove directory'''
727 names = os.listdir(dirPath)
728@@ -233,9 +306,10 @@ def recursive_rm(dirPath, contents_only=False):
729 os.unlink(path)
730 else:
731 recursive_rm(path)
732- if contents_only == False:
733+ if not contents_only:
734 os.rmdir(dirPath)
735
736+
737 def check_pidfile(exe, pidfile):
738 '''Checks if pid in pidfile is running'''
739 if not os.path.exists(pidfile):
740@@ -243,14 +317,14 @@ def check_pidfile(exe, pidfile):
741
742 # get the pid
743 try:
744- fd = open(pidfile, 'r')
745- pid = fd.readline().rstrip('\n')
746- fd.close()
747+ with open(pidfile, 'r') as fd:
748+ pid = fd.readline().rstrip('\n')
749 except:
750 return False
751
752 return check_pid(exe, pid)
753
754+
755 def check_pid(exe, pid):
756 '''Checks if pid is running'''
757 cmdline = "/proc/%s/cmdline" % (str(pid))
758@@ -259,9 +333,8 @@ def check_pid(exe, pid):
759
760 # get the command line
761 try:
762- fd = open(cmdline, 'r')
763- tmp = fd.readline().split('\0')
764- fd.close()
765+ with open(cmdline, 'r') as fd:
766+ tmp = fd.readline().split('\0')
767 except:
768 return False
769
770@@ -274,6 +347,7 @@ def check_pid(exe, pid):
771
772 return False
773
774+
775 def check_port(port, proto, ver=4):
776 '''Check if something is listening on the specified port.
777 WARNING: for some reason this does not work with a bind mounted /proc
778@@ -296,12 +370,29 @@ def check_port(port, proto, ver=4):
779 return True
780 return False
781
782+
783 def get_arch():
784 '''Get the current architecture'''
785 rc, report = cmd(['uname', '-m'])
786 assert (rc == 0)
787 return report.strip()
788
789+
790+def get_multiarch_tuple():
791+ '''Get the current debian multiarch tuple'''
792+
793+ # XXX dpkg-architecture on Ubuntu 12.04 requires -q be adjacent to
794+ # the variable name
795+ rc, report = cmd(['dpkg-architecture', '-qDEB_HOST_MULTIARCH'])
796+ assert (rc == 0)
797+ return report.strip()
798+
799+
800+def get_bits():
801+ '''return the default architecture number of bits (32 or 64, usually)'''
802+ return platform.architecture()[0][0:2]
803+
804+
805 def get_memory():
806 '''Gets total ram and swap'''
807 meminfo = "/proc/meminfo"
808@@ -322,7 +413,8 @@ def get_memory():
809 except:
810 return (False, False)
811
812- return (memtotal,swaptotal)
813+ return (memtotal, swaptotal)
814+
815
816 def is_running_in_vm():
817 '''Check if running under a VM'''
818@@ -333,6 +425,7 @@ def is_running_in_vm():
819 return True
820 return False
821
822+
823 def ubuntu_release():
824 '''Get the Ubuntu release'''
825 f = "/etc/lsb-release"
826@@ -341,16 +434,11 @@ def ubuntu_release():
827 except:
828 return "UNKNOWN"
829
830- if size > 1024*1024:
831- raise IOError, 'Could not open "%s" (too big)' % f
832-
833- try:
834- fh = open("/etc/lsb-release", 'r')
835- except:
836- raise
837+ if size > 1024 * 1024:
838+ raise IOError('Could not open "%s" (too big)' % f)
839
840- lines = fh.readlines()
841- fh.close()
842+ with open("/etc/lsb-release", 'r') as fh:
843+ lines = fh.readlines()
844
845 pat = re.compile(r'DISTRIB_CODENAME')
846 for line in lines:
847@@ -359,40 +447,53 @@ def ubuntu_release():
848
849 return "UNKNOWN"
850
851-def cmd(command, input = None, stderr = subprocess.STDOUT, stdout = subprocess.PIPE, stdin = None, timeout = None):
852+
853+def cmd(command, input=None, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, stdin=None, timeout=None, env=None, shell=False, text=True):
854 '''Try to execute given command (array) and return its stdout, or return
855 a textual error if it failed.'''
856
857 try:
858- sp = subprocess.Popen(command, stdin=stdin, stdout=stdout, stderr=stderr, close_fds=True, preexec_fn=subprocess_setup)
859- except OSError, e:
860+ sp = subprocess.Popen(command, stdin=stdin, stdout=stdout, stderr=stderr, close_fds=True, preexec_fn=subprocess_setup, env=env, universal_newlines=text, shell=shell)
861+ if sys.version_info[0] >= 3 and sys.version_info[1] > 3:
862+ out, outerr = sp.communicate(input, timeout=timeout)
863+ else:
864+ out, outerr = sp.communicate(input)
865+ except OSError as e:
866 return [127, str(e)]
867
868- out, outerr = sp.communicate(input)
869 # Handle redirection of stdout
870- if out == None:
871+ if out is None:
872 out = ''
873 # Handle redirection of stderr
874- if outerr == None:
875+ if outerr is None:
876 outerr = ''
877- return [sp.returncode,out+outerr]
878+ if text == False:
879+ out = str(out)
880+ return [sp.returncode, out + outerr]
881
882-def cmd_pipe(command1, command2, input = None, stderr = subprocess.STDOUT, stdin = None):
883+
884+def cmd_pipe(command1, command2, input=None, stderr=subprocess.STDOUT, stdin=None):
885 '''Try to pipe command1 into command2.'''
886 try:
887 sp1 = subprocess.Popen(command1, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr, close_fds=True)
888- sp2 = subprocess.Popen(command2, stdin=sp1.stdout, stdout=subprocess.PIPE, stderr=stderr, close_fds=True)
889- except OSError, e:
890+ sp2 = subprocess.Popen(command2, stdin=sp1.stdout, stdout=subprocess.PIPE, stderr=stderr, close_fds=True, universal_newlines=True)
891+ except OSError as e:
892 return [127, str(e)]
893
894 out = sp2.communicate(input)[0]
895- return [sp2.returncode,out]
896+ # ensure we don't leak open files and that sp1 is stopped
897+ sp1.stdout.close()
898+ sp1.wait()
899+ return [sp2.returncode, out]
900+
901
902 def cwd_has_enough_space(cdir, total_bytes):
903 '''Determine if the partition of the current working directory has 'bytes'
904 free.'''
905 rc, df_output = cmd(['df'])
906- result = 'Got exit code %d, expected %d\n' % (rc, 0)
907+ if rc != 0:
908+ result = 'df failed, got exit code %d, expected %d\n' % (rc, 0)
909+ raise OSError(result)
910 if rc != 0:
911 return False
912
913@@ -407,18 +508,13 @@ def cwd_has_enough_space(cdir, total_bytes):
914
915 cdir = os.getcwd()
916 while cdir != '/':
917- if not mounts.has_key(cdir):
918+ if cdir not in mounts:
919 cdir = os.path.dirname(cdir)
920 continue
921- if kb < mounts[cdir]:
922- return True
923- else:
924- return False
925+ return kb < mounts[cdir]
926
927- if kb < mounts['/']:
928- return True
929+ return kb < mounts['/']
930
931- return False
932
933 def get_md5(filename):
934 '''Gets the md5sum of the file specified'''
935@@ -429,6 +525,7 @@ def get_md5(filename):
936
937 return report.split(' ')[0]
938
939+
940 def dpkg_compare_installed_version(pkg, check, version):
941 '''Gets the version for the installed package, and compares it to the
942 specified version.
943@@ -449,6 +546,35 @@ def dpkg_compare_installed_version(pkg, check, version):
944 return True
945 return False
946
947+
948+def _run_apt_command(pkg_list, apt_cmd='install'):
949+ env = os.environ.copy()
950+ env['DEBIAN_FRONTEND'] = 'noninteractive'
951+ # debugging version, but on precise doesn't actually run dpkg
952+ # command = ['apt-get', '-y', '--force-yes', '-o', 'Dpkg::Options::=--force-confold', '-o', 'Debug::pkgDPkgPM=true', apt_cmd]
953+ command = ['apt-get', '-y', '--force-yes', '-o', 'Dpkg::Options::=--force-confold', apt_cmd]
954+ command.extend(pkg_list)
955+ rc, report = cmd(command)
956+ return rc, report
957+
958+
959+# note: for the following install_* functions, you probably want the
960+# versions in the TestlibCase class (see below)
961+def install_builddeps(src_pkg):
962+ rc, report = _run_apt_command([src_pkg], 'build-dep')
963+ assert(rc == 0)
964+
965+
966+def install_package(package):
967+ rc, report = _run_apt_command([package], 'install')
968+ assert(rc == 0)
969+
970+
971+def install_packages(pkg_list):
972+ rc, report = _run_apt_command(pkg_list, 'install')
973+ assert(rc == 0)
974+
975+
976 def prepare_source(source, builder, cached_src, build_src, patch_system):
977 '''Download and unpack source package, installing necessary build depends,
978 adjusting the permissions for the 'builder' user, and returning the
979@@ -468,7 +594,7 @@ def prepare_source(source, builder, cached_src, build_src, patch_system):
980 self.tmpdir = tempfile.mkdtemp(prefix='testlib', dir='/tmp')
981 self.builder = testlib.TestUser()
982 testlib.cmd(['chgrp', self.builder.login, self.tmpdir])
983- os.chmod(self.tmpdir, 0775)
984+ os.chmod(self.tmpdir, 0o775)
985
986 def tearDown(self):
987 ...
988@@ -488,21 +614,21 @@ def prepare_source(source, builder, cached_src, build_src, patch_system):
989 os.chdir(build_dir)
990
991 # Example for typical build, adjust as necessary
992- print ""
993- print " make clean"
994+ print("")
995+ print(" make clean")
996 rc, report = testlib.cmd(['sudo', '-u', self.builder.login, 'make', 'clean'])
997
998- print " configure"
999+ print(" configure")
1000 rc, report = testlib.cmd(['sudo', '-u', self.builder.login, './configure', '--prefix=%s' % self.tmpdir, '--enable-debug'])
1001
1002- print " make (will take a while)"
1003+ print(" make (will take a while)")
1004 rc, report = testlib.cmd(['sudo', '-u', self.builder.login, 'make'])
1005
1006- print " make check (will take a while)",
1007+ print(" make check (will take a while)",)
1008 rc, report = testlib.cmd(['sudo', '-u', self.builder.login, 'make', 'check'])
1009 expected = 0
1010 result = 'Got exit code %d, expected %d\n' % (rc, expected)
1011- self.assertEquals(expected, rc, result + report)
1012+ self.assertEqual(expected, rc, result + report)
1013
1014 def test_suite_cleanup(self):
1015 ...
1016@@ -526,20 +652,18 @@ def prepare_source(source, builder, cached_src, build_src, patch_system):
1017 os.chdir(build_src)
1018 else:
1019 # Only install the build dependencies on the initial setup
1020- rc, report = cmd(['apt-get','-y','--force-yes','build-dep',source])
1021- assert (rc == 0)
1022-
1023+ install_builddeps(source)
1024 os.makedirs(build_src)
1025 os.chdir(build_src)
1026
1027 # These are always needed
1028 pkgs = ['build-essential', 'dpkg-dev', 'fakeroot']
1029- rc, report = cmd(['apt-get','-y','--force-yes','install'] + pkgs)
1030- assert (rc == 0)
1031+ install_packages(pkgs)
1032
1033- rc, report = cmd(['apt-get','source',source])
1034+ rc, report = cmd(['apt-get', 'source', source])
1035 assert (rc == 0)
1036 shutil.copytree(build_src, cached_src)
1037+ print("(unpacked %s)" % os.path.basename(glob.glob('%s_*.dsc' % source)[0]), end=' ')
1038
1039 unpacked_dir = os.path.join(build_src, glob.glob('%s-*' % source)[0])
1040
1041@@ -547,9 +671,9 @@ def prepare_source(source, builder, cached_src, build_src, patch_system):
1042 # sources.
1043 os.chdir(unpacked_dir)
1044 assert (patch_system in ['cdbs', 'dpatch', 'quilt', 'quiltv3', None])
1045- if patch_system != None and patch_system != "quiltv3":
1046+ if patch_system is not None and patch_system != "quiltv3":
1047 if patch_system == "quilt":
1048- os.environ.setdefault('QUILT_PATCHES','debian/patches')
1049+ os.environ.setdefault('QUILT_PATCHES', 'debian/patches')
1050 rc, report = cmd(['quilt', 'push', '-a'])
1051 assert (rc == 0)
1052 elif patch_system == "cdbs":
1053@@ -564,6 +688,21 @@ def prepare_source(source, builder, cached_src, build_src, patch_system):
1054
1055 return unpacked_dir
1056
1057+
1058+def get_changelog_version(source_dir):
1059+ '''Extract a package version from a changelog'''
1060+ package_version = ""
1061+
1062+ changelog_file = os.path.join(source_dir, "debian/changelog")
1063+
1064+ if os.path.exists(changelog_file):
1065+ changelog = open(changelog_file, 'r')
1066+ header = changelog.readline().split()
1067+ package_version = header[1].strip('()')
1068+
1069+ return package_version
1070+
1071+
1072 def _aa_status():
1073 '''Get aa-status output'''
1074 exe = "/usr/sbin/aa-status"
1075@@ -572,6 +711,7 @@ def _aa_status():
1076 return cmd([exe])
1077 return cmd(['sudo', exe])
1078
1079+
1080 def is_apparmor_loaded(path):
1081 '''Check if profile is loaded'''
1082 rc, report = _aa_status()
1083@@ -583,6 +723,7 @@ def is_apparmor_loaded(path):
1084 return True
1085 return False
1086
1087+
1088 def is_apparmor_confined(path):
1089 '''Check if application is confined'''
1090 rc, report = _aa_status()
1091@@ -594,6 +735,7 @@ def is_apparmor_confined(path):
1092 return True
1093 return False
1094
1095+
1096 def check_apparmor(path, first_ubuntu_release, is_running=True):
1097 '''Check if path is loaded and confined for everything higher than the
1098 first Ubuntu release specified.
1099@@ -605,7 +747,7 @@ def check_apparmor(path, first_ubuntu_release, is_running=True):
1100
1101 expected = 0
1102 result = 'Got exit code %d, expected %d\n' % (rc, expected)
1103- self.assertEquals(expected, rc, result + report)
1104+ self.assertEqual(expected, rc, result + report)
1105 '''
1106 global manager
1107 rc = -1
1108@@ -629,13 +771,14 @@ def check_apparmor(path, first_ubuntu_release, is_running=True):
1109
1110 return (rc, msg)
1111
1112+
1113 def get_gcc_version(gcc, full=True):
1114 gcc_version = 'none'
1115 if not gcc.startswith('/'):
1116 gcc = '/usr/bin/%s' % (gcc)
1117 if os.path.exists(gcc):
1118 gcc_version = 'unknown'
1119- lines = cmd([gcc,'-v'])[1].strip().splitlines()
1120+ lines = cmd([gcc, '-v'])[1].strip().splitlines()
1121 version_lines = [x for x in lines if x.startswith('gcc version')]
1122 if len(version_lines) == 1:
1123 gcc_version = " ".join(version_lines[0].split()[2:])
1124@@ -643,6 +786,7 @@ def get_gcc_version(gcc, full=True):
1125 return gcc_version.split()[0]
1126 return gcc_version
1127
1128+
1129 def is_kdeinit_running():
1130 '''Test if kdeinit is running'''
1131 # applications that use kdeinit will spawn it if it isn't running in the
1132@@ -650,106 +794,154 @@ def is_kdeinit_running():
1133 # check for it.
1134 rc, report = cmd(['ps', 'x'])
1135 if 'kdeinit4 Running' not in report:
1136- print >>sys.stderr, ("kdeinit not running (you may start/stop any KDE application then run this script again)")
1137+ print("kdeinit not running (you may start/stop any KDE application then run this script again)", file=sys.stderr)
1138 return False
1139 return True
1140
1141+
1142 def get_pkgconfig_flags(libs=[]):
1143 '''Find pkg-config flags for libraries'''
1144 assert (len(libs) > 0)
1145 rc, pkg_config = cmd(['pkg-config', '--cflags', '--libs'] + libs)
1146 expected = 0
1147 if rc != expected:
1148- print >>sys.stderr, 'Got exit code %d, expected %d\n' % (rc, expected)
1149+ print('Got exit code %d, expected %d\n' % (rc, expected), file=sys.stderr)
1150 assert(rc == expected)
1151 return pkg_config.split()
1152
1153+
1154+def cap_to_name(cap_num):
1155+ '''given an integer, return the capability name'''
1156+ rc, output = cmd(['capsh', '--decode=%x' % cap_num])
1157+ expected = 0
1158+ if rc != expected:
1159+ print('capsh: got exit code %d, expected %d\n' % (rc, expected), file=sys.stderr)
1160+ cap_name = output.strip().split('=')[1]
1161+ return cap_name
1162+
1163+
1164+def enumerate_capabilities():
1165+ i = 0
1166+ cap_list = []
1167+ done = False
1168+ while not done:
1169+ cap_name = cap_to_name(pow(2, i))
1170+ if cap_name == str(i):
1171+ done = True
1172+ else:
1173+ cap_list.append(cap_name)
1174+ i += 1
1175+ if i > 64:
1176+ done = True
1177+ return cap_list
1178+
1179+
1180+def _run_snap_install(snapname, track="latest/stable", classic=False):
1181+ # crude, but effective
1182+ if os.path.exists(os.path.join("/snap", snapname)):
1183+ return 0, ""
1184+
1185+ command = ["snap", "install", "--channel=%s" % track, snapname]
1186+ if classic:
1187+ command.append("--classic")
1188+ rc, report = cmd(command)
1189+ return rc, report
1190+
1191+
1192+def _run_snap_remove(snapname):
1193+ # crude, but effective
1194+ if not os.path.exists(os.path.join("/snap", snapname)):
1195+ return 0, ""
1196+
1197+ command = ["snap", "remove", "--purge", snapname]
1198+ rc, report = cmd(command)
1199+ return rc, report
1200+
1201+
1202+def install_snap(self, snapname, track="latest/stable", classic=False):
1203+ rc, report = _run_snap_install(snapname, track, classic)
1204+ self.assertEqual(0, rc, "Failed to install snap %s\nOutput:\n%s" %
1205+ (snapname, report))
1206+
1207+
1208+def remove_snap(self, snapname):
1209+ rc, report = _run_snap_remove(snapname)
1210+ self.assertEqual(0, rc, "Failed to install snap %s\nOutput:\n%s" %
1211+ (snapname, report))
1212+
1213+
1214 class TestDaemon:
1215 '''Helper class to manage daemons consistently'''
1216- def __init__(self, init):
1217+ def __init__(self, service_name):
1218 '''Setup daemon attributes'''
1219- self.initscript = init
1220-
1221- def _build_command(self, suffix):
1222- command = []
1223- if isinstance(self.initscript, list):
1224- command.extend(self.initscript)
1225+ if os.path.isfile('/usr/sbin/service') or os.path.islink('/usr/sbin/service'):
1226+ self.test_daemon_commands = ['service', service_name]
1227 else:
1228- command.append(self.initscript)
1229- command.append(suffix)
1230- return command
1231+ self.test_daemon_commands = ['/etc/init.d/' + service_name]
1232
1233- def start(self):
1234- '''Start daemon'''
1235- rc, report = cmd(self._build_command('start'))
1236+ def _run_command(self, action, seconds_to_wait=2):
1237+ command = list(self.test_daemon_commands)
1238+ command.append(action)
1239+ rc, report = cmd(command)
1240 expected = 0
1241 result = 'Got exit code %d, expected %d\n' % (rc, expected)
1242- time.sleep(2)
1243+ time.sleep(seconds_to_wait)
1244 if expected != rc:
1245- return (False, result + report)
1246+ return False, result + report
1247
1248 if "fail" in report:
1249- return (False, "Found 'fail' in report\n" + report)
1250+ return False, "Found 'fail' in report\n" + report
1251
1252- return (True, "")
1253+ return True, ""
1254+
1255+ def start(self):
1256+ '''Start daemon'''
1257+ return self._run_command('start')
1258
1259 def stop(self):
1260 '''Stop daemon'''
1261- rc, report = cmd(self._build_command('stop'))
1262- expected = 0
1263- result = 'Got exit code %d, expected %d\n' % (rc, expected)
1264- if expected != rc:
1265- return (False, result + report)
1266-
1267- if "fail" in report:
1268- return (False, "Found 'fail' in report\n" + report)
1269-
1270- return (True, "")
1271+ return self._run_command('stop')
1272
1273 def reload(self):
1274 '''Reload daemon'''
1275- rc, report = cmd(self._build_command('force-reload'))
1276- expected = 0
1277- result = 'Got exit code %d, expected %d\n' % (rc, expected)
1278- if expected != rc:
1279- return (False, result + report)
1280+ return self._run_command('force-reload')
1281
1282- if "fail" in report:
1283- return (False, "Found 'fail' in report\n" + report)
1284+ def restart(self):
1285+ if self.test_daemon_commands[0] == 'service':
1286+ return self._run_command('restart')
1287+ else:
1288+ '''Restart daemon'''
1289+ (res, str) = self.stop()
1290+ if not res:
1291+ return res, str
1292
1293- return (True, "")
1294+ (res, str) = self.start()
1295+ if not res:
1296+ return res, str
1297
1298- def restart(self):
1299- '''Restart daemon'''
1300- (res, str) = self.stop()
1301- if not res:
1302- return (res, str)
1303+ return True, ""
1304+
1305+ def force_restart(self):
1306+ '''Restart daemon even if already stopped'''
1307+ self.stop()
1308
1309 (res, str) = self.start()
1310 if not res:
1311- return (res, str)
1312+ return res, str
1313
1314- return (True, "")
1315+ return True, ""
1316
1317 def status(self):
1318 '''Check daemon status'''
1319- rc, report = cmd(self._build_command('status'))
1320- expected = 0
1321- result = 'Got exit code %d, expected %d\n' % (rc, expected)
1322- if expected != rc:
1323- return (False, result + report)
1324-
1325- if "fail" in report:
1326- return (False, "Found 'fail' in report\n" + report)
1327+ return self._run_command('status')
1328
1329- return (True, "")
1330
1331 class TestlibManager(object):
1332 '''Singleton class used to set up per-test-run information'''
1333 def __init__(self):
1334 # Set glibc aborts to dump to stderr instead of the tty so test output
1335 # is more sane.
1336- os.environ.setdefault('LIBC_FATAL_STDERR_','1')
1337+ os.environ.setdefault('LIBC_FATAL_STDERR_', '1')
1338
1339 # check verbosity
1340 self.verbosity = False
1341@@ -759,17 +951,17 @@ class TestlibManager(object):
1342 # Load LSB release file
1343 self.lsb_release = dict()
1344 if not os.path.exists('/usr/bin/lsb_release') and not os.path.exists('/bin/lsb_release'):
1345- raise OSError, "Please install 'lsb-release'"
1346- for line in subprocess.Popen(['lsb_release','-a'],stdout=subprocess.PIPE,stderr=subprocess.PIPE).communicate()[0].splitlines():
1347- field, value = line.split(':',1)
1348- value=value.strip()
1349- field=field.strip()
1350+ raise OSError("Please install 'lsb-release'")
1351+ for line in subprocess.Popen(['lsb_release', '-a'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True).communicate()[0].splitlines():
1352+ field, value = line.split(':', 1)
1353+ value = value.strip()
1354+ field = field.strip()
1355 # Convert numerics
1356 try:
1357 value = float(value)
1358 except:
1359 pass
1360- self.lsb_release.setdefault(field,value)
1361+ self.lsb_release.setdefault(field, value)
1362
1363 # FIXME: hack OEM releases into known-Ubuntu versions
1364 if self.lsb_release['Distributor ID'] == "HP MIE (Mobile Internet Experience)":
1365@@ -777,20 +969,20 @@ class TestlibManager(object):
1366 self.lsb_release['Distributor ID'] = "Ubuntu"
1367 self.lsb_release['Release'] = 8.04
1368 else:
1369- raise OSError, "Unknown version of HP MIE"
1370+ raise OSError("Unknown version of HP MIE")
1371
1372 # FIXME: hack to assume a most-recent release if we're not
1373 # running under Ubuntu.
1374- if self.lsb_release['Distributor ID'] not in ["Ubuntu","Linaro"]:
1375+ if self.lsb_release['Distributor ID'] not in ["Ubuntu", "Linaro"]:
1376 self.lsb_release['Release'] = 10000
1377 # Adjust Linaro release to pretend to be Ubuntu
1378 if self.lsb_release['Distributor ID'] in ["Linaro"]:
1379- self.lsb_release['Distributor ID'] = "Ubuntu"
1380+ self.lsb_release['Distributor ID'] = "Ubuntu"
1381 self.lsb_release['Release'] -= 0.01
1382
1383 # Load arch
1384 if not os.path.exists('/usr/bin/dpkg'):
1385- machine = cmd(['uname','-m'])[1].strip()
1386+ machine = cmd(['uname', '-m'])[1].strip()
1387 if machine.endswith('86'):
1388 self.dpkg_arch = 'i386'
1389 elif machine.endswith('_64'):
1390@@ -798,26 +990,26 @@ class TestlibManager(object):
1391 elif machine.startswith('arm'):
1392 self.dpkg_arch = 'armel'
1393 else:
1394- raise ValueError, "Unknown machine type '%s'" % (machine)
1395+ raise ValueError("Unknown machine type '%s'" % (machine))
1396 else:
1397- self.dpkg_arch = cmd(['dpkg','--print-architecture'])[1].strip()
1398+ self.dpkg_arch = cmd(['dpkg', '--print-architecture'])[1].strip()
1399
1400 # Find kernel version
1401 self.kernel_is_ubuntu = False
1402 self.kernel_version_signature = None
1403- self.kernel_version = cmd(["uname","-r"])[1].strip()
1404+ self.kernel_version = cmd(["uname", "-r"])[1].strip()
1405 versig = '/proc/version_signature'
1406 if os.path.exists(versig):
1407 self.kernel_is_ubuntu = True
1408- self.kernel_version_signature = file(versig).read().strip()
1409+ self.kernel_version_signature = open(versig).read().strip()
1410 self.kernel_version_ubuntu = self.kernel_version
1411 elif os.path.exists('/usr/bin/dpkg'):
1412 # this can easily be inaccurate but is only an issue for Dapper
1413- rc, out = cmd(['dpkg','-l','linux-image-%s' % (self.kernel_version)])
1414+ rc, out = cmd(['dpkg', '-l', 'linux-image-%s' % (self.kernel_version)])
1415 if rc == 0:
1416 self.kernel_version_signature = out.strip().split('\n').pop().split()[2]
1417 self.kernel_version_ubuntu = self.kernel_version_signature
1418- if self.kernel_version_signature == None:
1419+ if self.kernel_version_signature is None:
1420 # Attempt to fall back to something for non-Debian-based
1421 self.kernel_version_signature = self.kernel_version
1422 self.kernel_version_ubuntu = self.kernel_version
1423@@ -831,25 +1023,25 @@ class TestlibManager(object):
1424 self.gcc_version = get_gcc_version('gcc')
1425
1426 # Find libc
1427- self.path_libc = [x.split()[2] for x in cmd(['ldd','/bin/ls'])[1].splitlines() if x.startswith('\tlibc.so.')][0]
1428+ self.path_libc = [x.split()[2] for x in cmd(['ldd', '/bin/ls'])[1].splitlines() if x.startswith('\tlibc.so.')][0]
1429
1430 # Report self
1431 if self.verbosity:
1432 kernel = self.kernel_version_ubuntu
1433 if kernel != self.kernel_version_signature:
1434 kernel += " (%s)" % (self.kernel_version_signature)
1435- print >>sys.stdout, "Running test: '%s' distro: '%s %.2f' kernel: '%s' arch: '%s' uid: %d/%d SUDO_USER: '%s')" % ( \
1436+ print("Running test: '%s' distro: '%s %.2f' kernel: '%s' arch: '%s' uid: %d/%d SUDO_USER: '%s')" % (
1437 sys.argv[0],
1438 self.lsb_release['Distributor ID'],
1439 self.lsb_release['Release'],
1440 kernel,
1441 self.dpkg_arch,
1442 os.geteuid(), os.getuid(),
1443- os.environ.get('SUDO_USER', ''))
1444+ os.environ.get('SUDO_USER', '')), file=sys.stdout)
1445 sys.stdout.flush()
1446
1447 # Additional heuristics
1448- #if os.environ.get('SUDO_USER', os.environ.get('USER', '')) in ['mdeslaur']:
1449+ # if os.environ.get('SUDO_USER', os.environ.get('USER', '')) in ['mdeslaur']:
1450 # sys.stdout.write("Replying to Marc Deslauriers in http://launchpad.net/bugs/%d: " % random.randint(600000, 980000))
1451 # sys.stdout.flush()
1452 # time.sleep(0.5)
1453@@ -857,10 +1049,11 @@ class TestlibManager(object):
1454 # time.sleep(0.5)
1455
1456 def hello(self, msg):
1457- print >>sys.stderr, "Hello from %s" % (msg)
1458+ print("Hello from %s" % (msg), file=sys.stderr)
1459 # The central instance
1460 manager = TestlibManager()
1461
1462+
1463 class TestlibCase(unittest.TestCase):
1464 def __init__(self, *args):
1465 '''This is called for each TestCase test instance, which isn't much better
1466@@ -870,7 +1063,7 @@ class TestlibCase(unittest.TestCase):
1467
1468 # Attach to and duplicate dicts from manager singleton
1469 self.manager = manager
1470- #self.manager.hello(repr(self) + repr(*args))
1471+ # self.manager.hello(repr(self) + repr(*args))
1472 self.my_verbosity = self.manager.verbosity
1473 self.lsb_release = self.manager.lsb_release
1474 self.dpkg_arch = self.manager.dpkg_arch
1475@@ -882,9 +1075,12 @@ class TestlibCase(unittest.TestCase):
1476 self.path_libc = self.manager.path_libc
1477
1478 def version_compare(self, one, two):
1479- return apt_pkg.VersionCompare(one,two)
1480+ if 'version_compare' in dir(apt_pkg):
1481+ return apt_pkg.version_compare(one, two)
1482+ else:
1483+ return apt_pkg.VersionCompare(one, two)
1484
1485- def assertFileType(self, filename, filetype):
1486+ def assertFileType(self, filename, filetype, strict=True):
1487 '''Checks the file type of the file specified'''
1488
1489 (rc, report, out) = self._testlib_shell_cmd(["/usr/bin/file", "-b", filename])
1490@@ -894,15 +1090,19 @@ class TestlibCase(unittest.TestCase):
1491 if self.lsb_release['Release'] == 8.04 and rc == 255 and len(out) > 0:
1492 rc = 0
1493 result = 'Got exit code %d, expected %d:\n%s\n' % (rc, expected, report)
1494- self.assertEquals(expected, rc, result)
1495+ self.assertEqual(expected, rc, result)
1496
1497- filetype = '^%s$' % (filetype)
1498+ if strict:
1499+ filetype = '^%s$' % (filetype)
1500+ else:
1501+ # accept if the beginning of the line matches
1502+ filetype = '^%s' % (filetype)
1503 result = 'File type reported by file: [%s], expected regex: [%s]\n' % (out, filetype)
1504- self.assertNotEquals(None, re.search(filetype, out), result)
1505+ self.assertNotEqual(None, re.search(filetype, out), result)
1506
1507 def yank_commonname_from_cert(self, certfile):
1508 '''Extract the commonName from a given PEM'''
1509- rc, out = cmd(['openssl','asn1parse','-in',certfile])
1510+ rc, out = cmd(['openssl', 'asn1parse', '-in', certfile])
1511 if rc == 0:
1512 ready = False
1513 for line in out.splitlines():
1514@@ -914,12 +1114,12 @@ class TestlibCase(unittest.TestCase):
1515
1516 def announce(self, text):
1517 if self.my_verbosity:
1518- print >>sys.stdout, "(%s) " % (text),
1519+ print("(%s) " % (text), file=sys.stderr, end='')
1520 sys.stdout.flush()
1521
1522 def make_clean(self):
1523- rc, output = self.shell_cmd(['make','clean'])
1524- self.assertEquals(rc, 0, output)
1525+ rc, output = self.shell_cmd(['make', 'clean'])
1526+ self.assertEqual(rc, 0, output)
1527
1528 def get_makefile_compiler(self):
1529 # Find potential compiler name
1530@@ -937,8 +1137,8 @@ class TestlibCase(unittest.TestCase):
1531 '''Compile a target and report output'''
1532
1533 compiler = self.get_makefile_compiler()
1534- rc, output = self.shell_cmd(['make',target])
1535- self.assertEquals(rc, expected, 'rc(%d)!=%d:\n' % (rc, expected) + output)
1536+ rc, output = self.shell_cmd(['make', target])
1537+ self.assertEqual(rc, expected, 'rc(%d)!=%d:\n' % (rc, expected) + output)
1538 self.assertTrue('%s ' % (compiler) in output, 'Expected "%s":' % (compiler) + output)
1539 return output
1540
1541@@ -950,28 +1150,43 @@ class TestlibCase(unittest.TestCase):
1542 self.announce("skipped%s" % (reason))
1543 return False
1544
1545- def _testlib_shell_cmd(self,args,stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT):
1546+ def _testlib_shell_cmd(self, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=None):
1547 argstr = "'" + "', '".join(args).strip() + "'"
1548- rc, out = cmd(args,stdin=stdin,stdout=stdout,stderr=stderr)
1549+ rc, out = cmd(args, stdin=stdin, stdout=stdout, stderr=stderr, env=env)
1550 report = 'Command: ' + argstr + '\nOutput:\n' + out
1551 return rc, report, out
1552
1553 def shell_cmd(self, args, stdin=None):
1554- return cmd(args,stdin=stdin)
1555+ return cmd(args, stdin=stdin)
1556
1557- def assertShellExitEquals(self, expected, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg=""):
1558+ def assertShellExitEquals(self, expected, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=None, msg="", bare_report=False):
1559 '''Test a shell command matches a specific exit code'''
1560- rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr)
1561+ rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr, env=env)
1562 result = 'Got exit code %d, expected %d\n' % (rc, expected)
1563- self.assertEquals(expected, rc, msg + result + report)
1564+ self.assertEqual(expected, rc, msg + result + report)
1565+ if bare_report:
1566+ return out
1567+ else:
1568+ return report
1569+
1570+ def assertShellExitSuccess(self, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=None, msg="", bare_report=False):
1571+ '''Same as assertShellEquals but with an expected exit status of 0 (success).'''
1572+ return self.assertShellExitEquals(0, args, stdin, stdout, stderr, env, msg, bare_report)
1573+
1574+ # make sure exit value is in a list of expected values
1575+ def assertShellExitIn(self, expected, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg=""):
1576+ '''Test a shell command matches a specific exit code'''
1577+ rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr)
1578+ result = 'Got exit code %d, expected one of %s\n' % (rc, ', '.join(map(str, expected)))
1579+ self.assertIn(rc, expected, msg + result + report)
1580
1581 def assertShellExitNotEquals(self, unwanted, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg=""):
1582 '''Test a shell command doesn't match a specific exit code'''
1583 rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr)
1584 result = 'Got (unwanted) exit code %d\n' % rc
1585- self.assertNotEquals(unwanted, rc, msg + result + report)
1586+ self.assertNotEqual(unwanted, rc, msg + result + report)
1587
1588- def assertShellOutputContains(self, text, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg="", invert=False):
1589+ def assertShellOutputContains(self, text, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg="", invert=False, expected=None):
1590 '''Test a shell command contains a specific output'''
1591 rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr)
1592 result = 'Got exit code %d. Looking for text "%s"\n' % (rc, text)
1593@@ -979,18 +1194,21 @@ class TestlibCase(unittest.TestCase):
1594 self.assertTrue(text in out, msg + result + report)
1595 else:
1596 self.assertFalse(text in out, msg + result + report)
1597+ if expected is not None:
1598+ result = 'Got exit code %d. Expected %d (%s)\n' % (rc, expected, " ".join(args))
1599+ self.assertEqual(rc, expected, msg + result + report)
1600
1601 def assertShellOutputEquals(self, text, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg="", invert=False, expected=None):
1602 '''Test a shell command matches a specific output'''
1603 rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr)
1604 result = 'Got exit code %d. Looking for exact text "%s" (%s)\n' % (rc, text, " ".join(args))
1605 if not invert:
1606- self.assertEquals(text, out, msg + result + report)
1607+ self.assertEqual(text, out, msg + result + report)
1608 else:
1609- self.assertNotEquals(text, out, msg + result + report)
1610- if expected != None:
1611+ self.assertNotEqual(text, out, msg + result + report)
1612+ if expected is not None:
1613 result = 'Got exit code %d. Expected %d (%s)\n' % (rc, expected, " ".join(args))
1614- self.assertEquals(rc, expected, msg + result + report)
1615+ self.assertEqual(rc, expected, msg + result + report)
1616
1617 def _word_find(self, report, content, invert=False):
1618 '''Check for a specific string'''
1619@@ -1003,20 +1221,22 @@ class TestlibCase(unittest.TestCase):
1620
1621 def _test_sysctl_value(self, path, expected, msg=None, exists=True):
1622 sysctl = '/proc/sys/%s' % (path)
1623- self.assertEquals(exists, os.path.exists(sysctl), sysctl)
1624+ self.assertEqual(exists, os.path.exists(sysctl), sysctl)
1625 value = None
1626 if exists:
1627- value = int(file(sysctl).read())
1628+ with open(sysctl) as sysctl_fd:
1629+ value = int(sysctl_fd.read())
1630 report = "%s is not %d: %d" % (sysctl, expected, value)
1631 if msg:
1632 report += " (%s)" % (msg)
1633- self.assertEquals(value, expected, report)
1634+ self.assertEqual(value, expected, report)
1635 return value
1636
1637 def set_sysctl_value(self, path, desired):
1638 sysctl = '/proc/sys/%s' % (path)
1639- self.assertTrue(os.path.exists(sysctl),"%s does not exist" % (sysctl))
1640- file(sysctl,'w').write(str(desired))
1641+ self.assertTrue(os.path.exists(sysctl), "%s does not exist" % (sysctl))
1642+ with open(sysctl, 'w') as sysctl_fh:
1643+ sysctl_fh.write(str(desired))
1644 self._test_sysctl_value(path, desired)
1645
1646 def kernel_at_least(self, introduced):
1647@@ -1027,10 +1247,33 @@ class TestlibCase(unittest.TestCase):
1648 changelog = "/usr/share/doc/linux-image-%s/changelog.Debian.gz" % (self.kernel_version)
1649 if os.path.exists(changelog):
1650 for line in gzip.open(changelog):
1651- if cve in line and not "revert" in line and not "Revert" in line:
1652+ if cve in line and "revert" not in line and "Revert" not in line:
1653 return True
1654 return False
1655
1656+ def install_builddeps(self, src_pkg):
1657+ rc, report = _run_apt_command([src_pkg], 'build-dep')
1658+ self.assertEqual(0, rc, 'Failed to install build-deps for %s\nOutput:\n%s' % (src_pkg, report))
1659+
1660+ def install_package(self, package):
1661+ rc, report = _run_apt_command([package], 'install')
1662+ self.assertEqual(0, rc, 'Failed to install package %s\nOutput:\n%s' % (package, report))
1663+
1664+ def install_packages(self, pkg_list):
1665+ rc, report = _run_apt_command(pkg_list, 'install')
1666+ self.assertEqual(0, rc, 'Failed to install packages %s\nOutput:\n%s' % (','.join(pkg_list), report))
1667+
1668+ def install_snap(self, snapname, track="latest/stable", classic=False):
1669+ rc, report = _run_snap_install(snapname, track, classic)
1670+ self.assertEqual(0, rc, "Failed to install snap %s\nOutput:\n%s" %
1671+ (snapname, report))
1672+
1673+ def remove_snap(self, snapname):
1674+ rc, report = _run_snap_remove(snapname)
1675+ self.assertEqual(0, rc, "Failed to install snap %s\nOutput:\n%s" %
1676+ (snapname, report))
1677+
1678+
1679 class TestGroup:
1680 '''Create a temporary test group and remove it again in the dtor.'''
1681
1682@@ -1040,14 +1283,14 @@ class TestGroup:
1683 self.group = None
1684 if group:
1685 if group_exists(group):
1686- raise ValueError, 'group name already exists'
1687+ raise ValueError('group name already exists')
1688 else:
1689 while(True):
1690- group = random_string(7,lower=lower)
1691+ group = random_string(7, lower=lower)
1692 if not group_exists(group):
1693 break
1694
1695- assert subprocess.call(['groupadd',group]) == 0
1696+ assert subprocess.call(['groupadd', group]) == 0
1697 self.group = group
1698 g = grp.getgrnam(self.group)
1699 self.gid = g[2]
1700@@ -1059,6 +1302,7 @@ class TestGroup:
1701 rc, report = cmd(['groupdel', self.group])
1702 assert rc == 0
1703
1704+
1705 class TestUser:
1706 '''Create a temporary test user and remove it again in the dtor.'''
1707
1708@@ -1072,34 +1316,38 @@ class TestUser:
1709 self.login = None
1710
1711 if os.geteuid() != 0:
1712- raise ValueError, "You must be root to run this test"
1713+ raise ValueError("You must be root to run this test")
1714
1715 if login:
1716 if login_exists(login):
1717- raise ValueError, 'login name already exists'
1718+ raise ValueError('login name already exists')
1719 else:
1720 while(True):
1721- login = 't' + random_string(7,lower=lower)
1722+ login = 't' + random_string(7, lower=lower)
1723 if not login_exists(login):
1724 break
1725
1726- self.salt = random_string(2)
1727- self.password = random_string(8,lower=lower)
1728+ # when fips is enabled, python2.7 crypt.crypt() returns None
1729+ # for DES salts for being too weak. Force sha256 with "$5$" for
1730+ # the salt. XXX in python3, the salt is optional and will
1731+ # generate strong passwords automatically.
1732+ self.salt = "$5$%s" % random_string(2)
1733+ self.password = random_string(8, lower=lower)
1734 self.crypted = crypt.crypt(self.password, self.salt)
1735
1736 creation = ['useradd', '-p', self.crypted]
1737 if home:
1738 creation += ['-m']
1739 if group:
1740- creation += ['-G',group]
1741+ creation += ['-G', group]
1742 if uidmin:
1743- creation += ['-K','UID_MIN=%d'%uidmin]
1744+ creation += ['-K', 'UID_MIN=%d' % uidmin]
1745 if shell:
1746- creation += ['-s',shell]
1747+ creation += ['-s', shell]
1748 creation += [login]
1749 assert subprocess.call(creation) == 0
1750 # Set GECOS
1751- assert subprocess.call(['usermod','-c','Buddy %s' % (login),login]) == 0
1752+ assert subprocess.call(['usermod', '-c', 'Buddy %s' % (login), login]) == 0
1753
1754 self.login = login
1755 p = pwd.getpwnam(self.login)
1756@@ -1114,8 +1362,8 @@ class TestUser:
1757
1758 if self.login:
1759 # sanity check the login name so we don't accidentally wipe too much
1760- if len(self.login)>3 and not '/' in self.login:
1761- subprocess.call(['rm','-rf', '/home/'+self.login, '/var/mail/'+self.login])
1762+ if len(self.login) > 3 and '/' not in self.login:
1763+ subprocess.call(['rm', '-rf', '/home/' + self.login, '/var/mail/' + self.login])
1764 rc, report = cmd(['userdel', '-f', self.login])
1765 assert rc == 0
1766
1767@@ -1123,13 +1371,99 @@ class TestUser:
1768 '''Add user to the specified group name'''
1769 rc, report = cmd(['usermod', '-G', group, self.login])
1770 if rc != 0:
1771- print report
1772+ print(report)
1773 assert rc == 0
1774
1775+
1776+class AddUser:
1777+ '''Create a temporary test user and remove it again in the dtor.'''
1778+
1779+ def __init__(self, login=None, home=True, encrypt_home=False, group=None, lower=False, shell=None):
1780+ '''Create a new user account with a random password.
1781+
1782+ By default, the login name is random, too, but can be explicitly
1783+ specified with 'login'. By default, a home directory is created, this
1784+ can be suppressed with 'home=False'.
1785+
1786+ This class differs from the TestUser class in that the adduser/deluser
1787+ tools are used rather than the useradd/user/del tools. The adduser
1788+ program is the only commandline program that can be used to add a new
1789+ user with an encrypted home directory. It is possible that the AddUser
1790+ class may replace the TestUser class in the future.'''
1791+
1792+ self.login = None
1793+
1794+ if os.geteuid() != 0:
1795+ raise ValueError("You must be root to run this test")
1796+
1797+ if login:
1798+ if login_exists(login):
1799+ raise ValueError('login name already exists')
1800+ else:
1801+ while(True):
1802+ login = 't' + random_string(7, lower=True)
1803+ if not login_exists(login):
1804+ break
1805+
1806+ self.password = random_string(8, lower=lower)
1807+
1808+ creation = ['adduser', '--quiet']
1809+
1810+ if not home:
1811+ creation += ['--no-create-home']
1812+ elif encrypt_home:
1813+ creation += ['--encrypt-home']
1814+
1815+ if shell:
1816+ creation += ['--shell', shell]
1817+
1818+ creation += ['--gecos', 'Buddy %s' % (login), login]
1819+
1820+ child = pexpect.spawn(creation.pop(0), creation, timeout=5)
1821+ assert child.expect(b'(Enter new UNIX|New) password:') == 0
1822+ child.sendline(self.password)
1823+ assert child.expect(b'Retype new (UNIX )?password:') == 0
1824+ child.sendline(self.password)
1825+
1826+ child.wait()
1827+ child.close()
1828+ assert child.exitstatus == 0
1829+ assert child.signalstatus is None
1830+
1831+ self.login = login
1832+
1833+ if group:
1834+ assert self.add_to_group(group) == 0
1835+
1836+ p = pwd.getpwnam(self.login)
1837+ self.uid = p[2]
1838+ self.gid = p[3]
1839+ self.gecos = p[4]
1840+ self.home = p[5]
1841+ self.shell = p[6]
1842+
1843+ def __del__(self):
1844+ '''Remove the created user account.'''
1845+
1846+ if self.login:
1847+ # sanity check the login name so we don't accidentally wipe too much
1848+ rc, report = cmd(['deluser', '--remove-home', self.login])
1849+ assert rc == 0
1850+
1851+ def add_to_group(self, group):
1852+ '''Add user to the specified group name'''
1853+ rc, report = cmd(['adduser', self.login, group])
1854+ if rc != 0:
1855+ print(report)
1856+ assert rc == 0
1857+
1858+
1859 # Timeout handler using alarm() from John P. Speno's Pythonic Avocado
1860 class TimeoutFunctionException(Exception):
1861 """Exception to raise on a timeout"""
1862 pass
1863+
1864+
1865 class TimeoutFunction:
1866 def __init__(self, function, timeout):
1867 self.timeout = timeout
1868@@ -1148,6 +1482,7 @@ class TimeoutFunction:
1869 signal.alarm(0)
1870 return result
1871
1872+
1873 def main():
1874- print "hi"
1875+ print("hi")
1876 unittest.main()
1877diff --git a/debian/tests/testsuite b/debian/tests/testsuite
1878index 47c7a70..29cf597 100644
1879--- a/debian/tests/testsuite
1880+++ b/debian/tests/testsuite
1881@@ -3,5 +3,4 @@
1882 # Testing open-iscsi
1883 #-------------------
1884 set -e
1885-python2 `dirname $0`/test-open-iscsi.py 2>&1
1886-
1887+python3 `dirname $0`/test-open-iscsi.py 2>&1

Subscribers

People subscribed via source and target branches