From e3d1617342fa5b286fec19d8fd4de04730d359c7 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 29 Sep 2017 11:33:45 +0200 Subject: [PATCH 01/22] systemd-sysusers.bbclass: fix removal of systemd_create_users Not sure whether it has been broken all along, but at least with current OE-core master, systemd_create_users was still active because it gets added to ROOTFS_POSTPROCESS_COMMAND with a semicolon directly after the word. Removing it is still relevant because YOCTO #9789 remains unfixed. Signed-off-by: Patrick Ohly --- meta-refkit-core/classes/systemd-sysusers.bbclass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta-refkit-core/classes/systemd-sysusers.bbclass b/meta-refkit-core/classes/systemd-sysusers.bbclass index a6bdcbc5cc..910cb1d74f 100644 --- a/meta-refkit-core/classes/systemd-sysusers.bbclass +++ b/meta-refkit-core/classes/systemd-sysusers.bbclass @@ -80,4 +80,4 @@ ROOTFS_POSTPROCESS_COMMAND += "${@bb.utils.contains('DISTRO_FEATURES', 'systemd' # available in OE-core. However, that code is still not suitable # (https://bugzilla.yoctoproject.org/show_bug.cgi?id=9789) and thus we # have to use our own version. -ROOTFS_POSTPROCESS_COMMAND_remove = "systemd_create_users" +ROOTFS_POSTPROCESS_COMMAND_remove = "systemd_create_users systemd_create_users;" From ecd71561a5fc88d6fa2601a4f4eb3336f3e5d6ad Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 18 Sep 2017 21:34:09 +0200 Subject: [PATCH 02/22] refkit_ostree.py: fix RefkitOSTreeUpdateTestDev The code which created temporary image recipes was obsolete and had no effect anymore, because permanent image recipes had to be added instead (otherwise bitbake removed image artifacts when it saw that a recipe was gone), and those now have higher priority than then created ones. However, OSTREE_BRANCHNAME must be set in the permanent recipe, otherwise updating doesn't do anything. Signed-off-by: Patrick Ohly --- .../lib/oeqa/selftest/cases/refkit_ostree.py | 13 ------------- .../images/refkit-image-update-ostree-modified.bb | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py b/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py index 09793ee530..af33bf8f09 100644 --- a/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py +++ b/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py @@ -176,16 +176,3 @@ class RefkitOSTreeUpdateTestDev(RefkitOSTreeUpdateTestAll, metaclass=RefkitOSTre IMAGE_PN_UPDATE = 'refkit-image-update-ostree-modified' IMAGE_BBAPPEND_UPDATE = IMAGE_PN_UPDATE + '.bbappend' - - def setUpLocal(self): - super().setUpLocal() - def create_image_bb(pn): - bb = pn + '.bb' - self.track_for_cleanup(bb) - self.append_config('BBFILES_append = " %s"' % os.path.abspath(bb)) - with open(bb, 'w') as f: - f.write('require ${META_REFKIT_CORE_BASE}/recipes-images/images/refkit-image-common.bb\n') - f.write('OSTREE_BRANCHNAME = "${DISTRO}/${MACHINE}/%s"\n' % self.IMAGE_PN) - f.write('''IMAGE_FEATURES_append = "${@ bb.utils.filter('DISTRO_FEATURES', 'stateless', d)}"\n''') - create_image_bb(self.IMAGE_PN) - create_image_bb(self.IMAGE_PN_UPDATE) diff --git a/meta-refkit-core/recipes-selftest/images/refkit-image-update-ostree-modified.bb b/meta-refkit-core/recipes-selftest/images/refkit-image-update-ostree-modified.bb index 1b4976ee1c..6708eba54d 100644 --- a/meta-refkit-core/recipes-selftest/images/refkit-image-update-ostree-modified.bb +++ b/meta-refkit-core/recipes-selftest/images/refkit-image-update-ostree-modified.bb @@ -4,4 +4,4 @@ SUMMARY = "test image for RefkitOSTreeUpdateTest: refkit-image-common + OSTree + # refkit-image-update-ostree-modified when running the test multiple # times. require refkit-image-update-ostree.bb -# OSTREE_BRANCHNAME = "${DISTRO}/${MACHINE}/refkit-image-update-ostree" +OSTREE_BRANCHNAME = "${DISTRO}/${MACHINE}/refkit-image-update-ostree" From d9a07b78bc5b87f529fbc9d46f1432f3c2fea6ea Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 18 Sep 2017 21:38:23 +0200 Subject: [PATCH 03/22] refkit_ostree.py: refactor code The part about running an update test with an HTTP server serving files is common to OSTree and swupd, so it makes sense to have that in a separate base class. Signed-off-by: Patrick Ohly --- .../lib/oeqa/selftest/cases/refkit_ostree.py | 146 ++++-------------- .../oeqa/selftest/systemupdate/httpupdate.py | 124 +++++++++++++++ 2 files changed, 150 insertions(+), 120 deletions(-) create mode 100644 meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py diff --git a/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py b/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py index af33bf8f09..af5eadfb85 100644 --- a/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py +++ b/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py @@ -1,15 +1,8 @@ -from oeqa.selftest.systemupdate.systemupdatebase import SystemUpdateBase +from oeqa.selftest.systemupdate.httpupdate import HTTPUpdate -from oeqa.utils.commands import runqemu, get_bb_vars, bitbake - -import errno -import http.server import os -import stat -import tempfile -import threading -class RefkitOSTreeUpdateBase(SystemUpdateBase): +class RefkitOSTreeUpdateBase(HTTPUpdate): """ System update tests for refkit-image-common using OSTree. """ @@ -21,52 +14,9 @@ class RefkitOSTreeUpdateBase(SystemUpdateBase): IMAGE_BBAPPEND = IMAGE_PN + '.bbappend' IMAGE_BBAPPEND_UPDATE = IMAGE_BBAPPEND - # Address and port of OSTree HTTPD inside the virtual machine's - # slirp network. - OSTREE_SERVER = '10.0.2.100:8080' - - # Global variables are the same for all recipes, - # but RECIPE_SYSROOT_NATIVE is specific to socat-native. - BB_VARS = get_bb_vars([ - 'DEPLOY_DIR', - 'MACHINE', - 'RECIPE_SYSROOT_NATIVE', - ], - 'socat-native') - - def track_for_cleanup(self, name): - """ - Run a single test with NO_CLEANUP= oe-selftest to not clean up after the test. - """ - if 'NO_CLEANUP' not in os.environ: - super().track_for_cleanup(name) - - def boot_image(self, overrides): - # We don't know the final port yet, so instead we create a placeholder script - # for qemu to use and rewrite that script once we are ready. The kernel refuses - # to execute a shell script while we have it open, so here we close it - # and clean up ourselves. - # - # The helper script also keeps command line handling a bit simpler (no whitespace - # in -netdev parameter), which may or may not be relevant. - self.ostree_netcat = tempfile.NamedTemporaryFile(mode='w', prefix='ostree-netcat-', dir=os.getcwd(), delete=False) - self.ostree_netcat.close() - os.chmod(self.ostree_netcat.name, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR) - self.track_for_cleanup(self.ostree_netcat.name) - - qemuboot_conf = os.path.join(self.image_dir_test, - '%s-%s.qemuboot.conf' % (self.IMAGE_PN, self.BB_VARS['MACHINE'])) - with open(qemuboot_conf) as f: - conf = f.read() - with open(qemuboot_conf, 'w') as f: - f.write('\n'.join([x for x in conf.splitlines() if not x.startswith('qb_slirp_opt')])) - f.write('\nqb_slirp_opt = -netdev user,id=net0,guestfwd=tcp:%s-cmd:%s\n' % \ - (self.OSTREE_SERVER, self.ostree_netcat.name)) - return runqemu(self.IMAGE_PN, - discard_writes=False, ssh=False, - overrides=overrides, - runqemuparams='ovmf slirp nographic', - image_fstype='wic') + # We cannot get the actual OSTREE_REPO for the + # image here, so we just assume that it is in the usual place. + REPO_DIR = os.path.join(HTTPUpdate.BB_VARS['DEPLOY_DIR'], 'ostree-repo') def stop_update_service(self, qemu): cmd = '''systemctl stop refkit-update.service''' @@ -76,72 +26,28 @@ def stop_update_service(self, qemu): return True def update_image(self, qemu): - # We need to bring up some simple HTTP server for the - # OSTree repo. We cannot get the actual OSTREE_REPO for the - # image here, so we just assume that it is in the usual place. - # For the sake of simplicity we change into that directory - # because then we can use SimpleHTTPRequestHandler. - ostree_repo = os.path.join(self.BB_VARS['DEPLOY_DIR'], 'ostree-repo') - old_cwd = os.getcwd() - server = None - try: - # We need to stop the refkit-udpate systemd service before starting - # the HTTP server (and thus making any update available) to prevent - # the service from racing with us and potentially winning, doing a - # full update cycle including a final reboot. - self.stop_update_service(qemu) - - os.chdir(ostree_repo) - class OSTreeHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): - def log_message(s, format, *args): - msg = format % args - self.logger.info(msg) - - handler = OSTreeHTTPRequestHandler - - def create_httpd(): - for port in range(9999, 10000): - try: - server = http.server.HTTPServer(('localhost', port), handler) - return server - except OSError as ex: - if ex.errno != errno.EADDRINUSE: - raise - self.fail('no port available for OSTree HTTP server') - - server = create_httpd() - port = server.server_port - self.logger.info('serving OSTree repo %s on port %d' % (ostree_repo, port)) - helper = threading.Thread(name='OSTree HTTPD', target=server.serve_forever) - helper.start() - # netcat can't be assumed to be present. Build and use socat instead. - # It's a bit more complicated but has the advantage that it is in OE-core. - socat = os.path.join(self.BB_VARS['RECIPE_SYSROOT_NATIVE'], 'usr', 'bin', 'socat') - if not os.path.exists(socat): - bitbake('socat-native:do_addto_recipe_sysroot', output_log=self.logger) - self.assertExists(socat, 'socat-native was not built as expected') - with open(self.ostree_netcat.name, 'w') as f: - f.write('''#!/bin/sh -exec %s 2>>/tmp/ostree.log -D -v -d -d -d -d STDIO TCP:localhost:%d -''' % (socat, port)) + # We need to stop the refkit-udpate systemd service before starting + # the HTTP server (and thus making any update available) to prevent + # the service from racing with us and potentially winning, doing a + # full update cycle including a final reboot. + self.stop_update_service(qemu) + + return super().update_image(qemu) + + def update_image_via_http(self, qemu): + # Use the updater, refkit-ostree-update, in a one-shot mode + # attempting just a single update cycle for the test case. + # Also override the post-apply hook to only run the UEFI app + # update hook. It is a bit of a hack but we don't want the rest + # of the hooks run, especially not the reboot hook, to avoid + # prematurely rebooting the qemu instance and this is the easiest + # way to achieve just that for now. + cmd = '''ostree config set 'remote "updates".url' http://%s && refkit-ostree-update --one-shot --post-apply-hook /usr/share/refkit-ostree/hooks/post-apply.d/00-update-uefi-app''' % self.HTTPD_SERVER + status, output = qemu.run_serial(cmd, timeout=600) + self.assertEqual(1, status, 'Failed to run command "%s":\n%s' % (cmd, output)) + self.logger.info('Successful (?) update with %s:\n%s' % (cmd, output)) + return True - # Use the updater, refkit-ostree-update, in a one-shot mode - # attempting just a single update cycle for the test case. - # Also override the post-apply hook to only run the UEFI app - # update hook. It is a bit of a hack but we don't want the rest - # of the hooks run, especially not the reboot hook, to avoid - # prematurely rebooting the qemu instance and this is the easiest - # way to achieve just that for now. - cmd = '''ostree config set 'remote "updates".url' http://%s && refkit-ostree-update --one-shot --post-apply-hook /usr/share/refkit-ostree/hooks/post-apply.d/00-update-uefi-app''' % self.OSTREE_SERVER - status, output = qemu.run_serial(cmd, timeout=600) - self.assertEqual(1, status, 'Failed to run command "%s":\n%s' % (cmd, output)) - self.logger.info('Successful (?) update:\n%s' % output) - return True - finally: - os.chdir(old_cwd) - if server: - server.shutdown() - server.server_close() class RefkitOSTreeUpdateTestAll(RefkitOSTreeUpdateBase): def test_update_all(self): diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py new file mode 100644 index 0000000000..f97e40633c --- /dev/null +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py @@ -0,0 +1,124 @@ +from oeqa.selftest.systemupdate.systemupdatebase import SystemUpdateBase +from oeqa.utils.commands import runqemu, get_bb_vars, bitbake + +import http.server +import os +import stat +import errno +import tempfile +import threading + +class HTTPUpdate(SystemUpdateBase): + """ + System update tests for image update mechanisms which depend on + and HTTP server that provides files to the virtual machine. + + Uses SLIRP networking and thus can be used for images which + rely on a DHCP server. + """ + + # Address and port of HTTPD inside the virtual machine's + # slirp network. + HTTPD_SERVER = '10.0.2.100:8080' + + # Global variables are the same for all recipes, + # but RECIPE_SYSROOT_NATIVE is specific to socat-native. + BB_VARS = get_bb_vars([ + 'DEPLOY_DIR', + 'MACHINE', + 'RECIPE_SYSROOT_NATIVE', + ], + 'socat-native') + + # To be set by derived class. + REPO_DIR = None + + def track_for_cleanup(self, name): + """ + Run a single test with NO_CLEANUP= oe-selftest to not clean up after the test. + """ + if 'NO_CLEANUP' not in os.environ: + super().track_for_cleanup(name) + + def boot_image(self, overrides): + # We don't know the final port yet, so instead we create a placeholder script + # for qemu to use and rewrite that script once we are ready. The kernel refuses + # to execute a shell script while we have it open, so here we close it + # and clean up ourselves. + # + # The helper script also keeps command line handling a bit simpler (no whitespace + # in -netdev parameter), which may or may not be relevant. + self.httpd_netcat = tempfile.NamedTemporaryFile(mode='w', prefix='httpd-netcat-', dir=os.getcwd(), delete=False) + self.httpd_netcat.close() + os.chmod(self.httpd_netcat.name, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR) + self.track_for_cleanup(self.httpd_netcat.name) + + qemuboot_conf = os.path.join(self.image_dir_test, + '%s-%s.qemuboot.conf' % (self.IMAGE_PN, self.BB_VARS['MACHINE'])) + with open(qemuboot_conf) as f: + conf = f.read() + with open(qemuboot_conf, 'w') as f: + f.write('\n'.join([x for x in conf.splitlines() if not x.startswith('qb_slirp_opt')])) + f.write('\nqb_slirp_opt = -netdev user,id=net0,guestfwd=tcp:%s-cmd:%s\n' % \ + (self.HTTPD_SERVER, self.httpd_netcat.name)) + return runqemu(self.IMAGE_PN, + discard_writes=False, ssh=False, + overrides=overrides, + runqemuparams='ovmf slirp nographic', + image_fstype='wic') + + def update_image(self, qemu): + # We need to bring up some simple HTTP server for the + # update repo. For the sake of simplicity we change into that directory + # because then we can use SimpleHTTPRequestHandler. + old_cwd = os.getcwd() + server = None + try: + os.chdir(self.REPO_DIR) + class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def log_message(s, format, *args): + msg = format % args + self.logger.info(msg) + + handler = HTTPRequestHandler + + def create_httpd(): + for port in range(9999, 10000): + try: + server = http.server.HTTPServer(('localhost', port), handler) + return server + except OSError as ex: + if ex.errno != errno.EADDRINUSE: + raise + self.fail('no port available for HTTP server') + + server = create_httpd() + port = server.server_port + self.logger.info('serving repo %s on port %d' % (self.REPO_DIR, port)) + helper = threading.Thread(name='HTTPD', target=server.serve_forever) + helper.start() + # netcat can't be assumed to be present. Build and use socat instead. + # It's a bit more complicated but has the advantage that it is in OE-core. + socat = os.path.join(self.BB_VARS['RECIPE_SYSROOT_NATIVE'], 'usr', 'bin', 'socat') + if not os.path.exists(socat): + bitbake('socat-native:do_addto_recipe_sysroot', output_log=self.logger) + self.assertExists(socat, 'socat-native was not built as expected') + with open(self.httpd_netcat.name, 'w') as f: + f.write('''#!/bin/sh +exec %s 2>/tmp/httpd.log -D -v -d -d -d -d STDIO TCP:localhost:%d +''' % (socat, port)) + + # Now run the real update command inside the virtual machine. + return self.update_image_via_http(qemu) + + finally: + os.chdir(old_cwd) + if server: + server.shutdown() + server.server_close() + + def update_image_via_http(self, qemu): + """ + Called by update_image() with the HTTPD server running. + """ + return False From 286e02915d6d8344aeecfab9b8c0817b66955c6f Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 12 Oct 2017 08:17:17 +0200 Subject: [PATCH 04/22] refkit_ostree.py: delay bitbake -e call Calling "bitbake -e" during class construction slows down all oe-selftest invocations, whether the result is needed or not, and happens before oe-selftest prints any output. It's better to trigger the call only when needed while still caching the result across all classes that need the information. Signed-off-by: Patrick Ohly --- .../lib/oeqa/selftest/cases/refkit_ostree.py | 8 +++--- .../oeqa/selftest/systemupdate/httpupdate.py | 27 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py b/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py index af5eadfb85..fd304ec5aa 100644 --- a/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py +++ b/meta-refkit-core/lib/oeqa/selftest/cases/refkit_ostree.py @@ -14,9 +14,11 @@ class RefkitOSTreeUpdateBase(HTTPUpdate): IMAGE_BBAPPEND = IMAGE_PN + '.bbappend' IMAGE_BBAPPEND_UPDATE = IMAGE_BBAPPEND - # We cannot get the actual OSTREE_REPO for the - # image here, so we just assume that it is in the usual place. - REPO_DIR = os.path.join(HTTPUpdate.BB_VARS['DEPLOY_DIR'], 'ostree-repo') + def setUp(self): + # We cannot get the actual OSTREE_REPO for the + # image here, so we just assume that it is in the usual place. + self.REPO_DIR = os.path.join(HTTPUpdate.BB_VARS['DEPLOY_DIR'], 'ostree-repo') + super().setUp() def stop_update_service(self, qemu): cmd = '''systemctl stop refkit-update.service''' diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py index f97e40633c..bce78d849a 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py @@ -23,14 +23,25 @@ class HTTPUpdate(SystemUpdateBase): # Global variables are the same for all recipes, # but RECIPE_SYSROOT_NATIVE is specific to socat-native. - BB_VARS = get_bb_vars([ - 'DEPLOY_DIR', - 'MACHINE', - 'RECIPE_SYSROOT_NATIVE', - ], - 'socat-native') - - # To be set by derived class. + # We store that in the class because then it can be shared by + # multiple derived instances. + class DelayedGetVars: + def __init__(self): + self._cache = None + + def __getitem__(self, key): + if self._cache is None: + self._cache = get_bb_vars([ + 'DEPLOY_DIR', + 'MACHINE', + 'RECIPE_SYSROOT_NATIVE', + ], + 'socat-native') + return self._cache[key] + + BB_VARS = DelayedGetVars() + + # To be set by derived class or instance. REPO_DIR = None def track_for_cleanup(self, name): From 6f794b21ee5b6b8585369a76892df2fa477406f2 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 20 Sep 2017 11:39:31 +0200 Subject: [PATCH 05/22] systemupdatebase.py: simplify image build configuration modify_image_build() is run outside of bitbake and thus is better suited for SystemUpdateBase. Extending SystemUpdateModify is harder because it needs to be pickled. Signed-off-by: Patrick Ohly --- .../selftest/systemupdate/systemupdatebase.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py index 2acc78e317..55de730bcb 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py @@ -43,16 +43,6 @@ class SystemUpdateModify(object): ( 'ssh/sshd_config', None ), ] - def modify_image_build(self, testname, updates, is_update): - """ - Returns additional settings that get stored in a .bbappend - of the test image. - """ - bbappend = [] - if 'kernel' in updates: - bbappend.append('APPEND_append = " modify_kernel_test=%s"' % ('updated' if is_update else 'original')) - return '\n'.join(bbappend) - def modify_kernel(self, testname, is_update, rootfs): """ Patch the kernel in an existing rootfs. Called during rootfs construction, @@ -215,6 +205,16 @@ def verify_image(self, testname, is_update, qemu, updates): for update in updates: getattr(self.IMAGE_MODIFY, 'verify_' + update)(testname, is_update, qemu, self) + def modify_image_build(self, testname, updates, is_update): + """ + Returns additional settings that get stored in a .bbappend + of the test image. + """ + bbappend = [] + if 'kernel' in updates: + bbappend.append('APPEND_append = " modify_kernel_test=%s"' % ('updated' if is_update else 'original')) + return '\n'.join(bbappend) + def do_update(self, testname, updates): """ Builds the image, makes a copy of the result, rebuilds to produce @@ -255,7 +255,7 @@ def create_image_bbappend(is_update): updates, is_update, self.IMAGE_CONFIG, - self.IMAGE_MODIFY.modify_image_build(testname, updates, is_update))) + self.modify_image_build(testname, updates, is_update))) # Creating a .bbappend for the image will trigger a rebuild. # To avoid this, use separate image recipes. From d7512f48b2b4b2e55b53ac02b301f02745d8a220 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 10 Oct 2017 15:22:26 +0200 Subject: [PATCH 06/22] systemupdatebase.py: don't depend on OpenSSH Modifying sshd_config only works when OpenSSH is installed, which is otherwise not needed for testing because commands are run via serial console. Using udev instead allows to use smaller test images. Signed-off-by: Patrick Ohly --- .../lib/oeqa/selftest/systemupdate/systemupdatebase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py index 55de730bcb..49dd8f00b0 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py @@ -40,7 +40,7 @@ class SystemUpdateModify(object): ETC_FILES = [ ( 'nsswitch.conf', 'edit' ), ( 'ssl/openssl.cnf', 'symlink' ), - ( 'ssh/sshd_config', None ), + ( 'udev/udev.conf', None ), ] def modify_kernel(self, testname, is_update, rootfs): From e606495ef490d5825f519ffda6c08d5cec42bd89 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 12 Oct 2017 07:35:47 +0200 Subject: [PATCH 07/22] systemupdatebase.py: test large file update This is relevant for checking delta computation in update mechanisms that support that, like swupd. While at it, rewrite the code to use pathlib more consistently. Signed-off-by: Patrick Ohly --- .../selftest/systemupdate/systemupdatebase.py | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py index 49dd8f00b0..ad5eb4859d 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py @@ -10,6 +10,7 @@ import fnmatch import pathlib import pickle +import random import shutil import subprocess @@ -43,6 +44,12 @@ class SystemUpdateModify(object): ( 'udev/udev.conf', None ), ] + # A large file with printable content that does not compress well. + LARGE_FILE_SIZE = 8 * 1024 * 1024 + random.seed(1) + LARGE_FILE_CONTENT = ''.join([ random.choice('!"#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~') for x in range(0, LARGE_FILE_SIZE) ]) + LARGE_FILE_CONTENT_APPEND = 'hello' + def modify_kernel(self, testname, is_update, rootfs): """ Patch the kernel in an existing rootfs. Called during rootfs construction, @@ -66,14 +73,29 @@ def modify_files(self, testname, is_update, rootfs): """ Simulate simple adding, removing and modifying of files under /usr/bin. """ - testdir = os.path.join(rootfs, 'usr', 'bin') + testdir = pathlib.Path(rootfs) / 'usr' / 'bin' + remove_me = testdir / 'modify_files_remove_me' + update_me = testdir / 'modify_files_update_me' + was_added = testdir / 'modify_files_was_added' + large = testdir / 'modify_files_large' + + # Add/remove file cases. if not is_update: - pathlib.Path(os.path.join(testdir, 'modify_files_remove_me')).touch() - pathlib.Path(os.path.join(testdir, 'modify_files_update_me')).touch() + remove_me.touch() else: - with open(os.path.join(testdir, 'modify_files_update_me'), 'w') as f: + was_added.touch() + + # This is case where the full new file is smaller than a delta. + with update_me.open('w') as f: + if is_update: f.write('updated\n') - pathlib.Path(os.path.join(testdir, 'modify_files_was_added')).touch() + + # Whereas for a large file, a binary delta is more efficient. + with large.open('w') as f: + f.write(self.LARGE_FILE_CONTENT) + if is_update: + f.write(self.LARGE_FILE_CONTENT_APPEND) + f.write('\n') def verify_files(self, testname, is_update, qemu, test): """ @@ -83,14 +105,25 @@ def verify_files(self, testname, is_update, qemu, test): status, output = qemu.run_serial(cmd) test.assertEqual(1, status, 'Failed to run command "%s":\n%s' % (cmd, output)) if not is_update: - test.assertEqual(output, '/usr/bin/modify_files_remove_me\r\n/usr/bin/modify_files_update_me') + test.assertEqual(output, '/usr/bin/modify_files_large\r\n/usr/bin/modify_files_remove_me\r\n/usr/bin/modify_files_update_me') else: - test.assertEqual(output, '/usr/bin/modify_files_update_me\r\n/usr/bin/modify_files_was_added') + test.assertEqual(output, '/usr/bin/modify_files_large\r\n/usr/bin/modify_files_update_me\r\n/usr/bin/modify_files_was_added') cmd = 'cat /usr/bin/modify_files_update_me' status, output = qemu.run_serial(cmd) test.assertEqual(1, status, 'Failed to run command "%s":\n%s' % (cmd, output)) test.assertEqual(output, 'updated') + cmd = 'head -c 20 /usr/bin/modify_files_large && tail -c 20 /usr/bin/modify_files_large' + status, output = qemu.run_serial(cmd) + test.assertEqual(1, status, 'Failed to run command "%s":\n%s' % (cmd, output)) + expected = self.LARGE_FILE_CONTENT + if is_update: + expected += self.LARGE_FILE_CONTENT_APPEND + # There's a trailing newline in the large file that we loose + # when capturing the output, hence the 19 instead of 20 bytes. + expected = expected[:20] + expected[-19:] + test.assertEqual(expected, output) + def modify_etc(self, testname, is_update, rootfs): """ If there are files in /etc, then it should be possible to update From a02423e238f09212a6280b34726f3489b8cba524 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 20 Nov 2017 15:44:00 +0100 Subject: [PATCH 08/22] systemupdatebase.py: add directory Besides adding a new file, adding a new directory also tends to be an interesting special case. For example, swupd server relies a lot on hardlinking, which does not work for directories. Signed-off-by: Patrick Ohly --- .../lib/oeqa/selftest/systemupdate/systemupdatebase.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py index ad5eb4859d..2372f39fe9 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py @@ -77,6 +77,7 @@ def modify_files(self, testname, is_update, rootfs): remove_me = testdir / 'modify_files_remove_me' update_me = testdir / 'modify_files_update_me' was_added = testdir / 'modify_files_was_added' + was_added_dir = testdir / 'modify_files_new_dir' large = testdir / 'modify_files_large' # Add/remove file cases. @@ -84,6 +85,7 @@ def modify_files(self, testname, is_update, rootfs): remove_me.touch() else: was_added.touch() + was_added_dir.mkdir() # This is case where the full new file is smaller than a delta. with update_me.open('w') as f: @@ -101,14 +103,14 @@ def verify_files(self, testname, is_update, qemu, test): """ Sanity check files before and after update. """ - cmd = 'ls -1 /usr/bin/modify_files_*' + cmd = 'ls -1 -d /usr/bin/modify_files_*' status, output = qemu.run_serial(cmd) test.assertEqual(1, status, 'Failed to run command "%s":\n%s' % (cmd, output)) if not is_update: test.assertEqual(output, '/usr/bin/modify_files_large\r\n/usr/bin/modify_files_remove_me\r\n/usr/bin/modify_files_update_me') else: - test.assertEqual(output, '/usr/bin/modify_files_large\r\n/usr/bin/modify_files_update_me\r\n/usr/bin/modify_files_was_added') - cmd = 'cat /usr/bin/modify_files_update_me' + test.assertEqual(output, '/usr/bin/modify_files_large\r\n/usr/bin/modify_files_new_dir\r\n/usr/bin/modify_files_update_me\r\n/usr/bin/modify_files_was_added') + cmd = 'test -d /usr/bin/modify_files_new_dir && cat /usr/bin/modify_files_update_me' status, output = qemu.run_serial(cmd) test.assertEqual(1, status, 'Failed to run command "%s":\n%s' % (cmd, output)) test.assertEqual(output, 'updated') From 6a9e8166dda157efca9d8c71930d81abd85533ac Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Tue, 10 Oct 2017 15:40:36 +0200 Subject: [PATCH 09/22] httpupdate.py: log HTTP requests Tests then can use that information to ensure that exactly the right HTTP requests were made by the client. Signed-off-by: Patrick Ohly --- meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py index bce78d849a..c74e4038d9 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py @@ -84,12 +84,15 @@ def update_image(self, qemu): # because then we can use SimpleHTTPRequestHandler. old_cwd = os.getcwd() server = None + self.http_log = [] + http_log = self.http_log try: os.chdir(self.REPO_DIR) class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): def log_message(s, format, *args): msg = format % args self.logger.info(msg) + self.http_log.append(msg) handler = HTTPRequestHandler From 91c3bdb1f8554a2d8a88c79b8f9924cabd70d408 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 11 Oct 2017 10:26:14 +0200 Subject: [PATCH 10/22] httpupdate.py: avoid changing directories Changing directories for SimpleHTTPRequestHandler has side effects like invoking bitbake in the repo directory, where bitbake then creates its bitbake-cookerdaemon.log file. That breaks testing randomly, depending on the timing of daemon startup/shutdown. We can continue to use SimpleHTTPRequestHandler without changing directories by tweaking its (internal?!) translate_path() implementation a bit. Signed-off-by: Patrick Ohly --- .../oeqa/selftest/systemupdate/httpupdate.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py index c74e4038d9..b3cd70409d 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py @@ -80,19 +80,34 @@ def boot_image(self, overrides): def update_image(self, qemu): # We need to bring up some simple HTTP server for the - # update repo. For the sake of simplicity we change into that directory - # because then we can use SimpleHTTPRequestHandler. - old_cwd = os.getcwd() + # update repo. server = None self.http_log = [] http_log = self.http_log try: - os.chdir(self.REPO_DIR) class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): - def log_message(s, format, *args): + parent = self + def log_message(self, format, *args): msg = format % args - self.logger.info(msg) - self.http_log.append(msg) + self.parent.logger.info(msg) + self.parent.http_log.append(msg) + + def translate_path(self, path): + """ + Return absolute path based on document root instead of current directory. + """ + + # The original implementation returns an absolute path rooted in the + # current directory. We need to serve a different + # directory without being able to chdir(), because + # doing that would cause commands like bitbake to + # run there, which is undesirable because for + # example bitbake creates a bitbake-cookerdaemon.log + # in the current directory. + path = super().translate_path(path) + relpath = os.path.relpath(path) + path = os.path.join(self.parent.REPO_DIR, relpath) + return path handler = HTTPRequestHandler @@ -126,7 +141,6 @@ def create_httpd(): return self.update_image_via_http(qemu) finally: - os.chdir(old_cwd) if server: server.shutdown() server.server_close() From 1bb357846a19bf42b5586bece21c1394cc06452f Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 20 Oct 2017 16:46:19 +0200 Subject: [PATCH 11/22] httpupdate.py: inject 500 errors This can be used test the behavior when there are server-side errors. Signed-off-by: Patrick Ohly --- .../lib/oeqa/selftest/systemupdate/httpupdate.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py index b3cd70409d..c962e56c3a 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py @@ -87,6 +87,7 @@ def update_image(self, qemu): try: class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): parent = self + request_counter = 0 def log_message(self, format, *args): msg = format % args self.parent.logger.info(msg) @@ -109,6 +110,18 @@ def translate_path(self, path): path = os.path.join(self.parent.REPO_DIR, relpath) return path + def do_GET(self): + """ + Inject errors. + """ + counter = HTTPRequestHandler.request_counter + HTTPRequestHandler.request_counter += 1 + stop_at = getattr(self.parent, 'stop_serving_http_at', None) + if stop_at is not None and counter >= stop_at: + self.send_error(500, 'test server is intentionally down') + else: + super().do_GET() + handler = HTTPRequestHandler def create_httpd(): From aeaa7e7f05dd19fbb05259cb06fb62dda6b6b9f8 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 23 Oct 2017 14:26:52 +0200 Subject: [PATCH 12/22] systemupdate: refactor code Much of the code relied on a running code in certain contexts. So far, that was solved via callbacks, which made reusing some subset of the functionality or parameterizing tests harder. Now the flow is broken up into individual steps, with context managers used whenever cleanup operations are needed. Signed-off-by: Patrick Ohly --- .../oeqa/selftest/systemupdate/httpupdate.py | 214 +++++++++++------- .../selftest/systemupdate/systemupdatebase.py | 91 +++++--- 2 files changed, 181 insertions(+), 124 deletions(-) diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py index c962e56c3a..dc65fd2caa 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py @@ -1,13 +1,101 @@ from oeqa.selftest.systemupdate.systemupdatebase import SystemUpdateBase from oeqa.utils.commands import runqemu, get_bb_vars, bitbake +import contextlib import http.server import os import stat import errno import tempfile +import traceback import threading +class HTTPServer(object): + """ + Dynamically finds an available port and serves a certain directory there. + To be used in a "with HTTPServer(dir) as httpd" construct. + """ + def __init__(self, root, logger): + self.root = root + self.logger = logger + self.server = None + self.http_log = [] + self.stop_at = None + + def __enter__(self): + try: + class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + parent = self + request_counter = 0 + def log_message(self, format, *args): + msg = format % args + self.parent.logger.info(msg) + self.parent.http_log.append(msg) + + def translate_path(self, path): + """ + Return absolute path based on document root instead of current directory. + """ + + # The original implementation returns an absolute path rooted in the + # current directory. We need to serve a different + # directory without being able to chdir(), because + # doing that would cause commands like bitbake to + # run there, which is undesirable because for + # example bitbake creates a bitbake-cookerdaemon.log + # in the current directory. + path = super().translate_path(path) + relpath = os.path.relpath(path) + path = os.path.join(self.parent.root, relpath) + return path + + def do_GET(self): + """ + Inject errors. + """ + counter = HTTPRequestHandler.request_counter + HTTPRequestHandler.request_counter += 1 + if self.parent.stop_at is not None and counter >= self.parent.stop_at: + self.send_error(500, 'test server is intentionally down') + else: + super().do_GET() + + handler = HTTPRequestHandler + + def create_httpd(): + for port in range(9999, 10000): + try: + server = http.server.HTTPServer(('localhost', port), handler) + return server + except OSError as ex: + if ex.errno != errno.EADDRINUSE: + raise + self.fail('no port available for HTTP server') + + self.server = create_httpd() + self.port = self.server.server_port + self.logger.info('serving repo %s on port %d' % (self.root, self.port)) + helper = threading.Thread(name='HTTPD', target=self.server.serve_forever) + helper.start() + + # Now let caller do its work while the server runs. + return self + except: + self._stop() + raise + + def __exit__(self, exc_type, exc_val, exc_tb): + self._stop() + + def _stop(self): + # We have to stop a running server under all circumstances, + # otherwise the helper thread will keep running and we end up + # with thread locking issues. + if self.server: + self.server.shutdown() + self.server.server_close() + self.server = None + class HTTPUpdate(SystemUpdateBase): """ System update tests for image update mechanisms which depend on @@ -51,7 +139,8 @@ def track_for_cleanup(self, name): if 'NO_CLEANUP' not in os.environ: super().track_for_cleanup(name) - def boot_image(self, overrides): + @contextlib.contextmanager + def boot_image(self, overrides = {}): # We don't know the final port yet, so instead we create a placeholder script # for qemu to use and rewrite that script once we are ready. The kernel refuses # to execute a shell script while we have it open, so here we close it @@ -60,106 +149,55 @@ def boot_image(self, overrides): # The helper script also keeps command line handling a bit simpler (no whitespace # in -netdev parameter), which may or may not be relevant. self.httpd_netcat = tempfile.NamedTemporaryFile(mode='w', prefix='httpd-netcat-', dir=os.getcwd(), delete=False) - self.httpd_netcat.close() - os.chmod(self.httpd_netcat.name, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR) - self.track_for_cleanup(self.httpd_netcat.name) - - qemuboot_conf = os.path.join(self.image_dir_test, - '%s-%s.qemuboot.conf' % (self.IMAGE_PN, self.BB_VARS['MACHINE'])) - with open(qemuboot_conf) as f: - conf = f.read() - with open(qemuboot_conf, 'w') as f: - f.write('\n'.join([x for x in conf.splitlines() if not x.startswith('qb_slirp_opt')])) - f.write('\nqb_slirp_opt = -netdev user,id=net0,guestfwd=tcp:%s-cmd:%s\n' % \ - (self.HTTPD_SERVER, self.httpd_netcat.name)) - return runqemu(self.IMAGE_PN, - discard_writes=False, ssh=False, - overrides=overrides, - runqemuparams='ovmf slirp nographic', - image_fstype='wic') - - def update_image(self, qemu): - # We need to bring up some simple HTTP server for the - # update repo. - server = None - self.http_log = [] - http_log = self.http_log try: - class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): - parent = self - request_counter = 0 - def log_message(self, format, *args): - msg = format % args - self.parent.logger.info(msg) - self.parent.http_log.append(msg) - - def translate_path(self, path): - """ - Return absolute path based on document root instead of current directory. - """ - - # The original implementation returns an absolute path rooted in the - # current directory. We need to serve a different - # directory without being able to chdir(), because - # doing that would cause commands like bitbake to - # run there, which is undesirable because for - # example bitbake creates a bitbake-cookerdaemon.log - # in the current directory. - path = super().translate_path(path) - relpath = os.path.relpath(path) - path = os.path.join(self.parent.REPO_DIR, relpath) - return path - - def do_GET(self): - """ - Inject errors. - """ - counter = HTTPRequestHandler.request_counter - HTTPRequestHandler.request_counter += 1 - stop_at = getattr(self.parent, 'stop_serving_http_at', None) - if stop_at is not None and counter >= stop_at: - self.send_error(500, 'test server is intentionally down') - else: - super().do_GET() + self.httpd_netcat.close() + os.chmod(self.httpd_netcat.name, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR) + qemuboot_conf = os.path.join(self.image_dir_test, + '%s-%s.qemuboot.conf' % (self.IMAGE_PN, self.BB_VARS['MACHINE'])) + with open(qemuboot_conf) as f: + conf = f.read() + with open(qemuboot_conf, 'w') as f: + f.write('\n'.join([x for x in conf.splitlines() if not x.startswith('qb_slirp_opt')])) + f.write('\nqb_slirp_opt = -netdev user,id=net0,guestfwd=tcp:%s-cmd:%s\n' % \ + (self.HTTPD_SERVER, self.httpd_netcat.name)) + with super().boot_image(ssh=False, + runqemuparams='ovmf slirp nographic', + image_fstype='wic') as qemu: + yield qemu + finally: + os.unlink(self.httpd_netcat.name) - handler = HTTPRequestHandler + @contextlib.contextmanager + def start_httpd(self): + """ + Bring up the HTTP server when entering the context and shut it down when done. + """ - def create_httpd(): - for port in range(9999, 10000): - try: - server = http.server.HTTPServer(('localhost', port), handler) - return server - except OSError as ex: - if ex.errno != errno.EADDRINUSE: - raise - self.fail('no port available for HTTP server') + # netcat can't be assumed to be present. Build and use socat instead. + # It's a bit more complicated but has the advantage that it is in OE-core. + socat = os.path.join(self.BB_VARS['RECIPE_SYSROOT_NATIVE'], 'usr', 'bin', 'socat') + if not os.path.exists(socat): + bitbake('socat-native:do_addto_recipe_sysroot', output_log=self.logger) + self.assertExists(socat, 'socat-native was not built as expected') - server = create_httpd() - port = server.server_port - self.logger.info('serving repo %s on port %d' % (self.REPO_DIR, port)) - helper = threading.Thread(name='HTTPD', target=server.serve_forever) - helper.start() - # netcat can't be assumed to be present. Build and use socat instead. - # It's a bit more complicated but has the advantage that it is in OE-core. - socat = os.path.join(self.BB_VARS['RECIPE_SYSROOT_NATIVE'], 'usr', 'bin', 'socat') - if not os.path.exists(socat): - bitbake('socat-native:do_addto_recipe_sysroot', output_log=self.logger) - self.assertExists(socat, 'socat-native was not built as expected') + with HTTPServer(self.REPO_DIR, self.logger) as httpd: with open(self.httpd_netcat.name, 'w') as f: f.write('''#!/bin/sh exec %s 2>/tmp/httpd.log -D -v -d -d -d -d STDIO TCP:localhost:%d -''' % (socat, port)) +''' % (socat, httpd.port)) + yield httpd + + def update_image(self, qemu): + # We need to bring up some simple HTTP server for the + # update repo. + with self.start_httpd() as self.httpd: # Now run the real update command inside the virtual machine. return self.update_image_via_http(qemu) - finally: - if server: - server.shutdown() - server.server_close() - def update_image_via_http(self, qemu): """ Called by update_image() with the HTTPD server running. """ return False + diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py index 2372f39fe9..6597a36bbc 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/systemupdatebase.py @@ -7,6 +7,7 @@ import oe.path import base64 +import contextlib import fnmatch import pathlib import pickle @@ -219,13 +220,20 @@ class SystemUpdateBase(OESelftestTestCase): # Expected to be replaced by derived class. IMAGE_MODIFY = SystemUpdateModify() - def boot_image(self, overrides): + def boot_image(self, overrides = {}, **kwargs): """ Calls runqemu() such that commands can be started via run_serial(). Derived classes need to replace with something that adds whatever other parameters are needed or useful. """ - return runqemu(self.IMAGE_PN, discard_writes=False, overrides=overrides) + # Change DEPLOY_DIR_IMAGE so that we use our copy of the + # images from before the update. Further customizations for booting can + # be done by rewriting self.image_dir_test/IMAGE_PN-MACHINE.qemuboot.conf + # (read, close, write, not just appending as that would also change + # the file copy under image_dir). + overrides = overrides.copy() + overrides['DEPLOY_DIR_IMAGE'] = self.image_dir_test + return runqemu(self.IMAGE_PN, discard_writes=False, overrides=overrides, **kwargs) def update_image(self, qemu): """ @@ -250,28 +258,18 @@ def modify_image_build(self, testname, updates, is_update): bbappend.append('APPEND_append = " modify_kernel_test=%s"' % ('updated' if is_update else 'original')) return '\n'.join(bbappend) - def do_update(self, testname, updates): + def create_image_bbappend(self, testname, updates, is_update): """ - Builds the image, makes a copy of the result, rebuilds to produce - an update with configurable changes, boots the original image, updates it, - reboots and then checks the updated image. - - 'update' is a list of modify_* function names which make the actual changes - (adding, removing, modifying files or kernel) that are part of the tests. + Creates an IMAGE_BBAPPEND which contains the pickled modification code. + A .bbappend is used because it can contain code and is guaranteed to be + applied also to image variants. """ - def create_image_bbappend(is_update): - """ - Creates an IMAGE_BBAPPEND which contains the pickled modification code. - A .bbappend is used because it can contain code and is guaranteed to be - applied also to image variants. - """ - - bbappend = self.IMAGE_BBAPPEND_UPDATE if is_update else self.IMAGE_BBAPPEND - self.append_config('BBFILES_append = " %s"' % os.path.abspath(bbappend)) - self.track_for_cleanup(bbappend) - with open(bbappend, 'w') as f: - f.write(''' + bbappend = self.IMAGE_BBAPPEND_UPDATE if is_update else self.IMAGE_BBAPPEND + self.append_config('BBFILES_append = " %s"' % os.path.abspath(bbappend)) + self.track_for_cleanup(bbappend) + with open(bbappend, 'w') as f: + f.write(''' python system_update_test_modify () { import base64 import pickle @@ -292,9 +290,14 @@ def create_image_bbappend(is_update): self.IMAGE_CONFIG, self.modify_image_build(testname, updates, is_update))) + def prepare_image(self, testname, updates): + """ + Builds the initial image and prepares it for booting. + """ + # Creating a .bbappend for the image will trigger a rebuild. # To avoid this, use separate image recipes. - create_image_bbappend(False) + self.create_image_bbappend(testname, updates, False) self.logger.info('Building base image') result = bitbake(self.IMAGE_PN, output_log=self.logger) @@ -312,28 +315,44 @@ def create_image_bbappend(is_update): # when the image recipes are different. self.clone_files(self.image_dir, ('ovmf*', vars['IMAGE_LINK_NAME'] + '*')) - # Change DEPLOY_DIR_IMAGE so that we use our copy of the - # images from before the update. Further customizations for booting can - # be done by rewriting self.image_dir_test/IMAGE_PN-MACHINE.qemuboot.conf - # (read, close, write, not just appending as that would also change - # the file copy under image_dir). - overrides = { 'DEPLOY_DIR_IMAGE': self.image_dir_test } - + @contextlib.contextmanager + def boot_and_verify_image(self, testname, updates): + """ + Boots the initial image and verifies its content. + To be used as: + with boot_initial_image() as qemu: + ... run additional checks in qemu ... + """ # Boot image, verify before and after update. - with self.boot_image(overrides) as qemu: + with self.boot_image() as qemu: self.verify_image(testname, False, qemu, updates) + yield qemu - # Now we change our .bbappend so that the updated state is generated - # during the next rebuild. - create_image_bbappend(True) - self.logger.info('Building updated image') - bitbake(self.IMAGE_PN_UPDATE, output_log=self.logger) + def prepare_update(self, testname, updates): + """ + Build the update image. + """ + self.create_image_bbappend(testname, updates, True) + self.logger.info('Building updated image') + bitbake(self.IMAGE_PN_UPDATE, output_log=self.logger) + def do_update(self, testname, updates): + """ + Builds the image, makes a copy of the result, rebuilds to produce + an update with configurable changes, boots the original image, updates it, + reboots and then checks the updated image. + + 'update' is a list of modify_* function names which make the actual changes + (adding, removing, modifying files or kernel) that are part of the tests. + """ + self.prepare_image(testname, updates) + with self.boot_and_verify_image(testname, updates) as qemu: + self.prepare_update(testname, updates) reboot = self.update_image(qemu) if not reboot: self.verify_image(testname, True, qemu, updates) if reboot: - with self.boot_image(overrides) as qemu: + with self.boot_image() as qemu: self.verify_image(testname, True, qemu, updates) def clone_files(self, dirname, file_patterns): From 0ea1c58d15194258b9bceb2055922014ca429360 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 24 Nov 2017 15:37:33 +0100 Subject: [PATCH 13/22] httpupdate.py: fix http server port selection The range only included a single port, which was set this way when debugging the code. Now it's a proper range. The error resulting from not finding a free port was not reported properly: the revised class has no access to the unittest fail(), so now it just throws a RuntimeError. Signed-off-by: Patrick Ohly --- meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py index dc65fd2caa..b633eca184 100644 --- a/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py +++ b/meta-refkit-core/lib/oeqa/selftest/systemupdate/httpupdate.py @@ -63,14 +63,14 @@ def do_GET(self): handler = HTTPRequestHandler def create_httpd(): - for port in range(9999, 10000): + for port in range(9999, 11000): try: server = http.server.HTTPServer(('localhost', port), handler) return server except OSError as ex: if ex.errno != errno.EADDRINUSE: raise - self.fail('no port available for HTTP server') + raise RuntimeError('no port available for HTTP server') self.server = create_httpd() self.port = self.server.server_port From 97d28dec7117614ecb1d403c85821937d2102903 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 24 Nov 2017 15:30:40 +0100 Subject: [PATCH 14/22] refkit-image.bbclass: make read-only rootfs truly read-only OE-core does not tell the initramfs to mount read-only when read-only-rootfs is set for an image. That looks like an oversight and (besides touching the filesystem by mounting it read/write for each boot) has the effect that systemd is modifying /etc/machine-id on first boot before making the filesystem read-only. This shows up in a "swupd verify" operation when using the active partition as source for the inactive partition. Signed-off-by: Patrick Ohly --- meta-refkit-core/classes/refkit-image.bbclass | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meta-refkit-core/classes/refkit-image.bbclass b/meta-refkit-core/classes/refkit-image.bbclass index 79da051c3c..6edbc3e833 100644 --- a/meta-refkit-core/classes/refkit-image.bbclass +++ b/meta-refkit-core/classes/refkit-image.bbclass @@ -378,6 +378,10 @@ DEPENDS += "${@ 'attr-native' if '${REFKIT_IMAGE_STRIP_SMACK}' else '' }" # made due to filesystem metadata time stamps being in future. APPEND_append = " fsck.mode=skip" +# Do not mount read/write in the initramfs when the goal is to have a read-only +# rootfs. Not sure why OE-core does not do that itself. +APPEND_append = "${@ bb.utils.contains('IMAGE_FEATURES', 'read-only-rootfs', ' ro', '', d)} " + # Ensure that images preserve Smack labels and IMA/EVM. inherit ${@bb.utils.contains_any('IMAGE_FEATURES', ['ima','smack'], 'xattr-images', '', d)} From 071337ee1f83da698db6d7f9ea4ee0a23d0b9590 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 24 Nov 2017 15:35:05 +0100 Subject: [PATCH 15/22] stateless.inc: do not ignore stateful files on read-only rootfs When using a read-only rootfs, files that normally would get modified at runtime (like /etc/machine-id) remain unchanged, so we do not need and shouldn't ignore them in the swupd update stream, because otherwise "swupd verify --extra-picky --fix" removes them. Signed-off-by: Patrick Ohly --- meta-refkit-core/conf/distro/include/stateless.inc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/meta-refkit-core/conf/distro/include/stateless.inc b/meta-refkit-core/conf/distro/include/stateless.inc index ef3b45e40d..e3f0a23e30 100644 --- a/meta-refkit-core/conf/distro/include/stateless.inc +++ b/meta-refkit-core/conf/distro/include/stateless.inc @@ -46,9 +46,12 @@ STATELESS_RM_pn-systemd += " \ # systemd/src/core/machine-id-setup.c). STATELESS_ETC_WHITELIST += "machine-id" -# These files must be ignored by swupd. +# These files must be ignored by swupd, unless the root file system +# is read-only, in which case they do not actually get modified in the +# partition. Using swupd in such a setup only works when doing A/B +# partitioning. STATEFUL_FILES += "/etc/machine-id" -SWUPD_FILE_BLACKLIST_append = " ${STATEFUL_FILES}" +SWUPD_FILE_BLACKLIST_append = "${@ bb.utils.contains('IMAGE_FEATURES', 'read-only-rootfs', '', ' ' + d.getVar('STATEFUL_FILES'), d) }" # Depend on the installed components and thus has to be computed on # the device. Handled by systemd during booting or updates. From 7fa6fac44294d09c01491f9e667bbc89befdbeb4 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 20 Sep 2017 11:38:01 +0200 Subject: [PATCH 16/22] refkit-image.bbclass: install efi-combo-trigger for swupd When using swupd and UEFI combo app, efi-combo-trigger is needed to copy the updated kernel from the rootfs to the EFI partition. Signed-off-by: Patrick Ohly --- meta-refkit-core/classes/refkit-image.bbclass | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/meta-refkit-core/classes/refkit-image.bbclass b/meta-refkit-core/classes/refkit-image.bbclass index 6edbc3e833..bfed093d8f 100644 --- a/meta-refkit-core/classes/refkit-image.bbclass +++ b/meta-refkit-core/classes/refkit-image.bbclass @@ -190,6 +190,11 @@ FEATURE_PACKAGES_tools-debug_append = " valgrind" FEATURE_PACKAGES_computervision = "packagegroup-computervision" FEATURE_PACKAGES_computervision-test = "packagegroup-computervision-test" +# If we use the UEFI combo app and do swupd-based image updates, then +# we also need to hook into the post-update systemd hook to update +# the combo app in the EFI partition. +FEATURE_PACKAGES_swupd = "${@ 'efi-combo-trigger' if oe.types.boolean(d.getVar('REFKIT_USE_DSK_IMAGES') or '0') else '' }" + LICENSE = "MIT" # See local.conf.sample for explanations. From 27cfcecc1e20f811729935cb98bf7c88074aeadf Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Mon, 18 Sep 2017 21:32:44 +0200 Subject: [PATCH 17/22] refkit: add meta-swupd submodule Having the layer around will allow adding tests for the remaining swupd support and of the layer itself. Signed-off-by: Patrick Ohly --- .gitmodules | 3 +++ meta-refkit/conf/bblayers.conf.sample | 3 ++- meta-refkit/conf/layer.conf | 2 +- meta-swupd | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) create mode 160000 meta-swupd diff --git a/.gitmodules b/.gitmodules index c213d8b611..635201a10a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,3 +40,6 @@ [submodule "iot-web-layers"] path = iot-web-layers url = https://github.com/intel/iot-web-layers.git +[submodule "meta-swupd"] + path = meta-swupd + url = https://git.yoctoproject.org/git/meta-swupd diff --git a/meta-refkit/conf/bblayers.conf.sample b/meta-refkit/conf/bblayers.conf.sample index 7e11cd7791..1f57c94e72 100644 --- a/meta-refkit/conf/bblayers.conf.sample +++ b/meta-refkit/conf/bblayers.conf.sample @@ -1,6 +1,6 @@ # LAYER_CONF_VERSION is increased each time build/conf/bblayers.conf # changes incompatibly -LCONF_VERSION = "11" +LCONF_VERSION = "12" BBPATH = "${TOPDIR}" BBFILES ?= "" @@ -24,6 +24,7 @@ REFKIT_LAYERS = " \ ##OEROOT##/../meta-clang \ ##OEROOT##/../meta-ros \ ##OEROOT##/../meta-flatpak \ + ##OEROOT##/../meta-swupd \ " # REFKIT_LAYERS += "##OEROOT##/../meta-openembedded/meta-efl" diff --git a/meta-refkit/conf/layer.conf b/meta-refkit/conf/layer.conf index b537871c18..41c9cfd760 100644 --- a/meta-refkit/conf/layer.conf +++ b/meta-refkit/conf/layer.conf @@ -33,7 +33,7 @@ REFKIT_LOCALCONF_VERSION = "3" LOCALCONF_VERSION = "${REFKIT_LOCALCONF_VERSION}" # Same for LCONF_VERSION in bblayer.conf.sample. -REFKIT_LAYER_CONF_VERSION = "11" +REFKIT_LAYER_CONF_VERSION = "12" LAYER_CONF_VERSION = "${REFKIT_LAYER_CONF_VERSION}" # The default error messages use shell meta* wildcards to find the diff --git a/meta-swupd b/meta-swupd new file mode 160000 index 0000000000..1098ec6306 --- /dev/null +++ b/meta-swupd @@ -0,0 +1 @@ +Subproject commit 1098ec6306284551ce17b69899ef085e339addb2 From 5442e99ebf4adda5aada7f3a460bbaf93c81a298 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 21 Sep 2017 15:27:04 +0200 Subject: [PATCH 18/22] meta-swupd: use experimental fork Future maintenance of Yocto meta-swupd is still uncertain. For now let's experiment in a private fork on GitHub. Signed-off-by: Patrick Ohly --- .gitmodules | 2 +- meta-swupd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 635201a10a..f178bc3323 100644 --- a/.gitmodules +++ b/.gitmodules @@ -42,4 +42,4 @@ url = https://github.com/intel/iot-web-layers.git [submodule "meta-swupd"] path = meta-swupd - url = https://git.yoctoproject.org/git/meta-swupd + url = https://github.com/pohly/meta-swupd.git diff --git a/meta-swupd b/meta-swupd index 1098ec6306..ea14449ee1 160000 --- a/meta-swupd +++ b/meta-swupd @@ -1 +1 @@ -Subproject commit 1098ec6306284551ce17b69899ef085e339addb2 +Subproject commit ea14449ee147f8c938856a31c6a17394ce921f20 From f895f23dcccae7cd80236bb3038185d9c4779b80 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 20 Sep 2017 11:46:40 +0200 Subject: [PATCH 19/22] meta-refkit-core: add swup update testing This uses the same infrastructure as OSTree testing. However, updating the kernel was found to be buggy (efi_combo_updater fails) and content in /etc cannot be customized locally, so those tests get disabled. RefkitSwupdUpdateTestIncremental preserves the www directory while wiping out rest of the swupd directory. The tmpdir itself is kept unchanged, for performance reasons. A more realistic test would be to wipe out tmp, too, and thus rebuild entirely from sstate, but the is not done here due to the performance impact on testing. Image recipes which define swupd bundles go through different code paths for producing the rootfs (they need a mega image which is different from the core OS rootfs). In addition, additional images with bundles pre-installed can be created. Building refkit-image-update-swupd-bundles covers the additional code paths. Building refkit-image-update-swupd-bundles-dev tests producing an image with additional bundles. To keep build times low and cover additional caveats, artificial packages which add users in a postinst are used as bundle content. Signed-off-by: Patrick Ohly --- meta-refkit-core/classes/image-dsk.bbclass | 4 + .../lib/oeqa/selftest/cases/refkit_swupd.py | 924 ++++++++++++++++++ .../refkit-image-update-swupd-bundles.bb | 10 + .../refkit-image-update-swupd-modified.bb | 10 + .../images/refkit-image-update-swupd.bb | 17 + .../images/refkit-test-feature.bb | 22 + 6 files changed, 987 insertions(+) create mode 100644 meta-refkit-core/lib/oeqa/selftest/cases/refkit_swupd.py create mode 100644 meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd-bundles.bb create mode 100644 meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd-modified.bb create mode 100644 meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd.bb create mode 100644 meta-refkit-core/recipes-selftest/images/refkit-test-feature.bb diff --git a/meta-refkit-core/classes/image-dsk.bbclass b/meta-refkit-core/classes/image-dsk.bbclass index 457077d0fd..5b506a5aa6 100644 --- a/meta-refkit-core/classes/image-dsk.bbclass +++ b/meta-refkit-core/classes/image-dsk.bbclass @@ -70,3 +70,7 @@ do_uefiapp_deploy_append () { } do_uefiapp_deploy[depends] += "rmc-db:do_deploy" + +# temporary fix, should be in meta-intel/classes/uefi-comboapp.bbclass +# Patch submitted: [meta-intel][PATCH] uefi-comboapp.bbclass: install files under pseudo +do_uefiapp_deploy[fakeroot] = "1" diff --git a/meta-refkit-core/lib/oeqa/selftest/cases/refkit_swupd.py b/meta-refkit-core/lib/oeqa/selftest/cases/refkit_swupd.py new file mode 100644 index 0000000000..dd4dedc85d --- /dev/null +++ b/meta-refkit-core/lib/oeqa/selftest/cases/refkit_swupd.py @@ -0,0 +1,924 @@ +from oeqa.selftest.systemupdate.httpupdate import HTTPUpdate + +import contextlib +import copy +import os +import re +import shutil +import tempfile + +class RefkitSwupdUpdateBase(HTTPUpdate): + """ + System update tests for refkit-image-common using swupd. + """ + + # We test the normal refkit-image-common with + # swupd system update enabled. + IMAGE_PN = 'refkit-image-update-swupd' + IMAGE_PN_UPDATE = IMAGE_PN + IMAGE_BBAPPEND = IMAGE_PN + '.bbappend' + IMAGE_BBAPPEND_UPDATE = IMAGE_BBAPPEND + IMAGE_BUNDLES = [] + + def setUp(self): + # IMAGE_BBAPPEND always refers to the base image, whereas IMAGE_PN might be a virtual image. + # The swupd repo gets produced for the base image. See RefkitSwupdBundleTestAll. + self.SWUPD_DIR = os.path.join(HTTPUpdate.BB_VARS['DEPLOY_DIR'], 'swupd', HTTPUpdate.BB_VARS['MACHINE'], + self.IMAGE_BBAPPEND[:-len('.bbappend')]) + self.REPO_DIR = os.path.join(self.SWUPD_DIR, 'www') + self.maxDiff = None + super().setUp() + + IMAGE_MODIFY = copy.copy(HTTPUpdate.IMAGE_MODIFY) + + # swupd cannot preserve local changes in /etc. + IMAGE_MODIFY.ETC_FILES = [x for x in IMAGE_MODIFY.ETC_FILES if x[1] != 'edit'] + + # efi_combo_updater currently fails during testing, which breaks the kernel update + # test. TODO: fix that instead of disabling that aspect of the test. + # + # Failure is (visible only when invoking manually): + # $ efi_combo_updater + # ROOT_BLOCK_DEVICE (null) + # Partition prefix: "p" + # sh: syntax error: unexpected "(" + # efi_combo_updater: /fast/build/refkit/intel-corei7-64/tmp-glibc/work/corei7-64-refkit-linux/efi-combo-trigger/1.0-r0/efi_combo_updater.c:99: main: Assertion `execute(&efi_partition_nr, EFI_PARTITION_NR_CMD, root_block_device, EFI_TYPE) == 0' failed. + # Aborted (core dumped) + IMAGE_MODIFY.UPDATES.remove('kernel') + + def modify_image_build(self, testname, updates, is_update): + """ + We use fixed versions 10 and 20 for original and modified image + and generate a delta between 10 and 20. + """ + bbappend = [super().modify_image_build(testname, updates, is_update)] + if is_update: + bbappend.append('OS_VERSION = "20"') + bbappend.append('SWUPD_DELTAPACK_VERSIONS = "10"') + else: + bbappend.append('OS_VERSION = "10"') + return '\n'.join(bbappend) + + def list_tree(self, dir): + """ + Returns list of all entries underneath dir (files, symlinks and directories), + with the full path relative to the base directory. Directories have a trailing + slash. + """ + items = [] + for root, dirs, files in os.walk(dir): + base = os.path.relpath(root, dir) + if base == '.': + base = '' + items.extend([os.path.join(base, item) for item in files]) + items.extend([os.path.join(base, item) + '/' for item in dirs]) + items.sort() + return items + + def list_manifest(self, version): + """ + Extract the list of still existing items recorded in the manifest, i.e. + deleted items are ignored. Same result as for list_tree() (no leading slash, + trailing slash for directories). + """ + items = [] + entry_re = re.compile('^(?P\S+)\s+(?P[0-9a-f]+)\s+(?P\d+)\s+(?P.*)$') + with open(os.path.join(self.REPO_DIR, str(version), 'Manifest.full')) as f: + for line in f: + m = entry_re.match(line) + if m and m.group('type') != '.d..': + items.append(m.group('path').lstrip('/') + + ('/' if m.group('type').startswith('D') else '')) + items.sort() + return items + + def clean_swupd_repo(self): + """ + Automatically clean the swupd repo when doing the normal testing + with a single image recipe. RefkitSwupdUpdateTestDev avoids + rebuilding but might fail because it does not always start from + scratch. + """ + if self.IMAGE_PN == self.IMAGE_PN_UPDATE and \ + os.path.exists(self.SWUPD_DIR): + shutil.rmtree(self.SWUPD_DIR) + + def do_update(self, testname, updates, have_zero_packs=[], full_repo=True): + self.clean_swupd_repo() + + # No need to reboot, unless the kernel was updated. + # Refkit itself does not detect the need to reboot, so we have to decide + # based on the test. + self.update_needs_reboot = 'kernel' in updates + + super().do_update(testname, updates) + + # Here we can do some additional sanity checking of the content + # of the swupd repo. Test-specific checks can be in the individual + # test_* methods. + repo_items = self.list_tree(self.REPO_DIR) + expected = """10/ +10/Manifest.MoM +10/Manifest.MoM.tar +10/Manifest.full +10/Manifest.full.tar +10/Manifest.os-core +10/Manifest.os-core.tar +20/ +20/Manifest-os-core-delta-from-10 +20/Manifest.MoM +20/Manifest.MoM.tar +20/Manifest.full +20/Manifest.full.tar +20/Manifest.os-core +20/Manifest.os-core.tar +20/format +20/pack-os-core-from-10.tar +20/swupd-server-src-version""".split('\n') + for version in ('10', '20'): + # Normal bundles always get packed to support installing them. + for bundle in self.IMAGE_BUNDLES: + expected.append('%s/pack-%s-from-0.tar' % (version, bundle)) + # os-core however is optional. + if have_zero_packs is True or version in have_zero_packs: + expected.append('%s/pack-os-core-from-0.tar' % version) + for bundle in self.IMAGE_BUNDLES: + # The bundles are not expected to change, therefore the + # Manifests from build 10 are reused by build 20. + for suffix in ('', '.tar'): + expected.append('10/Manifest.%s%s' % (bundle, suffix)) + expected.append('20/pack-%s-from-10.tar' % bundle) + if self.IMAGE_BUNDLES: + # Apparently it only gets created if enough changes, which happens + # to be when we have bundles. + expected.append('20/Manifest-MoM-delta-from-10') + if full_repo: + expected.extend(['10/format', '10/swupd-server-src-version']) + expected.sort() + # Hidden files get excluded because of https://github.com/clearlinux/swupd-server/issues/99. + self.assertEqual(expected, + [x for x in repo_items if \ + '/.' not in x and + '/delta/' not in x and + '/files/' not in x and + not x.startswith('version/')]) + + def update_image_via_http(self, qemu): + url = 'http://%s' % self.HTTPD_SERVER + cmd = 'swupd update -c {0} -v {0}'.format(url) + status, output = qemu.run_serial(cmd, timeout=600) + self.assertEqual(1, status, 'Failed to run command "%s":\n%s' % (cmd, output)) + self.logger.info('Successful (?) update with %s:\n%s' % (cmd, output)) + # TODO: verify that the delta pack was downloaded + return self.update_needs_reboot + + def update_image(self, qemu): + # Dump some information about changes in version 20. + lines = [] + lines.append('Changes in 20/Manifest.full:\n') + entry_re = re.compile('^(?P\S+)\s+(?P[0-9a-f]+)\s+(?P\d+)\s+(?P.*)$') + with open(os.path.join(self.REPO_DIR, '20', 'Manifest.full')) as f: + for line in f: + m = entry_re.match(line) + if m and m.group('version') == '20': + lines.append(line) + self.logger.info(''.join(lines)) + super().update_image(qemu) + +class RefkitSwupdUpdateTestAll(RefkitSwupdUpdateBase): + def test_update_all(self): + """ + Test all possible changes at once. + """ + self.do_update('test_update_all', self.IMAGE_MODIFY.UPDATES) + + repo_items = self.list_tree(self.REPO_DIR) + # 11 modified or new files, two directories. + files = [x for x in repo_items if x.startswith('20/files/')] + expected = 13 + if len(files) != expected: + prefix_len = len('20/files/') + hashes = [] + for item in repo_items: + m = re.match(r'20/files/(.*).tar', item) + if m: + hashes.append(m.group(1)) + file_names = [] + with open(os.path.join(self.REPO_DIR, '20', 'Manifest.full')) as f: + for line in f: + if any(map(lambda x: x in line, hashes)): + file_names.append(line) + self.fail('should have %d files, got %d:\n%s\n\nManifest.full:\n%s' % (expected, len(files), '\n'.join(files), ' '.join(file_names))) + deltas = [x for x in repo_items if x.startswith('20/delta/') and x != '20/delta/'] + self.assertEqual(len(deltas), 1, msg='should have 1 delta for modify_files_large, got: %s' % deltas) + + +class RefkitSwupdBundleTestAll(RefkitSwupdUpdateTestAll): + """ + This class inherits test_update_all, but applies it to a different set of images + where bundles are enabled. The actual image that we test with has the "dev" bundle + pre-installed. + """ + + IMAGE_PN = 'refkit-image-update-swupd-bundles-dev' + IMAGE_PN_UPDATE = IMAGE_PN + IMAGE_BBAPPEND = 'refkit-image-update-swupd-bundles.bbappend' + IMAGE_BBAPPEND_UPDATE = IMAGE_BBAPPEND + IMAGE_BUNDLES = ['feature_one', 'feature_two'] + + +class RefkitSwupdUpdateTestIncremental(RefkitSwupdUpdateBase): + + def modify_image_build(self, testname, updates, is_update): + """ + Preserve only www directory before the second build. + """ + bbappend = [super().modify_image_build(testname, updates, is_update)] + + if is_update: + # Move the www directory into a different location, then + # delete the swupd directory. The old content gets used + # via file:// URLs. + if os.path.exists(self.wwwdir): + shutil.rmtree(self.wwwdir) + os.rename(self.REPO_DIR, self.wwwdir) + shutil.rmtree(self.SWUPD_DIR) + bbappend.append('SWUPD_VERSION_BUILD_URL = "file:///%s"' % self.wwwdir) + bbappend.append('SWUPD_CONTENT_BUILD_URL = "file:///%s"' % self.wwwdir) + # Also do a delta pack. + bbappend.append('SWUPD_DELTAPACK_VERSIONS = "10"') + # Stage all files, to ensure that this works also for directories. + bbappend.append('SWUPD_GENERATE_OS_CORE_ZERO_PACK = "true"') + + return '\n'.join(bbappend) + + def setUp(self): + self.wwwdir = os.path.abspath('test-swupd-www') + # self.track_for_cleanup(self.wwwdir) + super().setUp() + + def test_update_incremental(self): + """ + Simulates the default workflow where each build starts with empty TMPDIR + and previous swupd repo data must be retrieved via the content and version + build URLs. Enables delta packs, too. + """ + self.do_update('test_update_incremental', self.IMAGE_MODIFY.UPDATES, + full_repo=False, + have_zero_packs=['20']) + +class RefkitSwupdUpdateMeta(type): + """ + Generates individual instances of test_update_, one for each type of change. + """ + def __new__(mcs, name, bases, dict): + def add_test(update): + test_name = 'test_update_' + update + def test(self): + self.do_update(test_name, [update]) + dict[test_name] = test + for update in RefkitSwupdUpdateBase.IMAGE_MODIFY.UPDATES: + add_test(update) + return type.__new__(mcs, name, bases, dict) + +class RefkitSwupdUpdateTestIndividual(RefkitSwupdUpdateBase, metaclass=RefkitSwupdUpdateMeta): + pass + +class RefkitSwupdUpdateTestDev(RefkitSwupdUpdateTestAll, metaclass=RefkitSwupdUpdateMeta): + """ + This class avoids rootfs rebuilding by using two separate image + recipes. The other tests are more realistic. Use this one when debugging problems, + and beware that the swupd repo must be removed manually (if necessary). + """ + + IMAGE_PN_UPDATE = 'refkit-image-update-swupd-modified' + IMAGE_BBAPPEND_UPDATE = IMAGE_PN_UPDATE + '.bbappend' + +class RefkitSwupdPartitionTest(RefkitSwupdUpdateBase): + """ + Builds two OS releases and then exercises various code paths + in swupd-update-partition. + """ + + # Run tests with "REFKIT_SWUPD_REUSE_REPO=1 oe-selftest -r refkit_swupd.RefkitSwupdPartitionTest" + # to avoid rebuilding. Beware that the repo must be cleaned manually when making content + # changes in that case. + if 'REFKIT_SWUPD_REUSE_REPO' in os.environ: + IMAGE_PN_UPDATE = 'refkit-image-update-swupd-modified' + IMAGE_BBAPPEND_UPDATE = IMAGE_PN_UPDATE + '.bbappend' + + # Derived from INT_STORAGE_ROOTFS_PARTUUID_VALUE by reversing the digits in the first value. + # We just need something that is unique. + PARTUUID = "87654321-9abc-def0-0fed-cba987654320" + + def modify_image_build(self, testname, updates, is_update): + bbappend = [super().modify_image_build(testname, updates, is_update)] + # A/B partitioning scheme where the system partition is read-only. + bbappend.append('REFKIT_EXTRA_PARTITION = "part ${REFKIT_IMAGE_SIZE} --fstype=ext4 --label inactive --align 1024 --uuid %s"' % self.PARTUUID) + bbappend.append('REFKIT_IMAGE_EXTRA_FEATURES_append = " read-only-rootfs"') + # Needed for installing from scratch. + bbappend.append('SWUPD_GENERATE_OS_CORE_ZERO_PACK = "true"') + # Needed for formatting the partition. + bbappend.append('REFKIT_IMAGE_EXTRA_INSTALL_append = " e2fsprogs"') + return '\n'.join(bbappend) + + def normalize_partition_output(self, output, unknown_missing=False): + # Strip CR. + output = output.replace('\r', '') + # Replace random mktemp names. + output, _ = re.subn(r'/swupd-(version|mount|mount-source)\..{6}', r'/swupd-\1.X', output) + # mkfs output is irrelevant and varies (version number, block numbers, etc.) + output, _ = re.subn(r'(^swupd-update-partition: (?:sh -c .)?mkfs[^\n]*\n).*?Writing superblocks and filesystem accounting information: [^\n]*done\n', + r'\1...', + output, + flags=re.MULTILINE|re.DOTALL) + # Replace or even remove (when on their own line) progress percentages. + output, _ = re.subn(r'(\.\.\.\d+%\s*)+', '...100%\n', output) + output, _ = re.subn(r'^\s*...100%\s*\n', '', output, flags=re.MULTILINE) + # We don't care about the swupd version and copyright. + output, _ = re.subn(r'^swupd-client software.*\n Copyright.*\n', 'swupd-client software ...\n', output, flags=re.MULTILINE) + # Number of files may vary. + output, _ = re.subn(r'Inspected \d+ files', 'Inspected xxx files', output) + # Timing varies. + output, _ = re.subn(r'Update took \d+.\d seconds', 'Update took x.y seconds', output) + # Re-installing from scratch means we don't know how many files actually miss (depends on OS). + if unknown_missing: + output, _ = re.subn(r'\d+ files were missing', 'xxx files were missing', output) + output, _ = re.subn(r'\d+ of \d+ missing files were replaced', 'xxx of xxx missing files were replaced', output) + output, _ = re.subn(r'0 of \d+ missing files were not replaced', '0 of xxx missing files were not replaced', output) + return output + + def update_partition(self, cmd, expected, version, network_error=None, **kwargs): + """ + Run a single swupd-update-partition command and check the result, including the HTTP log. + """ + self.httpd.http_log.clear() + self.httpd.stop_at = network_error + self.logger.info(cmd) + status, output = self.qemu.run_serial(cmd, timeout=600) + self.assertEqual(1, status, 'Failed to run command "%s":\n%s' % (cmd, output)) + self.logger.info(output) + output = self.normalize_partition_output(output, **kwargs) + # Normalize the HTTP log by replacing /10/files/57de850a026aee38bb06a2a8d6c014a773c2dc3268032b44c0b5b3e7e4ec53f2.tar + # with + log = [] + manifest = {} + def hash2file(hash): + if not manifest: + # L... 7392ad572c8a806372e327a1908e69266f319da796284e9758ddadd888bbf1d4 10 /bin + entry_re = re.compile('^(?P\S+)\s+(?P[0-9a-f]+)\s+(?P\d+)\s+(?P.*)$') + with open(os.path.join(self.REPO_DIR, str(version), 'Manifest.full')) as f: + for line in f: + m = entry_re.match(line) + if m: + # Use the first path associated with a hash. There might be more than one. + manifest.setdefault(m.group('hash'), m.group('path')) + return manifest.get(hash, hash) + file_re = re.compile(r'/(?P\d+)/files/(?P[0-9a-f]+).tar') + for request in self.httpd.http_log: + request = file_re.sub(lambda m: '/%s/files/<%s>.tar' % (m.group('version'), hash2file(m.group('hash'))), + request) + log.append(request) + output += '\n\n' + '\n'.join(log) + '\n' + self.assertEqual(expected, output) + + # Verify partition content. + cmd = 'mountpoint=`mktemp -d` && ' + \ + 'mount -oro /dev/disk/by-partuuid/{0} $mountpoint && '.format(self.PARTUUID) + \ + '''find $mountpoint -mindepth 1 | while read item; do [ -d "$item" ] && ! [ -L "$item" ] && echo "$item/" || echo "$item"; done | sed -e "s;^$mountpoint/;;"' && ''' + \ + 'umount $mountpoint' + # TODO: actually enable the code - currently it fails because extra items + # under /etc and /var are not getting removed by swupd (https://github.com/clearlinux/swupd-client/issues/293) + #status, output = self.qemu.run_serial(cmd, timeout=600) + #self.assertEqual(1, status, 'Failed to run command "%s":\n%s' % (cmd, output)) + #expected = self.list_manifest(version) + #self.assertEqual(expected, sorted(map(str.strip, output.split('\n')))) + + + def verify_image(self, testname, is_update, qemu, updates): + # Nothing to verify. + pass + + @classmethod + def setUpClass(cls): + """ + Build update stream and boot image only once for all tests + by tracking whether we have done the work already and + cleaning up if we have. + """ + cls.qemu = None + cls.exit_stack = contextlib.ExitStack() + super().setUpClass() + + def setUp(self): + super().setUp() + self.url = 'http://%s' % self.HTTPD_SERVER + testname = 'RefkitSwupdPartitionTest' + updates = self.IMAGE_MODIFY.UPDATES + if RefkitSwupdPartitionTest.qemu is None: + self.logger.info('Preparing testing with the following modifications: ' + ' '.join(updates)) + self.clean_swupd_repo() + self.prepare_image(testname, updates) + RefkitSwupdPartitionTest.qemu = self.exit_stack.enter_context(self.boot_and_verify_image(testname, updates)) + self.prepare_update(testname, updates) + RefkitSwupdPartitionTest.httpd = self.exit_stack.enter_context(self.start_httpd()) + + @classmethod + def tearDownClass(cls): + RefkitSwupdPartitionTest.qemu = None + cls.exit_stack.close() + + def test_update_without_source(self): + """ + Install and update without source. + """ + + # Install after force formatting. + cmd = 'swupd-update-partition -c {0} -p /dev/disk/by-partuuid/{1} -m 10 -f "mkfs.ext4 -F /dev/disk/by-partuuid/{1}" -F'.format(self.url, self.PARTUUID) + expected = '''swupd-update-partition: Updating to 10 from {url}. +swupd-update-partition: Reinstalling from scratch. +swupd-update-partition: Formatting partition. +swupd-update-partition: mkfs.ext4 -F /dev/disk/by-partuuid/{uuid} +... +swupd-update-partition: mount /dev/disk/by-partuuid/{uuid} /tmp/swupd-mount.X +swupd-update-partition: rm -rf /tmp/swupd-mount.X/lost+found +swupd-update-partition: Installing into empty partition. +swupd-update-partition: swupd verify --install --no-scripts -F 4 -c {url} -v file:///tmp/swupd-version.X -m 10 -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Verifying version 10 +Downloading packs... + +Extracting os-core pack for version 10 +Adding any missing files +Inspected xxx files + xxx files were missing + xxx of xxx missing files were replaced + 0 of xxx missing files were not replaced +WARNING: post-update helper scripts skipped due to --no-scripts argument +Fix successful +swupd-update-partition: umount /tmp/swupd-mount.X +swupd-update-partition: Update successful. + +"GET /10/Manifest.MoM.tar HTTP/1.1" 200 - +"GET /10/Manifest.os-core.tar HTTP/1.1" 200 - +"GET /10/pack-os-core-from-0.tar HTTP/1.1" 200 - +'''.format( + uuid=self.PARTUUID, + url=self.url +) + self.update_partition(cmd, expected, 10, unknown_missing=True) + + # Incremental update. + cmd = 'swupd-update-partition -c {0} -p /dev/disk/by-partuuid/{1} -m 20 -f "mkfs.ext4 -F /dev/disk/by-partuuid/{1}"'.format(self.url, self.PARTUUID) + expected = '''swupd-update-partition: Updating to 20 from {url}. +swupd-update-partition: Trying to update. +swupd-update-partition: mount /dev/disk/by-partuuid/{uuid} /tmp/swupd-mount.X +swupd-update-partition: Trying to update. +swupd-update-partition: swupd update --no-scripts -c {url} -v file:///tmp/swupd-version.X -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Update started. +Attempting to download version string to memory +Preparing to update from 10 to 20 +Running script 'Pre-update' +Downloading packs... + +Extracting os-core pack for version 20 +Statistics for going from version 10 to version 20: + + changed bundles : 1 + new bundles : 0 + deleted bundles : 0 + + changed files : 11 + new files : 3 + deleted files : 1 + +Starting download of remaining update content. This may take a while... +Finishing download of update content... +Staging file content +Applying update +Update was applied. +WARNING: post-update helper scripts skipped due to --no-scripts argument +Update took x.y seconds +Update successful. System updated from version 10 to version 20 +swupd-update-partition: Verifying and fixing content. +swupd-update-partition: swupd verify --fix --picky --picky-tree / --picky-whitelist ^/swupd-state/ --no-scripts -F 4 -c {url} -v file:///tmp/swupd-version.X -m 20 -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Verifying version 20 +Starting download of remaining update content. This may take a while... +Finishing download of update content... +Adding any missing files + +Fixing modified files +--picky removing extra files under /tmp/swupd-mount.X/ +Inspected xxx files + 0 files were missing + 0 files found which should be deleted +WARNING: post-update helper scripts skipped due to --no-scripts argument +Fix successful +swupd-update-partition: umount /tmp/swupd-mount.X +swupd-update-partition: Update successful. + +"GET /10/Manifest.MoM.tar HTTP/1.1" 200 - +"GET /20/Manifest.MoM.tar HTTP/1.1" 200 - +"GET /10/Manifest.os-core.tar HTTP/1.1" 200 - +"GET /20/Manifest-os-core-delta-from-10 HTTP/1.1" 200 - +"GET /20/pack-os-core-from-10.tar HTTP/1.1" 200 - +'''.format( + uuid=self.PARTUUID, + url=self.url +) + self.update_partition(cmd, expected, 10) + + + def test_update_with_source(self): + """ + Install and update with source partition. + """ + + # Install after force formatting, with source partition. + # Source and target have the same version. + cmd = 'swupd-update-partition -c {0} -p /dev/disk/by-partuuid/{1} -m 10 -f "mkfs.ext4 -F /dev/disk/by-partuuid/{1}" -F -s /'.format(self.url, self.PARTUUID) + # Some /etc files get modified at runtime due to the writable + # rootfs and thus do not match. + expected = '''swupd-update-partition: Bind-mounting source tree. +swupd-update-partition: mount -obind,ro / /tmp/swupd-mount-source.X +swupd-update-partition: Updating to 10 from {url}. +swupd-update-partition: Reinstalling from scratch. +swupd-update-partition: Formatting partition. +swupd-update-partition: mkfs.ext4 -F /dev/disk/by-partuuid/{uuid} +... +swupd-update-partition: mount /dev/disk/by-partuuid/{uuid} /tmp/swupd-mount.X +swupd-update-partition: rm -rf /tmp/swupd-mount.X/lost+found +swupd-update-partition: Copy from source /. +swupd-update-partition: Trying to update. +swupd-update-partition: swupd update --no-scripts -c http://10.0.2.100:8080 -v file:///tmp/swupd-version.X -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Update started. +Attempting to download version string to memory +Version on server (10) is not newer than system version (10) +Update complete. System already up-to-date at version 10 +swupd-update-partition: Verifying and fixing content. +swupd-update-partition: swupd verify --fix --picky --picky-tree / --picky-whitelist ^/swupd-state/ --no-scripts -F 4 -c {url} -v file:///tmp/swupd-version.X -m 10 -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Verifying version 10 +Starting download of remaining update content. This may take a while... +Finishing download of update content... +Adding any missing files + +Fixing modified files +--picky removing extra files under /tmp/swupd-mount.X/ +Inspected xxx files + 0 files were missing + 0 files found which should be deleted +WARNING: post-update helper scripts skipped due to --no-scripts argument +Fix successful +swupd-update-partition: umount /tmp/swupd-mount.X +swupd-update-partition: Update successful. + +"GET /10/Manifest.MoM.tar HTTP/1.1" 200 - +"GET /10/Manifest.os-core.tar HTTP/1.1" 200 - +'''.format( + uuid=self.PARTUUID, + url=self.url +) + self.update_partition(cmd, expected, 10) + + # Update, with source, without formatting. + cmd = 'swupd-update-partition -c {0} -p /dev/disk/by-partuuid/{1} -m 20 -f "mkfs.ext4 -F /dev/disk/by-partuuid/{1}" -s /'.format(self.url, self.PARTUUID) + # Some /etc files get modified at runtime due to the writable + # rootfs and thus do not match. + expected = '''swupd-update-partition: Bind-mounting source tree. +swupd-update-partition: mount -obind,ro / /tmp/swupd-mount-source.X +swupd-update-partition: Updating to 20 from {url}. +swupd-update-partition: Trying to update. +swupd-update-partition: mount /dev/disk/by-partuuid/{uuid} /tmp/swupd-mount.X +swupd-update-partition: Copy from source /. +swupd-update-partition: Trying to update. +swupd-update-partition: swupd update --no-scripts -c {url} -v file:///tmp/swupd-version.X -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Update started. +Attempting to download version string to memory +Preparing to update from 10 to 20 +Running script 'Pre-update' +Downloading packs... + +Extracting os-core pack for version 20 +Statistics for going from version 10 to version 20: + + changed bundles : 1 + new bundles : 0 + deleted bundles : 0 + + changed files : 11 + new files : 3 + deleted files : 1 + +Starting download of remaining update content. This may take a while... +Finishing download of update content... +Staging file content +Applying update +Update was applied. +WARNING: post-update helper scripts skipped due to --no-scripts argument +Update took x.y seconds +Update successful. System updated from version 10 to version 20 +swupd-update-partition: Verifying and fixing content. +swupd-update-partition: swupd verify --fix --picky --picky-tree / --picky-whitelist ^/swupd-state/ --no-scripts -F 4 -c http://10.0.2.100:8080 -v file:///tmp/swupd-version.X -m 20 -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Verifying version 20 +Starting download of remaining update content. This may take a while... +Finishing download of update content... +Adding any missing files + +Fixing modified files +--picky removing extra files under /tmp/swupd-mount.X/ +Inspected xxx files + 0 files were missing + 0 files found which should be deleted +WARNING: post-update helper scripts skipped due to --no-scripts argument +Fix successful +swupd-update-partition: umount /tmp/swupd-mount.X +swupd-update-partition: Update successful. + +"GET /10/Manifest.MoM.tar HTTP/1.1" 200 - +"GET /20/Manifest.MoM.tar HTTP/1.1" 200 - +"GET /10/Manifest.os-core.tar HTTP/1.1" 200 - +"GET /20/Manifest-os-core-delta-from-10 HTTP/1.1" 200 - +"GET /20/pack-os-core-from-10.tar HTTP/1.1" 200 - +'''.format( + uuid=self.PARTUUID, + url=self.url +) + self.update_partition(cmd, expected, 20) + + + def test_update(self): + """ + Update, with formatting. + """ + + # Update, with source, with formatting. + cmd = 'swupd-update-partition -c {0} -p /dev/disk/by-partuuid/{1} -m 20 -f "mkfs.ext4 -F /dev/disk/by-partuuid/{1}" -F -s /; ret=$?; [ $ret -eq 0 ]'.format(self.url, self.PARTUUID) + # Some /etc files get modified at runtime due to the writable + # rootfs and thus do not match. + expected = '''swupd-update-partition: Bind-mounting source tree. +swupd-update-partition: mount -obind,ro / /tmp/swupd-mount-source.X +swupd-update-partition: Updating to 20 from {url}. +swupd-update-partition: Reinstalling from scratch. +swupd-update-partition: Formatting partition. +swupd-update-partition: mkfs.ext4 -F /dev/disk/by-partuuid/87654321-9abc-def0-0fed-cba987654320 +... +swupd-update-partition: mount /dev/disk/by-partuuid/{uuid} /tmp/swupd-mount.X +swupd-update-partition: rm -rf /tmp/swupd-mount.X/lost+found +swupd-update-partition: Copy from source /. +swupd-update-partition: Trying to update. +swupd-update-partition: swupd update --no-scripts -c {url} -v file:///tmp/swupd-version.X -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Update started. +Attempting to download version string to memory +Preparing to update from 10 to 20 +Running script 'Pre-update' +Downloading packs... + +Extracting os-core pack for version 20 +Statistics for going from version 10 to version 20: + + changed bundles : 1 + new bundles : 0 + deleted bundles : 0 + + changed files : 11 + new files : 3 + deleted files : 1 + +Starting download of remaining update content. This may take a while... +Finishing download of update content... +Staging file content +Applying update +Update was applied. +WARNING: post-update helper scripts skipped due to --no-scripts argument +Update took x.y seconds +Update successful. System updated from version 10 to version 20 +swupd-update-partition: Verifying and fixing content. +swupd-update-partition: swupd verify --fix --picky --picky-tree / --picky-whitelist ^/swupd-state/ --no-scripts -F 4 -c http://10.0.2.100:8080 -v file:///tmp/swupd-version.X -m 20 -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Verifying version 20 +Starting download of remaining update content. This may take a while... +Finishing download of update content... +Adding any missing files + +Fixing modified files +--picky removing extra files under /tmp/swupd-mount.X/ +Inspected xxx files + 0 files were missing + 0 files found which should be deleted +WARNING: post-update helper scripts skipped due to --no-scripts argument +Fix successful +swupd-update-partition: umount /tmp/swupd-mount.X +swupd-update-partition: Update successful. + +"GET /10/Manifest.MoM.tar HTTP/1.1" 200 - +"GET /20/Manifest.MoM.tar HTTP/1.1" 200 - +"GET /10/Manifest.os-core.tar HTTP/1.1" 200 - +"GET /20/Manifest-os-core-delta-from-10 HTTP/1.1" 200 - +"GET /20/pack-os-core-from-10.tar HTTP/1.1" 200 - +'''.format( + uuid=self.PARTUUID, + url=self.url +) + self.update_partition(cmd, expected, 20) + + + def test_extra_files(self): + """ + Update, with formatting, then remove extra files. + """ + + # Update, with source, with formatting. Our source partition + # is read-only, so in order to place extra files into the + # target partition we use a trick: we add additional commands + # to the format command that swupd-update-partition invokes. + mkfscmd = "sh -c 'mkfs.ext4 -F /dev/disk/by-partuuid/{0} && " \ + "mkdir /tmp/extra-files && " \ + "mount /dev/disk/by-partuuid/{0} /tmp/extra-files && " \ + "mkdir /tmp/extra-files/remove && " \ + "touch /tmp/extra-files/remove/me && " \ + "mkdir -p /tmp/extra-files/usr/local && " \ + "touch /tmp/extra-files/usr/local/remove-me && " \ + "chmod -R a-r /tmp/extra-files/remove /tmp/extra-files/usr/local && " \ + "umount /tmp/extra-files && rmdir /tmp/extra-files'".format(self.PARTUUID) + cmd = 'swupd-update-partition -c {0} -p /dev/disk/by-partuuid/{1} -m 20 -f "{2}" -F -s /; ret=$?; [ $ret -eq 0 ]'.format(self.url, self.PARTUUID, mkfscmd) + # Some /etc files get modified at runtime due to the writable + # rootfs and thus do not match. + expected = '''swupd-update-partition: Bind-mounting source tree. +swupd-update-partition: mount -obind,ro / /tmp/swupd-mount-source.X +swupd-update-partition: Updating to 20 from {url}. +swupd-update-partition: Reinstalling from scratch. +swupd-update-partition: Formatting partition. +swupd-update-partition: {mkfscmd} +... +swupd-update-partition: mount /dev/disk/by-partuuid/{uuid} /tmp/swupd-mount.X +swupd-update-partition: rm -rf /tmp/swupd-mount.X/lost+found +swupd-update-partition: Copy from source /. +swupd-update-partition: Trying to update. +swupd-update-partition: swupd update --no-scripts -c {url} -v file:///tmp/swupd-version.X -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Update started. +Attempting to download version string to memory +Preparing to update from 10 to 20 +Running script 'Pre-update' +Downloading packs... + +Extracting os-core pack for version 20 +Statistics for going from version 10 to version 20: + + changed bundles : 1 + new bundles : 0 + deleted bundles : 0 + + changed files : 11 + new files : 3 + deleted files : 1 + +Starting download of remaining update content. This may take a while... +Finishing download of update content... +Staging file content +Applying update +Update was applied. +WARNING: post-update helper scripts skipped due to --no-scripts argument +Update took x.y seconds +Update successful. System updated from version 10 to version 20 +swupd-update-partition: Verifying and fixing content. +swupd-update-partition: swupd verify --fix --picky --picky-tree / --picky-whitelist /swupd-state --no-scripts -F 4 -c http://10.0.2.100:8080 -v file:///tmp/swupd-version.X -m 20 -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Verifying version 20 +Starting download of remaining update content. This may take a while... +Finishing download of update content... +Adding any missing files + +Fixing modified files +--picky removing extra files under /tmp/swupd-mount.X/ +REMOVING /usr/local/remove-me +REMOVING DIR /usr/local/ +REMOVING /remove/me +REMOVING DIR /remove/ +Inspected xxx files + 0 files were missing + 0 files found which should be deleted +WARNING: post-update helper scripts skipped due to --no-scripts argument +Fix successful +swupd-update-partition: umount /tmp/swupd-mount.X +swupd-update-partition: Update successful. + +"GET /10/Manifest.MoM.tar HTTP/1.1" 200 - +"GET /20/Manifest.MoM.tar HTTP/1.1" 200 - +"GET /10/Manifest.os-core.tar HTTP/1.1" 200 - +"GET /20/Manifest-os-core-delta-from-10 HTTP/1.1" 200 - +"GET /20/pack-os-core-from-10.tar HTTP/1.1" 200 - +'''.format( + mkfscmd=mkfscmd, + uuid=self.PARTUUID, + url=self.url +) + self.update_partition(cmd, expected, 20) + + + def test_update_network_0(self): + """ + Same as test_update, but with network errors before first HTTP request. + """ + + # Update, with source, with formatting. + cmd = 'swupd-update-partition -c {0} -p /dev/disk/by-partuuid/{1} -m 20 -f "mkfs.ext4 -F /dev/disk/by-partuuid/{1}" -F -s /; ret=$?; [ $ret -eq 2 ]'.format(self.url, self.PARTUUID) + # Some /etc files get modified at runtime due to the writable + # rootfs and thus do not match. + expected = '''swupd-update-partition: Bind-mounting source tree. +swupd-update-partition: mount -obind,ro / /tmp/swupd-mount-source.X +swupd-update-partition: Updating to 20 from {url}. +swupd-update-partition: Reinstalling from scratch. +swupd-update-partition: Formatting partition. +swupd-update-partition: mkfs.ext4 -F /dev/disk/by-partuuid/87654321-9abc-def0-0fed-cba987654320 +... +swupd-update-partition: mount /dev/disk/by-partuuid/{uuid} /tmp/swupd-mount.X +swupd-update-partition: rm -rf /tmp/swupd-mount.X/lost+found +swupd-update-partition: Copy from source /. +swupd-update-partition: Trying to update. +swupd-update-partition: swupd update --no-scripts -c {url} -v file:///tmp/swupd-version.X -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Update started. +Attempting to download version string to memory +Preparing to update from 10 to 20 +Failed to retrieve 10 MoM manifest +Retry #1 downloading from/to MoM Manifests +Failed to retrieve 10 MoM manifest +Retry #2 downloading from/to MoM Manifests +Failed to retrieve 10 MoM manifest +Retry #3 downloading from/to MoM Manifests +Failed to retrieve 10 MoM manifest +Failure retrieving manifest from server +Update took x.y seconds +swupd-update-partition: swupd: EMOM_NOTFOUND = 4 = MoM cannot be loaded into memory (this could imply network issue) +swupd-update-partition: Update failed temporarily. + +code 500, message test server is intentionally down +"GET /10/Manifest.MoM.tar HTTP/1.1" 500 - +code 500, message test server is intentionally down +"GET /10/Manifest.MoM.tar HTTP/1.1" 500 - +code 500, message test server is intentionally down +"GET /10/Manifest.MoM.tar HTTP/1.1" 500 - +code 500, message test server is intentionally down +"GET /10/Manifest.MoM.tar HTTP/1.1" 500 - +'''.format( + uuid=self.PARTUUID, + url=self.url +) + self.update_partition(cmd, expected, 20, network_error=0) + + + def test_update_network_4(self): + """ + Same as test_update, but with network errors before fifth HTTP request. + """ + + self.skipTest('swupd does not detect this network error as it should - https://github.com/clearlinux/swupd-client/issues/323') + + # Update, with source, with formatting. + cmd = 'swupd-update-partition -c {0} -p /dev/disk/by-partuuid/{1} -m 20 -f "mkfs.ext4 -F /dev/disk/by-partuuid/{1}" -F -s /; ret=$?; [ $ret -eq 2 ]'.format(self.url, self.PARTUUID) + # Some /etc files get modified at runtime due to the writable + # rootfs and thus do not match. + expected = '''swupd-update-partition: Bind-mounting source tree. +swupd-update-partition: mount -obind,ro / /tmp/swupd-mount-source.X +swupd-update-partition: Updating to 20 from {url}. +swupd-update-partition: Reinstalling from scratch. +swupd-update-partition: Formatting partition. +swupd-update-partition: mkfs.ext4 -F /dev/disk/by-partuuid/87654321-9abc-def0-0fed-cba987654320 +... +swupd-update-partition: mount /dev/disk/by-partuuid/{uuid} /tmp/swupd-mount.X +swupd-update-partition: rm -rf /tmp/swupd-mount.X/lost+found +swupd-update-partition: Copy from source /. +swupd-update-partition: Trying to update. +swupd-update-partition: swupd update --no-scripts -c {url} -v file:///tmp/swupd-version.X -S /tmp/swupd-mount.X/swupd-state -p /tmp/swupd-mount.X +swupd-client software ... + +Update started. +Attempting to download version string to memory +Preparing to update from 10 to 20 + +TODO: insert actual output. +'''.format( + uuid=self.PARTUUID, + url=self.url +) + self.update_partition(cmd, expected, 20, network_error=4) diff --git a/meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd-bundles.bb b/meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd-bundles.bb new file mode 100644 index 0000000000..f508b8fb86 --- /dev/null +++ b/meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd-bundles.bb @@ -0,0 +1,10 @@ +SUMMARY = "test image: refkit-image-update-swupd + bundles" + +require refkit-image-update-swupd.bb + +SWUPD_BUNDLES = "feature_one feature_two" +BUNDLE_CONTENTS[feature_one] = "refkit-test-feature-hello" +BUNDLE_CONTENTS[feature_two] = "refkit-test-feature-world" + +SWUPD_IMAGES = "dev" +SWUPD_IMAGES[dev] = "feature_one feature_two" diff --git a/meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd-modified.bb b/meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd-modified.bb new file mode 100644 index 0000000000..4b2e2c379e --- /dev/null +++ b/meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd-modified.bb @@ -0,0 +1,10 @@ +SUMMARY = "test image for RefkitSwupdUpdateTestDev: refkit-image-common + swupd + modifications" + +# RefkitSwupdUpdateTestDev uses this to avoid rebuilding +# refkit-image-update-swupd when running the test multiple +# times. +require refkit-image-update-swupd.bb + +DEPLOY_DIR_SWUPD = "${DEPLOY_DIR}/swupd/${MACHINE}/refkit-image-update-swupd" +SWUPD_VERSION_URL = "http://download.example.com/updates/my-distro/milestone/${MACHINE}/refkit-image-update-swupd" +SWUPD_CONTENT_URL = "http://download.example.com/updates/my-distro/builds/${MACHINE}/refkit-image-update-swupd" diff --git a/meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd.bb b/meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd.bb new file mode 100644 index 0000000000..a4e3a88d48 --- /dev/null +++ b/meta-refkit-core/recipes-selftest/images/refkit-image-update-swupd.bb @@ -0,0 +1,17 @@ +SUMMARY = "test image for RefkitSwupdUpdateTest: refkit-image-minimal + swupd" + +# Must be set before parsing refkit-image.bbclass because it pulls in +# swupd-image.bbclass during parsing. +IMAGE_FEATURES_append = " swupd" +require ${META_REFKIT_CORE_BASE}/recipes-images/images/refkit-image-minimal.bb + +# We need network connectivity (basically, DHCP). +REFKIT_IMAGE_EXTRA_FEATURES += "connectivity" + +# Speed up testing by disabling the os-core zero pack. +# It is only needed for "swupd verify --install". +SWUPD_GENERATE_OS_CORE_ZERO_PACK = "false" + +# BUILD_ID is fixed in the CI system and variable in local builds (= +# ${DATETIME}). To ensure consistent test results, we keep it fixed here. +BUILD_ID = "swupd-test-build" diff --git a/meta-refkit-core/recipes-selftest/images/refkit-test-feature.bb b/meta-refkit-core/recipes-selftest/images/refkit-test-feature.bb new file mode 100644 index 0000000000..f6fb4511d5 --- /dev/null +++ b/meta-refkit-core/recipes-selftest/images/refkit-test-feature.bb @@ -0,0 +1,22 @@ +DESCRIPTION = "test content with user IDs" +LICENSE = "MIT" + +inherit useradd allarch + +do_install () { + install -d ${D}${datadir} + echo "hello" >${D}${datadir}/refkit-test-content-hello + echo "world" >${D}${datadir}/refkit-test-content-world + chmod 0644 ${D}${datadir}/* + chown groupcheck:groupcheck ${D}${datadir}/refkit-test-content-hello + chown polkitd:polkitd ${D}${datadir}/refkit-test-content-world +} + +# We pick users here for which Refkit already has static IDs. +USERADD_PACKAGES = "${PN}-hello ${PN}-world" +USERADD_PARAM_${PN}-hello = "--system --no-create-home --user-group groupcheck" +USERADD_PARAM_${PN}-world = "--system --no-create-home --user-group polkitd" + +PACKAGES = "${PN}-hello ${PN}-world" +FILES_${PN}-hello = "${datadir}/refkit-test-content-hello" +FILES_${PN}-world = "${datadir}/refkit-test-content-world" From c6aca04751238ea13b058232787096fcba5b3f8f Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 5 Oct 2017 21:47:45 +0200 Subject: [PATCH 20/22] refkit-ci.inc: enable swupd testing RefkitSwupdUpdateTestAll currently contains a single test which covers various live updating. RefkitSwupdUpdateTestIncremental simulates a CI setup where each build starts without a local swupd repo. RefkitSwupdPartitionTest covers swupd-update-partition. Signed-off-by: Patrick Ohly --- meta-refkit/conf/distro/include/refkit-ci.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta-refkit/conf/distro/include/refkit-ci.inc b/meta-refkit/conf/distro/include/refkit-ci.inc index 575bb5ff69..c1cc387ef7 100644 --- a/meta-refkit/conf/distro/include/refkit-ci.inc +++ b/meta-refkit/conf/distro/include/refkit-ci.inc @@ -53,7 +53,7 @@ REFKIT_VM_IMAGE_TYPES ?= "wic.xz wic.bmap" REFKIT_CI_PREBUILD_SELFTESTS="iotsstatetests.SStateTests.test_sstate_samesigs" # https://bugzilla.yoctoproject.org/show_bug.cgi?id=11756 currently causes # oe-selftest to ignore the order. -REFKIT_CI_POSTBUILD_SELFTESTS="refkit_secureboot refkit_poky refkit_license_check refkit_ostree.RefkitOSTreeUpdateTestAll image_installer" +REFKIT_CI_POSTBUILD_SELFTESTS="refkit_secureboot refkit_poky refkit_license_check refkit_ostree.RefkitOSTreeUpdateTestAll image_installer refkit_swupd.RefkitSwupdUpdateTestAll refkit_swupd.RefkitSwupdUpdateTestIncremental refkit_swupd.RefkitSwupdPartitionTest" # # Automated build targets # Those targets should be space separated list of items, From e33635301407340da3dd639b37e1f87b2845aafd Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Wed, 11 Oct 2017 08:23:49 +0200 Subject: [PATCH 21/22] TEST: run swupd tests early The test is failing only in the CI, so run it immediately to speed up debugging. Signed-off-by: Patrick Ohly --- meta-refkit/conf/distro/include/refkit-ci.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meta-refkit/conf/distro/include/refkit-ci.inc b/meta-refkit/conf/distro/include/refkit-ci.inc index c1cc387ef7..3b6adb026f 100644 --- a/meta-refkit/conf/distro/include/refkit-ci.inc +++ b/meta-refkit/conf/distro/include/refkit-ci.inc @@ -50,10 +50,10 @@ REFKIT_VM_IMAGE_TYPES ?= "wic.xz wic.bmap" # # pre/post-build oe-selftests started by CI builder, whitespace-separated. # -REFKIT_CI_PREBUILD_SELFTESTS="iotsstatetests.SStateTests.test_sstate_samesigs" +REFKIT_CI_PREBUILD_SELFTESTS="refkit_swupd.RefkitSwupdUpdateTestAll refkit_swupd.RefkitSwupdUpdateTestIncremental refkit_swupd.RefkitSwupdPartitionTest" # https://bugzilla.yoctoproject.org/show_bug.cgi?id=11756 currently causes # oe-selftest to ignore the order. -REFKIT_CI_POSTBUILD_SELFTESTS="refkit_secureboot refkit_poky refkit_license_check refkit_ostree.RefkitOSTreeUpdateTestAll image_installer refkit_swupd.RefkitSwupdUpdateTestAll refkit_swupd.RefkitSwupdUpdateTestIncremental refkit_swupd.RefkitSwupdPartitionTest" +REFKIT_CI_POSTBUILD_SELFTESTS="refkit_secureboot refkit_poky refkit_license_check refkit_ostree.RefkitOSTreeUpdateTestAll image_installer" # # Automated build targets # Those targets should be space separated list of items, From 927b9616a61d000a9e699467fd122614b2294d71 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 10 Nov 2017 18:08:38 +0100 Subject: [PATCH 22/22] librealsense_1.12.1.bbappend: fix SRC_URI after branch rewrite librealsense has rebased their master branch, leading to: "Unable to find revision 7332ecadc057552c178addd577d24a2756f8789a in branch master even from upstream". This is a hotfix until meta-intel-realsense gets updated. Signed-off-by: Patrick Ohly --- .../recipes-support/librealsense/librealsense_1.12.1.bbappend | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 meta-refkit-core/bbappends/meta-intel-realsense/recipes-support/librealsense/librealsense_1.12.1.bbappend diff --git a/meta-refkit-core/bbappends/meta-intel-realsense/recipes-support/librealsense/librealsense_1.12.1.bbappend b/meta-refkit-core/bbappends/meta-intel-realsense/recipes-support/librealsense/librealsense_1.12.1.bbappend new file mode 100644 index 0000000000..82d34ac089 --- /dev/null +++ b/meta-refkit-core/bbappends/meta-intel-realsense/recipes-support/librealsense/librealsense_1.12.1.bbappend @@ -0,0 +1,4 @@ +# History of the git repo was rewritten so that the SRCREV is no longer on the master +# branch... +SRC_URI_remove_df-refkit-config = "git://github.com/IntelRealSense/librealsense.git;branch=master" +SRC_URI_prepend_df-refkit-config = "git://github.com/IntelRealSense/librealsense.git;nobranch=1 "