Merge lp:~jamesodhunt/ubuntu/vivid/ubuntu-core-upgrader/handle-invalid-removed-file into lp:ubuntu/vivid/ubuntu-core-upgrader
- Vivid (15.04)
- handle-invalid-removed-file
- Merge into vivid
Proposed by
James Hunt
Status: | Merged |
---|---|
Merged at revision: | 24 |
Proposed branch: | lp:~jamesodhunt/ubuntu/vivid/ubuntu-core-upgrader/handle-invalid-removed-file |
Merge into: | lp:ubuntu/vivid/ubuntu-core-upgrader |
Diff against target: |
406 lines (+157/-56) 5 files modified
debian/changelog (+9/-0) functional/test_upgrader.py (+78/-20) ubuntucoreupgrader/tests/test_upgrader.py (+46/-10) ubuntucoreupgrader/tests/utils.py (+6/-3) ubuntucoreupgrader/upgrader.py (+18/-23) |
To merge this branch: | bzr merge lp:~jamesodhunt/ubuntu/vivid/ubuntu-core-upgrader/handle-invalid-removed-file |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Vogt (community) | Needs Information | ||
Review via email: mp+254372@code.launchpad.net |
Commit message
Description of the change
* ubuntucoreupgra
to avoid the upgrade failing attempting to remove '/writable/cache'
(see LP: #1437225).
To post a comment you must log in.
Revision history for this message
Michael Vogt (mvo) wrote : | # |
Revision history for this message
James Hunt (jamesodhunt) wrote : | # |
Tests added.
Revision history for this message
Michael Vogt (mvo) wrote : | # |
Thanks for adding the test. It runs fine.
One thing that is a odd is that if I revert the change in r23 (the bugfix for the empty file) and run the tests they still succeed. Is this intended?
Revision history for this message
James Hunt (jamesodhunt) wrote : | # |
Branch updated. Phew...
Revision history for this message
Michael Vogt (mvo) wrote : | # |
Thanks, I put some suggestions inline.
- 26. By James Hunt
-
* Review changes.
Revision history for this message
James Hunt (jamesodhunt) wrote : | # |
Thanks for reviewing - branch updated.
Revision history for this message
Michael Vogt (mvo) wrote : | # |
Thanks, looks good, one question about the strip() below seems to have slipped through, would be nice to get this clarified in some way. Otherwise fine and ready to go.
review:
Needs Information
Revision history for this message
Michael Vogt (mvo) wrote : | # |
I merged/
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 2015-03-13 08:14:06 +0000 |
3 | +++ debian/changelog 2015-04-08 14:54:30 +0000 |
4 | @@ -1,3 +1,12 @@ |
5 | +ubuntu-core-upgrader (0.7.8) UNRELEASED; urgency=medium |
6 | + |
7 | + * ubuntucoreupgrader/upgrader.py: Tolerate an invalid 'removed' file |
8 | + to avoid the upgrade failing attempting to remove '/writable/cache' |
9 | + (see LP: #1437225). |
10 | + * functional/test_upgrader.py: Add tests for an invalid removed file. |
11 | + |
12 | + -- James Hunt <james.hunt@ubuntu.com> Fri, 27 Mar 2015 09:33:15 +0000 |
13 | + |
14 | ubuntu-core-upgrader (0.7.7) vivid; urgency=low |
15 | |
16 | * fix entry point for s-i 3.0 |
17 | |
18 | === modified file 'functional/test_upgrader.py' |
19 | --- functional/test_upgrader.py 2015-03-04 16:27:30 +0000 |
20 | +++ functional/test_upgrader.py 2015-04-08 14:54:30 +0000 |
21 | @@ -25,7 +25,6 @@ |
22 | import os |
23 | import logging |
24 | import unittest |
25 | -import shutil |
26 | |
27 | from ubuntucoreupgrader.upgrader import ( |
28 | Upgrader, |
29 | @@ -37,7 +36,6 @@ |
30 | sys.path.append(base_dir) |
31 | |
32 | from ubuntucoreupgrader.tests.utils import ( |
33 | - make_tmp_dir, |
34 | append_file, |
35 | TEST_DIR_MODE, |
36 | create_file, |
37 | @@ -68,21 +66,11 @@ |
38 | args.append(command_file) |
39 | commands = file_to_list(command_file) |
40 | |
41 | - cache_dir = make_tmp_dir() |
42 | - |
43 | - def mock_get_cache_dir(): |
44 | - cache_dir = update.tmp_dir |
45 | - sys_dir = os.path.join(cache_dir, 'system') |
46 | - os.makedirs(sys_dir, exist_ok=True) |
47 | - return cache_dir |
48 | - |
49 | upgrader = Upgrader(parse_args(args), commands, []) |
50 | - upgrader.get_cache_dir = mock_get_cache_dir |
51 | + upgrader.cache_dir = root_dir |
52 | upgrader.MOUNTPOINT_CMD = "true" |
53 | upgrader.run() |
54 | |
55 | - shutil.rmtree(cache_dir) |
56 | - |
57 | |
58 | def create_device_file(path, type='c', major=-1, minor=-1): |
59 | ''' |
60 | @@ -216,7 +204,10 @@ |
61 | self.assertTrue(os.path.exists(file_path)) |
62 | self.assertTrue(os.path.isfile(file_path)) |
63 | |
64 | - call_upgrader(cmd_file, self.victim_dir, self.update) |
65 | + # remove 'system' suffix that upgrader will add back on |
66 | + vdir = os.path.split(self.victim_dir)[0] |
67 | + |
68 | + call_upgrader(cmd_file, vdir, self.update) |
69 | |
70 | self.assertFalse(os.path.exists(file_path)) |
71 | |
72 | @@ -240,7 +231,8 @@ |
73 | self.assertTrue(os.path.exists(dir_path)) |
74 | self.assertTrue(os.path.isdir(dir_path)) |
75 | |
76 | - call_upgrader(cmd_file, self.victim_dir, self.update) |
77 | + vdir = os.path.split(self.victim_dir)[0] |
78 | + call_upgrader(cmd_file, vdir, self.update) |
79 | |
80 | self.assertFalse(os.path.exists(dir_path)) |
81 | |
82 | @@ -272,7 +264,8 @@ |
83 | self.assertTrue(os.path.exists(link_file_path)) |
84 | self.assertTrue(os.path.islink(link_file_path)) |
85 | |
86 | - call_upgrader(cmd_file, self.victim_dir, self.update) |
87 | + vdir = os.path.split(self.victim_dir)[0] |
88 | + call_upgrader(cmd_file, vdir, self.update) |
89 | |
90 | # original file should still be there |
91 | self.assertTrue(os.path.exists(src_file_path)) |
92 | @@ -310,7 +303,8 @@ |
93 | self.assertTrue(os.path.exists(link_file_path)) |
94 | self.assertTrue(os.path.islink(link_file_path)) |
95 | |
96 | - call_upgrader(cmd_file, self.victim_dir, self.update) |
97 | + vdir = os.path.split(self.victim_dir)[0] |
98 | + call_upgrader(cmd_file, vdir, self.update) |
99 | |
100 | # original directory should still be there |
101 | self.assertTrue(os.path.exists(src_dir_path)) |
102 | @@ -352,7 +346,8 @@ |
103 | |
104 | self.assertTrue(src_inode == link_inode) |
105 | |
106 | - call_upgrader(cmd_file, self.victim_dir, self.update) |
107 | + vdir = os.path.split(self.victim_dir)[0] |
108 | + call_upgrader(cmd_file, vdir, self.update) |
109 | |
110 | # original file should still be there |
111 | self.assertTrue(os.path.exists(src_file_path)) |
112 | @@ -392,9 +387,11 @@ |
113 | # device because it won't be :) |
114 | self.assertTrue(os.path.isfile(file_path)) |
115 | |
116 | - call_upgrader(cmd_file, self.victim_dir, self.update) |
117 | + vdir = os.path.split(self.victim_dir)[0] |
118 | + call_upgrader(cmd_file, vdir, self.update) |
119 | |
120 | - self.assertFalse(os.path.exists(file_path)) |
121 | + # upgrader doesn't currently remove device files |
122 | + self.assertTrue(os.path.exists(file_path)) |
123 | |
124 | |
125 | class UpgraderFileAddTestCase(UbuntuCoreUpgraderTestCase): |
126 | @@ -595,6 +592,67 @@ |
127 | self.assertTrue(is_sym_link_broken(victim_link_file_path)) |
128 | |
129 | |
130 | +class UpgraderRemoveFileTests(UbuntuCoreUpgraderTestCase): |
131 | + |
132 | + def common_removed_file_test(self, contents): |
133 | + ''' |
134 | + Common code to test for an invalid removed file. |
135 | + |
136 | + The contents parameter specifies the contents of the removed |
137 | + file. |
138 | + ''' |
139 | + file = 'created-regular-file' |
140 | + |
141 | + create_file(self.update.removed_file, contents) |
142 | + |
143 | + file_path = os.path.join(self.update.system_dir, file) |
144 | + create_file(file_path, 'foo bar') |
145 | + |
146 | + archive = self.update.create_archive(self.TARFILE) |
147 | + self.assertTrue(os.path.exists(archive)) |
148 | + |
149 | + cmd_file = os.path.join(self.update.tmp_dir, CMD_FILE) |
150 | + make_command_file(cmd_file, [self.TARFILE]) |
151 | + |
152 | + vdir = os.path.split(self.victim_dir)[0] |
153 | + |
154 | + file_path = os.path.join(vdir, file) |
155 | + self.assertFalse(os.path.exists(file_path)) |
156 | + |
157 | + # XXX: There is an implicit test here since if the upgrader |
158 | + # fails (as documented on LP: #1437225), this test will also |
159 | + # fail. |
160 | + call_upgrader(cmd_file, vdir, self.update) |
161 | + |
162 | + self.assertTrue(os.path.exists(vdir)) |
163 | + self.assertTrue(os.path.exists(self.victim_dir)) |
164 | + self.assertTrue(os.path.exists(file_path)) |
165 | + |
166 | + # ensure the empty removed file hasn't removed the directory the |
167 | + # unpack applies to. |
168 | + self.assertTrue(self.victim_dir) |
169 | + |
170 | + def test_removed_file_empty(self): |
171 | + ''' |
172 | + Ensure the upgrader ignores an empty 'removed' file. |
173 | + ''' |
174 | + self.common_removed_file_test('') |
175 | + |
176 | + def test_removed_file_space(self): |
177 | + ''' |
178 | + Ensure the upgrader handles a 'removed' file containing just a |
179 | + space. |
180 | + ''' |
181 | + self.common_removed_file_test(' ') |
182 | + |
183 | + def test_removed_file_nl(self): |
184 | + ''' |
185 | + Ensure the upgrader handles a 'removed' file containing just a |
186 | + newline |
187 | + ''' |
188 | + self.common_removed_file_test('\n') |
189 | + |
190 | + |
191 | def main(): |
192 | kwargs = {} |
193 | format = \ |
194 | |
195 | === modified file 'ubuntucoreupgrader/tests/test_upgrader.py' |
196 | --- ubuntucoreupgrader/tests/test_upgrader.py 2015-03-04 16:50:08 +0000 |
197 | +++ ubuntucoreupgrader/tests/test_upgrader.py 2015-04-08 14:54:30 +0000 |
198 | @@ -196,15 +196,6 @@ |
199 | |
200 | class UbuntuCoreUpgraderObectTestCase(UbuntuCoreUpgraderTestCase): |
201 | |
202 | - def mock_get_cache_dir(self): |
203 | - ''' |
204 | - Mock to allow the tests to control the cache directory location. |
205 | - ''' |
206 | - assert(self.cache_dir) |
207 | - self.sys_dir = os.path.join(self.cache_dir, 'system') |
208 | - os.makedirs(self.sys_dir, exist_ok=True) |
209 | - return self.cache_dir |
210 | - |
211 | def test_create_object(self): |
212 | root_dir = make_tmp_dir() |
213 | |
214 | @@ -233,7 +224,6 @@ |
215 | options.root_dir = root_dir |
216 | |
217 | upgrader = Upgrader(options, commands, []) |
218 | - upgrader.get_cache_dir = self.mock_get_cache_dir |
219 | upgrader.MOUNTPOINT_CMD = "true" |
220 | upgrader.run() |
221 | |
222 | @@ -242,6 +232,52 @@ |
223 | |
224 | shutil.rmtree(root_dir) |
225 | |
226 | + def test_empty_removed_file(self): |
227 | + root_dir = make_tmp_dir() |
228 | + |
229 | + file = 'created-regular-file' |
230 | + |
231 | + file_path = os.path.join(self.update.system_dir, file) |
232 | + create_file(file_path, 'foo bar') |
233 | + |
234 | + self.cache_dir = self.update.tmp_dir |
235 | + |
236 | + removed_file = self.update.removed_file |
237 | + # Create an empty removed file |
238 | + create_file(removed_file, '') |
239 | + |
240 | + archive = self.update.create_archive(self.TARFILE) |
241 | + self.assertTrue(os.path.exists(archive)) |
242 | + |
243 | + dest = os.path.join(self.cache_dir, os.path.basename(archive)) |
244 | + touch_file('{}.asc'.format(dest)) |
245 | + |
246 | + commands = make_commands([self.TARFILE]) |
247 | + |
248 | + options = make_default_options() |
249 | + |
250 | + # XXX: doesn't actually exist, but the option must be set since |
251 | + # the upgrader looks for the update archives in the directory |
252 | + # where this file is claimed to be. |
253 | + options.cmdfile = os.path.join(self.cache_dir, 'ubuntu_command') |
254 | + |
255 | + options.root_dir = root_dir |
256 | + |
257 | + upgrader = Upgrader(options, commands, []) |
258 | + upgrader.cache_dir = self.cache_dir |
259 | + upgrader.MOUNTPOINT_CMD = "true" |
260 | + |
261 | + si_file = self.TARFILE |
262 | + si_signature = "{}.asc".format(self.TARFILE) |
263 | + upgrader._cmd_update([si_file, si_signature]) |
264 | + |
265 | + # Ensure that the upgrader has not attempted to remove the |
266 | + # cache_dir when the 'removed' file is empty. |
267 | + # |
268 | + # (Regression test for LP: #1437225). |
269 | + self.assertTrue(os.path.exists(upgrader.cache_dir)) |
270 | + |
271 | + shutil.rmtree(self.cache_dir) |
272 | |
273 | if __name__ == "__main__": |
274 | unittest.main() |
275 | |
276 | === modified file 'ubuntucoreupgrader/tests/utils.py' |
277 | --- ubuntucoreupgrader/tests/utils.py 2015-03-04 16:50:08 +0000 |
278 | +++ ubuntucoreupgrader/tests/utils.py 2015-04-08 14:54:30 +0000 |
279 | @@ -119,7 +119,8 @@ |
280 | ''' |
281 | # all files listed in the removed list must be system files |
282 | final = list(map(lambda a: |
283 | - '{}{}'.format(self.TEST_SYSTEM_DIR, a), removed_files)) |
284 | + os.path.normpath('{}{}'.format(self.TEST_SYSTEM_DIR, |
285 | + a)), removed_files)) |
286 | |
287 | contents = "".join(final) |
288 | append_file(self.removed_file, contents) |
289 | @@ -194,7 +195,7 @@ |
290 | |
291 | This creates 2 temporary directories: |
292 | |
293 | - - self.system dir: Used as to generate an update archive from. |
294 | + - self.system_dir: Used to generate an update archive from. |
295 | |
296 | - self.tmp_dir: Used to write the generated archive file to. The |
297 | intention is that this directory should also be used to hold |
298 | @@ -244,7 +245,9 @@ |
299 | |
300 | # The directory which will have the update archive applied to |
301 | # it. |
302 | - self.victim_dir = make_tmp_dir(tag='victim') |
303 | + self.victim_dir_base = make_tmp_dir(tag='victim') |
304 | + self.victim_dir = os.path.join(self.victim_dir_base, 'system') |
305 | + os.makedirs(self.victim_dir, mode=0o750) |
306 | |
307 | def tearDown(self): |
308 | ''' |
309 | |
310 | === modified file 'ubuntucoreupgrader/upgrader.py' |
311 | --- ubuntucoreupgrader/upgrader.py 2015-03-05 13:24:40 +0000 |
312 | +++ ubuntucoreupgrader/upgrader.py 2015-04-08 14:54:30 +0000 |
313 | @@ -472,29 +472,26 @@ |
314 | # Note: Only used by UPGRADE_IN_PLACE. |
315 | self.other_is_empty = False |
316 | |
317 | + # cache_dir is used as a scratch pad, for downloading new images |
318 | + # to and bind mounting the rootfs. |
319 | + if self.options.tmpdir: |
320 | + self.cache_dir = self.options.tmpdir |
321 | + else: |
322 | + self.cache_dir = self.DEFAULT_CACHE_DIR |
323 | + |
324 | def update_timestamp(self): |
325 | ''' |
326 | Update the timestamp file to record the time the last upgrade |
327 | completed successfully. |
328 | ''' |
329 | - file = os.path.join(self.get_cache_dir(), self.TIMESTAMP_FILE) |
330 | + file = os.path.join(self.cache_dir, self.TIMESTAMP_FILE) |
331 | open(file, 'w').close() |
332 | |
333 | - def get_cache_dir(self): |
334 | - ''' |
335 | - Returns the full path to the cache directory, which is used as a |
336 | - scratch pad, for downloading new images to and bind mounting the |
337 | - rootfs. |
338 | - ''' |
339 | - return self.options.tmpdir \ |
340 | - if self.options.tmpdir \ |
341 | - else self.DEFAULT_CACHE_DIR |
342 | - |
343 | def get_mount_target(self): |
344 | ''' |
345 | Get the full path to the mount target directory. |
346 | ''' |
347 | - return os.path.join(self.get_cache_dir(), self.MOUNT_TARGET) |
348 | + return os.path.join(self.cache_dir, self.MOUNT_TARGET) |
349 | |
350 | def prepare(self): |
351 | ''' |
352 | @@ -740,13 +737,17 @@ |
353 | to_remove = [] |
354 | |
355 | if not self.full_image: |
356 | - |
357 | if found_removed_file: |
358 | log.debug('processing {} file' |
359 | .format(self.removed_file)) |
360 | |
361 | # process backwards to work around bug LP:#1381134. |
362 | for remove in sorted(to_remove, reverse=True): |
363 | + |
364 | + if not remove: |
365 | + # handle invalid removed file entry (see LP:#1437225) |
366 | + continue |
367 | + |
368 | remove = remove.strip() |
369 | |
370 | # don't remove devices |
371 | @@ -762,13 +763,7 @@ |
372 | if '../' in remove: |
373 | continue |
374 | |
375 | - if self.options.root_dir == '/': |
376 | - final = os.path.join(self.get_cache_dir(), remove) |
377 | - else: |
378 | - # os.path.join() refuses to work if the file |
379 | - # begins with a slash. |
380 | - base = remove_prefix(remove) |
381 | - final = '{}{}'.format(self.options.root_dir, base) |
382 | + final = os.path.join(self.cache_dir, remove) |
383 | |
384 | if not os.path.exists(final): |
385 | # This scenario can only mean there is a bug |
386 | @@ -804,9 +799,9 @@ |
387 | from unittest.mock import patch |
388 | with patch("grp.getgrnam") as m: |
389 | m.side_effect = KeyError() |
390 | - tar.extractall(path=self.get_cache_dir(), |
391 | + tar.extractall(path=self.cache_dir, |
392 | members=tar_generator( |
393 | - tar, self.get_cache_dir(), |
394 | + tar, self.cache_dir, |
395 | self.removed_file, |
396 | self.options.root_dir)) |
397 | tar.close() |
398 | @@ -821,7 +816,7 @@ |
399 | ''' |
400 | target = self.get_mount_target() |
401 | bindmount_rootfs_dir = tempfile.mkdtemp(prefix=script_name, |
402 | - dir=self.get_cache_dir()) |
403 | + dir=self.cache_dir) |
404 | bind_mount("/", bindmount_rootfs_dir) |
405 | |
406 | cwd = os.getcwd() |
Thanks for adding this bugfix. I think we should also include a regression test.