Merge lp:~mvo/ubuntu/vivid/ubuntu-core-upgrader/more-tests-tweaks into lp:ubuntu/vivid/ubuntu-core-upgrader

Proposed by Michael Vogt
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
Reviewer Review Type Date Requested Status
James Hunt Pending
Review via email: mp+251626@code.launchpad.net

Description of the change

Some tweaks to the tests.

To post a comment you must log in.
Revision history for this message
Michael Vogt (mvo) wrote :

Hrm, looks like LP didn't take the Prerequisite branch into account when generating the diff. Thats a bit disappointing.

Revision history for this message
William Grant (wgrant) wrote :

The prereq seems to have been rebased (r13 here and r15 in the prereq seem suspiciously identical).

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

merged lp:ubuntu/vivid/ubuntu-core-upgrader

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches

to all changes: