def test_image_update_broken_kernel( self, bitbake_variables, connection, latest_mender_image, http_server, board_type, use_s3, s3_address, ): """Test that an update with a broken kernel rolls back correctly. This is distinct from the test_broken_image_update test, which corrupts the filesystem. When grub.d integration is enabled, these two scenarios trigger very different code paths.""" file_flag = Helpers.get_file_flag(bitbake_variables) (active_before, passive_before) = determine_active_passive_part( bitbake_variables, connection) image_type = bitbake_variables["MENDER_DEVICE_TYPE"] temp_artifact = "temporary_artifact.mender" try: shutil.copyfile(latest_mender_image, temp_artifact) # Assume that artifact has the same kernel names as the currently # running image. kernels = connection.run( "find /boot/ -maxdepth 1 -name '*linu[xz]*' -o -name '*Image'" ).stdout.split() for kernel in kernels: # Inefficient, but there shouldn't be too many kernels. subprocess.check_call( ["mender-artifact", "rm", f"{temp_artifact}:{kernel}"]) Helpers.install_update( temp_artifact, connection, http_server, board_type, use_s3, s3_address, ) reboot(connection) # Now qemu is auto-rebooted twice; once to boot the dummy image, # where it fails, and the boot loader auto-reboots a second time # into the original partition. output = run_after_connect("mount", connection) # The update should have reverted to the original active partition, # since the kernel was missing. assert output.find(active_before) >= 0 assert output.find(passive_before) < 0 finally: os.remove(temp_artifact)
def test_broken_image_update(self, bitbake_variables, connection): """Test that an update with a broken filesystem rolls back correctly.""" file_flag = Helpers.get_file_flag(bitbake_variables) (active_before, passive_before) = determine_active_passive_part( bitbake_variables, connection) image_type = bitbake_variables["MENDER_DEVICE_TYPE"] try: # Make a dummy/broken update retcode = subprocess.call( "dd if=/dev/zero of=image.dat bs=1M count=0 seek=16", shell=True) if retcode != 0: raise Exception("error creating dummy image") retcode = subprocess.call( "mender-artifact write rootfs-image -t %s -n test-update %s image.dat -o image.mender" % (image_type, file_flag), shell=True, ) if retcode != 0: raise Exception( "error writing mender artifact using command: mender-artifact write rootfs-image -t %s -n test-update %s image.dat -o image.mender" % (image_type, file_flag)) put_no_sftp("image.mender", connection, remote="/var/tmp/image.mender") connection.run("mender install /var/tmp/image.mender") reboot(connection) # Now qemu is auto-rebooted twice; once to boot the dummy image, # where it fails, and the boot loader auto-reboots a second time # into the original partition. output = run_after_connect("mount", connection) # The update should have reverted to the original active partition, # since the image was bogus. assert output.find(active_before) >= 0 assert output.find(passive_before) < 0 finally: # Cleanup. if os.path.exists("image.mender"): os.remove("image.mender") if os.path.exists("image.dat"): os.remove("image.dat")
def test_too_big_image_update(self, bitbake_variables, connection): file_flag = Helpers.get_file_flag(bitbake_variables) install_flag = Helpers.get_install_flag(connection) image_type = bitbake_variables["MENDER_DEVICE_TYPE"] try: # Make a too big update subprocess.call( "dd if=/dev/zero of=image.dat bs=1M count=0 seek=4096", shell=True) subprocess.call( "mender-artifact write rootfs-image -t %s -n test-update-too-big %s image.dat -o image-too-big.mender" % (image_type, file_flag), shell=True, ) put_no_sftp( "image-too-big.mender", connection, remote="/var/tmp/image-too-big.mender", ) output = connection.run( "mender %s /var/tmp/image-too-big.mender ; echo 'ret_code=$?'" % install_flag) assert any([ "no space left on device" in out for out in [output.stderr, output.stdout] ]), output assert "ret_code=0" not in output.stdout, output finally: # Cleanup. if os.path.exists("image-too-big.mender"): os.remove("image-too-big.mender") if os.path.exists("image.dat"): os.remove("image.dat")
def test_uboot_mender_saveenv_canary(self, bitbake_variables, connection): """Tests that the mender_saveenv_canary works correctly, which tests that Mender will not proceed unless the U-Boot boot loader has saved the environment.""" file_flag = Helpers.get_file_flag(bitbake_variables) image_type = bitbake_variables["MACHINE"] try: # Make a dummy/broken update subprocess.call( "dd if=/dev/zero of=image.dat bs=1M count=0 seek=16", shell=True) subprocess.call( "mender-artifact write rootfs-image -t %s -n test-update %s image.dat -o image.mender" % (image_type, file_flag), shell=True, ) put_no_sftp("image.mender", connection, remote="/var/tmp/image.mender") env_conf = connection.run("cat /etc/fw_env.config").stdout env_conf_lines = env_conf.rstrip("\n\r").split("\n") assert len(env_conf_lines) == 2 for i in [0, 1]: entry = env_conf_lines[i].split() connection.run( "dd if=%s skip=%d bs=%d count=1 iflag=skip_bytes > /data/old_env%d" % (entry[0], int(entry[1], 0), int(entry[2], 0), i)) try: bootenv_print, bootenv_set = bootenv_tools(connection) # Try to manually remove the canary first. connection.run(f"{bootenv_set} mender_saveenv_canary") result = connection.run("mender install /var/tmp/image.mender", warn=True) assert (result.return_code != 0), "Update succeeded when canary was not present!" output = connection.run( f"{bootenv_print} upgrade_available").stdout.rstrip("\n") # Upgrade should not have been triggered. assert output == "upgrade_available=0" # Then zero the environment, causing the libubootenv to fail # completely. for i in [0, 1]: entry = env_conf_lines[i].split() connection.run( "dd if=/dev/zero of=%s seek=%d bs=%d count=1 oflag=seek_bytes" % (entry[0], int(entry[1], 0), int(entry[2], 0))) result = connection.run("mender install /var/tmp/image.mender", warn=True) assert (result.return_code != 0), "Update succeeded when canary was not present!" finally: # Restore environment to what it was. for i in [0, 1]: entry = env_conf_lines[i].split() connection.run( "dd of=%s seek=%d bs=%d count=1 oflag=seek_bytes < /data/old_env%d" % (entry[0], int(entry[1], 0), int(entry[2], 0), i)) connection.run("rm -f /data/old_env%d" % i) finally: # Cleanup. os.remove("image.mender") os.remove("image.dat")
def test_signed_updates(self, sig_case, bitbake_variables, connection): """Test various combinations of signed and unsigned, present and non- present verification keys.""" file_flag = Helpers.get_file_flag(bitbake_variables) # mmc mount points are named: /dev/mmcblk0p1 # ubi volumes are named: ubi0_1 (active, passive) = determine_active_passive_part(bitbake_variables, connection) if passive.startswith("ubi"): passive = "/dev/" + passive # Generate "update" appropriate for this test case. # Cheat a little. Instead of spending a lot of time on a lot of reboots, # just verify that the contents of the update are correct. new_content = sig_case.label with open("image.dat", "w") as fd: fd.write(new_content) # Write some extra data just to make sure the update is big enough # to be written even if the checksum is wrong. If it's too small it # may fail before it has a chance to be written. fd.write("\x00" * (1048576 * 8)) artifact_args = "" # Generate artifact with or without signature. if sig_case.signature: artifact_args += " -k %s" % signing_key(sig_case.key_type).private # Generate artifact with specific version. None means default. if sig_case.artifact_version is not None: artifact_args += " -v %d" % sig_case.artifact_version if sig_case.key_type: sig_key = signing_key(sig_case.key_type) else: sig_key = None image_type = bitbake_variables["MENDER_DEVICE_TYPE"] subprocess.check_call( "mender-artifact write rootfs-image %s -t %s -n test-update %s image.dat -o image.mender" % (artifact_args, image_type, file_flag), shell=True, ) # If instructed to, corrupt the signature and/or checksum. if ((sig_case.signature and not sig_case.signature_ok) or not sig_case.checksum_ok or not sig_case.header_checksum_ok): tar = subprocess.check_output(["tar", "tf", "image.mender"]) tar_list = tar.split() tmpdir = tempfile.mkdtemp() try: shutil.copy("image.mender", os.path.join(tmpdir, "image.mender")) cwd = os.open(".", os.O_RDONLY) os.chdir(tmpdir) try: tar = subprocess.check_output( ["tar", "xf", "image.mender"]) if not sig_case.signature_ok: # Corrupt signature. with open("manifest.sig", "r+") as fd: Helpers.corrupt_middle_byte(fd) if not sig_case.checksum_ok: os.chdir("data") try: data_list = subprocess.check_output( ["tar", "tzf", "0000.tar.gz"]) data_list = data_list.split() subprocess.check_call( ["tar", "xzf", "0000.tar.gz"]) # Corrupt checksum by changing file slightly. with open("image.dat", "r+") as fd: Helpers.corrupt_middle_byte(fd) # Pack it up again in same order. os.remove("0000.tar.gz") subprocess.check_call( ["tar", "czf", "0000.tar.gz"] + data_list) for data_file in data_list: os.remove(data_file) finally: os.chdir("..") if not sig_case.header_checksum_ok: data_list = subprocess.check_output( ["tar", "tzf", "header.tar.gz"]) data_list = data_list.split() subprocess.check_call(["tar", "xzf", "header.tar.gz"]) # Corrupt checksum by changing file slightly. with open("headers/0000/files", "a") as fd: # Some extra data to corrupt the header checksum, # but still valid JSON. fd.write(" ") # Pack it up again in same order. os.remove("header.tar.gz") subprocess.check_call(["tar", "czf", "header.tar.gz"] + data_list) for data_file in data_list: os.remove(data_file) # Make sure we put it back in the same order. os.remove("image.mender") subprocess.check_call(["tar", "cf", "image.mender"] + tar_list) finally: os.fchdir(cwd) os.close(cwd) shutil.move(os.path.join(tmpdir, "image.mender"), "image.mender") finally: shutil.rmtree(tmpdir, ignore_errors=True) put_no_sftp("image.mender", connection, remote="/data/image.mender") # mender-convert'ed images don't have transient mender.conf device_has_mender_conf = (connection.run( "test -f /etc/mender/mender.conf", warn=True).return_code == 0) # mender-convert'ed images don't have this directory, but the test uses # it to save certificates connection.run("mkdir -p /data/etc/mender") try: # Get configuration from device or create an empty one if device_has_mender_conf: connection.run( "cp /etc/mender/mender.conf /data/etc/mender/mender.conf.bak" ) get_no_sftp("/etc/mender/mender.conf", connection) else: with open("mender.conf", "w") as fd: json.dump({}, fd) # Update key in configuration. with open("mender.conf") as fd: config = json.load(fd) if sig_case.key: config[ "ArtifactVerifyKey"] = "/data/etc/mender/%s" % os.path.basename( sig_key.public) put_no_sftp( sig_key.public, connection, remote="/data/etc/mender/%s" % os.path.basename(sig_key.public), ) else: if config.get("ArtifactVerifyKey"): del config["ArtifactVerifyKey"] # Send new configuration to device with open("mender.conf", "w") as fd: json.dump(config, fd) put_no_sftp("mender.conf", connection, remote="/etc/mender/mender.conf") os.remove("mender.conf") # Start by writing known "old" content in the partition. old_content = "Preexisting partition content" if "ubi" in passive: # ubi volumes cannot be directly written to, we have to use # ubiupdatevol connection.run('echo "%s" | dd of=/tmp/update.tmp && ' "ubiupdatevol %s /tmp/update.tmp; " "rm -f /tmp/update.tmp" % (old_content, passive)) else: connection.run('echo "%s" | dd of=%s' % (old_content, passive)) result = connection.run("mender install /data/image.mender", warn=True) if sig_case.success: if result.return_code != 0: pytest.fail( "Update failed when it should have succeeded: %s, Output: %s" % (sig_case.label, result)) else: if result.return_code == 0: pytest.fail( "Update succeeded when it should not have: %s, Output: %s" % (sig_case.label, result)) if sig_case.update_written: expected_content = new_content else: expected_content = old_content try: content = connection.run( "dd if=%s bs=%d count=1" % (passive, len(expected_content))).stdout assert content == expected_content, "Case: %s" % sig_case.label # In Fabric context, SystemExit means CalledProcessError. We should # not catch all exceptions, because we want to leave assertions # alone. # In Fabric2 there might be different exception thrown in that case # which is UnexpectedExit. except (SystemExit, UnexpectedExit): if ("mender-ubi" in bitbake_variables.get( "MENDER_FEATURES", "").split() or "mender-ubi" in bitbake_variables.get( "DISTRO_FEATURES", "").split()): # For UBI volumes specifically: The UBI_IOCVOLUP call which # Mender uses prior to writing the data, takes a size # argument, and if you don't write that amount of bytes, the # volume is marked corrupted as a security measure. This # sometimes triggers in our checksum mismatch tests, so # accept the volume being unreadable in that case. pass else: raise finally: # Reset environment to what it was. _, bootenv_set = bootenv_tools(connection) connection.run(f"{bootenv_set} mender_boot_part %s" % active[-1:]) connection.run(f"{bootenv_set} mender_boot_part_hex %x" % int(active[-1:])) connection.run(f"{bootenv_set} upgrade_available 0") if device_has_mender_conf: connection.run( "cp -L /data/etc/mender/mender.conf.bak $(realpath /etc/mender/mender.conf)" ) if sig_key: connection.run("rm -f /etc/mender/%s" % os.path.basename(sig_key.public))