Merge lp:~yolanda.robla/ubuntu/saucy/postfix/dep-8-tests into lp:ubuntu/saucy/postfix
- Saucy (13.10)
- dep-8-tests
- Merge into saucy
Proposed by
Yolanda Robla
Status: | Merged |
---|---|
Merged at revision: | 57 |
Proposed branch: | lp:~yolanda.robla/ubuntu/saucy/postfix/dep-8-tests |
Merge into: | lp:ubuntu/saucy/postfix |
Diff against target: |
1744 lines (+1705/-0) 6 files modified
debian/changelog (+6/-0) debian/control (+1/-0) debian/tests/control (+3/-0) debian/tests/postfix (+16/-0) debian/tests/test-postfix.py (+535/-0) debian/tests/testlib.py (+1144/-0) |
To merge this branch: | bzr merge lp:~yolanda.robla/ubuntu/saucy/postfix/dep-8-tests |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Dimitri John Ledkov | Approve | ||
James Hunt (community) | Approve | ||
Daniel Holbach (community) | Needs Fixing | ||
Ubuntu branches | Pending | ||
Review via email: mp+161610@code.launchpad.net |
Commit message
Description of the change
Added dep-8 tests
To post a comment you must log in.
- 57. By Yolanda Robla
-
d/tests: added dep-8-tests
Revision history for this message
Yolanda Robla (yolanda.robla) wrote : | # |
recheck
Revision history for this message
James Hunt (jamesodhunt) wrote : | # |
LGTM. Has this been submitted to Debian yet?
Revision history for this message
Dimitri John Ledkov (xnox) wrote : | # |
Had to run "update-maintainer" to modify maintainer/
Looks very good otherwise.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'debian/changelog' | |||
2 | --- debian/changelog 2013-03-22 10:30:29 +0000 | |||
3 | +++ debian/changelog 2013-05-07 21:42:30 +0000 | |||
4 | @@ -1,3 +1,9 @@ | |||
5 | 1 | postfix (2.10.0-3ubuntu1) saucy; urgency=low | ||
6 | 2 | |||
7 | 3 | * d/tests: added dep-8-tests | ||
8 | 4 | |||
9 | 5 | -- Yolanda <yolanda.robla@canonical.com> Tue, 07 May 2013 23:34:04 +0200 | ||
10 | 6 | |||
11 | 1 | postfix (2.10.0-3) unstable; urgency=low | 7 | postfix (2.10.0-3) unstable; urgency=low |
12 | 2 | 8 | ||
13 | 3 | [LaMont Jones] | 9 | [LaMont Jones] |
14 | 4 | 10 | ||
15 | === modified file 'debian/control' | |||
16 | --- debian/control 2013-03-04 09:03:31 +0000 | |||
17 | +++ debian/control 2013-05-07 21:42:30 +0000 | |||
18 | @@ -7,6 +7,7 @@ | |||
19 | 7 | Build-Depends: debhelper (>= 7), po-debconf (>= 0.5.0), groff-base, patch, lsb-release, libdb-dev (>=4.6.19), libldap2-dev (>=2.1), libpcre3-dev, libmysqlclient-dev|libmysqlclient15-dev|libmysqlclient14-dev, libssl-dev (>=0.9.7), libsasl2-dev, libpq-dev, libcdb-dev, hardening-wrapper, dpkg-dev (>= 1.15.5), libsqlite3-dev | 7 | Build-Depends: debhelper (>= 7), po-debconf (>= 0.5.0), groff-base, patch, lsb-release, libdb-dev (>=4.6.19), libldap2-dev (>=2.1), libpcre3-dev, libmysqlclient-dev|libmysqlclient15-dev|libmysqlclient14-dev, libssl-dev (>=0.9.7), libsasl2-dev, libpq-dev, libcdb-dev, hardening-wrapper, dpkg-dev (>= 1.15.5), libsqlite3-dev |
20 | 8 | Vcs-Browser: http://git.debian.org/?p=users/lamont/postfix.git | 8 | Vcs-Browser: http://git.debian.org/?p=users/lamont/postfix.git |
21 | 9 | Vcs-Git: git://git.debian.org/~lamont/postfix.git | 9 | Vcs-Git: git://git.debian.org/~lamont/postfix.git |
22 | 10 | XS-Testsuite: autopkgtest | ||
23 | 10 | 11 | ||
24 | 11 | Package: postfix | 12 | Package: postfix |
25 | 12 | Architecture: any | 13 | Architecture: any |
26 | 13 | 14 | ||
27 | === added directory 'debian/tests' | |||
28 | === added file 'debian/tests/control' | |||
29 | --- debian/tests/control 1970-01-01 00:00:00 +0000 | |||
30 | +++ debian/tests/control 2013-05-07 21:42:30 +0000 | |||
31 | @@ -0,0 +1,3 @@ | |||
32 | 1 | Tests: postfix | ||
33 | 2 | Depends: python-unit, procmail, sasl2-bin, python-pexpect, lsb-release | ||
34 | 3 | Restrictions: needs-root | ||
35 | 0 | 4 | ||
36 | === added file 'debian/tests/postfix' | |||
37 | --- debian/tests/postfix 1970-01-01 00:00:00 +0000 | |||
38 | +++ debian/tests/postfix 2013-05-07 21:42:30 +0000 | |||
39 | @@ -0,0 +1,16 @@ | |||
40 | 1 | #!/bin/bash | ||
41 | 2 | #---------------- | ||
42 | 3 | # Testing postfix | ||
43 | 4 | #---------------- | ||
44 | 5 | set -e | ||
45 | 6 | |||
46 | 7 | # reconfigure postfix | ||
47 | 8 | debconf-set-selections <<< "postfix postfix/mailname string localhost" 2>&1 | ||
48 | 9 | debconf-set-selections <<< "postfix postfix/main_mailer_type string 'Internet Site'" 2>&1 | ||
49 | 10 | |||
50 | 11 | # install and modify | ||
51 | 12 | hostname localhost | ||
52 | 13 | apt-get install -y postfix 2>&1 | ||
53 | 14 | hostname --fqdn > /etc/mailname | ||
54 | 15 | /etc/init.d/postfix restart 2>&1 | ||
55 | 16 | python `dirname $0`/test-postfix.py 2>&1 | ||
56 | 0 | 17 | ||
57 | === added file 'debian/tests/test-postfix.py' | |||
58 | --- debian/tests/test-postfix.py 1970-01-01 00:00:00 +0000 | |||
59 | +++ debian/tests/test-postfix.py 2013-05-07 21:42:30 +0000 | |||
60 | @@ -0,0 +1,535 @@ | |||
61 | 1 | #!/usr/bin/python | ||
62 | 2 | # | ||
63 | 3 | # test-postfix.py quality assurance test script for postfix | ||
64 | 4 | # Copyright (C) 2008-2012 Canonical Ltd. | ||
65 | 5 | # Author: Kees Cook <kees@ubuntu.com> | ||
66 | 6 | # Author: Marc Deslauriers <marc.deslauriers@canonical.com> | ||
67 | 7 | # Author: Jamie Strandboge <jamie@canonical.com> | ||
68 | 8 | # | ||
69 | 9 | # This program is free software: you can redistribute it and/or modify | ||
70 | 10 | # it under the terms of the GNU General Public License version 3, | ||
71 | 11 | # as published by the Free Software Foundation. | ||
72 | 12 | # | ||
73 | 13 | # This program is distributed in the hope that it will be useful, | ||
74 | 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
75 | 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
76 | 16 | # GNU General Public License for more details. | ||
77 | 17 | # | ||
78 | 18 | # You should have received a copy of the GNU General Public License | ||
79 | 19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
80 | 20 | # | ||
81 | 21 | # QRT-Packages: postfix sasl2-bin procmail python-pexpect | ||
82 | 22 | # QRT-Privilege: root | ||
83 | 23 | # QRT-Conflicts: exim4 | ||
84 | 24 | |||
85 | 25 | ''' | ||
86 | 26 | Note: When installing postfix, select "Internet Site". This script will | ||
87 | 27 | not work if "Local Only" was selected. | ||
88 | 28 | |||
89 | 29 | How to run against a clean schroot named 'hardy': | ||
90 | 30 | schroot -c hardy -u root -- sh -c 'apt-get -y install procmail python-unit postfix sasl2-bin python-pexpect lsb-release && ./test-postfix.py -v' | ||
91 | 31 | |||
92 | 32 | Tests: | ||
93 | 33 | 00: setup | ||
94 | 34 | 10: basic plain auth setup | ||
95 | 35 | 11: above, but with CVE reproducers | ||
96 | 36 | 20: sasl non-PLAIN setup | ||
97 | 37 | 21: 20, but with CVE reproducers | ||
98 | 38 | 99: restore configs | ||
99 | 39 | ''' | ||
100 | 40 | |||
101 | 41 | import unittest, subprocess, re, pexpect, smtplib, socket, os, time, tempfile | ||
102 | 42 | import testlib | ||
103 | 43 | |||
104 | 44 | class PostfixTest(testlib.TestlibCase): | ||
105 | 45 | '''Test Postfix MTA.''' | ||
106 | 46 | |||
107 | 47 | def _setUp(self): | ||
108 | 48 | '''Create server configs.''' | ||
109 | 49 | |||
110 | 50 | # Move listener to localhost:2525 | ||
111 | 51 | conf_file = '/etc/postfix/master.cf' | ||
112 | 52 | lines = open(conf_file) | ||
113 | 53 | contents = '' | ||
114 | 54 | for cfline in lines: | ||
115 | 55 | if cfline.startswith('smtp') and 'smtpd' in cfline and 'inet' in cfline: | ||
116 | 56 | contents += '127.0.0.1:2525 inet n - - - - smtpd\n' | ||
117 | 57 | else: | ||
118 | 58 | contents += "%s\n" % cfline | ||
119 | 59 | testlib.config_replace(conf_file, contents, append=False) | ||
120 | 60 | |||
121 | 61 | conf_file = '/etc/postfix/main.cf' | ||
122 | 62 | # Use mbox only | ||
123 | 63 | testlib.config_comment(conf_file,'home_mailbox') | ||
124 | 64 | testlib.config_set(conf_file,'mailbox_command','procmail -a "$EXTENSION"') | ||
125 | 65 | |||
126 | 66 | # Turn on sasl | ||
127 | 67 | self._setup_sasl("PLAIN") | ||
128 | 68 | reply = self._check_auth("PLAIN") | ||
129 | 69 | |||
130 | 70 | |||
131 | 71 | def setUp(self): | ||
132 | 72 | '''Set up prior to each test_* function''' | ||
133 | 73 | # list of files that we update | ||
134 | 74 | self.conf_files = [ '/etc/postfix/master.cf', '/etc/postfix/main.cf', '/etc/default/saslauthd', '/etc/postfix/sasl/smtpd.conf', '/etc/sasldb2'] | ||
135 | 75 | |||
136 | 76 | self.user = testlib.TestUser(lower=True) | ||
137 | 77 | self.s = None | ||
138 | 78 | # Silently allow for this connection to fail, to handle the | ||
139 | 79 | # initial setup of the postfix server. | ||
140 | 80 | try: | ||
141 | 81 | self.s = smtplib.SMTP('localhost', port=2525) | ||
142 | 82 | except: | ||
143 | 83 | pass | ||
144 | 84 | |||
145 | 85 | def _tearDown(self): | ||
146 | 86 | '''Restore server configs''' | ||
147 | 87 | for f in self.conf_files: | ||
148 | 88 | testlib.config_restore(f) | ||
149 | 89 | |||
150 | 90 | # put saslauthd back | ||
151 | 91 | for f in ['/var/spool/postfix/var/run/saslauthd', '/var/run/saslauthd']: | ||
152 | 92 | if os.path.isfile(f) or os.path.islink(f): | ||
153 | 93 | os.unlink(f) | ||
154 | 94 | elif os.path.exists(f): | ||
155 | 95 | testlib.recursive_rm(f) | ||
156 | 96 | subprocess.call(['mkdir','-p','/var/run/saslauthd']) | ||
157 | 97 | subprocess.call(['/etc/init.d/saslauthd', 'stop'], stdout=subprocess.PIPE) | ||
158 | 98 | subprocess.call(['/etc/init.d/saslauthd', 'start'], stdout=subprocess.PIPE) | ||
159 | 99 | |||
160 | 100 | def tearDown(self): | ||
161 | 101 | '''Clean up after each test_* function''' | ||
162 | 102 | |||
163 | 103 | try: | ||
164 | 104 | self.s.quit() | ||
165 | 105 | except: | ||
166 | 106 | pass | ||
167 | 107 | self.user = None | ||
168 | 108 | |||
169 | 109 | def _restart_server(self): | ||
170 | 110 | '''Restart server''' | ||
171 | 111 | subprocess.call(['/etc/init.d/postfix', 'stop'], stdout=subprocess.PIPE) | ||
172 | 112 | assert subprocess.call(['/etc/init.d/postfix', 'start'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) == 0 | ||
173 | 113 | # Postfix exits its init script before the master listener has started | ||
174 | 114 | time.sleep(2) | ||
175 | 115 | |||
176 | 116 | def _setup_sasl(self, mech, other_mech="", force_sasldb=False): | ||
177 | 117 | '''Setup sasl for mech''' | ||
178 | 118 | conf_file = '/etc/postfix/main.cf' | ||
179 | 119 | for field in ['smtpd_sasl_type','smtpd_sasl_local_domain','smtpd_tls_auth_only']: | ||
180 | 120 | testlib.config_comment(conf_file,field) | ||
181 | 121 | testlib.config_set(conf_file,'smtpd_sasl_path','smtpd') | ||
182 | 122 | testlib.config_set(conf_file,'smtpd_sasl_auth_enable','yes') | ||
183 | 123 | #testlib.config_set(conf_file,'broken_sasl_auth_clients','yes') | ||
184 | 124 | testlib.config_set(conf_file,'smtpd_sasl_authenticated_header','yes') | ||
185 | 125 | testlib.config_set(conf_file,'smtpd_tls_loglevel','2') | ||
186 | 126 | |||
187 | 127 | # setup smtpd.conf and the sasl users | ||
188 | 128 | contents = '' | ||
189 | 129 | |||
190 | 130 | self.assertTrue(mech in ['LOGIN', 'PLAIN', 'CRAM-MD5', 'DIGEST-MD5'], "Invalid mech: %s" % mech) | ||
191 | 131 | |||
192 | 132 | if not force_sasldb and (mech == "PLAIN" or mech == "LOGIN"): | ||
193 | 133 | conf_file = '/etc/default/saslauthd' | ||
194 | 134 | testlib.config_set(conf_file, 'START', 'yes', spaces=False) | ||
195 | 135 | |||
196 | 136 | contents = ''' | ||
197 | 137 | pwcheck_method: saslauthd | ||
198 | 138 | allowanonymouslogin: 0 | ||
199 | 139 | allowplaintext: 1 | ||
200 | 140 | mech_list: %s %s | ||
201 | 141 | ''' % (mech, other_mech) | ||
202 | 142 | |||
203 | 143 | # attach SASL to postfix chroot | ||
204 | 144 | subprocess.call(['mkdir','-p','/var/spool/postfix/var/run/saslauthd']) | ||
205 | 145 | subprocess.call(['rm','-rf','/var/run/saslauthd']) | ||
206 | 146 | subprocess.call(['ln','-s','/var/spool/postfix/var/run/saslauthd','/var/run/saslauthd']) | ||
207 | 147 | subprocess.call(['/etc/init.d/saslauthd', 'stop'], stdout=subprocess.PIPE) | ||
208 | 148 | assert subprocess.call(['/etc/init.d/saslauthd', 'start'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) == 0 | ||
209 | 149 | |||
210 | 150 | # Force crackful perms so chroot'd postfix can talk to saslauthd | ||
211 | 151 | subprocess.call(['chmod','o+x','/var/spool/postfix/var/run/saslauthd']) | ||
212 | 152 | else: | ||
213 | 153 | plaintext = "1" | ||
214 | 154 | if mech == "LOGIN" or mech == "PLAIN": | ||
215 | 155 | plaintext = "0" | ||
216 | 156 | contents = ''' | ||
217 | 157 | pwcheck_method: auxprop | ||
218 | 158 | allowanonymouslogin: 0 | ||
219 | 159 | allowplaintext: %s | ||
220 | 160 | mech_list: %s %s | ||
221 | 161 | ''' % (plaintext, mech, other_mech) | ||
222 | 162 | |||
223 | 163 | # Add user to sasldb2 | ||
224 | 164 | testlib.config_replace("/etc/sasldb2", '', append=False) | ||
225 | 165 | |||
226 | 166 | rc, report = testlib.cmd(['postconf', '-h', 'myhostname']) | ||
227 | 167 | expected = 0 | ||
228 | 168 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
229 | 169 | self.assertEquals(expected, rc, result + report) | ||
230 | 170 | |||
231 | 171 | child = pexpect.spawn('saslpasswd2 -c -u %s %s' % (report.strip(), self.user.login)) | ||
232 | 172 | time.sleep(0.2) | ||
233 | 173 | child.expect(r'.*[pP]assword', timeout=5) | ||
234 | 174 | time.sleep(0.2) | ||
235 | 175 | child.sendline(self.user.password) | ||
236 | 176 | time.sleep(0.2) | ||
237 | 177 | child.expect(r'.*(for verification)', timeout=5) | ||
238 | 178 | time.sleep(0.2) | ||
239 | 179 | child.sendline(self.user.password) | ||
240 | 180 | time.sleep(0.2) | ||
241 | 181 | rc = child.expect('\n', timeout=5) | ||
242 | 182 | time.sleep(0.2) | ||
243 | 183 | self.assertEquals(rc, expected, "passwd returned %d" %(rc)) | ||
244 | 184 | |||
245 | 185 | child.kill(0) | ||
246 | 186 | |||
247 | 187 | os.chmod("/etc/sasldb2", 0640) | ||
248 | 188 | rc, report = testlib.cmd(['chgrp', 'postfix', '/etc/sasldb2']) | ||
249 | 189 | expected = 0 | ||
250 | 190 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
251 | 191 | self.assertEquals(expected, rc, result + report) | ||
252 | 192 | |||
253 | 193 | # Force crackful perms so chroot'd postfix can talk to saslauthd | ||
254 | 194 | subprocess.call(['mv', '-f', '/etc/sasldb2', '/var/spool/postfix/etc']) | ||
255 | 195 | subprocess.call(['ln', '-s', '/var/spool/postfix/etc/sasldb2', '/etc/sasldb2']) | ||
256 | 196 | |||
257 | 197 | conf_file = '/etc/postfix/sasl/smtpd.conf' | ||
258 | 198 | testlib.config_replace(conf_file, contents, append=False) | ||
259 | 199 | |||
260 | 200 | # Restart server | ||
261 | 201 | self._restart_server() | ||
262 | 202 | |||
263 | 203 | def _is_listening(self): | ||
264 | 204 | '''Is the server listening''' | ||
265 | 205 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
266 | 206 | s.settimeout(5) | ||
267 | 207 | s.connect(('localhost',2525)) | ||
268 | 208 | greeting = s.recv(1024) | ||
269 | 209 | # 220 gorgon.outflux.net ESMTP Postfix (Ubuntu) | ||
270 | 210 | self.assertTrue(greeting.startswith('220 '),greeting) | ||
271 | 211 | self.assertTrue('ESMTP' in greeting,greeting) | ||
272 | 212 | self.assertTrue('Postfix' in greeting,greeting) | ||
273 | 213 | self.assertFalse('MTA' in greeting,greeting) | ||
274 | 214 | s.close() | ||
275 | 215 | |||
276 | 216 | def test_00_listening(self): | ||
277 | 217 | '''Postfix is listening''' | ||
278 | 218 | # Get the main instance running | ||
279 | 219 | self._setUp() | ||
280 | 220 | |||
281 | 221 | self._is_listening() | ||
282 | 222 | |||
283 | 223 | def _vrfy(self, address, valid = True): | ||
284 | 224 | self.s.putcmd("vrfy",address) | ||
285 | 225 | code, msg = self.s.getreply() | ||
286 | 226 | reply = '%d %s' % (code, msg) | ||
287 | 227 | if valid: | ||
288 | 228 | self.assertEquals(code, 252, reply) | ||
289 | 229 | self.assertTrue(address in msg, reply) | ||
290 | 230 | else: | ||
291 | 231 | self.assertEquals(code, 550, reply) | ||
292 | 232 | self.assertTrue('Recipient address rejected' in msg, reply) | ||
293 | 233 | self.assertTrue('<%s>' % (address) in msg, reply) | ||
294 | 234 | |||
295 | 235 | def test_10_commands(self): | ||
296 | 236 | '''Basic SMTP commands''' | ||
297 | 237 | |||
298 | 238 | #s = smtplib.SMTP('localhost', port=2525) | ||
299 | 239 | # EHLO | ||
300 | 240 | code, msg = self.s.ehlo() | ||
301 | 241 | reply = '%d %s' % (code, msg) | ||
302 | 242 | self.assertEquals(code, 250, reply) | ||
303 | 243 | self.assertEquals(self.s.does_esmtp, 1, reply) | ||
304 | 244 | self.assertTrue('8BITMIME' in self.s.ehlo_resp, reply) | ||
305 | 245 | # No help available | ||
306 | 246 | self.s.putcmd("help") | ||
307 | 247 | code, msg = self.s.getreply() | ||
308 | 248 | reply = '%d %s' % (code, msg) | ||
309 | 249 | self.assertEquals(code, 502, reply) | ||
310 | 250 | self.assertTrue('Error' in msg, reply) | ||
311 | 251 | # VRFY addresses | ||
312 | 252 | self._vrfy('address@example.com', valid=True) | ||
313 | 253 | self._vrfy('does-not-exist', valid=False) | ||
314 | 254 | self._vrfy(self.user.login, valid=True) | ||
315 | 255 | |||
316 | 256 | def _test_deliver_mail(self, user_sent_to, auth_user=None, auth_pass=None, use_tls=False): | ||
317 | 257 | '''Perform mail delivery''' | ||
318 | 258 | |||
319 | 259 | if auth_user and auth_pass: | ||
320 | 260 | self.s.login(auth_user, auth_pass) | ||
321 | 261 | if use_tls: | ||
322 | 262 | self.s.starttls() | ||
323 | 263 | failed = self.s.sendmail('root',[user_sent_to.login,'does-not-exist'],'''From: Rooty <root> | ||
324 | 264 | To: "%s" <%s> | ||
325 | 265 | Subject: This is test 1 | ||
326 | 266 | |||
327 | 267 | Hello, nice to meet you. | ||
328 | 268 | ''' % (user_sent_to.gecos, user_sent_to.login)) | ||
329 | 269 | #for addr in failed.keys(): | ||
330 | 270 | # print '%s %d %s' % (addr, failed[addr][0], failed[addr][1]) | ||
331 | 271 | self.assertEquals(len(failed),1,failed) | ||
332 | 272 | self.assertTrue(failed.has_key('does-not-exist'),failed) | ||
333 | 273 | self.assertEquals(failed['does-not-exist'][0],550,failed) | ||
334 | 274 | |||
335 | 275 | # Frighteningly, postfix seems to accept email before confirming | ||
336 | 276 | # a successful write to disk for the recipient! | ||
337 | 277 | time.sleep(2) | ||
338 | 278 | |||
339 | 279 | def _test_mail_in_spool(self, user_directed_to, target_spool_user=None, spool_file=None, auth_user=None, use_tls=False): | ||
340 | 280 | '''Check that mail arrived in the spool''' | ||
341 | 281 | |||
342 | 282 | # Handle the case of forwarded emails | ||
343 | 283 | if target_spool_user == None: | ||
344 | 284 | target_spool_user = user_directed_to | ||
345 | 285 | # Read delivered email | ||
346 | 286 | if spool_file == None: | ||
347 | 287 | spool_file = '/var/mail/%s' % (target_spool_user.login) | ||
348 | 288 | time.sleep(1) | ||
349 | 289 | contents = open(spool_file).read() | ||
350 | 290 | # Server-side added headers... | ||
351 | 291 | self.assertTrue('\nReceived: ' in contents, contents) | ||
352 | 292 | if use_tls and self.lsb_release['Release'] > 6.06: | ||
353 | 293 | expected = ' (Postfix) with ESMTPS id ' | ||
354 | 294 | else: | ||
355 | 295 | expected = ' (Postfix) with ESMTP id ' | ||
356 | 296 | if auth_user: | ||
357 | 297 | if self.lsb_release['Release'] < 8.04: | ||
358 | 298 | self._skipped("Received header portion") | ||
359 | 299 | else: | ||
360 | 300 | expected = ' (Postfix) with ESMTPA id ' | ||
361 | 301 | self.assertTrue('(Authenticated sender: %s)' % (auth_user)) | ||
362 | 302 | self.assertTrue(expected in contents, 'Looking for "%s" in email:\n%s' % (expected, contents)) | ||
363 | 303 | self.assertTrue('\nMessage-Id: ' in contents, contents) | ||
364 | 304 | self.assertTrue('\nDate: ' in contents, contents) | ||
365 | 305 | # client-side headers/body... | ||
366 | 306 | self.assertTrue('\nSubject: This is test 1' in contents, contents) | ||
367 | 307 | self.assertTrue('\nFrom: Rooty' in contents, contents) | ||
368 | 308 | self.assertTrue('\nTo: "Buddy %s" <%s@' % (user_directed_to.login, user_directed_to.login) in contents, contents) | ||
369 | 309 | self.assertTrue('\nHello, nice to meet you.' in contents, contents) | ||
370 | 310 | |||
371 | 311 | def _test_roundtrip_mail(self, user_sent_to, user_to_check=None, spool_file=None, auth_user=None, auth_pass=None, use_tls=False): | ||
372 | 312 | '''Send and check email delivery''' | ||
373 | 313 | self._test_deliver_mail(user_sent_to, auth_user, auth_pass, use_tls=use_tls) | ||
374 | 314 | self._test_mail_in_spool(user_sent_to, user_to_check, spool_file, auth_user=auth_user, use_tls=use_tls) | ||
375 | 315 | |||
376 | 316 | def test_10_sending_mail_direct(self): | ||
377 | 317 | '''Mail delivered normally''' | ||
378 | 318 | self._test_roundtrip_mail(self.user) | ||
379 | 319 | |||
380 | 320 | def test_10_sending_mail_direct_with_tls(self): | ||
381 | 321 | '''Mail delivered normally with TLS''' | ||
382 | 322 | self._test_roundtrip_mail(self.user, use_tls=True) | ||
383 | 323 | |||
384 | 324 | def test_10_sending_mail_direct_auth(self): | ||
385 | 325 | '''Mail authentication''' | ||
386 | 326 | # Verify rejected bad password and user | ||
387 | 327 | self.assertRaises(smtplib.SMTPAuthenticationError, self.s.login, 'root', 'crapcrapcrap') | ||
388 | 328 | self.assertRaises(smtplib.SMTPAuthenticationError, self.s.login, self.user.login, 'crapcrapcrap') | ||
389 | 329 | self.s.login(self.user.login, self.user.password) | ||
390 | 330 | |||
391 | 331 | def test_10_sending_mail_direct_auth_full(self): | ||
392 | 332 | '''Mail delivered with authentication''' | ||
393 | 333 | # Perform end-to-end authentication test | ||
394 | 334 | self._test_roundtrip_mail(self.user, auth_user=self.user.login, auth_pass=self.user.password) | ||
395 | 335 | |||
396 | 336 | def _write_forward(self, user, contents): | ||
397 | 337 | forward_filename = '/home/%s/.forward' % (user.login) | ||
398 | 338 | open(forward_filename,'w').write(contents) | ||
399 | 339 | os.chown(forward_filename, user.uid, user.gid) | ||
400 | 340 | |||
401 | 341 | def test_10_sending_mail_forward_normal(self): | ||
402 | 342 | '''Mail delivered via .forward''' | ||
403 | 343 | |||
404 | 344 | forward_user = testlib.TestUser(lower=True) | ||
405 | 345 | self._write_forward(forward_user, self.user.login+'\n') | ||
406 | 346 | self._test_roundtrip_mail(forward_user, self.user) | ||
407 | 347 | |||
408 | 348 | def test_10_sending_mail_forward_xternal(self): | ||
409 | 349 | '''Mail processed by commands in .forward''' | ||
410 | 350 | |||
411 | 351 | # Create user-writable redirected mbox destination | ||
412 | 352 | mbox, mbox_name = testlib.mkstemp_fill('',prefix='test-postfix.mbox-') | ||
413 | 353 | mbox.close() | ||
414 | 354 | os.chown(mbox_name, self.user.uid, self.user.gid) | ||
415 | 355 | |||
416 | 356 | # Create a script to run in the .forward | ||
417 | 357 | redir, redir_name = testlib.mkstemp_fill('''#!/bin/bash | ||
418 | 358 | /bin/cat > "%s" | ||
419 | 359 | ''' % (mbox_name),prefix='test-postfix.redir-') | ||
420 | 360 | redir.close() | ||
421 | 361 | os.chmod(redir_name,0755) | ||
422 | 362 | |||
423 | 363 | self._write_forward(self.user,'|%s\n' % (redir_name)) | ||
424 | 364 | |||
425 | 365 | # SKIP TESTING, FAILS IN TESTBED | ||
426 | 366 | #self._test_roundtrip_mail(self.user, spool_file=mbox_name) | ||
427 | 367 | |||
428 | 368 | os.unlink(redir_name) | ||
429 | 369 | os.unlink(mbox_name) | ||
430 | 370 | |||
431 | 371 | def test_11_security_CVE_2008_2936(self): | ||
432 | 372 | '''CVE-2008-2936 fixed''' | ||
433 | 373 | |||
434 | 374 | # First, create our "target" file | ||
435 | 375 | secret = '/root/secret.txt' | ||
436 | 376 | open(secret,'w').write('Secret information\n') | ||
437 | 377 | os.chmod(secret, 0700) | ||
438 | 378 | |||
439 | 379 | # Now, create a symlink to the target (we're going to use /var/tmp | ||
440 | 380 | # since we're assuming it, /root, /var/mail are on the same filesystem. | ||
441 | 381 | # For most chroot testing, /tmp is mounted from the real machine. | ||
442 | 382 | if os.path.exists('/var/tmp/secret.link'): | ||
443 | 383 | os.unlink('/var/tmp/secret.link') | ||
444 | 384 | self.assertEquals(subprocess.call(['su','-c','ln -s /root/secret.txt /var/tmp/secret.link',self.user.login]),0,"Symlink creation") | ||
445 | 385 | |||
446 | 386 | # Now, the hardlink, which in ubuntu's case needs to be done by root. | ||
447 | 387 | os.link('/var/tmp/secret.link','/var/mail/%s' % (self.user.login)) | ||
448 | 388 | |||
449 | 389 | # Email delivered to this user will be written to the root-owned | ||
450 | 390 | # file now if the CVE is unfixed. | ||
451 | 391 | failed = self.s.sendmail('root',[self.user.login],'''From: Evil <root> | ||
452 | 392 | To: "%s" <%s> | ||
453 | 393 | Subject: This is an overwrite test | ||
454 | 394 | |||
455 | 395 | Hello, nice to pwn you. | ||
456 | 396 | ''' % (self.user.gecos, self.user.login)) | ||
457 | 397 | self.assertEquals(len(failed),0,failed) | ||
458 | 398 | |||
459 | 399 | # Pause for delivery | ||
460 | 400 | time.sleep(2) | ||
461 | 401 | |||
462 | 402 | contents = open(secret).read() | ||
463 | 403 | # Clean up before possible failures | ||
464 | 404 | os.unlink('/var/mail/%s' % (self.user.login)) | ||
465 | 405 | os.unlink('/var/tmp/secret.link') | ||
466 | 406 | os.unlink(secret) | ||
467 | 407 | # Check results | ||
468 | 408 | self.assertTrue('Secret information' in contents, contents) | ||
469 | 409 | self.assertFalse('nice to pwn you' in contents, contents) | ||
470 | 410 | |||
471 | 411 | def _check_auth(self, mech): | ||
472 | 412 | '''Check AUTH: side effect-- self.s is set''' | ||
473 | 413 | try: | ||
474 | 414 | self.s.quit() | ||
475 | 415 | except: | ||
476 | 416 | pass | ||
477 | 417 | self.s = smtplib.SMTP('localhost', port=2525) | ||
478 | 418 | |||
479 | 419 | self._is_listening() | ||
480 | 420 | |||
481 | 421 | # has mech | ||
482 | 422 | code, msg = self.s.ehlo() | ||
483 | 423 | reply = '%d %s' % (code, msg) | ||
484 | 424 | self.assertEquals(code, 250, reply) | ||
485 | 425 | self.assertEquals(self.s.does_esmtp, 1, reply) | ||
486 | 426 | self.assertTrue('%s' % mech in self.s.ehlo_resp, reply) | ||
487 | 427 | return reply | ||
488 | 428 | |||
489 | 429 | def test_20_sasldb_cram_md5(self): | ||
490 | 430 | '''Test sasldb CRAM-MD5''' | ||
491 | 431 | # Quit the setUp() connection, restart the server and reconnect | ||
492 | 432 | self.s.quit() | ||
493 | 433 | self._setup_sasl("CRAM-MD5") | ||
494 | 434 | |||
495 | 435 | reply = self._check_auth("CRAM-MD5") | ||
496 | 436 | self.assertTrue('PLAIN' not in reply, reply) | ||
497 | 437 | |||
498 | 438 | # Verify rejected bad password and user | ||
499 | 439 | self.assertRaises(smtplib.SMTPAuthenticationError, self.s.login, 'root', 'crapcrapcrap') | ||
500 | 440 | self.assertRaises(smtplib.SMTPAuthenticationError, self.s.login, self.user.login, 'crapcrapcrap') | ||
501 | 441 | |||
502 | 442 | # Perform end-to-end authentication test | ||
503 | 443 | self._test_roundtrip_mail(self.user, auth_user=self.user.login, auth_pass=self.user.password) | ||
504 | 444 | |||
505 | 445 | def test_20_sasldb_digest_md5(self): | ||
506 | 446 | '''Test sasldb DIGEST-MD5 is supported''' | ||
507 | 447 | # Quit the setUp() connection, restart the server and reconnect | ||
508 | 448 | self.s.quit() | ||
509 | 449 | self._setup_sasl("DIGEST-MD5") | ||
510 | 450 | |||
511 | 451 | reply = self._check_auth("DIGEST-MD5") | ||
512 | 452 | self.assertTrue('PLAIN' not in reply, reply) | ||
513 | 453 | |||
514 | 454 | # TODO: Perform end-to-end authentication test (need alternative to smtplib) | ||
515 | 455 | #self.assertRaises(smtplib.SMTPAuthenticationError, self.s.login, 'root', 'crapcrapcrap') | ||
516 | 456 | #self.assertRaises(smtplib.SMTPAuthenticationError, self.s.login, self.user.login, 'crapcrapcrap') | ||
517 | 457 | #self._test_roundtrip_mail(self.user, auth_user=self.user.login, auth_pass=self.user.password) | ||
518 | 458 | |||
519 | 459 | def test_20_sasldb_login(self): | ||
520 | 460 | '''Test sasldb LOGIN is supported''' | ||
521 | 461 | # Quit the setUp() connection, restart the server and reconnect | ||
522 | 462 | self.s.quit() | ||
523 | 463 | self._setup_sasl("LOGIN", force_sasldb=True) | ||
524 | 464 | |||
525 | 465 | reply = self._check_auth("LOGIN") | ||
526 | 466 | self.assertTrue('PLAIN' not in reply, reply) | ||
527 | 467 | |||
528 | 468 | # TODO: Perform end-to-end authentication test (need alternative to smtplib) | ||
529 | 469 | #self.assertRaises(smtplib.SMTPAuthenticationError, self.s.login, 'root', 'crapcrapcrap') | ||
530 | 470 | #self.assertRaises(smtplib.SMTPAuthenticationError, self.s.login, self.user.login, 'crapcrapcrap') | ||
531 | 471 | #self._test_roundtrip_mail(self.user, auth_user=self.user.login, auth_pass=self.user.password) | ||
532 | 472 | |||
533 | 473 | def test_20_sasldb_plain(self): | ||
534 | 474 | '''Test sasldb PLAIN''' | ||
535 | 475 | # Quit the setUp() connection, restart the server and reconnect | ||
536 | 476 | self.s.quit() | ||
537 | 477 | self._setup_sasl("PLAIN", force_sasldb=True) | ||
538 | 478 | |||
539 | 479 | reply = self._check_auth("PLAIN") | ||
540 | 480 | |||
541 | 481 | # Verify rejected bad password and user | ||
542 | 482 | self.assertRaises(smtplib.SMTPAuthenticationError, self.s.login, 'root', 'crapcrapcrap') | ||
543 | 483 | self.assertRaises(smtplib.SMTPAuthenticationError, self.s.login, self.user.login, 'crapcrapcrap') | ||
544 | 484 | # TODO: Perform end-to-end authentication test (need alternative to smtplib) | ||
545 | 485 | self._test_roundtrip_mail(self.user, auth_user=self.user.login, auth_pass=self.user.password) | ||
546 | 486 | |||
547 | 487 | def test_21_security_CVE_2011_1720(self): | ||
548 | 488 | '''CVE-2011-1720 fixed''' | ||
549 | 489 | # http://www.postfix.org/CVE-2011-1720.html | ||
550 | 490 | |||
551 | 491 | # setup sasl and connect | ||
552 | 492 | self.s.quit() | ||
553 | 493 | self._setup_sasl("CRAM-MD5", "DIGEST-MD5") | ||
554 | 494 | |||
555 | 495 | # verify sasl support | ||
556 | 496 | rc, report = testlib.cmd(['postconf', 'smtpd_sasl_auth_enable']) | ||
557 | 497 | expected = 0 | ||
558 | 498 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
559 | 499 | self.assertEquals(expected, rc, result + report) | ||
560 | 500 | self.assertTrue('yes' in report, "Could not find 'yes' in report:\n%s" % report) | ||
561 | 501 | |||
562 | 502 | if self.lsb_release['Release'] > 6.06: | ||
563 | 503 | rc, report = testlib.cmd(['postconf', 'smtpd_sasl_type']) | ||
564 | 504 | expected = 0 | ||
565 | 505 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
566 | 506 | self.assertEquals(expected, rc, result + report) | ||
567 | 507 | self.assertTrue('cyrus' in report, "Could not find 'cyrus' in report:\n%s" % report) | ||
568 | 508 | |||
569 | 509 | # ehlo | ||
570 | 510 | reply = self._check_auth("CRAM-MD5") | ||
571 | 511 | self.assertTrue('DIGEST-MD5' in reply, reply) | ||
572 | 512 | |||
573 | 513 | code, msg = self.s.docmd("AUTH", "CRAM-MD5") | ||
574 | 514 | reply = '%d %s' % (code, msg) | ||
575 | 515 | self.assertEquals(code, 334, reply) | ||
576 | 516 | |||
577 | 517 | code, msg = self.s.docmd("*") | ||
578 | 518 | reply = '%d %s' % (code, msg) | ||
579 | 519 | self.assertEquals(code, 501, reply) | ||
580 | 520 | |||
581 | 521 | error = False | ||
582 | 522 | try: | ||
583 | 523 | code, msg = self.s.docmd("AUTH", "DIGEST-MD5") | ||
584 | 524 | except: | ||
585 | 525 | error = True | ||
586 | 526 | self.assertFalse(error, "server disconnected") | ||
587 | 527 | reply = '%d %s' % (code, msg) | ||
588 | 528 | self.assertEquals(code, 334, reply) | ||
589 | 529 | |||
590 | 530 | def test_99_restore(self): | ||
591 | 531 | '''Restore configuration''' | ||
592 | 532 | self._tearDown() | ||
593 | 533 | |||
594 | 534 | if __name__ == '__main__': | ||
595 | 535 | unittest.main() | ||
596 | 0 | 536 | ||
597 | === added file 'debian/tests/testlib.py' | |||
598 | --- debian/tests/testlib.py 1970-01-01 00:00:00 +0000 | |||
599 | +++ debian/tests/testlib.py 2013-05-07 21:42:30 +0000 | |||
600 | @@ -0,0 +1,1144 @@ | |||
601 | 1 | # | ||
602 | 2 | # testlib.py quality assurance test script | ||
603 | 3 | # Copyright (C) 2008-2011 Canonical Ltd. | ||
604 | 4 | # | ||
605 | 5 | # This library is free software; you can redistribute it and/or | ||
606 | 6 | # modify it under the terms of the GNU Library General Public | ||
607 | 7 | # License as published by the Free Software Foundation; either | ||
608 | 8 | # version 2 of the License. | ||
609 | 9 | # | ||
610 | 10 | # This library is distributed in the hope that it will be useful, | ||
611 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
612 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
613 | 13 | # Library General Public License for more details. | ||
614 | 14 | # | ||
615 | 15 | # You should have received a copy of the GNU Library General Public | ||
616 | 16 | # License along with this program. If not, see | ||
617 | 17 | # <http://www.gnu.org/licenses/>. | ||
618 | 18 | # | ||
619 | 19 | |||
620 | 20 | '''Common classes and functions for package tests.''' | ||
621 | 21 | |||
622 | 22 | import string, random, crypt, subprocess, pwd, grp, signal, time, unittest, tempfile, shutil, os, os.path, re, glob | ||
623 | 23 | import sys, socket, gzip | ||
624 | 24 | from stat import * | ||
625 | 25 | from encodings import string_escape | ||
626 | 26 | |||
627 | 27 | import warnings | ||
628 | 28 | warnings.filterwarnings('ignore', message=r'.*apt_pkg\.TagFile.*', category=DeprecationWarning) | ||
629 | 29 | try: | ||
630 | 30 | import apt_pkg | ||
631 | 31 | apt_pkg.InitSystem(); | ||
632 | 32 | except: | ||
633 | 33 | # On non-Debian system, fall back to simple comparison without debianisms | ||
634 | 34 | class apt_pkg(object): | ||
635 | 35 | def VersionCompare(one, two): | ||
636 | 36 | list_one = one.split('.') | ||
637 | 37 | list_two = two.split('.') | ||
638 | 38 | while len(list_one)>0 and len(list_two)>0: | ||
639 | 39 | if list_one[0] > list_two[0]: | ||
640 | 40 | return 1 | ||
641 | 41 | if list_one[0] < list_two[0]: | ||
642 | 42 | return -1 | ||
643 | 43 | list_one.pop(0) | ||
644 | 44 | list_two.pop(0) | ||
645 | 45 | return 0 | ||
646 | 46 | |||
647 | 47 | bogus_nxdomain = "208.69.32.132" | ||
648 | 48 | |||
649 | 49 | # http://www.chiark.greenend.org.uk/ucgi/~cjwatson/blosxom/2009-07-02-python-sigpipe.html | ||
650 | 50 | # This is needed so that the subprocesses that produce endless output | ||
651 | 51 | # actually quit when the reader goes away. | ||
652 | 52 | import signal | ||
653 | 53 | def subprocess_setup(): | ||
654 | 54 | # Python installs a SIGPIPE handler by default. This is usually not what | ||
655 | 55 | # non-Python subprocesses expect. | ||
656 | 56 | signal.signal(signal.SIGPIPE, signal.SIG_DFL) | ||
657 | 57 | |||
658 | 58 | class TimedOutException(Exception): | ||
659 | 59 | def __init__(self, value = "Timed Out"): | ||
660 | 60 | self.value = value | ||
661 | 61 | def __str__(self): | ||
662 | 62 | return repr(self.value) | ||
663 | 63 | |||
664 | 64 | def _restore_backup(path): | ||
665 | 65 | pathbackup = path + '.autotest' | ||
666 | 66 | if os.path.exists(pathbackup): | ||
667 | 67 | shutil.move(pathbackup, path) | ||
668 | 68 | |||
669 | 69 | def _save_backup(path): | ||
670 | 70 | pathbackup = path + '.autotest' | ||
671 | 71 | if os.path.exists(path) and not os.path.exists(pathbackup): | ||
672 | 72 | shutil.copy2(path, pathbackup) | ||
673 | 73 | # copy2 does not copy ownership, so do it here. | ||
674 | 74 | # Reference: http://docs.python.org/library/shutil.html | ||
675 | 75 | a = os.stat(path) | ||
676 | 76 | os.chown(pathbackup, a[4], a[5]) | ||
677 | 77 | |||
678 | 78 | def config_copydir(path): | ||
679 | 79 | if os.path.exists(path) and not os.path.isdir(path): | ||
680 | 80 | raise OSError, "'%s' is not a directory" % (path) | ||
681 | 81 | _restore_backup(path) | ||
682 | 82 | |||
683 | 83 | pathbackup = path + '.autotest' | ||
684 | 84 | if os.path.exists(path): | ||
685 | 85 | shutil.copytree(path, pathbackup, symlinks=True) | ||
686 | 86 | |||
687 | 87 | def config_replace(path,contents,append=False): | ||
688 | 88 | '''Replace (or append) to a config file''' | ||
689 | 89 | _restore_backup(path) | ||
690 | 90 | if os.path.exists(path): | ||
691 | 91 | _save_backup(path) | ||
692 | 92 | if append: | ||
693 | 93 | contents = file(path).read() + contents | ||
694 | 94 | open(path, 'w').write(contents) | ||
695 | 95 | |||
696 | 96 | def config_comment(path, field): | ||
697 | 97 | _save_backup(path) | ||
698 | 98 | contents = "" | ||
699 | 99 | for line in file(path): | ||
700 | 100 | if re.search("^\s*%s\s*=" % (field), line): | ||
701 | 101 | line = "#" + line | ||
702 | 102 | contents += line | ||
703 | 103 | |||
704 | 104 | open(path+'.new', 'w').write(contents) | ||
705 | 105 | os.rename(path+'.new', path) | ||
706 | 106 | |||
707 | 107 | def config_set(path, field, value, spaces=True): | ||
708 | 108 | _save_backup(path) | ||
709 | 109 | contents = "" | ||
710 | 110 | if spaces==True: | ||
711 | 111 | setting = '%s = %s\n' % (field, value) | ||
712 | 112 | else: | ||
713 | 113 | setting = '%s=%s\n' % (field, value) | ||
714 | 114 | found = False | ||
715 | 115 | for line in file(path): | ||
716 | 116 | if re.search("^\s*%s\s*=" % (field), line): | ||
717 | 117 | found = True | ||
718 | 118 | line = setting | ||
719 | 119 | contents += line | ||
720 | 120 | if not found: | ||
721 | 121 | contents += setting | ||
722 | 122 | |||
723 | 123 | open(path+'.new', 'w').write(contents) | ||
724 | 124 | os.rename(path+'.new', path) | ||
725 | 125 | |||
726 | 126 | def config_patch(path, patch, depth=1): | ||
727 | 127 | '''Patch a config file''' | ||
728 | 128 | _restore_backup(path) | ||
729 | 129 | _save_backup(path) | ||
730 | 130 | |||
731 | 131 | handle, name = mkstemp_fill(patch) | ||
732 | 132 | rc = subprocess.call(['/usr/bin/patch', '-p%s' %(depth), path], stdin=handle, stdout=subprocess.PIPE) | ||
733 | 133 | os.unlink(name) | ||
734 | 134 | if rc != 0: | ||
735 | 135 | raise Exception("Patch failed") | ||
736 | 136 | |||
737 | 137 | def config_restore(path): | ||
738 | 138 | '''Rename a replaced config file back to its initial state''' | ||
739 | 139 | _restore_backup(path) | ||
740 | 140 | |||
741 | 141 | def timeout(secs, f, *args): | ||
742 | 142 | def handler(signum, frame): | ||
743 | 143 | raise TimedOutException() | ||
744 | 144 | |||
745 | 145 | old = signal.signal(signal.SIGALRM, handler) | ||
746 | 146 | result = None | ||
747 | 147 | signal.alarm(secs) | ||
748 | 148 | try: | ||
749 | 149 | result = f(*args) | ||
750 | 150 | finally: | ||
751 | 151 | signal.alarm(0) | ||
752 | 152 | signal.signal(signal.SIGALRM, old) | ||
753 | 153 | |||
754 | 154 | return result | ||
755 | 155 | |||
756 | 156 | def require_nonroot(): | ||
757 | 157 | if os.geteuid() == 0: | ||
758 | 158 | print >>sys.stderr, "This series of tests should be run as a regular user with sudo access, not as root." | ||
759 | 159 | sys.exit(1) | ||
760 | 160 | |||
761 | 161 | def require_root(): | ||
762 | 162 | if os.geteuid() != 0: | ||
763 | 163 | print >>sys.stderr, "This series of tests should be run with root privileges (e.g. via sudo)." | ||
764 | 164 | sys.exit(1) | ||
765 | 165 | |||
766 | 166 | def require_sudo(): | ||
767 | 167 | if os.geteuid() != 0 or os.environ.get('SUDO_USER', None) == None: | ||
768 | 168 | print >>sys.stderr, "This series of tests must be run under sudo." | ||
769 | 169 | sys.exit(1) | ||
770 | 170 | if os.environ['SUDO_USER'] == 'root': | ||
771 | 171 | print >>sys.stderr, 'Please run this test using sudo from a regular user. (You ran sudo from root.)' | ||
772 | 172 | sys.exit(1) | ||
773 | 173 | |||
774 | 174 | def random_string(length,lower=False): | ||
775 | 175 | '''Return a random string, consisting of ASCII letters, with given | ||
776 | 176 | length.''' | ||
777 | 177 | |||
778 | 178 | s = '' | ||
779 | 179 | selection = string.letters | ||
780 | 180 | if lower: | ||
781 | 181 | selection = string.lowercase | ||
782 | 182 | maxind = len(selection)-1 | ||
783 | 183 | for l in range(length): | ||
784 | 184 | s += selection[random.randint(0, maxind)] | ||
785 | 185 | return s | ||
786 | 186 | |||
787 | 187 | def mkstemp_fill(contents,suffix='',prefix='testlib-',dir=None): | ||
788 | 188 | '''As tempfile.mkstemp does, return a (file, name) pair, but with | ||
789 | 189 | prefilled contents.''' | ||
790 | 190 | |||
791 | 191 | handle, name = tempfile.mkstemp(suffix=suffix,prefix=prefix,dir=dir) | ||
792 | 192 | os.close(handle) | ||
793 | 193 | handle = file(name,"w+") | ||
794 | 194 | handle.write(contents) | ||
795 | 195 | handle.flush() | ||
796 | 196 | handle.seek(0) | ||
797 | 197 | |||
798 | 198 | return handle, name | ||
799 | 199 | |||
800 | 200 | def create_fill(path, contents, mode=0644): | ||
801 | 201 | '''Safely create a page''' | ||
802 | 202 | # make the temp file in the same dir as the destination file so we | ||
803 | 203 | # don't get invalid cross-device link errors when we rename | ||
804 | 204 | handle, name = mkstemp_fill(contents, dir=os.path.dirname(path)) | ||
805 | 205 | handle.close() | ||
806 | 206 | os.rename(name, path) | ||
807 | 207 | os.chmod(path, mode) | ||
808 | 208 | |||
809 | 209 | def login_exists(login): | ||
810 | 210 | '''Checks whether the given login exists on the system.''' | ||
811 | 211 | |||
812 | 212 | try: | ||
813 | 213 | pwd.getpwnam(login) | ||
814 | 214 | return True | ||
815 | 215 | except KeyError: | ||
816 | 216 | return False | ||
817 | 217 | |||
818 | 218 | def group_exists(group): | ||
819 | 219 | '''Checks whether the given login exists on the system.''' | ||
820 | 220 | |||
821 | 221 | try: | ||
822 | 222 | grp.getgrnam(group) | ||
823 | 223 | return True | ||
824 | 224 | except KeyError: | ||
825 | 225 | return False | ||
826 | 226 | |||
827 | 227 | def recursive_rm(dirPath, contents_only=False): | ||
828 | 228 | '''recursively remove directory''' | ||
829 | 229 | names = os.listdir(dirPath) | ||
830 | 230 | for name in names: | ||
831 | 231 | path = os.path.join(dirPath, name) | ||
832 | 232 | if os.path.islink(path) or not os.path.isdir(path): | ||
833 | 233 | os.unlink(path) | ||
834 | 234 | else: | ||
835 | 235 | recursive_rm(path) | ||
836 | 236 | if contents_only == False: | ||
837 | 237 | os.rmdir(dirPath) | ||
838 | 238 | |||
839 | 239 | def check_pidfile(exe, pidfile): | ||
840 | 240 | '''Checks if pid in pidfile is running''' | ||
841 | 241 | if not os.path.exists(pidfile): | ||
842 | 242 | return False | ||
843 | 243 | |||
844 | 244 | # get the pid | ||
845 | 245 | try: | ||
846 | 246 | fd = open(pidfile, 'r') | ||
847 | 247 | pid = fd.readline().rstrip('\n') | ||
848 | 248 | fd.close() | ||
849 | 249 | except: | ||
850 | 250 | return False | ||
851 | 251 | |||
852 | 252 | return check_pid(exe, pid) | ||
853 | 253 | |||
854 | 254 | def check_pid(exe, pid): | ||
855 | 255 | '''Checks if pid is running''' | ||
856 | 256 | cmdline = "/proc/%s/cmdline" % (str(pid)) | ||
857 | 257 | if not os.path.exists(cmdline): | ||
858 | 258 | return False | ||
859 | 259 | |||
860 | 260 | # get the command line | ||
861 | 261 | try: | ||
862 | 262 | fd = open(cmdline, 'r') | ||
863 | 263 | tmp = fd.readline().split('\0') | ||
864 | 264 | fd.close() | ||
865 | 265 | except: | ||
866 | 266 | return False | ||
867 | 267 | |||
868 | 268 | # this allows us to match absolute paths or just the executable name | ||
869 | 269 | if re.match('^' + exe + '$', tmp[0]) or \ | ||
870 | 270 | re.match('.*/' + exe + '$', tmp[0]) or \ | ||
871 | 271 | re.match('^' + exe + ': ', tmp[0]) or \ | ||
872 | 272 | re.match('^\(' + exe + '\)', tmp[0]): | ||
873 | 273 | return True | ||
874 | 274 | |||
875 | 275 | return False | ||
876 | 276 | |||
877 | 277 | def check_port(port, proto, ver=4): | ||
878 | 278 | '''Check if something is listening on the specified port. | ||
879 | 279 | WARNING: for some reason this does not work with a bind mounted /proc | ||
880 | 280 | ''' | ||
881 | 281 | assert (port >= 1) | ||
882 | 282 | assert (port <= 65535) | ||
883 | 283 | assert (proto.lower() == "tcp" or proto.lower() == "udp") | ||
884 | 284 | assert (ver == 4 or ver == 6) | ||
885 | 285 | |||
886 | 286 | fn = "/proc/net/%s" % (proto) | ||
887 | 287 | if ver == 6: | ||
888 | 288 | fn += str(ver) | ||
889 | 289 | |||
890 | 290 | rc, report = cmd(['cat', fn]) | ||
891 | 291 | assert (rc == 0) | ||
892 | 292 | |||
893 | 293 | hport = "%0.4x" % port | ||
894 | 294 | |||
895 | 295 | if re.search(': [0-9a-f]{8}:%s [0-9a-f]' % str(hport).lower(), report.lower()): | ||
896 | 296 | return True | ||
897 | 297 | return False | ||
898 | 298 | |||
899 | 299 | def get_arch(): | ||
900 | 300 | '''Get the current architecture''' | ||
901 | 301 | rc, report = cmd(['uname', '-m']) | ||
902 | 302 | assert (rc == 0) | ||
903 | 303 | return report.strip() | ||
904 | 304 | |||
905 | 305 | def get_memory(): | ||
906 | 306 | '''Gets total ram and swap''' | ||
907 | 307 | meminfo = "/proc/meminfo" | ||
908 | 308 | memtotal = 0 | ||
909 | 309 | swaptotal = 0 | ||
910 | 310 | if not os.path.exists(meminfo): | ||
911 | 311 | return (False, False) | ||
912 | 312 | |||
913 | 313 | try: | ||
914 | 314 | fd = open(meminfo, 'r') | ||
915 | 315 | for line in fd.readlines(): | ||
916 | 316 | splitline = line.split() | ||
917 | 317 | if splitline[0] == 'MemTotal:': | ||
918 | 318 | memtotal = int(splitline[1]) | ||
919 | 319 | elif splitline[0] == 'SwapTotal:': | ||
920 | 320 | swaptotal = int(splitline[1]) | ||
921 | 321 | fd.close() | ||
922 | 322 | except: | ||
923 | 323 | return (False, False) | ||
924 | 324 | |||
925 | 325 | return (memtotal,swaptotal) | ||
926 | 326 | |||
927 | 327 | def is_running_in_vm(): | ||
928 | 328 | '''Check if running under a VM''' | ||
929 | 329 | # add other virtualization environments here | ||
930 | 330 | for search in ['QEMU Virtual CPU']: | ||
931 | 331 | rc, report = cmd_pipe(['dmesg'], ['grep', search]) | ||
932 | 332 | if rc == 0: | ||
933 | 333 | return True | ||
934 | 334 | return False | ||
935 | 335 | |||
936 | 336 | def ubuntu_release(): | ||
937 | 337 | '''Get the Ubuntu release''' | ||
938 | 338 | f = "/etc/lsb-release" | ||
939 | 339 | try: | ||
940 | 340 | size = os.stat(f)[ST_SIZE] | ||
941 | 341 | except: | ||
942 | 342 | return "UNKNOWN" | ||
943 | 343 | |||
944 | 344 | if size > 1024*1024: | ||
945 | 345 | raise IOError, 'Could not open "%s" (too big)' % f | ||
946 | 346 | |||
947 | 347 | try: | ||
948 | 348 | fh = open("/etc/lsb-release", 'r') | ||
949 | 349 | except: | ||
950 | 350 | raise | ||
951 | 351 | |||
952 | 352 | lines = fh.readlines() | ||
953 | 353 | fh.close() | ||
954 | 354 | |||
955 | 355 | pat = re.compile(r'DISTRIB_CODENAME') | ||
956 | 356 | for line in lines: | ||
957 | 357 | if pat.search(line): | ||
958 | 358 | return line.split('=')[1].rstrip('\n').rstrip('\r') | ||
959 | 359 | |||
960 | 360 | return "UNKNOWN" | ||
961 | 361 | |||
962 | 362 | def cmd(command, input = None, stderr = subprocess.STDOUT, stdout = subprocess.PIPE, stdin = None, timeout = None): | ||
963 | 363 | '''Try to execute given command (array) and return its stdout, or return | ||
964 | 364 | a textual error if it failed.''' | ||
965 | 365 | |||
966 | 366 | try: | ||
967 | 367 | sp = subprocess.Popen(command, stdin=stdin, stdout=stdout, stderr=stderr, close_fds=True, preexec_fn=subprocess_setup) | ||
968 | 368 | except OSError, e: | ||
969 | 369 | return [127, str(e)] | ||
970 | 370 | |||
971 | 371 | out, outerr = sp.communicate(input) | ||
972 | 372 | # Handle redirection of stdout | ||
973 | 373 | if out == None: | ||
974 | 374 | out = '' | ||
975 | 375 | # Handle redirection of stderr | ||
976 | 376 | if outerr == None: | ||
977 | 377 | outerr = '' | ||
978 | 378 | return [sp.returncode,out+outerr] | ||
979 | 379 | |||
980 | 380 | def cmd_pipe(command1, command2, input = None, stderr = subprocess.STDOUT, stdin = None): | ||
981 | 381 | '''Try to pipe command1 into command2.''' | ||
982 | 382 | try: | ||
983 | 383 | sp1 = subprocess.Popen(command1, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr, close_fds=True) | ||
984 | 384 | sp2 = subprocess.Popen(command2, stdin=sp1.stdout, stdout=subprocess.PIPE, stderr=stderr, close_fds=True) | ||
985 | 385 | except OSError, e: | ||
986 | 386 | return [127, str(e)] | ||
987 | 387 | |||
988 | 388 | out = sp2.communicate(input)[0] | ||
989 | 389 | return [sp2.returncode,out] | ||
990 | 390 | |||
991 | 391 | def cwd_has_enough_space(cdir, total_bytes): | ||
992 | 392 | '''Determine if the partition of the current working directory has 'bytes' | ||
993 | 393 | free.''' | ||
994 | 394 | rc, df_output = cmd(['df']) | ||
995 | 395 | result = 'Got exit code %d, expected %d\n' % (rc, 0) | ||
996 | 396 | if rc != 0: | ||
997 | 397 | return False | ||
998 | 398 | |||
999 | 399 | kb = total_bytes / 1024 | ||
1000 | 400 | |||
1001 | 401 | mounts = dict() | ||
1002 | 402 | for line in df_output.splitlines(): | ||
1003 | 403 | if '/' not in line: | ||
1004 | 404 | continue | ||
1005 | 405 | tmp = line.split() | ||
1006 | 406 | mounts[tmp[5]] = int(tmp[3]) | ||
1007 | 407 | |||
1008 | 408 | cdir = os.getcwd() | ||
1009 | 409 | while cdir != '/': | ||
1010 | 410 | if not mounts.has_key(cdir): | ||
1011 | 411 | cdir = os.path.dirname(cdir) | ||
1012 | 412 | continue | ||
1013 | 413 | if kb < mounts[cdir]: | ||
1014 | 414 | return True | ||
1015 | 415 | else: | ||
1016 | 416 | return False | ||
1017 | 417 | |||
1018 | 418 | if kb < mounts['/']: | ||
1019 | 419 | return True | ||
1020 | 420 | |||
1021 | 421 | return False | ||
1022 | 422 | |||
1023 | 423 | def get_md5(filename): | ||
1024 | 424 | '''Gets the md5sum of the file specified''' | ||
1025 | 425 | |||
1026 | 426 | (rc, report) = cmd(["/usr/bin/md5sum", "-b", filename]) | ||
1027 | 427 | expected = 0 | ||
1028 | 428 | assert (expected == rc) | ||
1029 | 429 | |||
1030 | 430 | return report.split(' ')[0] | ||
1031 | 431 | |||
1032 | 432 | def dpkg_compare_installed_version(pkg, check, version): | ||
1033 | 433 | '''Gets the version for the installed package, and compares it to the | ||
1034 | 434 | specified version. | ||
1035 | 435 | ''' | ||
1036 | 436 | (rc, report) = cmd(["/usr/bin/dpkg", "-s", pkg]) | ||
1037 | 437 | assert (rc == 0) | ||
1038 | 438 | assert ("Status: install ok installed" in report) | ||
1039 | 439 | installed_version = "" | ||
1040 | 440 | for line in report.splitlines(): | ||
1041 | 441 | if line.startswith("Version: "): | ||
1042 | 442 | installed_version = line.split()[1] | ||
1043 | 443 | |||
1044 | 444 | assert (installed_version != "") | ||
1045 | 445 | |||
1046 | 446 | (rc, report) = cmd(["/usr/bin/dpkg", "--compare-versions", installed_version, check, version]) | ||
1047 | 447 | assert (rc == 0 or rc == 1) | ||
1048 | 448 | if rc == 0: | ||
1049 | 449 | return True | ||
1050 | 450 | return False | ||
1051 | 451 | |||
1052 | 452 | def prepare_source(source, builder, cached_src, build_src, patch_system): | ||
1053 | 453 | '''Download and unpack source package, installing necessary build depends, | ||
1054 | 454 | adjusting the permissions for the 'builder' user, and returning the | ||
1055 | 455 | directory of the unpacked source. Patch system can be one of: | ||
1056 | 456 | - cdbs | ||
1057 | 457 | - dpatch | ||
1058 | 458 | - quilt | ||
1059 | 459 | - quiltv3 | ||
1060 | 460 | - None (not the string) | ||
1061 | 461 | |||
1062 | 462 | This is normally used like this: | ||
1063 | 463 | |||
1064 | 464 | def setUp(self): | ||
1065 | 465 | ... | ||
1066 | 466 | self.topdir = os.getcwd() | ||
1067 | 467 | self.cached_src = os.path.join(os.getcwd(), "source") | ||
1068 | 468 | self.tmpdir = tempfile.mkdtemp(prefix='testlib', dir='/tmp') | ||
1069 | 469 | self.builder = testlib.TestUser() | ||
1070 | 470 | testlib.cmd(['chgrp', self.builder.login, self.tmpdir]) | ||
1071 | 471 | os.chmod(self.tmpdir, 0775) | ||
1072 | 472 | |||
1073 | 473 | def tearDown(self): | ||
1074 | 474 | ... | ||
1075 | 475 | self.builder = None | ||
1076 | 476 | self.topdir = os.getcwd() | ||
1077 | 477 | if os.path.exists(self.tmpdir): | ||
1078 | 478 | testlib.recursive_rm(self.tmpdir) | ||
1079 | 479 | |||
1080 | 480 | def test_suite_build(self): | ||
1081 | 481 | ... | ||
1082 | 482 | build_dir = testlib.prepare_source('foo', \ | ||
1083 | 483 | self.builder, \ | ||
1084 | 484 | self.cached_src, \ | ||
1085 | 485 | os.path.join(self.tmpdir, \ | ||
1086 | 486 | os.path.basename(self.cached_src)), | ||
1087 | 487 | "quilt") | ||
1088 | 488 | os.chdir(build_dir) | ||
1089 | 489 | |||
1090 | 490 | # Example for typical build, adjust as necessary | ||
1091 | 491 | print "" | ||
1092 | 492 | print " make clean" | ||
1093 | 493 | rc, report = testlib.cmd(['sudo', '-u', self.builder.login, 'make', 'clean']) | ||
1094 | 494 | |||
1095 | 495 | print " configure" | ||
1096 | 496 | rc, report = testlib.cmd(['sudo', '-u', self.builder.login, './configure', '--prefix=%s' % self.tmpdir, '--enable-debug']) | ||
1097 | 497 | |||
1098 | 498 | print " make (will take a while)" | ||
1099 | 499 | rc, report = testlib.cmd(['sudo', '-u', self.builder.login, 'make']) | ||
1100 | 500 | |||
1101 | 501 | print " make check (will take a while)", | ||
1102 | 502 | rc, report = testlib.cmd(['sudo', '-u', self.builder.login, 'make', 'check']) | ||
1103 | 503 | expected = 0 | ||
1104 | 504 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
1105 | 505 | self.assertEquals(expected, rc, result + report) | ||
1106 | 506 | |||
1107 | 507 | def test_suite_cleanup(self): | ||
1108 | 508 | ... | ||
1109 | 509 | if os.path.exists(self.cached_src): | ||
1110 | 510 | testlib.recursive_rm(self.cached_src) | ||
1111 | 511 | |||
1112 | 512 | It is up to the caller to clean up cached_src and build_src (as in the | ||
1113 | 513 | above example, often the build_src is in a tmpdir that is cleaned in | ||
1114 | 514 | tearDown() and the cached_src is cleaned in a one time clean-up | ||
1115 | 515 | operation (eg 'test_suite_cleanup()) which must be run after the build | ||
1116 | 516 | suite test (obviously). | ||
1117 | 517 | ''' | ||
1118 | 518 | |||
1119 | 519 | # Make sure we have a clean slate | ||
1120 | 520 | assert (os.path.exists(os.path.dirname(build_src))) | ||
1121 | 521 | assert (not os.path.exists(build_src)) | ||
1122 | 522 | |||
1123 | 523 | cdir = os.getcwd() | ||
1124 | 524 | if os.path.exists(cached_src): | ||
1125 | 525 | shutil.copytree(cached_src, build_src) | ||
1126 | 526 | os.chdir(build_src) | ||
1127 | 527 | else: | ||
1128 | 528 | # Only install the build dependencies on the initial setup | ||
1129 | 529 | rc, report = cmd(['apt-get','-y','--force-yes','build-dep',source]) | ||
1130 | 530 | assert (rc == 0) | ||
1131 | 531 | |||
1132 | 532 | os.makedirs(build_src) | ||
1133 | 533 | os.chdir(build_src) | ||
1134 | 534 | |||
1135 | 535 | # These are always needed | ||
1136 | 536 | pkgs = ['build-essential', 'dpkg-dev', 'fakeroot'] | ||
1137 | 537 | rc, report = cmd(['apt-get','-y','--force-yes','install'] + pkgs) | ||
1138 | 538 | assert (rc == 0) | ||
1139 | 539 | |||
1140 | 540 | rc, report = cmd(['apt-get','source',source]) | ||
1141 | 541 | assert (rc == 0) | ||
1142 | 542 | shutil.copytree(build_src, cached_src) | ||
1143 | 543 | |||
1144 | 544 | unpacked_dir = os.path.join(build_src, glob.glob('%s-*' % source)[0]) | ||
1145 | 545 | |||
1146 | 546 | # Now apply the patches. Do it here so that we don't mess up our cached | ||
1147 | 547 | # sources. | ||
1148 | 548 | os.chdir(unpacked_dir) | ||
1149 | 549 | assert (patch_system in ['cdbs', 'dpatch', 'quilt', 'quiltv3', None]) | ||
1150 | 550 | if patch_system != None and patch_system != "quiltv3": | ||
1151 | 551 | if patch_system == "quilt": | ||
1152 | 552 | os.environ.setdefault('QUILT_PATCHES','debian/patches') | ||
1153 | 553 | rc, report = cmd(['quilt', 'push', '-a']) | ||
1154 | 554 | assert (rc == 0) | ||
1155 | 555 | elif patch_system == "cdbs": | ||
1156 | 556 | rc, report = cmd(['./debian/rules', 'apply-patches']) | ||
1157 | 557 | assert (rc == 0) | ||
1158 | 558 | elif patch_system == "dpatch": | ||
1159 | 559 | rc, report = cmd(['dpatch', 'apply-all']) | ||
1160 | 560 | assert (rc == 0) | ||
1161 | 561 | |||
1162 | 562 | cmd(['chown', '-R', '%s:%s' % (builder.uid, builder.gid), build_src]) | ||
1163 | 563 | os.chdir(cdir) | ||
1164 | 564 | |||
1165 | 565 | return unpacked_dir | ||
1166 | 566 | |||
1167 | 567 | def _aa_status(): | ||
1168 | 568 | '''Get aa-status output''' | ||
1169 | 569 | exe = "/usr/sbin/aa-status" | ||
1170 | 570 | assert (os.path.exists(exe)) | ||
1171 | 571 | if os.geteuid() == 0: | ||
1172 | 572 | return cmd([exe]) | ||
1173 | 573 | return cmd(['sudo', exe]) | ||
1174 | 574 | |||
1175 | 575 | def is_apparmor_loaded(path): | ||
1176 | 576 | '''Check if profile is loaded''' | ||
1177 | 577 | rc, report = _aa_status() | ||
1178 | 578 | if rc != 0: | ||
1179 | 579 | return False | ||
1180 | 580 | |||
1181 | 581 | for line in report.splitlines(): | ||
1182 | 582 | if line.endswith(path): | ||
1183 | 583 | return True | ||
1184 | 584 | return False | ||
1185 | 585 | |||
1186 | 586 | def is_apparmor_confined(path): | ||
1187 | 587 | '''Check if application is confined''' | ||
1188 | 588 | rc, report = _aa_status() | ||
1189 | 589 | if rc != 0: | ||
1190 | 590 | return False | ||
1191 | 591 | |||
1192 | 592 | for line in report.splitlines(): | ||
1193 | 593 | if re.search('%s \(' % path, line): | ||
1194 | 594 | return True | ||
1195 | 595 | return False | ||
1196 | 596 | |||
1197 | 597 | def check_apparmor(path, first_ubuntu_release, is_running=True): | ||
1198 | 598 | '''Check if path is loaded and confined for everything higher than the | ||
1199 | 599 | first Ubuntu release specified. | ||
1200 | 600 | |||
1201 | 601 | Usage: | ||
1202 | 602 | rc, report = testlib.check_apparmor('/usr/sbin/foo', 8.04, is_running=True) | ||
1203 | 603 | if rc < 0: | ||
1204 | 604 | return self._skipped(report) | ||
1205 | 605 | |||
1206 | 606 | expected = 0 | ||
1207 | 607 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
1208 | 608 | self.assertEquals(expected, rc, result + report) | ||
1209 | 609 | ''' | ||
1210 | 610 | global manager | ||
1211 | 611 | rc = -1 | ||
1212 | 612 | |||
1213 | 613 | if manager.lsb_release["Release"] < first_ubuntu_release: | ||
1214 | 614 | return (rc, "Skipped apparmor check") | ||
1215 | 615 | |||
1216 | 616 | if not os.path.exists('/sbin/apparmor_parser'): | ||
1217 | 617 | return (rc, "Skipped (couldn't find apparmor_parser)") | ||
1218 | 618 | |||
1219 | 619 | rc = 0 | ||
1220 | 620 | msg = "" | ||
1221 | 621 | if not is_apparmor_loaded(path): | ||
1222 | 622 | rc = 1 | ||
1223 | 623 | msg = "Profile not loaded for '%s'" % path | ||
1224 | 624 | |||
1225 | 625 | # this check only makes sense it the 'path' is currently executing | ||
1226 | 626 | if is_running and rc == 0 and not is_apparmor_confined(path): | ||
1227 | 627 | rc = 1 | ||
1228 | 628 | msg = "'%s' is not running in enforce mode" % path | ||
1229 | 629 | |||
1230 | 630 | return (rc, msg) | ||
1231 | 631 | |||
1232 | 632 | def get_gcc_version(gcc, full=True): | ||
1233 | 633 | gcc_version = 'none' | ||
1234 | 634 | if not gcc.startswith('/'): | ||
1235 | 635 | gcc = '/usr/bin/%s' % (gcc) | ||
1236 | 636 | if os.path.exists(gcc): | ||
1237 | 637 | gcc_version = 'unknown' | ||
1238 | 638 | lines = cmd([gcc,'-v'])[1].strip().splitlines() | ||
1239 | 639 | version_lines = [x for x in lines if x.startswith('gcc version')] | ||
1240 | 640 | if len(version_lines) == 1: | ||
1241 | 641 | gcc_version = " ".join(version_lines[0].split()[2:]) | ||
1242 | 642 | if not full: | ||
1243 | 643 | return gcc_version.split()[0] | ||
1244 | 644 | return gcc_version | ||
1245 | 645 | |||
1246 | 646 | def is_kdeinit_running(): | ||
1247 | 647 | '''Test if kdeinit is running''' | ||
1248 | 648 | # applications that use kdeinit will spawn it if it isn't running in the | ||
1249 | 649 | # test. This is a problem because it does not exit. This is a helper to | ||
1250 | 650 | # check for it. | ||
1251 | 651 | rc, report = cmd(['ps', 'x']) | ||
1252 | 652 | if 'kdeinit4 Running' not in report: | ||
1253 | 653 | print >>sys.stderr, ("kdeinit not running (you may start/stop any KDE application then run this script again)") | ||
1254 | 654 | return False | ||
1255 | 655 | return True | ||
1256 | 656 | |||
1257 | 657 | def get_pkgconfig_flags(libs=[]): | ||
1258 | 658 | '''Find pkg-config flags for libraries''' | ||
1259 | 659 | assert (len(libs) > 0) | ||
1260 | 660 | rc, pkg_config = cmd(['pkg-config', '--cflags', '--libs'] + libs) | ||
1261 | 661 | expected = 0 | ||
1262 | 662 | if rc != expected: | ||
1263 | 663 | print >>sys.stderr, 'Got exit code %d, expected %d\n' % (rc, expected) | ||
1264 | 664 | assert(rc == expected) | ||
1265 | 665 | return pkg_config.split() | ||
1266 | 666 | |||
1267 | 667 | class TestDaemon: | ||
1268 | 668 | '''Helper class to manage daemons consistently''' | ||
1269 | 669 | def __init__(self, init): | ||
1270 | 670 | '''Setup daemon attributes''' | ||
1271 | 671 | self.initscript = init | ||
1272 | 672 | |||
1273 | 673 | def start(self): | ||
1274 | 674 | '''Start daemon''' | ||
1275 | 675 | rc, report = cmd([self.initscript, 'start']) | ||
1276 | 676 | expected = 0 | ||
1277 | 677 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
1278 | 678 | time.sleep(2) | ||
1279 | 679 | if expected != rc: | ||
1280 | 680 | return (False, result + report) | ||
1281 | 681 | |||
1282 | 682 | if "fail" in report: | ||
1283 | 683 | return (False, "Found 'fail' in report\n" + report) | ||
1284 | 684 | |||
1285 | 685 | return (True, "") | ||
1286 | 686 | |||
1287 | 687 | def stop(self): | ||
1288 | 688 | '''Stop daemon''' | ||
1289 | 689 | rc, report = cmd([self.initscript, 'stop']) | ||
1290 | 690 | expected = 0 | ||
1291 | 691 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
1292 | 692 | if expected != rc: | ||
1293 | 693 | return (False, result + report) | ||
1294 | 694 | |||
1295 | 695 | if "fail" in report: | ||
1296 | 696 | return (False, "Found 'fail' in report\n" + report) | ||
1297 | 697 | |||
1298 | 698 | return (True, "") | ||
1299 | 699 | |||
1300 | 700 | def reload(self): | ||
1301 | 701 | '''Reload daemon''' | ||
1302 | 702 | rc, report = cmd([self.initscript, 'force-reload']) | ||
1303 | 703 | expected = 0 | ||
1304 | 704 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
1305 | 705 | if expected != rc: | ||
1306 | 706 | return (False, result + report) | ||
1307 | 707 | |||
1308 | 708 | if "fail" in report: | ||
1309 | 709 | return (False, "Found 'fail' in report\n" + report) | ||
1310 | 710 | |||
1311 | 711 | return (True, "") | ||
1312 | 712 | |||
1313 | 713 | def restart(self): | ||
1314 | 714 | '''Restart daemon''' | ||
1315 | 715 | (res, str) = self.stop() | ||
1316 | 716 | if not res: | ||
1317 | 717 | return (res, str) | ||
1318 | 718 | |||
1319 | 719 | (res, str) = self.start() | ||
1320 | 720 | if not res: | ||
1321 | 721 | return (res, str) | ||
1322 | 722 | |||
1323 | 723 | return (True, "") | ||
1324 | 724 | |||
1325 | 725 | def status(self): | ||
1326 | 726 | '''Check daemon status''' | ||
1327 | 727 | rc, report = cmd([self.initscript, 'status']) | ||
1328 | 728 | expected = 0 | ||
1329 | 729 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
1330 | 730 | if expected != rc: | ||
1331 | 731 | return (False, result + report) | ||
1332 | 732 | |||
1333 | 733 | if "fail" in report: | ||
1334 | 734 | return (False, "Found 'fail' in report\n" + report) | ||
1335 | 735 | |||
1336 | 736 | return (True, "") | ||
1337 | 737 | |||
1338 | 738 | class TestlibManager(object): | ||
1339 | 739 | '''Singleton class used to set up per-test-run information''' | ||
1340 | 740 | def __init__(self): | ||
1341 | 741 | # Set glibc aborts to dump to stderr instead of the tty so test output | ||
1342 | 742 | # is more sane. | ||
1343 | 743 | os.environ.setdefault('LIBC_FATAL_STDERR_','1') | ||
1344 | 744 | |||
1345 | 745 | # check verbosity | ||
1346 | 746 | self.verbosity = False | ||
1347 | 747 | if (len(sys.argv) > 1 and '-v' in sys.argv[1:]): | ||
1348 | 748 | self.verbosity = True | ||
1349 | 749 | |||
1350 | 750 | # Load LSB release file | ||
1351 | 751 | self.lsb_release = dict() | ||
1352 | 752 | if not os.path.exists('/usr/bin/lsb_release') and not os.path.exists('/bin/lsb_release'): | ||
1353 | 753 | raise OSError, "Please install 'lsb-release'" | ||
1354 | 754 | for line in subprocess.Popen(['lsb_release','-a'],stdout=subprocess.PIPE,stderr=subprocess.PIPE).communicate()[0].splitlines(): | ||
1355 | 755 | field, value = line.split(':',1) | ||
1356 | 756 | value=value.strip() | ||
1357 | 757 | field=field.strip() | ||
1358 | 758 | # Convert numerics | ||
1359 | 759 | try: | ||
1360 | 760 | value = float(value) | ||
1361 | 761 | except: | ||
1362 | 762 | pass | ||
1363 | 763 | self.lsb_release.setdefault(field,value) | ||
1364 | 764 | |||
1365 | 765 | # FIXME: hack OEM releases into known-Ubuntu versions | ||
1366 | 766 | if self.lsb_release['Distributor ID'] == "HP MIE (Mobile Internet Experience)": | ||
1367 | 767 | if self.lsb_release['Release'] == 1.0: | ||
1368 | 768 | self.lsb_release['Distributor ID'] = "Ubuntu" | ||
1369 | 769 | self.lsb_release['Release'] = 8.04 | ||
1370 | 770 | else: | ||
1371 | 771 | raise OSError, "Unknown version of HP MIE" | ||
1372 | 772 | |||
1373 | 773 | # FIXME: hack to assume a most-recent release if we're not | ||
1374 | 774 | # running under Ubuntu. | ||
1375 | 775 | if self.lsb_release['Distributor ID'] not in ["Ubuntu","Linaro"]: | ||
1376 | 776 | self.lsb_release['Release'] = 10000 | ||
1377 | 777 | # Adjust Linaro release to pretend to be Ubuntu | ||
1378 | 778 | if self.lsb_release['Distributor ID'] in ["Linaro"]: | ||
1379 | 779 | self.lsb_release['Distributor ID'] = "Ubuntu" | ||
1380 | 780 | self.lsb_release['Release'] -= 0.01 | ||
1381 | 781 | |||
1382 | 782 | # Load arch | ||
1383 | 783 | if not os.path.exists('/usr/bin/dpkg'): | ||
1384 | 784 | machine = cmd(['uname','-m'])[1].strip() | ||
1385 | 785 | if machine.endswith('86'): | ||
1386 | 786 | self.dpkg_arch = 'i386' | ||
1387 | 787 | elif machine.endswith('_64'): | ||
1388 | 788 | self.dpkg_arch = 'amd64' | ||
1389 | 789 | elif machine.startswith('arm'): | ||
1390 | 790 | self.dpkg_arch = 'armel' | ||
1391 | 791 | else: | ||
1392 | 792 | raise ValueError, "Unknown machine type '%s'" % (machine) | ||
1393 | 793 | else: | ||
1394 | 794 | self.dpkg_arch = cmd(['dpkg','--print-architecture'])[1].strip() | ||
1395 | 795 | |||
1396 | 796 | # Find kernel version | ||
1397 | 797 | self.kernel_is_ubuntu = False | ||
1398 | 798 | self.kernel_version_signature = None | ||
1399 | 799 | self.kernel_version = cmd(["uname","-r"])[1].strip() | ||
1400 | 800 | versig = '/proc/version_signature' | ||
1401 | 801 | if os.path.exists(versig): | ||
1402 | 802 | self.kernel_is_ubuntu = True | ||
1403 | 803 | self.kernel_version_signature = file(versig).read().strip() | ||
1404 | 804 | self.kernel_version_ubuntu = self.kernel_version | ||
1405 | 805 | elif os.path.exists('/usr/bin/dpkg'): | ||
1406 | 806 | # this can easily be inaccurate but is only an issue for Dapper | ||
1407 | 807 | rc, out = cmd(['dpkg','-l','linux-image-%s' % (self.kernel_version)]) | ||
1408 | 808 | if rc == 0: | ||
1409 | 809 | self.kernel_version_signature = out.strip().split('\n').pop().split()[2] | ||
1410 | 810 | self.kernel_version_ubuntu = self.kernel_version_signature | ||
1411 | 811 | if self.kernel_version_signature == None: | ||
1412 | 812 | # Attempt to fall back to something for non-Debian-based | ||
1413 | 813 | self.kernel_version_signature = self.kernel_version | ||
1414 | 814 | self.kernel_version_ubuntu = self.kernel_version | ||
1415 | 815 | # Build ubuntu version without hardware suffix | ||
1416 | 816 | try: | ||
1417 | 817 | self.kernel_version_ubuntu = "-".join([x for x in self.kernel_version_signature.split(' ')[1].split('-') if re.search('^[0-9]', x)]) | ||
1418 | 818 | except: | ||
1419 | 819 | pass | ||
1420 | 820 | |||
1421 | 821 | # Find gcc version | ||
1422 | 822 | self.gcc_version = get_gcc_version('gcc') | ||
1423 | 823 | |||
1424 | 824 | # Find libc | ||
1425 | 825 | self.path_libc = [x.split()[2] for x in cmd(['ldd','/bin/ls'])[1].splitlines() if x.startswith('\tlibc.so.')][0] | ||
1426 | 826 | |||
1427 | 827 | # Report self | ||
1428 | 828 | if self.verbosity: | ||
1429 | 829 | kernel = self.kernel_version_ubuntu | ||
1430 | 830 | if kernel != self.kernel_version_signature: | ||
1431 | 831 | kernel += " (%s)" % (self.kernel_version_signature) | ||
1432 | 832 | print >>sys.stdout, "Running test: '%s' distro: '%s %.2f' kernel: '%s' arch: '%s' uid: %d/%d SUDO_USER: '%s')" % ( \ | ||
1433 | 833 | sys.argv[0], | ||
1434 | 834 | self.lsb_release['Distributor ID'], | ||
1435 | 835 | self.lsb_release['Release'], | ||
1436 | 836 | kernel, | ||
1437 | 837 | self.dpkg_arch, | ||
1438 | 838 | os.geteuid(), os.getuid(), | ||
1439 | 839 | os.environ.get('SUDO_USER', '')) | ||
1440 | 840 | sys.stdout.flush() | ||
1441 | 841 | |||
1442 | 842 | # Additional heuristics | ||
1443 | 843 | #if os.environ.get('SUDO_USER', os.environ.get('USER', '')) in ['mdeslaur']: | ||
1444 | 844 | # sys.stdout.write("Replying to Marc Deslauriers in http://launchpad.net/bugs/%d: " % random.randint(600000, 980000)) | ||
1445 | 845 | # sys.stdout.flush() | ||
1446 | 846 | # time.sleep(0.5) | ||
1447 | 847 | # sys.stdout.write("destroyed\n") | ||
1448 | 848 | # time.sleep(0.5) | ||
1449 | 849 | |||
1450 | 850 | def hello(self, msg): | ||
1451 | 851 | print >>sys.stderr, "Hello from %s" % (msg) | ||
1452 | 852 | # The central instance | ||
1453 | 853 | manager = TestlibManager() | ||
1454 | 854 | |||
1455 | 855 | class TestlibCase(unittest.TestCase): | ||
1456 | 856 | def __init__(self, *args): | ||
1457 | 857 | '''This is called for each TestCase test instance, which isn't much better | ||
1458 | 858 | than SetUp.''' | ||
1459 | 859 | |||
1460 | 860 | unittest.TestCase.__init__(self, *args) | ||
1461 | 861 | |||
1462 | 862 | # Attach to and duplicate dicts from manager singleton | ||
1463 | 863 | self.manager = manager | ||
1464 | 864 | #self.manager.hello(repr(self) + repr(*args)) | ||
1465 | 865 | self.my_verbosity = self.manager.verbosity | ||
1466 | 866 | self.lsb_release = self.manager.lsb_release | ||
1467 | 867 | self.dpkg_arch = self.manager.dpkg_arch | ||
1468 | 868 | self.kernel_version = self.manager.kernel_version | ||
1469 | 869 | self.kernel_version_signature = self.manager.kernel_version_signature | ||
1470 | 870 | self.kernel_version_ubuntu = self.manager.kernel_version_ubuntu | ||
1471 | 871 | self.kernel_is_ubuntu = self.manager.kernel_is_ubuntu | ||
1472 | 872 | self.gcc_version = self.manager.gcc_version | ||
1473 | 873 | self.path_libc = self.manager.path_libc | ||
1474 | 874 | |||
1475 | 875 | def version_compare(self, one, two): | ||
1476 | 876 | return apt_pkg.VersionCompare(one,two) | ||
1477 | 877 | |||
1478 | 878 | def assertFileType(self, filename, filetype): | ||
1479 | 879 | '''Checks the file type of the file specified''' | ||
1480 | 880 | |||
1481 | 881 | (rc, report, out) = self._testlib_shell_cmd(["/usr/bin/file", "-b", filename]) | ||
1482 | 882 | out = out.strip() | ||
1483 | 883 | expected = 0 | ||
1484 | 884 | # Absolutely no idea why this happens on Hardy | ||
1485 | 885 | if self.lsb_release['Release'] == 8.04 and rc == 255 and len(out) > 0: | ||
1486 | 886 | rc = 0 | ||
1487 | 887 | result = 'Got exit code %d, expected %d:\n%s\n' % (rc, expected, report) | ||
1488 | 888 | self.assertEquals(expected, rc, result) | ||
1489 | 889 | |||
1490 | 890 | filetype = '^%s$' % (filetype) | ||
1491 | 891 | result = 'File type reported by file: [%s], expected regex: [%s]\n' % (out, filetype) | ||
1492 | 892 | self.assertNotEquals(None, re.search(filetype, out), result) | ||
1493 | 893 | |||
1494 | 894 | def yank_commonname_from_cert(self, certfile): | ||
1495 | 895 | '''Extract the commonName from a given PEM''' | ||
1496 | 896 | rc, out = cmd(['openssl','asn1parse','-in',certfile]) | ||
1497 | 897 | if rc == 0: | ||
1498 | 898 | ready = False | ||
1499 | 899 | for line in out.splitlines(): | ||
1500 | 900 | if ready: | ||
1501 | 901 | return line.split(':')[-1] | ||
1502 | 902 | if ':commonName' in line: | ||
1503 | 903 | ready = True | ||
1504 | 904 | return socket.getfqdn() | ||
1505 | 905 | |||
1506 | 906 | def announce(self, text): | ||
1507 | 907 | if self.my_verbosity: | ||
1508 | 908 | print >>sys.stdout, "(%s) " % (text), | ||
1509 | 909 | sys.stdout.flush() | ||
1510 | 910 | |||
1511 | 911 | def make_clean(self): | ||
1512 | 912 | rc, output = self.shell_cmd(['make','clean']) | ||
1513 | 913 | self.assertEquals(rc, 0, output) | ||
1514 | 914 | |||
1515 | 915 | def get_makefile_compiler(self): | ||
1516 | 916 | # Find potential compiler name | ||
1517 | 917 | compiler = 'gcc' | ||
1518 | 918 | if os.path.exists('Makefile'): | ||
1519 | 919 | for line in open('Makefile'): | ||
1520 | 920 | if line.startswith('CC') and '=' in line: | ||
1521 | 921 | items = [x.strip() for x in line.split('=')] | ||
1522 | 922 | if items[0] == 'CC': | ||
1523 | 923 | compiler = items[1] | ||
1524 | 924 | break | ||
1525 | 925 | return compiler | ||
1526 | 926 | |||
1527 | 927 | def make_target(self, target, expected=0): | ||
1528 | 928 | '''Compile a target and report output''' | ||
1529 | 929 | |||
1530 | 930 | compiler = self.get_makefile_compiler() | ||
1531 | 931 | rc, output = self.shell_cmd(['make',target]) | ||
1532 | 932 | self.assertEquals(rc, expected, 'rc(%d)!=%d:\n' % (rc, expected) + output) | ||
1533 | 933 | self.assertTrue('%s ' % (compiler) in output, 'Expected "%s":' % (compiler) + output) | ||
1534 | 934 | return output | ||
1535 | 935 | |||
1536 | 936 | # call as return testlib.skipped() | ||
1537 | 937 | def _skipped(self, reason=""): | ||
1538 | 938 | '''Provide a visible way to indicate that a test was skipped''' | ||
1539 | 939 | if reason != "": | ||
1540 | 940 | reason = ': %s' % (reason) | ||
1541 | 941 | self.announce("skipped%s" % (reason)) | ||
1542 | 942 | return False | ||
1543 | 943 | |||
1544 | 944 | def _testlib_shell_cmd(self,args,stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT): | ||
1545 | 945 | argstr = "'" + "', '".join(args).strip() + "'" | ||
1546 | 946 | rc, out = cmd(args,stdin=stdin,stdout=stdout,stderr=stderr) | ||
1547 | 947 | report = 'Command: ' + argstr + '\nOutput:\n' + out | ||
1548 | 948 | return rc, report, out | ||
1549 | 949 | |||
1550 | 950 | def shell_cmd(self, args, stdin=None): | ||
1551 | 951 | return cmd(args,stdin=stdin) | ||
1552 | 952 | |||
1553 | 953 | def assertShellExitEquals(self, expected, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg=""): | ||
1554 | 954 | '''Test a shell command matches a specific exit code''' | ||
1555 | 955 | rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr) | ||
1556 | 956 | result = 'Got exit code %d, expected %d\n' % (rc, expected) | ||
1557 | 957 | self.assertEquals(expected, rc, msg + result + report) | ||
1558 | 958 | |||
1559 | 959 | def assertShellExitNotEquals(self, unwanted, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg=""): | ||
1560 | 960 | '''Test a shell command doesn't match a specific exit code''' | ||
1561 | 961 | rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr) | ||
1562 | 962 | result = 'Got (unwanted) exit code %d\n' % rc | ||
1563 | 963 | self.assertNotEquals(unwanted, rc, msg + result + report) | ||
1564 | 964 | |||
1565 | 965 | def assertShellOutputContains(self, text, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg="", invert=False): | ||
1566 | 966 | '''Test a shell command contains a specific output''' | ||
1567 | 967 | rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr) | ||
1568 | 968 | result = 'Got exit code %d. Looking for text "%s"\n' % (rc, text) | ||
1569 | 969 | if not invert: | ||
1570 | 970 | self.assertTrue(text in out, msg + result + report) | ||
1571 | 971 | else: | ||
1572 | 972 | self.assertFalse(text in out, msg + result + report) | ||
1573 | 973 | |||
1574 | 974 | def assertShellOutputEquals(self, text, args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, msg="", invert=False, expected=None): | ||
1575 | 975 | '''Test a shell command matches a specific output''' | ||
1576 | 976 | rc, report, out = self._testlib_shell_cmd(args, stdin=stdin, stdout=stdout, stderr=stderr) | ||
1577 | 977 | result = 'Got exit code %d. Looking for exact text "%s" (%s)\n' % (rc, text, " ".join(args)) | ||
1578 | 978 | if not invert: | ||
1579 | 979 | self.assertEquals(text, out, msg + result + report) | ||
1580 | 980 | else: | ||
1581 | 981 | self.assertNotEquals(text, out, msg + result + report) | ||
1582 | 982 | if expected != None: | ||
1583 | 983 | result = 'Got exit code %d. Expected %d (%s)\n' % (rc, expected, " ".join(args)) | ||
1584 | 984 | self.assertEquals(rc, expected, msg + result + report) | ||
1585 | 985 | |||
1586 | 986 | def _word_find(self, report, content, invert=False): | ||
1587 | 987 | '''Check for a specific string''' | ||
1588 | 988 | if invert: | ||
1589 | 989 | warning = 'Found "%s"\n' % content | ||
1590 | 990 | self.assertTrue(content not in report, warning + report) | ||
1591 | 991 | else: | ||
1592 | 992 | warning = 'Could not find "%s"\n' % content | ||
1593 | 993 | self.assertTrue(content in report, warning + report) | ||
1594 | 994 | |||
1595 | 995 | def _test_sysctl_value(self, path, expected, msg=None, exists=True): | ||
1596 | 996 | sysctl = '/proc/sys/%s' % (path) | ||
1597 | 997 | self.assertEquals(exists, os.path.exists(sysctl), sysctl) | ||
1598 | 998 | value = None | ||
1599 | 999 | if exists: | ||
1600 | 1000 | value = int(file(sysctl).read()) | ||
1601 | 1001 | report = "%s is not %d: %d" % (sysctl, expected, value) | ||
1602 | 1002 | if msg: | ||
1603 | 1003 | report += " (%s)" % (msg) | ||
1604 | 1004 | self.assertEquals(value, expected, report) | ||
1605 | 1005 | return value | ||
1606 | 1006 | |||
1607 | 1007 | def set_sysctl_value(self, path, desired): | ||
1608 | 1008 | sysctl = '/proc/sys/%s' % (path) | ||
1609 | 1009 | self.assertTrue(os.path.exists(sysctl),"%s does not exist" % (sysctl)) | ||
1610 | 1010 | file(sysctl,'w').write(str(desired)) | ||
1611 | 1011 | self._test_sysctl_value(path, desired) | ||
1612 | 1012 | |||
1613 | 1013 | def kernel_at_least(self, introduced): | ||
1614 | 1014 | return self.version_compare(self.kernel_version_ubuntu, | ||
1615 | 1015 | introduced) >= 0 | ||
1616 | 1016 | |||
1617 | 1017 | def kernel_claims_cve_fixed(self, cve): | ||
1618 | 1018 | changelog = "/usr/share/doc/linux-image-%s/changelog.Debian.gz" % (self.kernel_version) | ||
1619 | 1019 | if os.path.exists(changelog): | ||
1620 | 1020 | for line in gzip.open(changelog): | ||
1621 | 1021 | if cve in line and not "revert" in line and not "Revert" in line: | ||
1622 | 1022 | return True | ||
1623 | 1023 | return False | ||
1624 | 1024 | |||
1625 | 1025 | class TestGroup: | ||
1626 | 1026 | '''Create a temporary test group and remove it again in the dtor.''' | ||
1627 | 1027 | |||
1628 | 1028 | def __init__(self, group=None, lower=False): | ||
1629 | 1029 | '''Create a new group''' | ||
1630 | 1030 | |||
1631 | 1031 | self.group = None | ||
1632 | 1032 | if group: | ||
1633 | 1033 | if group_exists(group): | ||
1634 | 1034 | raise ValueError, 'group name already exists' | ||
1635 | 1035 | else: | ||
1636 | 1036 | while(True): | ||
1637 | 1037 | group = random_string(7,lower=lower) | ||
1638 | 1038 | if not group_exists(group): | ||
1639 | 1039 | break | ||
1640 | 1040 | |||
1641 | 1041 | assert subprocess.call(['groupadd',group]) == 0 | ||
1642 | 1042 | self.group = group | ||
1643 | 1043 | g = grp.getgrnam(self.group) | ||
1644 | 1044 | self.gid = g[2] | ||
1645 | 1045 | |||
1646 | 1046 | def __del__(self): | ||
1647 | 1047 | '''Remove the created group.''' | ||
1648 | 1048 | |||
1649 | 1049 | if self.group: | ||
1650 | 1050 | rc, report = cmd(['groupdel', self.group]) | ||
1651 | 1051 | assert rc == 0 | ||
1652 | 1052 | |||
1653 | 1053 | class TestUser: | ||
1654 | 1054 | '''Create a temporary test user and remove it again in the dtor.''' | ||
1655 | 1055 | |||
1656 | 1056 | def __init__(self, login=None, home=True, group=None, uidmin=None, lower=False, shell=None): | ||
1657 | 1057 | '''Create a new user account with a random password. | ||
1658 | 1058 | |||
1659 | 1059 | By default, the login name is random, too, but can be explicitly | ||
1660 | 1060 | specified with 'login'. By default, a home directory is created, this | ||
1661 | 1061 | can be suppressed with 'home=False'.''' | ||
1662 | 1062 | |||
1663 | 1063 | self.login = None | ||
1664 | 1064 | |||
1665 | 1065 | if os.geteuid() != 0: | ||
1666 | 1066 | raise ValueError, "You must be root to run this test" | ||
1667 | 1067 | |||
1668 | 1068 | if login: | ||
1669 | 1069 | if login_exists(login): | ||
1670 | 1070 | raise ValueError, 'login name already exists' | ||
1671 | 1071 | else: | ||
1672 | 1072 | while(True): | ||
1673 | 1073 | login = 't' + random_string(7,lower=lower) | ||
1674 | 1074 | if not login_exists(login): | ||
1675 | 1075 | break | ||
1676 | 1076 | |||
1677 | 1077 | self.salt = random_string(2) | ||
1678 | 1078 | self.password = random_string(8,lower=lower) | ||
1679 | 1079 | self.crypted = crypt.crypt(self.password, self.salt) | ||
1680 | 1080 | |||
1681 | 1081 | creation = ['useradd', '-p', self.crypted] | ||
1682 | 1082 | if home: | ||
1683 | 1083 | creation += ['-m'] | ||
1684 | 1084 | if group: | ||
1685 | 1085 | creation += ['-G',group] | ||
1686 | 1086 | if uidmin: | ||
1687 | 1087 | creation += ['-K','UID_MIN=%d'%uidmin] | ||
1688 | 1088 | if shell: | ||
1689 | 1089 | creation += ['-s',shell] | ||
1690 | 1090 | creation += [login] | ||
1691 | 1091 | assert subprocess.call(creation) == 0 | ||
1692 | 1092 | # Set GECOS | ||
1693 | 1093 | assert subprocess.call(['usermod','-c','Buddy %s' % (login),login]) == 0 | ||
1694 | 1094 | |||
1695 | 1095 | self.login = login | ||
1696 | 1096 | p = pwd.getpwnam(self.login) | ||
1697 | 1097 | self.uid = p[2] | ||
1698 | 1098 | self.gid = p[3] | ||
1699 | 1099 | self.gecos = p[4] | ||
1700 | 1100 | self.home = p[5] | ||
1701 | 1101 | self.shell = p[6] | ||
1702 | 1102 | |||
1703 | 1103 | def __del__(self): | ||
1704 | 1104 | '''Remove the created user account.''' | ||
1705 | 1105 | |||
1706 | 1106 | if self.login: | ||
1707 | 1107 | # sanity check the login name so we don't accidentally wipe too much | ||
1708 | 1108 | if len(self.login)>3 and not '/' in self.login: | ||
1709 | 1109 | subprocess.call(['rm','-rf', '/home/'+self.login, '/var/mail/'+self.login]) | ||
1710 | 1110 | rc, report = cmd(['userdel', '-f', self.login]) | ||
1711 | 1111 | assert rc == 0 | ||
1712 | 1112 | |||
1713 | 1113 | def add_to_group(self, group): | ||
1714 | 1114 | '''Add user to the specified group name''' | ||
1715 | 1115 | rc, report = cmd(['usermod', '-G', group, self.login]) | ||
1716 | 1116 | if rc != 0: | ||
1717 | 1117 | print report | ||
1718 | 1118 | assert rc == 0 | ||
1719 | 1119 | |||
1720 | 1120 | # Timeout handler using alarm() from John P. Speno's Pythonic Avocado | ||
1721 | 1121 | class TimeoutFunctionException(Exception): | ||
1722 | 1122 | """Exception to raise on a timeout""" | ||
1723 | 1123 | pass | ||
1724 | 1124 | class TimeoutFunction: | ||
1725 | 1125 | def __init__(self, function, timeout): | ||
1726 | 1126 | self.timeout = timeout | ||
1727 | 1127 | self.function = function | ||
1728 | 1128 | |||
1729 | 1129 | def handle_timeout(self, signum, frame): | ||
1730 | 1130 | raise TimeoutFunctionException() | ||
1731 | 1131 | |||
1732 | 1132 | def __call__(self, *args, **kwargs): | ||
1733 | 1133 | old = signal.signal(signal.SIGALRM, self.handle_timeout) | ||
1734 | 1134 | signal.alarm(self.timeout) | ||
1735 | 1135 | try: | ||
1736 | 1136 | result = self.function(*args, **kwargs) | ||
1737 | 1137 | finally: | ||
1738 | 1138 | signal.signal(signal.SIGALRM, old) | ||
1739 | 1139 | signal.alarm(0) | ||
1740 | 1140 | return result | ||
1741 | 1141 | |||
1742 | 1142 | def main(): | ||
1743 | 1143 | print "hi" | ||
1744 | 1144 | unittest.main() |
Could you add a changelog entry in debian/changelog for the upload? It might also be a good idea to forward the change to Debian.