From 152c64e7cb0477fdab7a02fc78b95ee5aecd8ddf Mon Sep 17 00:00:00 2001 From: "bodong.yang" Date: Thu, 21 Nov 2024 08:44:21 +0000 Subject: [PATCH 1/8] boot_control.protocol: post_update API is not a Generator anymore, define new finalizing_update API --- src/otaclient/boot_control/protocol.py | 31 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/otaclient/boot_control/protocol.py b/src/otaclient/boot_control/protocol.py index ab8b49de6..5fda7d60b 100644 --- a/src/otaclient/boot_control/protocol.py +++ b/src/otaclient/boot_control/protocol.py @@ -17,7 +17,7 @@ from abc import abstractmethod from pathlib import Path -from typing import Generator, Protocol +from typing import Protocol from typing_extensions import deprecated @@ -55,23 +55,40 @@ def get_standby_boot_dir(self) -> Path: them to actual boot dir. """ + @abstractmethod + def load_version(self) -> str: + """Read the version info from the current slot.""" + + @abstractmethod + def on_operation_failure(self) -> None: + """Cleanup by boot_control implementation when OTA failed.""" + + # + # ------ update ------ # + # + @abstractmethod def pre_update( self, version: str, *, standby_as_ref: bool, erase_standby: bool ): ... @abstractmethod - def pre_rollback(self): ... + def post_update(self) -> None: ... @abstractmethod - def post_update(self) -> Generator[None, None, None]: ... + def finalizing_update(self) -> None: + """Normally this method only reboots the device.""" + + # + # ------ rollback ------ # + # @abstractmethod - def post_rollback(self): ... + def pre_rollback(self) -> None: ... @abstractmethod - def load_version(self) -> str: - """Read the version info from the current slot.""" + def post_rollback(self): ... @abstractmethod - def on_operation_failure(self): ... + def finalizing_rollback(self) -> None: + """Normally this method only reboots the device.""" From e346699e5b5348d31c438387a40250ca64362048 Mon Sep 17 00:00:00 2001 From: "bodong.yang" Date: Thu, 21 Nov 2024 09:08:23 +0000 Subject: [PATCH 2/8] boot_control: implement finalizing_* API --- src/otaclient/boot_control/_grub.py | 18 +++++++++++++----- src/otaclient/boot_control/_jetson_cboot.py | 18 ++++++++++++++---- src/otaclient/boot_control/_jetson_uefi.py | 18 ++++++++++++++---- src/otaclient/boot_control/_rpi_boot.py | 18 ++++++++++++++---- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/otaclient/boot_control/_grub.py b/src/otaclient/boot_control/_grub.py index 3edc4aa2f..5ea621027 100644 --- a/src/otaclient/boot_control/_grub.py +++ b/src/otaclient/boot_control/_grub.py @@ -40,7 +40,7 @@ from pathlib import Path from pprint import pformat from subprocess import CalledProcessError -from typing import ClassVar, Dict, Generator, List, Optional, Tuple +from typing import ClassVar, Dict, Generator, List, NoReturn, Optional, Tuple from otaclient import errors as ota_errors from otaclient._types import OTAStatus @@ -895,7 +895,7 @@ def pre_update(self, version: str, *, standby_as_ref: bool, erase_standby=False) _err_msg, module=__name__ ) from e - def post_update(self) -> Generator[None, None, None]: + def post_update(self) -> None: try: logger.info("grub_boot: post-update setup...") # ------ update fstab ------ # @@ -916,11 +916,18 @@ def post_update(self) -> Generator[None, None, None]: # ------ pre-reboot ------ # self._mp_control.umount_all(ignore_error=True) self._boot_control.grub_reboot_to_standby() + except Exception as e: + _err_msg = f"failed on post_update: {e!r}" + logger.error(_err_msg) + raise ota_errors.BootControlPostUpdateFailed( + _err_msg, module=__name__ + ) from e - yield # hand over control to otaclient + def finalizing_update(self) -> NoReturn: + try: cmdhelper.reboot() except Exception as e: - _err_msg = f"failed on post_update: {e!r}" + _err_msg = f"reboot failed: {e!r}" logger.error(_err_msg) raise ota_errors.BootControlPostUpdateFailed( _err_msg, module=__name__ @@ -944,10 +951,11 @@ def post_rollback(self): logger.info("grub_boot: post-rollback setup...") self._boot_control.grub_reboot_to_standby() self._mp_control.umount_all(ignore_error=True) - cmdhelper.reboot() except Exception as e: _err_msg = f"failed on pre_rollback: {e!r}" logger.error(_err_msg) raise ota_errors.BootControlPostRollbackFailed( _err_msg, module=__name__ ) from e + + finalizing_rollback = finalizing_update diff --git a/src/otaclient/boot_control/_jetson_cboot.py b/src/otaclient/boot_control/_jetson_cboot.py index dab6ba37e..cad019747 100644 --- a/src/otaclient/boot_control/_jetson_cboot.py +++ b/src/otaclient/boot_control/_jetson_cboot.py @@ -22,7 +22,7 @@ import logging import subprocess from pathlib import Path -from typing import Generator, Optional +from typing import Generator, NoReturn, Optional from otaclient import errors as ota_errors from otaclient._types import OTAStatus @@ -616,7 +616,7 @@ def pre_update(self, version: str, *, standby_as_ref: bool, erase_standby: bool) _err_msg, module=__name__ ) from e - def post_update(self) -> Generator[None, None, None]: + def post_update(self) -> None: try: logger.info("jetson-cboot: post-update ...") # ------ update extlinux.conf ------ # @@ -677,8 +677,6 @@ def post_update(self) -> Generator[None, None, None]: self._mp_control.umount_all(ignore_error=True) logger.info(f"[post-update]: \n{NVBootctrlJetsonCBOOT.dump_slots_info()}") logger.info("post update finished, wait for reboot ...") - yield # hand over control back to otaclient - cmdhelper.reboot() except Exception as e: _err_msg = f"failed on post_update: {e!r}" logger.error(_err_msg) @@ -686,6 +684,16 @@ def post_update(self) -> Generator[None, None, None]: _err_msg, module=__name__ ) from e + def finalizing_update(self) -> NoReturn: + try: + cmdhelper.reboot() + except Exception as e: + _err_msg = f"reboot failed: {e!r}" + logger.error(_err_msg) + raise ota_errors.BootControlPostUpdateFailed( + _err_msg, module=__name__ + ) from e + def pre_rollback(self): try: logger.info("jetson-cboot: pre-rollback setup ...") @@ -712,6 +720,8 @@ def post_rollback(self): _err_msg, module=__name__ ) from e + finalizing_rollback = finalizing_update + def on_operation_failure(self): """Failure registering and cleanup at failure.""" logger.warning("on failure try to unmounting standby slot...") diff --git a/src/otaclient/boot_control/_jetson_uefi.py b/src/otaclient/boot_control/_jetson_uefi.py index b494a2385..5be4960f2 100644 --- a/src/otaclient/boot_control/_jetson_uefi.py +++ b/src/otaclient/boot_control/_jetson_uefi.py @@ -27,7 +27,7 @@ import shutil import subprocess from pathlib import Path -from typing import Any, ClassVar, Generator, Literal +from typing import Any, ClassVar, Generator, Literal, NoReturn from pydantic import BaseModel from typing_extensions import Self @@ -982,7 +982,7 @@ def pre_update(self, version: str, *, standby_as_ref: bool, erase_standby: bool) _err_msg, module=__name__ ) from e - def post_update(self) -> Generator[None, None, None]: + def post_update(self) -> None: try: logger.info("jetson-uefi: post-update ...") # ------ update extlinux.conf ------ # @@ -1063,8 +1063,6 @@ def post_update(self) -> Generator[None, None, None]: # ------ prepare to reboot ------ # self._mp_control.umount_all(ignore_error=True) logger.info("post update finished, wait for reboot ...") - yield # hand over control back to otaclient - cmdhelper.reboot() except Exception as e: _err_msg = f"jetson-uefi: failed on post_update: {e!r}" logger.error(_err_msg) @@ -1072,6 +1070,16 @@ def post_update(self) -> Generator[None, None, None]: _err_msg, module=__name__ ) from e + def finalizing_update(self) -> NoReturn: + try: + cmdhelper.reboot() + except Exception as e: + _err_msg = f"reboot failed: {e!r}" + logger.error(_err_msg) + raise ota_errors.BootControlPostUpdateFailed( + _err_msg, module=__name__ + ) from e + def pre_rollback(self): try: logger.info("jetson-uefi: pre-rollback setup ...") @@ -1098,6 +1106,8 @@ def post_rollback(self): _err_msg, module=__name__ ) from e + finalizing_rollback = finalizing_update + def on_operation_failure(self): """Failure registering and cleanup at failure.""" logger.warning("on failure try to unmounting standby slot...") diff --git a/src/otaclient/boot_control/_rpi_boot.py b/src/otaclient/boot_control/_rpi_boot.py index 01bdb3ca4..312af357a 100644 --- a/src/otaclient/boot_control/_rpi_boot.py +++ b/src/otaclient/boot_control/_rpi_boot.py @@ -22,7 +22,7 @@ import subprocess from pathlib import Path from string import Template -from typing import Any, Generator, Literal +from typing import Any, Generator, Literal, NoReturn from typing_extensions import Self @@ -531,7 +531,7 @@ def post_rollback(self): _err_msg, module=__name__ ) from e - def post_update(self) -> Generator[None, None, None]: + def post_update(self) -> None: try: logger.info("rpi_boot: post-update setup...") self._mp_control.preserve_ota_folder_to_standby() @@ -542,8 +542,6 @@ def post_update(self) -> Generator[None, None, None]: ) self._rpiboot_control.prepare_tryboot_txt() self._mp_control.umount_all(ignore_error=True) - yield # hand over control back to otaclient - self._rpiboot_control.reboot_tryboot() except Exception as e: _err_msg = f"failed on post_update: {e!r}" logger.error(_err_msg) @@ -551,6 +549,18 @@ def post_update(self) -> Generator[None, None, None]: _err_msg, module=__name__ ) from e + def finalizing_update(self) -> NoReturn: + try: + self._rpiboot_control.reboot_tryboot() + except Exception as e: + _err_msg = f"reboot failed: {e!r}" + logger.error(_err_msg) + raise ota_errors.BootControlPostUpdateFailed( + _err_msg, module=__name__ + ) from e + + finalizing_rollback = finalizing_update + def on_operation_failure(self): """Failure registering and cleanup at failure.""" logger.warning("on failure try to unmounting standby slot...") From 81f01580a87b01b98d696a9809dd6b3acaeffa47 Mon Sep 17 00:00:00 2001 From: "bodong.yang" Date: Thu, 21 Nov 2024 09:10:42 +0000 Subject: [PATCH 3/8] cmdhelper: minor update --- src/otaclient_common/cmdhelper.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/otaclient_common/cmdhelper.py b/src/otaclient_common/cmdhelper.py index 085a98580..5ca2e9ef2 100644 --- a/src/otaclient_common/cmdhelper.py +++ b/src/otaclient_common/cmdhelper.py @@ -443,16 +443,15 @@ def reboot(args: list[str] | None = None) -> NoReturn: # pragma: no cover Args: args (Optional[list[str]], optional): args passed to reboot command. Defaults to None, not passing any args. + + Raises: + CalledProcessError for the reboot call, or SystemExit on sys.exit(0). """ cmd = ["reboot"] if args: logger.info(f"will reboot with argument: {args=}") cmd.extend(args) - try: - logger.warning("system will reboot now!") - subprocess_call(cmd, raise_exception=True) - sys.exit(0) - except CalledProcessError: - logger.exception("failed to reboot") - raise + logger.warning("system will reboot now!") + subprocess_call(cmd, raise_exception=True) + sys.exit(0) From 03aefd5d69d5ba18eeda7bda1e0ab78ea10d4630 Mon Sep 17 00:00:00 2001 From: "bodong.yang" Date: Thu, 21 Nov 2024 09:11:05 +0000 Subject: [PATCH 4/8] ota_core: integrate new boot_controller --- src/otaclient/ota_core.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/otaclient/ota_core.py b/src/otaclient/ota_core.py index c4490c800..1da559465 100644 --- a/src/otaclient/ota_core.py +++ b/src/otaclient/ota_core.py @@ -548,9 +548,7 @@ def _execute_update(self): ) # NOTE(20240219): move persist file handling here self._process_persistents(otameta) - - # boot controller postupdate - next(_postupdate_gen := self._boot_controller.post_update()) + self._boot_controller.post_update() # ------ finalizing update ------ # logger.info("local update finished, wait on all subecs...") @@ -571,7 +569,7 @@ def _execute_update(self): logger.info(f"device will reboot in {WAIT_BEFORE_REBOOT} seconds!") time.sleep(WAIT_BEFORE_REBOOT) - next(_postupdate_gen, None) # reboot + self._boot_controller.finalizing_update() # API From 6b3c744eae56edcd7e3ee3fd1f6a46f7abb27aef Mon Sep 17 00:00:00 2001 From: "bodong.yang" Date: Thu, 21 Nov 2024 09:15:06 +0000 Subject: [PATCH 5/8] ota_core: rollback execute finalizing_rollback --- src/otaclient/ota_core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/otaclient/ota_core.py b/src/otaclient/ota_core.py index 1da559465..452487ea7 100644 --- a/src/otaclient/ota_core.py +++ b/src/otaclient/ota_core.py @@ -598,6 +598,7 @@ def execute(self): try: self._boot_controller.pre_rollback() self._boot_controller.post_rollback() + self._boot_controller.finalizing_rollback() except ota_errors.OTAError as e: logger.error(f"rollback failed: {e!r}") self._boot_controller.on_operation_failure() From 99a4a479d2d9ef9601a43ae9e1e4f1338c9cb3f2 Mon Sep 17 00:00:00 2001 From: "bodong.yang" Date: Thu, 21 Nov 2024 09:18:26 +0000 Subject: [PATCH 6/8] fix up tests --- tests/test_otaclient/test_boot_control/test_grub.py | 9 ++++++--- .../test_otaclient/test_boot_control/test_rpi_boot.py | 11 ++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/test_otaclient/test_boot_control/test_grub.py b/tests/test_otaclient/test_boot_control/test_grub.py index 6bfd0f6b9..7dfb9230f 100644 --- a/tests/test_otaclient/test_boot_control/test_grub.py +++ b/tests/test_otaclient/test_boot_control/test_grub.py @@ -20,6 +20,7 @@ import shutil import typing from pathlib import Path +from typing import Any import pytest import pytest_mock @@ -368,10 +369,12 @@ def test_grub_normal_update( shutil.copy(slot_a_ota_partition_dir / _initrd, slot_b / "boot") logger.info("pre-update completed, entering post-update...") + # test post-update - _post_updater = grub_controller.post_update() - next(_post_updater) - next(_post_updater, None) + grub_controller: Any # for typing + grub_controller.post_update() + grub_controller.finalizing_update() + assert ( slot_b / Path(cfg.FSTAB_FILE).relative_to("/") ).read_text().strip() == self.FSTAB_UPDATED.strip() diff --git a/tests/test_otaclient/test_boot_control/test_rpi_boot.py b/tests/test_otaclient/test_boot_control/test_rpi_boot.py index ad2a89500..780a623a8 100644 --- a/tests/test_otaclient/test_boot_control/test_rpi_boot.py +++ b/tests/test_otaclient/test_boot_control/test_rpi_boot.py @@ -19,6 +19,7 @@ import typing from pathlib import Path from string import Template +from typing import Any import pytest import pytest_mock @@ -262,10 +263,10 @@ def test_rpi_boot_normal_update(self, mocker: pytest_mock.MockerFixture): mocker.patch(f"{MODULE}.cfg", _mock_otaclient_cfg) # ------ boot_controller_inst1.stage1: init ------ # - rpi_boot_controller1 = RPIBootController() + rpi_boot_controller = RPIBootController() # ------ boot_controller_inst1.stage2: pre_update ------ # - rpi_boot_controller1.pre_update( + rpi_boot_controller.pre_update( version=VERSION, standby_as_ref=False, erase_standby=False, @@ -299,9 +300,9 @@ def test_rpi_boot_normal_update(self, mocker: pytest_mock.MockerFixture): shutil.copy(os.path.realpath(_initrd_img), self.slot_b_boot_dir) # ------ boot_controller_inst1.stage3: post_update, reboot switch boot ------ # - _post_updater = rpi_boot_controller1.post_update() - next(_post_updater) - next(_post_updater, None) # actual reboot here + rpi_boot_controller: Any # for typing only + rpi_boot_controller.post_update() + rpi_boot_controller.finalizing_update() # --- assertion --- # self.reboot_tryboot_mock.assert_called_once() From e89ffda581899278956ac80d9cdb9bab8f4b1576 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:21:28 +0000 Subject: [PATCH 7/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/otaclient/boot_control/_grub.py | 2 +- src/otaclient/boot_control/_jetson_cboot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/otaclient/boot_control/_grub.py b/src/otaclient/boot_control/_grub.py index 5ea621027..996764917 100644 --- a/src/otaclient/boot_control/_grub.py +++ b/src/otaclient/boot_control/_grub.py @@ -40,7 +40,7 @@ from pathlib import Path from pprint import pformat from subprocess import CalledProcessError -from typing import ClassVar, Dict, Generator, List, NoReturn, Optional, Tuple +from typing import ClassVar, Dict, List, NoReturn, Optional, Tuple from otaclient import errors as ota_errors from otaclient._types import OTAStatus diff --git a/src/otaclient/boot_control/_jetson_cboot.py b/src/otaclient/boot_control/_jetson_cboot.py index cad019747..7141877c0 100644 --- a/src/otaclient/boot_control/_jetson_cboot.py +++ b/src/otaclient/boot_control/_jetson_cboot.py @@ -22,7 +22,7 @@ import logging import subprocess from pathlib import Path -from typing import Generator, NoReturn, Optional +from typing import NoReturn, Optional from otaclient import errors as ota_errors from otaclient._types import OTAStatus From f14e0187bb5ac677dfc6cd699f8cd500b22d5a1a Mon Sep 17 00:00:00 2001 From: "bodong.yang" Date: Thu, 21 Nov 2024 09:24:18 +0000 Subject: [PATCH 8/8] fix forgotting remove reboot from post_rollback API --- src/otaclient/boot_control/_jetson_cboot.py | 1 - src/otaclient/boot_control/_jetson_uefi.py | 1 - src/otaclient/boot_control/_rpi_boot.py | 1 - 3 files changed, 3 deletions(-) diff --git a/src/otaclient/boot_control/_jetson_cboot.py b/src/otaclient/boot_control/_jetson_cboot.py index 7141877c0..0c0bcd5d4 100644 --- a/src/otaclient/boot_control/_jetson_cboot.py +++ b/src/otaclient/boot_control/_jetson_cboot.py @@ -712,7 +712,6 @@ def post_rollback(self): logger.info("jetson-cboot: post-rollback setup...") self._mp_control.umount_all(ignore_error=True) self._cboot_control.switch_boot_to_standby() - cmdhelper.reboot() except Exception as e: _err_msg = f"failed on post_rollback: {e!r}" logger.error(_err_msg) diff --git a/src/otaclient/boot_control/_jetson_uefi.py b/src/otaclient/boot_control/_jetson_uefi.py index 5be4960f2..dd9a6523b 100644 --- a/src/otaclient/boot_control/_jetson_uefi.py +++ b/src/otaclient/boot_control/_jetson_uefi.py @@ -1098,7 +1098,6 @@ def post_rollback(self): logger.info("jetson-uefi: post-rollback setup...") self._mp_control.umount_all(ignore_error=True) self._uefi_control.switch_boot_to_standby() - cmdhelper.reboot() except Exception as e: _err_msg = f"jetson-uefi: failed on post_rollback: {e!r}" logger.error(_err_msg) diff --git a/src/otaclient/boot_control/_rpi_boot.py b/src/otaclient/boot_control/_rpi_boot.py index 312af357a..ace80c7b0 100644 --- a/src/otaclient/boot_control/_rpi_boot.py +++ b/src/otaclient/boot_control/_rpi_boot.py @@ -523,7 +523,6 @@ def post_rollback(self): logger.info("rpi_boot: post-rollback setup...") self._rpiboot_control.prepare_tryboot_txt() self._mp_control.umount_all(ignore_error=True) - self._rpiboot_control.reboot_tryboot() except Exception as e: _err_msg = f"failed on post_rollback: {e!r}" logger.error(_err_msg)