Merge lp:~mvo/ubuntu/vivid/ubuntu-core-upgrader/more-tests-tweaks into lp:ubuntu/vivid/ubuntu-core-upgrader
- Vivid (15.04)
- more-tests-tweaks
- Merge into vivid
Status: | Needs review |
---|---|
Proposed branch: | lp:~mvo/ubuntu/vivid/ubuntu-core-upgrader/more-tests-tweaks |
Merge into: | lp:ubuntu/vivid/ubuntu-core-upgrader |
Prerequisite: | lp:~jamesodhunt/ubuntu/vivid/ubuntu-core-upgrader/add-more-tests |
Diff against target: |
979 lines (+594/-172) 4 files modified
bin/ubuntu-core-upgrade (+8/-97) debian/changelog (+15/-0) ubuntucoreupgrader/tests/test_upgrader.py (+383/-11) ubuntucoreupgrader/upgrader.py (+188/-64) |
To merge this branch: | bzr merge lp:~mvo/ubuntu/vivid/ubuntu-core-upgrader/more-tests-tweaks |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Hunt | Pending | ||
Review via email: mp+251626@code.launchpad.net |
Commit message
Description of the change
Some tweaks to the tests.
William Grant (wgrant) wrote : | # |
The prereq seems to have been rebased (r13 here and r15 in the prereq seem suspiciously identical).
Michael Vogt (mvo) wrote : | # |
Hey William, thanks for looking at this and I'm sorry for blaming LP. I think you are right and its a rebase in the prereq thats causing the issue. It will also make merging more difficult I guess (without having tried it).
Unmerged revisions
- 17. By Michael Vogt
- 16. By Michael Vogt
-
some more simplifications
- 15. By James Hunt
-
* Fluff removal.
- 14. By James Hunt
-
* Review changes.
- 13. By James Hunt
-
* bin/ubuntu-
core-upgrade:
- parse_args(): Return a dict for easier testing.
- Update for options dict.
* ubuntucoreupgrader/tests/ test_upgrader. py:
- More tar_generator() tests.
- Added first Upgrader() object test.
* ubuntucoreupgrader/upgrader. py:
- Comments.
- tar_generator():
- Fix to allow '--root-dir' to work again.
- Fix bug where path not set (only affecting tests).
- Updates for accessing self.options dict.
- Assert root_dir option set.
Preview Diff
1 | === modified file 'bin/ubuntu-core-upgrade' |
2 | --- bin/ubuntu-core-upgrade 2015-02-20 17:00:34 +0000 |
3 | +++ bin/ubuntu-core-upgrade 2015-03-03 16:49:20 +0000 |
4 | @@ -34,20 +34,14 @@ |
5 | import atexit |
6 | import shutil |
7 | import subprocess |
8 | -import argparse |
9 | |
10 | from ubuntucoreupgrader.unshare import unshare, UnshareFlags |
11 | -from ubuntucoreupgrader.upgrader import Upgrader |
12 | +from ubuntucoreupgrader.upgrader import Upgrader, parse_args |
13 | |
14 | # Extension added to the command-file to show the new image is in the |
15 | # process of being applied. |
16 | IN_PROGRESS_SUFFIX = 'applying' |
17 | |
18 | -# seconds to wait before a reboot (should one be required) |
19 | -DEFAULT_REBOOT_DELAY = 2 |
20 | - |
21 | -DEFAULT_ROOT = '/' |
22 | - |
23 | log = logging.getLogger() |
24 | |
25 | |
26 | @@ -80,91 +74,6 @@ |
27 | log.addHandler(handler) |
28 | |
29 | |
30 | -def parse_args(): |
31 | - ''' |
32 | - Handle command-line options. |
33 | - ''' |
34 | - |
35 | - parser = argparse.ArgumentParser(description='System image Upgrader') |
36 | - |
37 | - parser.add_argument( |
38 | - '--check-reboot', |
39 | - action='store_true', |
40 | - help=''' |
41 | - Attempt to determine if a reboot may be required. |
42 | - |
43 | - This option is similar to --dry-run: no system changes are made. |
44 | - |
45 | - Note that the result of this command cannot be definitive since a |
46 | - reboot would be triggered if a service failed to start (but this |
47 | - option does not attempt to restart any services). |
48 | - ''') |
49 | - |
50 | - parser.add_argument( |
51 | - '--clean-only', |
52 | - action='store_true', |
53 | - help='Clean up from a previous upgrader run, but do not upgrade)') |
54 | - |
55 | - parser.add_argument( |
56 | - '--debug', |
57 | - nargs='?', const=1, default=0, type=int, |
58 | - help='Dump debug info (specify numeric value to increase verbosity)') |
59 | - |
60 | - parser.add_argument( |
61 | - '-n', '--dry-run', |
62 | - action='store_true', |
63 | - help=''' |
64 | - Simulate an update including showing processes that have locks |
65 | - on files |
66 | - ''') |
67 | - |
68 | - parser.add_argument( |
69 | - '--force-inplace-upgrade', |
70 | - action='store_true', |
71 | - help='Apply an upgrade to current rootfs even if running " \ |
72 | - "on dual rootfs system') |
73 | - |
74 | - parser.add_argument( |
75 | - '--leave-files', |
76 | - action='store_true', |
77 | - help='Do not remove the downloaded system image files after upgrade') |
78 | - |
79 | - parser.add_argument( |
80 | - '--no-reboot', |
81 | - action='store_true', |
82 | - help='Do not reboot even if one would normally be required') |
83 | - |
84 | - parser.add_argument( |
85 | - '--reboot-delay', |
86 | - type=int, default=DEFAULT_REBOOT_DELAY, |
87 | - help=''' |
88 | - Wait for specified number of seconds before rebooting |
89 | - (default={}) |
90 | - '''.format(DEFAULT_REBOOT_DELAY)) |
91 | - |
92 | - parser.add_argument( |
93 | - '--root-dir', |
94 | - default=DEFAULT_ROOT, |
95 | - help='Specify an alternative root directory (for testing ONLY)') |
96 | - |
97 | - parser.add_argument( |
98 | - '--show-other-details', |
99 | - action='store_true', |
100 | - help='Dump the details of the system-image vesion on the " \ |
101 | - "other root partition') |
102 | - |
103 | - parser.add_argument( |
104 | - '-t', '--tmpdir', |
105 | - help='Specify name for pre-existing temporary directory to use') |
106 | - |
107 | - parser.add_argument( |
108 | - 'cmdfile', action="store", |
109 | - nargs='?', |
110 | - help='Name of file containing commands to execute') |
111 | - |
112 | - return parser.parse_args() |
113 | - |
114 | - |
115 | def upgrade(options, commands, remove_list): |
116 | upgrader = Upgrader(options, commands, remove_list) |
117 | upgrader.run() |
118 | @@ -194,14 +103,14 @@ |
119 | |
120 | |
121 | def main(): |
122 | - options = parse_args() |
123 | + options = parse_args(sys.argv[1:]) |
124 | |
125 | - if options.reboot_delay < 0: |
126 | + if options.reboot_delay and options.reboot_delay < 0: |
127 | sys.exit('ERROR: must specify a positive number') |
128 | |
129 | if not os.path.exists(options.root_dir): |
130 | sys.exit('ERROR: root directory does not exist: {}' |
131 | - .format(options.root)) |
132 | + .format(options.root_dir)) |
133 | |
134 | if options.check_reboot: |
135 | # check options must be inert |
136 | @@ -219,9 +128,11 @@ |
137 | if not options.cmdfile: |
138 | sys.exit('ERROR: need command file') |
139 | |
140 | - if not os.path.exists(options.cmdfile): |
141 | + cmdfile = options.cmdfile |
142 | + |
143 | + if not os.path.exists(cmdfile): |
144 | sys.exit('ERROR: command file does not exist: {}' |
145 | - .format(options.cmdfile)) |
146 | + .format(cmdfile)) |
147 | |
148 | if os.getuid() != 0: |
149 | sys.exit("ERROR: need to be root") |
150 | |
151 | === modified file 'debian/changelog' |
152 | --- debian/changelog 2015-03-03 15:22:49 +0000 |
153 | +++ debian/changelog 2015-03-03 16:49:20 +0000 |
154 | @@ -1,3 +1,18 @@ |
155 | +ubuntu-core-upgrader (0.7.5) UNRELEASED; urgency=medium |
156 | + |
157 | + * ubuntucoreupgrader/tests/test_upgrader.py: |
158 | + - More tar_generator() tests. |
159 | + - Added first Upgrader() object test. |
160 | + * ubuntucoreupgrader/upgrader.py: |
161 | + - Comments. |
162 | + - tar_generator(): |
163 | + - Fix to allow '--root-dir' to work again. |
164 | + - Fix bug where path not set (only affecting tests). |
165 | + - Updates for accessing self.options dict. |
166 | + - Assert root_dir option set. |
167 | + |
168 | + -- James Hunt <james.hunt@ubuntu.com> Mon, 02 Mar 2015 14:59:43 +0000 |
169 | + |
170 | ubuntu-core-upgrader (0.7.4) vivid; urgency=low |
171 | |
172 | * ubuntucoreupgrader/tests/test_upgrader.py: |
173 | |
174 | === modified file 'ubuntucoreupgrader/tests/test_upgrader.py' |
175 | --- ubuntucoreupgrader/tests/test_upgrader.py 2015-03-03 14:36:01 +0000 |
176 | +++ ubuntucoreupgrader/tests/test_upgrader.py 2015-03-03 16:49:20 +0000 |
177 | @@ -1,27 +1,399 @@ |
178 | -#!/usr/bin/python |
179 | +#!/usr/bin/python3 |
180 | +# -*- coding: utf-8 -*- |
181 | +# -------------------------------------------------------------------- |
182 | +# Copyright © 2014-2015 Canonical Ltd. |
183 | +# |
184 | +# This program is free software: you can redistribute it and/or modify |
185 | +# it under the terms of the GNU General Public License as published by |
186 | +# the Free Software Foundation, version 3 of the License, or |
187 | +# (at your option) any later version. |
188 | +# |
189 | +# This program is distributed in the hope that it will be useful, |
190 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
191 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
192 | +# GNU General Public License for more details. |
193 | +# |
194 | +# You should have received a copy of the GNU General Public License |
195 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
196 | +# -------------------------------------------------------------------- |
197 | + |
198 | +# -------------------------------------------------------------------- |
199 | +# Terminology: |
200 | +# |
201 | +# - "update archive": a fake system-image "ubuntu-hash.tar.xz" tar |
202 | +# archive. |
203 | +# -------------------------------------------------------------------- |
204 | |
205 | import tarfile |
206 | import tempfile |
207 | import unittest |
208 | +import os |
209 | +import shutil |
210 | + |
211 | +from unittest.mock import patch |
212 | |
213 | from ubuntucoreupgrader.upgrader import ( |
214 | tar_generator, |
215 | + Upgrader, |
216 | + parse_args, |
217 | ) |
218 | |
219 | +script_name = os.path.basename(__file__) |
220 | + |
221 | +# file mode to use for creating test directories. |
222 | +TEST_DIR_MODE = 0o750 |
223 | + |
224 | +CMD_FILE = 'ubuntu_command' |
225 | + |
226 | + |
227 | +def make_default_options(): |
228 | + return parse_args([]) |
229 | + |
230 | + |
231 | +def make_tmp_dir(tag=None): |
232 | + ''' |
233 | + Create a temporary directory. |
234 | + ''' |
235 | + |
236 | + if tag: |
237 | + prefix = '{}-{}-'.format(script_name, tag) |
238 | + else: |
239 | + prefix = script_name |
240 | + |
241 | + return tempfile.mkdtemp(prefix=prefix) |
242 | + |
243 | |
244 | class UpgradeTestCase(unittest.TestCase): |
245 | |
246 | - def test_tar_generator_regression_unpack_assets(self): |
247 | + def setUp(self): |
248 | + self.root_dir = make_tmp_dir() |
249 | + self.cache_dir = make_tmp_dir() |
250 | + self.sys_dir = os.path.join(self.cache_dir, 'system') |
251 | + os.makedirs(self.sys_dir) |
252 | tempf = tempfile.TemporaryFile() |
253 | - with tarfile.TarFile(fileobj=tempf, mode="w") as tar: |
254 | - tar.add(__file__, "system/bin/true") |
255 | - tar.add(__file__, "assets/kernel") |
256 | - tar.add(__file__, "unreleated/something") |
257 | - tar.add(__file__, "hardware.yaml") |
258 | - |
259 | - result = [m.name for m in tar_generator(tar, "cache_dir", [], "/")] |
260 | - self.assertEqual( |
261 | - result, ["system/bin/true", "assets/kernel", "hardware.yaml"]) |
262 | + self.tar = tarfile.TarFile(fileobj=tempf, mode="w") |
263 | + |
264 | + def tearDown(self): |
265 | + shutil.rmtree(self.root_dir) |
266 | + shutil.rmtree(self.cache_dir) |
267 | + self.tar.close() |
268 | + |
269 | + def test_tar_generator_unpack_assets(self): |
270 | + # special top-level file that should not be unpacked |
271 | + self.tar.add(__file__, "removed") |
272 | + |
273 | + self.tar.add(__file__, "system/bin/true") |
274 | + self.tar.add(__file__, "assets/vmlinuz") |
275 | + self.tar.add(__file__, "assets/initrd.img") |
276 | + self.tar.add(__file__, "unreleated/something") |
277 | + self.tar.add(__file__, "hardware.yaml") |
278 | + |
279 | + result = [m.name for m in tar_generator(self.tar, "cache_dir", [], "/")] |
280 | + self.assertEqual(result, ["system/bin/true", "assets/vmlinuz", |
281 | + "assets/initrd.img", "hardware.yaml"]) |
282 | + |
283 | + def test_tar_generator_system_files_unpack(self): |
284 | + os.makedirs(os.path.join(self.root_dir, 'dev')) |
285 | + |
286 | + self.tar.add(__file__, "assets/vmlinuz") |
287 | + self.tar.add(__file__, "assets/initrd.img") |
288 | + |
289 | + device_a = '/dev/null' |
290 | + path = (os.path.normpath('{}/{}'.format(self.sys_dir, device_a))) |
291 | + touch_file(path) |
292 | + |
293 | + self.assertTrue(os.path.exists(os.path.join(self.root_dir, device_a))) |
294 | + |
295 | + # should not be unpacked as already exists |
296 | + self.tar.add(__file__, "system{}".format(device_a)) |
297 | + |
298 | + device_b = '/dev/does-not-exist' |
299 | + self.assertFalse(os.path.exists(os.path.join(self.root_dir, device_b))) |
300 | + |
301 | + # should be unpacked as does not exist |
302 | + self.tar.add(__file__, "system{}".format(device_b)) |
303 | + |
304 | + expected = ["assets/vmlinuz", "assets/initrd.img", |
305 | + "dev/does-not-exist"] |
306 | + |
307 | + expected_results = [os.path.join(self.root_dir, file) |
308 | + for file in expected] |
309 | + |
310 | + result = [m.name for m in |
311 | + tar_generator(self.tar, self.cache_dir, [], self.root_dir)] |
312 | + self.assertEqual(result, expected_results) |
313 | + |
314 | + def test_tar_generator_system_files_remove_before_unpack(self): |
315 | + """ |
316 | + Test that the upgrader removes certain files just prior to |
317 | + overwriting them via the unpack. |
318 | + """ |
319 | + file = 'a/file' |
320 | + dir = 'a/dir' |
321 | + |
322 | + file_path = os.path.normpath('{}/{}'.format(self.root_dir, file)) |
323 | + touch_file(file_path) |
324 | + self.assertTrue(os.path.exists(file_path)) |
325 | + |
326 | + dir_path = os.path.normpath('{}/{}'.format(self.root_dir, dir)) |
327 | + os.makedirs(dir_path) |
328 | + self.assertTrue(os.path.exists(dir_path)) |
329 | + |
330 | + self.tar.add(__file__, "system/{}".format(file)) |
331 | + |
332 | + expected = [file] |
333 | + expected_results = [os.path.join(self.root_dir, f) |
334 | + for f in expected] |
335 | + |
336 | + result = [m.name for m in |
337 | + tar_generator(self.tar, self.cache_dir, [], self.root_dir)] |
338 | + |
339 | + self.assertEqual(result, expected_results) |
340 | + |
341 | + # file should be removed |
342 | + self.assertFalse(os.path.exists(file_path)) |
343 | + |
344 | + # directory should not be removed |
345 | + self.assertTrue(os.path.exists(dir_path)) |
346 | + |
347 | + |
348 | +def append_file(path, contents): |
349 | + ''' |
350 | + Append to a regular file (create it doesn't exist). |
351 | + ''' |
352 | + dirname = os.path.dirname(path) |
353 | + os.makedirs(dirname, mode=TEST_DIR_MODE, exist_ok=True) |
354 | + |
355 | + with open(path, 'a') as fh: |
356 | + fh.writelines(contents) |
357 | + |
358 | + if not contents.endswith('\n'): |
359 | + fh.write('\n') |
360 | + |
361 | + |
362 | +def create_file(path, contents): |
363 | + ''' |
364 | + Create a regular file. |
365 | + ''' |
366 | + append_file(path, contents) |
367 | + |
368 | + |
369 | +def touch_file(path): |
370 | + ''' |
371 | + Create an empty file (creating any necessary intermediate |
372 | + directories in @path. |
373 | + ''' |
374 | + create_file(path, '') |
375 | + |
376 | + |
377 | +def make_commands(update_list): |
378 | + """ |
379 | + Convert the specified list of update archives into a list of command |
380 | + file commands. |
381 | + """ |
382 | + l = [] |
383 | + |
384 | + # FIXME: we don't currently add a mount verb (which would be more |
385 | + # realistics) since we can't handle that in the tests. |
386 | + # ##l.append('mount system') |
387 | + |
388 | + for file in update_list: |
389 | + l.append('update {} {}.asc'.format(file, file)) |
390 | + |
391 | + # FIXME: we don't currently add an unmount verb (which would be more |
392 | + # realistics) since we can't handle that in the tests. |
393 | + # ##l.append('unmount system') |
394 | + |
395 | + return l |
396 | + |
397 | + |
398 | +class UpdateTree(): |
399 | + ''' |
400 | + Representation of a directory tree that will be converted into an |
401 | + update archive. |
402 | + ''' |
403 | + TEST_REMOVED_FILE = 'removed' |
404 | + TEST_SYSTEM_DIR = 'system/' |
405 | + |
406 | + def __init__(self): |
407 | + # Directory tree used to construct the tar file from. |
408 | + # Also used to hold the TEST_REMOVED_FILE file. |
409 | + self.dir = make_tmp_dir(tag='UpdateTree-tar-source') |
410 | + |
411 | + self.removed_file = os.path.join(self.dir, self.TEST_REMOVED_FILE) |
412 | + |
413 | + # Directory to place create/modify files into. |
414 | + self.system_dir = os.path.join(self.dir, self.TEST_SYSTEM_DIR) |
415 | + |
416 | + # Directory used to write the generated tarfile to. |
417 | + # This directory should also be used to write the command file |
418 | + # to. |
419 | + self.tmp_dir = make_tmp_dir(tag='UpdateTree-cache') |
420 | + |
421 | + def destroy(self): |
422 | + if os.path.exists(self.dir): |
423 | + shutil.rmtree(self.dir) |
424 | + |
425 | + if os.path.exists(self.tmp_dir): |
426 | + shutil.rmtree(self.tmp_dir) |
427 | + |
428 | + def tar_filter(self, member): |
429 | + ''' |
430 | + Function to filter the tarinfo members before creating the |
431 | + archive. |
432 | + ''' |
433 | + # members are created with relative paths (no leading slash) |
434 | + path = os.sep + member.name |
435 | + |
436 | + if member.name == '/.': |
437 | + return None |
438 | + |
439 | + i = path.find(self.dir) |
440 | + assert(i == 0) |
441 | + |
442 | + # remove the temporary directory elements |
443 | + # (+1 for the os.sep we added above) |
444 | + member.name = path[len(self.dir)+1:] |
445 | + |
446 | + return member |
447 | + |
448 | + def create_archive(self, name): |
449 | + ''' |
450 | + Create an archive with the specified name from the UpdateTree |
451 | + object. Also creates a fake signature file alongside the archive |
452 | + file since this is currently required by the upgrader (although |
453 | + it is not validated). |
454 | + |
455 | + :param name: name of tarfile. |
456 | + :param name: full path to xz archive to create. |
457 | + :return full path to tar file with name @name. |
458 | + ''' |
459 | + |
460 | + tar_path = os.path.join(self.tmp_dir, name) |
461 | + tar = tarfile.open(tar_path, 'w:xz') |
462 | + |
463 | + # We can't just add recursively since that would attempt to add |
464 | + # the parent directory. However, the real update tars don't |
465 | + # include that, and attempting to ignore the parent directory |
466 | + # results in an empty archive. So, walk the tree and add |
467 | + # file-by-file. |
468 | + for path, names, files in os.walk(self.dir): |
469 | + for file in files: |
470 | + full = os.path.join(path, file) |
471 | + tar.add(full, recursive=False, filter=self.tar_filter) |
472 | + if not files and not names: |
473 | + # add (empty) directories |
474 | + tar.add(path, recursive=False, filter=self.tar_filter) |
475 | + |
476 | + tar.close() |
477 | + |
478 | + signature = '{}.asc'.format(tar_path) |
479 | + |
480 | + with open(signature, 'w') as fh: |
481 | + fh.write('fake signature file') |
482 | + |
483 | + return tar_path |
484 | + |
485 | + |
486 | +class UbuntuCoreUpgraderTestCase(unittest.TestCase): |
487 | + ''' |
488 | + Base class for Upgrader tests. |
489 | + ''' |
490 | + |
491 | + TARFILE = 'update.tar.xz' |
492 | + |
493 | + # result of last test run. Hack to deal with fact that even if a |
494 | + # test fails, unittest still calls .tearDown() (whomever thought |
495 | + # that was a good idea...?) |
496 | + currentResult = None |
497 | + |
498 | + def setUp(self): |
499 | + ''' |
500 | + Test setup. |
501 | + ''' |
502 | + # Create an object to hold the tree that will be converted into |
503 | + # an upgrade archive. |
504 | + self.update = UpdateTree() |
505 | + |
506 | + # The directory which will have the update archive applied to |
507 | + # it. |
508 | + self.victim_dir = make_tmp_dir(tag='victim') |
509 | + |
510 | + def tearDown(self): |
511 | + ''' |
512 | + Test cleanup. |
513 | + ''' |
514 | + |
515 | + if not self.currentResult.wasSuccessful(): |
516 | + # Do not clean up - the only sane option if a test fails. |
517 | + return |
518 | + |
519 | + self.update.destroy() |
520 | + self.update = None |
521 | + |
522 | + shutil.rmtree(self.victim_dir) |
523 | + self.victim_dir = None |
524 | + |
525 | + def run(self, result=None): |
526 | + self.currentResult = result |
527 | + unittest.TestCase.run(self, result) |
528 | + |
529 | + |
530 | +def mock_get_root_partitions_by_label(): |
531 | + matches = [] |
532 | + |
533 | + matches.append(('system-a', '/dev/sda3', '/')) |
534 | + matches.append(('system-b', '/dev/sda4', '/writable/cache/system')) |
535 | + |
536 | + return matches |
537 | + |
538 | + |
539 | +def mock_make_mount_private(target): |
540 | + pass |
541 | + |
542 | + |
543 | +class UbuntuCoreUpgraderObectTestCase(UbuntuCoreUpgraderTestCase): |
544 | + |
545 | + @patch('ubuntucoreupgrader.upgrader.get_root_partitions_by_label', |
546 | + mock_get_root_partitions_by_label) |
547 | + @patch('ubuntucoreupgrader.upgrader.make_mount_private', |
548 | + mock_make_mount_private) |
549 | + def test_create_object(self): |
550 | + root_dir = make_tmp_dir() |
551 | + |
552 | + file = 'created-regular-file' |
553 | + |
554 | + file_path = os.path.join(self.update.system_dir, file) |
555 | + create_file(file_path, 'foo bar') |
556 | + |
557 | + self.cache_dir = self.update.tmp_dir |
558 | + os.makedirs(os.path.join(self.cache_dir, 'system')) |
559 | + |
560 | + archive = self.update.create_archive(self.TARFILE) |
561 | + self.assertTrue(os.path.exists(archive)) |
562 | + |
563 | + dest = os.path.join(self.cache_dir, os.path.basename(archive)) |
564 | + touch_file('{}.asc'.format(dest)) |
565 | + |
566 | + commands = make_commands([self.TARFILE]) |
567 | + |
568 | + options = make_default_options() |
569 | + |
570 | + # XXX: doesn't actually exist, but the option must be set since |
571 | + # the upgrader looks for the update archives in the directory |
572 | + # where this file is claimed to be. |
573 | + options.cmdfile = os.path.join(self.cache_dir, 'ubuntu_command') |
574 | + |
575 | + options.root_dir = root_dir |
576 | + |
577 | + upgrader = Upgrader(options, commands, []) |
578 | + Upgrader.get_cache_dir = lambda s: self.cache_dir |
579 | + upgrader.run() |
580 | + |
581 | + path = os.path.join(root_dir, file) |
582 | + self.assertTrue(os.path.exists(path)) |
583 | + |
584 | + shutil.rmtree(root_dir) |
585 | |
586 | |
587 | if __name__ == "__main__": |
588 | |
589 | === modified file 'ubuntucoreupgrader/upgrader.py' |
590 | --- ubuntucoreupgrader/upgrader.py 2015-03-03 14:36:01 +0000 |
591 | +++ ubuntucoreupgrader/upgrader.py 2015-03-03 16:49:20 +0000 |
592 | @@ -48,6 +48,7 @@ |
593 | import stat |
594 | import errno |
595 | import dbus |
596 | +import argparse |
597 | |
598 | from enum import Enum |
599 | from time import sleep |
600 | @@ -56,6 +57,11 @@ |
601 | |
602 | log = logging.getLogger() |
603 | |
604 | +# seconds to wait before a reboot (should one be required) |
605 | +DEFAULT_REBOOT_DELAY = 2 |
606 | + |
607 | +DEFAULT_ROOT = '/' |
608 | + |
609 | # Name of the writable user data partition label as created by |
610 | # ubuntu-device-flash(1). |
611 | WRITABLE_DATA_LABEL = 'writable' |
612 | @@ -87,7 +93,6 @@ |
613 | TAR_FILE_SYSTEM_PREFIX = 'system/' |
614 | |
615 | # boot assets (kernel, initrd, .dtb's) |
616 | -# (XXX: note no trailing slash to ensure we unpack the directory itself). |
617 | TAR_FILE_ASSETS_PREFIX = 'assets/' |
618 | |
619 | # the file that describes the boot assets |
620 | @@ -98,6 +103,93 @@ |
621 | SYSTEM_IMAGE_CHANNEL_CONFIG = '/etc/system-image/channel.ini' |
622 | |
623 | |
624 | +def parse_args(args): |
625 | + ''' |
626 | + Handle command-line options. |
627 | + |
628 | + Returns: dict of command-line options. |
629 | + ''' |
630 | + |
631 | + parser = argparse.ArgumentParser(description='System image Upgrader') |
632 | + |
633 | + parser.add_argument( |
634 | + '--check-reboot', |
635 | + action='store_true', |
636 | + help=''' |
637 | + Attempt to determine if a reboot may be required. |
638 | + |
639 | + This option is similar to --dry-run: no system changes are made. |
640 | + |
641 | + Note that the result of this command cannot be definitive since a |
642 | + reboot would be triggered if a service failed to start (but this |
643 | + option does not attempt to restart any services). |
644 | + ''') |
645 | + |
646 | + parser.add_argument( |
647 | + '--clean-only', |
648 | + action='store_true', |
649 | + help='Clean up from a previous upgrader run, but do not upgrade)') |
650 | + |
651 | + parser.add_argument( |
652 | + '--debug', |
653 | + nargs='?', const=1, default=0, type=int, |
654 | + help='Dump debug info (specify numeric value to increase verbosity)') |
655 | + |
656 | + parser.add_argument( |
657 | + '-n', '--dry-run', |
658 | + action='store_true', |
659 | + help=''' |
660 | + Simulate an update including showing processes that have locks |
661 | + on files |
662 | + ''') |
663 | + |
664 | + parser.add_argument( |
665 | + '--force-inplace-upgrade', |
666 | + action='store_true', |
667 | + help='Apply an upgrade to current rootfs even if running " \ |
668 | + "on dual rootfs system') |
669 | + |
670 | + parser.add_argument( |
671 | + '--leave-files', |
672 | + action='store_true', |
673 | + help='Do not remove the downloaded system image files after upgrade') |
674 | + |
675 | + parser.add_argument( |
676 | + '--no-reboot', |
677 | + action='store_true', |
678 | + help='Do not reboot even if one would normally be required') |
679 | + |
680 | + parser.add_argument( |
681 | + '--reboot-delay', |
682 | + type=int, default=DEFAULT_REBOOT_DELAY, |
683 | + help=''' |
684 | + Wait for specified number of seconds before rebooting |
685 | + (default={}) |
686 | + '''.format(DEFAULT_REBOOT_DELAY)) |
687 | + |
688 | + parser.add_argument( |
689 | + '--root-dir', |
690 | + default=DEFAULT_ROOT, |
691 | + help='Specify an alternative root directory (for testing ONLY)') |
692 | + |
693 | + parser.add_argument( |
694 | + '--show-other-details', |
695 | + action='store_true', |
696 | + help='Dump the details of the system-image vesion on the " \ |
697 | + "other root partition') |
698 | + |
699 | + parser.add_argument( |
700 | + '-t', '--tmpdir', |
701 | + help='Specify name for pre-existing temporary directory to use') |
702 | + |
703 | + parser.add_argument( |
704 | + 'cmdfile', action="store", |
705 | + nargs='?', |
706 | + help='Name of file containing commands to execute') |
707 | + |
708 | + return parser.parse_args(args=args) |
709 | + |
710 | + |
711 | def remove_prefix(path, prefix=TAR_FILE_SYSTEM_PREFIX): |
712 | ''' |
713 | Remove specified prefix from path and return the result. |
714 | @@ -733,7 +825,7 @@ |
715 | def tar_generator(tar, cache_dir, removed_files, root_dir): |
716 | ''' |
717 | Generator function to handle extracting members from the system |
718 | - image tar file. |
719 | + image tar files. |
720 | ''' |
721 | for member in tar: |
722 | |
723 | @@ -749,6 +841,11 @@ |
724 | # extracted! |
725 | device_prefix = '{}dev/'.format(TAR_FILE_SYSTEM_PREFIX) |
726 | |
727 | + # The partition to update is mounted at "cache_dir/system". |
728 | + # However, the unpack occurs in directory cache_dir (since the |
729 | + # tar files prefixes all system files with |
730 | + # TAR_FILE_SYSTEM_PREFIX). For example, member.name may be |
731 | + # something like "system/dev/null". |
732 | mount_path = '{}/{}'.format(cache_dir, member.name) |
733 | |
734 | unpack = True |
735 | @@ -762,56 +859,72 @@ |
736 | # device already exists |
737 | unpack = False |
738 | |
739 | - if not (member.name == TAR_FILE_HARDWARE_YAML or |
740 | - member.name.startswith(TAR_FILE_SYSTEM_PREFIX) or |
741 | - member.name.startswith(TAR_FILE_ASSETS_PREFIX)): |
742 | + member_prefix = None |
743 | + |
744 | + if member.name.startswith(TAR_FILE_SYSTEM_PREFIX): |
745 | + member_prefix = TAR_FILE_SYSTEM_PREFIX |
746 | + elif member.name.startswith(TAR_FILE_ASSETS_PREFIX): |
747 | + member_prefix = TAR_FILE_ASSETS_PREFIX |
748 | + elif member.name == TAR_FILE_HARDWARE_YAML: |
749 | + member_prefix = "/" |
750 | + |
751 | + if not member_prefix: |
752 | # unrecognised prefix directory |
753 | unpack = False |
754 | |
755 | if not unpack: |
756 | log.debug('not unpacking file {}'.format(member.name)) |
757 | + continue |
758 | + |
759 | + # A modified root directory requires convering |
760 | + # absolute paths to be located below the modified root |
761 | + # directory. |
762 | + if root_dir != '/': |
763 | + if member_prefix == TAR_FILE_SYSTEM_PREFIX: |
764 | + base = remove_prefix(member.name, prefix=member_prefix) |
765 | + member.name = os.path.normpath('{}/{}'.format(root_dir, |
766 | + base)) |
767 | + else: |
768 | + member.name = os.path.normpath('{}/{}'.format(root_dir, |
769 | + member.name)) |
770 | + |
771 | + if member.type in (tarfile.LNKTYPE, tarfile.SYMTYPE) \ |
772 | + and member.linkname.startswith('/'): |
773 | + # Hard and symbolic links also need their |
774 | + # 'source' updated to take account of the root |
775 | + # directory. |
776 | + # |
777 | + # But rather than remove the prefix, we add the |
778 | + # root directory as a prefix to contain the link |
779 | + # within that root. |
780 | + base = os.path.join(root_dir, member.linkname) |
781 | + |
782 | + member.linkname = '{}{}'.format(root_dir, base) |
783 | + |
784 | + path = member.name |
785 | else: |
786 | - # A modified root directory requires convering |
787 | - # absolute paths to be located below the modified root |
788 | - # directory. |
789 | - if root_dir != '/': |
790 | - base = remove_prefix(member.name) |
791 | - member.name = '{}{}'.format(root_dir, base) |
792 | - |
793 | - if member.type in (tarfile.LNKTYPE, tarfile.SYMTYPE) \ |
794 | - and member.linkname.startswith('/'): |
795 | - # Hard and symbolic links also need their |
796 | - # 'source' updated to take account of the root |
797 | - # directory. |
798 | - # |
799 | - # But rather than remove the prefix, we add the |
800 | - # root directory as a prefix to contain the link |
801 | - # within that root. |
802 | - base = os.path.join(root_dir, member.linkname) |
803 | - |
804 | - member.linkname = '{}{}'.format(root_dir, base) |
805 | - else: |
806 | - path = mount_path |
807 | - |
808 | - log.debug('unpacking file {}'.format(member.name)) |
809 | - |
810 | - # If the file is a binary and that binary is currently |
811 | - # being executed by a process, attempting to unpack it |
812 | - # will result in ETXTBSY (OSError: [Errno 26] Text file |
813 | - # busy). The simplest way around this issue is to unlink |
814 | - # the file just before unpacking it (ideally, we'd catch |
815 | - # the exception and handle it separately). This allows |
816 | - # the unpack to continue, and the running process to |
817 | - # continue to use it's (old) version of the binary until |
818 | - # it's corresponding service is restarted. |
819 | - # |
820 | - # Note that at this point, we already have another copy |
821 | - # of the inode below /lost+found/. |
822 | - if not member.isdir() and os.path.lexists(path): |
823 | - log.debug('removing file {}'.format(path)) |
824 | - os.unlink(path) |
825 | - |
826 | - yield member |
827 | + path = mount_path |
828 | + |
829 | + log.debug('unpacking file {} with path {}' |
830 | + .format(member.name, path)) |
831 | + |
832 | + # If the file is a binary and that binary is currently |
833 | + # being executed by a process, attempting to unpack it |
834 | + # will result in ETXTBSY (OSError: [Errno 26] Text file |
835 | + # busy). The simplest way around this issue is to unlink |
836 | + # the file just before unpacking it (ideally, we'd catch |
837 | + # the exception and handle it separately). This allows |
838 | + # the unpack to continue, and the running process to |
839 | + # continue to use it's (old) version of the binary until |
840 | + # it's corresponding service is restarted. |
841 | + # |
842 | + # Note that at this point, we already have another copy |
843 | + # of the inode below /lost+found/. |
844 | + if not member.isdir() and os.path.lexists(path): |
845 | + log.debug('removing file {}'.format(path)) |
846 | + os.unlink(path) |
847 | + |
848 | + yield member |
849 | |
850 | |
851 | class Upgrader(): |
852 | @@ -853,6 +966,14 @@ |
853 | return os.path.join(self.get_cache_dir(), self.MOUNT_TARGET) |
854 | |
855 | def __init__(self, options, commands, remove_list): |
856 | + """ |
857 | + @options list of command-line options. |
858 | + @commands: array of declarative commands to run to apply the new |
859 | + s-i rootfs update. |
860 | + @remove_list: list of files to remove before applying the new |
861 | + image. |
862 | + |
863 | + """ |
864 | self.dispatcher = { |
865 | 'format': self._cmd_format, |
866 | 'load_keyring': self._cmd_load_keyring, |
867 | @@ -882,6 +1003,8 @@ |
868 | |
869 | self.options = options |
870 | |
871 | + assert('root_dir' in self.options) |
872 | + |
873 | # array of imperative commands to run, as generated by |
874 | # system-image-cli(1). |
875 | self.commands = commands |
876 | @@ -909,8 +1032,6 @@ |
877 | if self.options.cmdfile: |
878 | self.file_dir = os.path.dirname(self.options.cmdfile) |
879 | |
880 | - self.systemd = Systemd() |
881 | - |
882 | # list of systemd unit D-Bus paths (such as |
883 | # '/org/freedesktop/systemd1/unit/cron_2eservice') that need to |
884 | # be restarted since they have running processes that are |
885 | @@ -924,12 +1045,6 @@ |
886 | # Note: Only used by UPGRADE_IN_PLACE. |
887 | self.other_is_empty = False |
888 | |
889 | - target = self.get_mount_target() |
890 | - |
891 | - # Note that we need the stat of the mountpoint, |
892 | - # *NOT* the device itself. |
893 | - self.mountpoint_stat = os.stat(target) |
894 | - |
895 | def set_reboot_reason(self, reason): |
896 | ''' |
897 | Set the reboot reason, if not already set. This ensures the |
898 | @@ -959,11 +1074,14 @@ |
899 | Required setup. |
900 | ''' |
901 | |
902 | + target = self.get_mount_target() |
903 | + |
904 | + # Note that we need the stat of the mountpoint, |
905 | + # *NOT* the device itself. |
906 | + self.mountpoint_stat = os.stat(target) |
907 | + |
908 | self.determine_upgrade_type() |
909 | |
910 | - log.debug('System is using systemd version {}' |
911 | - .format(self.systemd.version())) |
912 | - |
913 | log.debug('system upgrade type is {}'.format(self.upgrade_type)) |
914 | if self.options.debug > 1: |
915 | log.debug('current rootfs device is {}' |
916 | @@ -1080,10 +1198,11 @@ |
917 | log.warning('Not rebooting at user request') |
918 | return |
919 | |
920 | + reboot_delay = self.options.reboot_delay |
921 | # give the admin a chance to see the message |
922 | log.debug('Waiting for {} seconds before rebooting' |
923 | - .format(self.options.reboot_delay)) |
924 | - sleep(self.options.reboot_delay) |
925 | + .format(reboot_delay)) |
926 | + sleep(reboot_delay) |
927 | os.system('/sbin/reboot') |
928 | |
929 | def _cmd_format(self, args): |
930 | @@ -1436,7 +1555,7 @@ |
931 | # It will either be a sym-link or a bind-mount |
932 | # unrelated to those specified by writable-paths(5). |
933 | if st.st_dev != self.mountpoint_stat.st_dev: |
934 | - if self.options.debug > 1: |
935 | + if self.options.debug and self.options.debug > 1: |
936 | log.debug('Ignoring cross-FS file: {}' |
937 | .format(file)) |
938 | continue |
939 | @@ -1455,7 +1574,7 @@ |
940 | # - sym links to directories. |
941 | # |
942 | try: |
943 | - if self.options.debug > 1: |
944 | + if self.options.debug and self.options.debug > 1: |
945 | log.debug('creating directory {}' |
946 | .format(saved_link_dirname)) |
947 | os.makedirs(saved_link_dirname, |
948 | @@ -1631,8 +1750,7 @@ |
949 | # os.path.join() refuses to work if the file |
950 | # begins with a slash. |
951 | base = remove_prefix(remove) |
952 | - final = '{}{}'.format(self.options.root_dir, |
953 | - base) |
954 | + final = '{}{}'.format(self.options.root_dir, base) |
955 | |
956 | if not os.path.exists(final): |
957 | # This scenario can only mean there is a bug |
958 | @@ -1672,7 +1790,8 @@ |
959 | tar.extractall(path=self.get_cache_dir(), |
960 | members=tar_generator( |
961 | tar, self.get_cache_dir(), |
962 | - self.removed_file, self.options.root_dir)) |
963 | + self.removed_file, |
964 | + self.options.root_dir)) |
965 | tar.close() |
966 | |
967 | if self.upgrade_type == self.upgrade_types.UPGRADE_AB_PARTITIONS: |
968 | @@ -1793,6 +1912,11 @@ |
969 | ''' |
970 | failed = False |
971 | |
972 | + self.systemd = Systemd() |
973 | + |
974 | + log.debug('System is using systemd version {}' |
975 | + .format(self.systemd.version())) |
976 | + |
977 | for file in self.pid_file_map: |
978 | for pid in self.pid_file_map[file]: |
979 | ret = self.restart_associated_service(pid, file) |
Hrm, looks like LP didn't take the Prerequisite branch into account when generating the diff. Thats a bit disappointing.