diff --git a/otaclient/_utils/__init__.py b/otaclient/_utils/__init__.py index 4b7060019..c4cb3ca96 100644 --- a/otaclient/_utils/__init__.py +++ b/otaclient/_utils/__init__.py @@ -11,8 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Callable, TypeVar -from typing_extensions import ParamSpec, Concatenate + + +from __future__ import annotations +from math import ceil +from pathlib import Path +from typing import Any, Callable, Optional, TypeVar +from typing_extensions import Literal, ParamSpec, Concatenate P = ParamSpec("P") @@ -42,3 +47,22 @@ def _decorator(target: Callable[..., RT]) -> Callable[Concatenate[Any, P], RT]: return target # type: ignore return _decorator + + +_MultiUnits = Literal["GiB", "MiB", "KiB", "Bytes", "KB", "MB", "GB"] +# fmt: off +_multiplier: dict[_MultiUnits, int] = { + "GiB": 1024 ** 3, "MiB": 1024 ** 2, "KiB": 1024 ** 1, + "GB": 1000 ** 3, "MB": 1000 ** 2, "KB": 1000 ** 1, + "Bytes": 1, +} +# fmt: on + + +def get_file_size( + swapfile_fpath: str | Path, units: _MultiUnits = "Bytes" +) -> Optional[int]: + """Helper for get file size with .""" + swapfile_fpath = Path(swapfile_fpath) + if swapfile_fpath.is_file(): + return ceil(swapfile_fpath.stat().st_size / _multiplier[units]) diff --git a/otaclient/_utils/linux.py b/otaclient/_utils/linux.py new file mode 100644 index 000000000..ec990d115 --- /dev/null +++ b/otaclient/_utils/linux.py @@ -0,0 +1,65 @@ +# Copyright 2022 TIER IV, INC. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations +from pathlib import Path +from subprocess import check_call + + +def create_swapfile( + swapfile_fpath: str | Path, size_in_MiB: int, *, timeout=900 +) -> Path: + """Create swapfile at with MiB. + + Reference: https://wiki.archlinux.org/title/swap#Swap_file_creation + + Args: + swapfile_fpath(StrOrPath): the path to place the created swapfile. + size_in_MiB(int): the size of to-be-created swapfile. + timeout: timeout of swapfile creating, default is 15mins. + + Returns: + The Path object to the newly created swapfile. + + Raises: + ValueError on file already exists at , SubprocessCallFailed + on failed swapfile creation. + """ + swapfile_fpath = Path(swapfile_fpath) + if swapfile_fpath.exists(): + raise ValueError(f"{swapfile_fpath=} exists, skip") + + # create a new file with MiB size + # executes: + # dd if=/dev/zero of=/swapfile bs=1M count=8k + # chmod 0600 /swapfile + check_call( + [ + "dd", + "if=/dev/zero", + f"of={str(swapfile_fpath)}", + "bs=1M", + f"count={size_in_MiB}", + ], + timeout=timeout, + ) + swapfile_fpath.chmod(0o600) + + # prepare the created file as swapfile + # executes: + # mkswap /swapfile + check_call(["mkswap", str(swapfile_fpath)], timeout=timeout) + + return swapfile_fpath diff --git a/otaclient/app/ota_client.py b/otaclient/app/ota_client.py index 730615bc7..753a82ea7 100644 --- a/otaclient/app/ota_client.py +++ b/otaclient/app/ota_client.py @@ -46,6 +46,9 @@ ) from . import log_setting +from otaclient._utils import get_file_size +from otaclient._utils.linux import create_swapfile + try: from otaclient import __version__ # type: ignore except ImportError: @@ -304,13 +307,23 @@ def _process_persistents(self): for _perinf in self._otameta.iter_metafile( ota_metadata.MetafilesV1.PERSISTENT_FNAME ): - _perinf_path = Path(_perinf.path) + _per_fpath = Path(_perinf.path) + + # NOTE(20240220): fast fix for handling swapfile + if str(_per_fpath) in ["/swapfile", "/swap.img"]: + _new_swapfile = standby_slot_mp / _per_fpath + try: + _swapfile_size = get_file_size(_per_fpath, units="MiB") + assert _swapfile_size is not None, f"{_per_fpath} doesn't exist" + create_swapfile(_new_swapfile, _swapfile_size) + except Exception as e: + logger.warning(f"failed to create {_per_fpath}, skip: {e!r}") + continue + if ( - _perinf_path.is_file() - or _perinf_path.is_dir() - or _perinf_path.is_symlink() + _per_fpath.is_file() or _per_fpath.is_dir() or _per_fpath.is_symlink() ): # NOTE: not equivalent to perinf.path.exists() - _copy_tree.copy_with_parents(_perinf_path, standby_slot_mp) + _copy_tree.copy_with_parents(_per_fpath, standby_slot_mp) def _execute_update( self,