Skip to content

Commit

Permalink
refactor: project restruct phase2 (#311)
Browse files Browse the repository at this point in the history
Project restruct phase2, in this phase most of the changes are restructring the packages to make the project more modularized and decoupling each components as much as possible.

# Major changes
1. ota_metadata becomes a standalone package, current implementation is treated as the implementation for OTA image format legacy version.
2. create new otaclient_api, current version is v2 and all the protobuf pb2 generated code and proto wrappers are grouped under v2.
3. create new otaclient_common, all common shared libs, helper funcs and types are grouped under this package, categorized by functionality.
4. fix according to import paths changes.
5. restruct tests according to the new project layout.

# Other changes
1. cleanup bootstrap, refactor into samples, which provide sample ecu_info.yaml, proxy_info.yaml and otaclient.service for single ECU setup.
2. fix up tools/offline_ota_image_builder and status_monitor package.
3. not use relative imports anymore.
  • Loading branch information
Bodong-Yang authored Jun 5, 2024
1 parent 9f975ad commit bfa3bb5
Show file tree
Hide file tree
Showing 108 changed files with 1,983 additions and 1,846 deletions.
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,6 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# build related
build
*.egg-info

# local vscode configs
.devcontainer
.vscode
Expand Down
31 changes: 14 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
# OTA client
# OTAClient

## Overview

This OTA client is a client software to perform over-the-air software updates for linux devices.
To enable updating of software at any layer (kernel, kernel module, user library, user application), the OTA client targets the entire rootfs for updating.
When the OTA client receives an update request, it downloads a list from the OTA server that contains the file paths and the hash values of the files, etc., to be updated, and compares them with the files in its own storage and if there is a match, that file is used to update the rootfs. By this delta mechanism, it is possible to reduce the download size even if the entire rootfs is targeted and this mechanism does not require any specific server implementation, nor does it require the server to keep a delta for each version of the rootfs.
OTAClient is software to perform over-the-air software updates for linux devices.
It provides a set of APIs for user to start the OTA and monitor the progress and status.

It is designed to work with web.auto FMS OTA component.

## Feature

- Rootfs updating
- Delta updating
- Redundant configuration with A/B partition update
- Arbitrary files can be copied from A to B partition. This can be used to take over individual files.
- No specific server implementation is required. The server that supports HTTP GET is only required.
- TLS connection is also required.
- Delta management is not required for server side.
- To restrict access to the server, cookie can be used.
- All files to be updated are verified by the hash included in the metadata, and the metadata is also verified by X.509 certificate locally installed.
- Transfer data is encrypted by TLS
- Multiple ECU(Electronic Control Unit) support
- By the internal proxy cache mechanism, the cache can be used for the download requests to the same file from multiple ECU.
- A/B partition update with support for generic x86_64 device, NVIDIA Jetson series based devices and Raspberry Pi device.
- Full Rootfs update, with delta update support.
- Local delta calculation, allowing update to any version of OTA image without the need of a pre-generated delta OTA package.
- Support persist files from active slot to newly updated slot.
- Verification over OTA image by digital signature and PKI.
- Support for protected OTA server with cookie.
- Optional OTA proxy support and OTA cache support.
- Multiple ECU OTA supports.

## License

OTA client is licensed under the Apache License, Version 2.0.
OTAClient is licensed under the Apache License, Version 2.0.
7 changes: 0 additions & 7 deletions bootstrap/root/boot/ota/ecu_info.yaml

This file was deleted.

3 changes: 3 additions & 0 deletions proto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# OTA Service API proto

This folder includes the OTA service API proto file, and a set of tools to generate the python lib from the proto files.
14 changes: 10 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ dependencies = [
"pyopenssl==24.1",
"pyyaml>=3.12",
"requests<2.32,>=2.31",
"typing-extensions>=4.6.3; python_version<'3.11'",
"typing-extensions>=4.6.3",
"urllib3<2,>=1.26.8",
"uvicorn[standard]==0.20",
"zstandard==0.18",
Expand All @@ -60,9 +60,14 @@ version-file = "src/_otaclient_version.py"
[tool.hatch.build.targets.sdist]
exclude = [
"/tools",
".github",
]

[tool.hatch.build.targets.wheel]
exclude = [
"**/.gitignore",
"**/*README.md",
]
only-include = [
"src",
]
Expand Down Expand Up @@ -98,9 +103,6 @@ log_auto_indent = true
log_format = "%(asctime)s %(levelname)s %(filename)s %(funcName)s,%(lineno)d %(message)s"
log_cli = true
log_cli_level = "INFO"
pythonpath = [
"otaclient",
]
testpaths = [
"./tests",
]
Expand All @@ -110,6 +112,10 @@ branch = false
relative_files = true
source = [
"otaclient",
"otaclient_api",
"otaclient_common",
"ota_metadata",
"ota_proxy",
]

[tool.coverage.report]
Expand Down
3 changes: 3 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# OTAClient configuration files samples

This folder contains the sample otaclient configuration files **ecu_info.yaml**, **proxy_info.yaml** and systemd service unit file **otaclient.service** for a single ECU OTA setup.
7 changes: 7 additions & 0 deletions samples/ecu_info.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# This is the sample ecu_info.yaml for a single x86_64 ECU setup.
# Please check ecu_info.yaml spec for more details: https://tier4.atlassian.net/l/cp/AGmpqFFc.
format_version: 1
ecu_id: autoware
bootloader: grub
available_ecu_ids:
- autoware
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# otaclient.service

[Unit]
Description=OTA Client
After=network-online.target nss-lookup.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/bin/bash -c 'source /opt/ota/.venv/bin/activate && PYTHONPATH=/opt/ota python3 -m otaclient'
ExecStart=/opt/ota/client/venv/bin/python3 -m otaclient
Restart=always
RestartSec=10
RestartSec=16

[Install]
WantedBy=multi-user.target
9 changes: 9 additions & 0 deletions samples/proxy_info.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This is the sample proxy_info.yaml for a single ECU setup.
# Please check proxy_info.yaml spec for more details: https://tier4.atlassian.net/l/cp/qT4N4K0X.
format_version: 1
enable_local_ota_proxy: true
enable_local_ota_proxy_cache: true
local_ota_proxy_listen_addr: 127.0.0.1
local_ota_proxy_listen_port: 8082
# if otaclient-logger is installed locally
logging_server: "http://127.0.0.1:8083"
3 changes: 3 additions & 0 deletions src/ota_metadata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# OTA image metadata

Libs for parsing OTA image.
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,24 @@
# 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.
"""Modules for registering wrapped compiled protobuf types."""
"""OTA image metadata, legacy version."""


from ._common import * # noqa: F403, F401
from ._ota_metafiles_wrapper import * # noqa: F403, F401
from ._otaclient_v2_pb2_wrapper import * # noqa: F403, F401
from __future__ import annotations

import sys
from pathlib import Path

from otaclient_common import import_from_file

SUPORTED_COMPRESSION_TYPES = ("zst", "zstd")

# ------ dynamically import pb2 generated code ------ #

_PROTO_DIR = Path(__file__).parent
_PB2_FPATH = _PROTO_DIR / "ota_metafiles_pb2.py"
_PACKAGE_PREFIX = ".".join(__name__.split(".")[:-1])

_module_name, _module = import_from_file(_PB2_FPATH)
sys.modules[_module_name] = _module
sys.modules[f"{_PACKAGE_PREFIX}.{_module_name}"] = _module
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,17 @@
from typing_extensions import Self

from ota_proxy import OTAFileCacheControl

from .common import RetryTaskMap, get_backoff, urljoin_ensure_base
from .configs import config as cfg
from .downloader import Downloader
from .proto.streamer import Uint32LenDelimitedMsgReader, Uint32LenDelimitedMsgWriter
from .proto.wrapper import (
DirectoryInf,
MessageWrapper,
PersistentInf,
RegularInf,
SymbolicLinkInf,
from otaclient_common.common import get_backoff, urljoin_ensure_base
from otaclient_common.downloader import Downloader
from otaclient_common.proto_streamer import (
Uint32LenDelimitedMsgReader,
Uint32LenDelimitedMsgWriter,
)
from otaclient_common.proto_wrapper import MessageWrapper
from otaclient_common.retry_task_map import RetryTaskMap

from . import SUPORTED_COMPRESSION_TYPES
from .types import DirectoryInf, PersistentInf, RegularInf, SymbolicLinkInf

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -592,10 +591,25 @@ class OTAMetadata:
),
}

def __init__(self, *, url_base: str, downloader: Downloader) -> None:
MAX_COCURRENT = 2
BACKOFF_FACTOR = 1
BACKOFF_MAX = 6

def __init__(
self,
*,
url_base: str,
downloader: Downloader,
run_dir: Path,
certs_dir: Path,
download_max_idle_time: int,
) -> None:
self.url_base = url_base
self._downloader = downloader
self._tmp_dir = TemporaryDirectory(prefix="ota_metadata", dir=cfg.RUN_DIR)
self.run_dir = run_dir
self.certs_dir = certs_dir
self.download_max_idle_time = download_max_idle_time
self._tmp_dir = TemporaryDirectory(prefix="ota_metadata", dir=run_dir)
self._tmp_dir_path = Path(self._tmp_dir.name)

# download and parse the metadata.jwt
Expand All @@ -622,7 +636,7 @@ def _process_metadata_jwt(self) -> _MetadataJWTClaimsLayout:
"""Download, loading and parsing metadata.jwt."""
logger.debug("process metadata.jwt...")
# download and parse metadata.jwt
with NamedTemporaryFile(prefix="metadata_jwt", dir=cfg.RUN_DIR) as meta_f:
with NamedTemporaryFile(prefix="metadata_jwt", dir=self.run_dir) as meta_f:
_downloaded_meta_f = Path(meta_f.name)
self._downloader.download_retry_inf(
urljoin_ensure_base(self.url_base, self.METADATA_JWT),
Expand All @@ -636,13 +650,13 @@ def _process_metadata_jwt(self) -> _MetadataJWTClaimsLayout:
)

_parser = _MetadataJWTParser(
_downloaded_meta_f.read_text(), certs_dir=cfg.CERTS_DIR
_downloaded_meta_f.read_text(), certs_dir=self.certs_dir
)
# get not yet verified parsed ota_metadata
_ota_metadata = _parser.get_otametadata()

# download certificate and verify metadata against this certificate
with NamedTemporaryFile(prefix="metadata_cert", dir=cfg.RUN_DIR) as cert_f:
with NamedTemporaryFile(prefix="metadata_cert", dir=self.run_dir) as cert_f:
cert_info = _ota_metadata.certificate
cert_fname, cert_hash = cert_info.file, cert_info.hash
cert_file = Path(cert_f.name)
Expand Down Expand Up @@ -696,11 +710,11 @@ def _process_text_base_otameta_file(_metafile: MetaFile):

last_active_timestamp = int(time.time())
_mapper = RetryTaskMap(
max_concurrent=cfg.MAX_CONCURRENT_DOWNLOAD_TASKS,
max_concurrent=self.MAX_COCURRENT,
backoff_func=partial(
get_backoff,
factor=cfg.DOWNLOAD_GROUP_BACKOFF_FACTOR,
_max=cfg.DOWNLOAD_GROUP_BACKOFF_MAX,
factor=self.BACKOFF_FACTOR,
_max=self.BACKOFF_MAX,
),
max_retry=0, # NOTE: we use another strategy below
)
Expand All @@ -718,12 +732,9 @@ def _process_text_base_otameta_file(_metafile: MetaFile):
last_active_timestamp = max(
last_active_timestamp, self._downloader.last_active_timestamp
)
if (
int(time.time()) - last_active_timestamp
> cfg.DOWNLOAD_GROUP_INACTIVE_TIMEOUT
):
if int(time.time()) - last_active_timestamp > self.download_max_idle_time:
logger.error(
f"downloader becomes stuck for {cfg.DOWNLOAD_GROUP_INACTIVE_TIMEOUT=} seconds, abort"
f"downloader becomes stuck for {self.download_max_idle_time=} seconds, abort"
)
_mapper.shutdown(raise_last_exc=True)

Expand Down Expand Up @@ -753,7 +764,7 @@ def get_download_url(self, reg_inf: RegularInf) -> Tuple[str, Optional[str]]:
if (
self.image_compressed_rootfs_url
and reg_inf.compressed_alg
and reg_inf.compressed_alg in cfg.SUPPORTED_COMPRESS_ALG
and reg_inf.compressed_alg in SUPORTED_COMPRESSION_TYPES
):
return (
urljoin_ensure_base(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@
from pathlib import Path
from typing import Union

import ota_metafiles_pb2 as ota_metafiles

from ._common import MessageWrapper, calculate_slots
from ota_metadata.legacy import ota_metafiles_pb2 as ota_metafiles
from otaclient_common.proto_wrapper import MessageWrapper, calculate_slots

# helper mixin

Expand Down
2 changes: 1 addition & 1 deletion src/ota_proxy/cache_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from typing_extensions import Self

from otaclient._utils import copy_callable_typehint_to_method
from otaclient_common.typing import copy_callable_typehint_to_method

_FIELDS = "_fields"

Expand Down
2 changes: 1 addition & 1 deletion src/ota_proxy/server_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import aiohttp

from otaclient._utils.logging import BurstSuppressFilter
from otaclient_common.logging import BurstSuppressFilter

from ._consts import (
BHEADER_AUTHORIZATION,
Expand Down
Loading

1 comment on commit bfa3bb5

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/ota_metadata/legacy
   __init__.py110100% 
   parser.py3223290%142, 147, 183–184, 194–195, 198, 210, 268, 278–281, 320–323, 403, 406, 414–416, 429, 438–439, 442–443, 731–732, 735–736, 739
   types.py841384%37, 40–42, 112–116, 122–125
src/ota_proxy
   __init__.py361072%59, 61, 63, 72, 81–82, 102, 104–106
   __main__.py770%16–18, 20, 22–23, 25
   _consts.py150100% 
   cache_control.py68494%71, 91, 113, 121
   config.py180100% 
   db.py1461589%75, 81, 103, 113, 116, 145–147, 166, 199, 208–209, 229, 258, 300
   errors.py50100% 
   orm.py1121091%92, 97, 102, 108, 114, 141–142, 155, 232, 236
   ota_cache.py4019676%98–99, 218, 229, 238, 241–242, 256–258, 278, 294–295, 297, 320–321, 327, 331, 333, 359–362, 364–366, 378, 439–440, 482–483, 553, 558–560, 566–569, 619, 638–639, 671–672, 683, 717–721, 725–727, 729, 731–738, 740–742, 745–746, 750–751, 755, 802, 810–812, 891–894, 898, 901–902, 916–917, 919–921, 925–926, 932–933, 964, 970, 997, 1026–1028
   server_app.py1383971%76, 79, 85, 101, 103, 162, 171, 213–214, 216–218, 221, 226–228, 231–232, 235, 238, 241, 244, 257–258, 261–262, 264, 267, 293–296, 299, 313–315, 321–323
   utils.py23195%33
src/otaclient
   __init__.py5260%17, 19
   __main__.py110%16
src/otaclient/app
   __main__.py110%16
   configs.py750100% 
   errors.py1120100% 
   interface.py50100% 
   log_setting.py53590%54, 56, 65–67
   main.py46589%52–53, 75–77
   ota_client.py39813765%67, 75, 96, 204–205, 218, 221, 225, 228, 265–268, 280–283, 286–287, 291–296, 306–309, 314–315, 317, 326, 329, 334–335, 338, 344, 346, 349, 391–394, 399, 403, 406, 422–425, 428–435, 438–445, 451–454, 483, 486–487, 489, 492–495, 497–498, 503–504, 507, 521–528, 535, 538–544, 591–594, 602, 638, 643–646, 651–653, 656–657, 659–660, 662–663, 665, 725–726, 729, 737–738, 741, 752–753, 756, 764–765, 768, 779, 798, 825, 844, 862
   ota_client_stub.py39410972%76–78, 80–81, 89–92, 95–97, 101, 106–107, 109–110, 113, 115–116, 119–121, 124–125, 128–130, 135–140, 144, 147–151, 153–154, 162–164, 167, 204–206, 211, 247, 272, 275, 278, 382, 406, 408, 432, 478, 535, 605–606, 645, 664–666, 672–675, 679–681, 688–690, 693, 697–700, 753, 842–844, 851, 881–882, 885–889, 898–907, 914, 920, 923–924, 928, 931
   update_stats.py106298%162, 172
src/otaclient/app/boot_control
   __init__.py40100% 
   _common.py2337667%73–74, 95–97, 113–114, 134–135, 154–155, 174–175, 194–195, 210–211, 232, 240, 258, 266, 285–286, 289–290, 313, 315–324, 326–335, 337–339, 358, 361, 369, 377, 393–395, 397–402, 495, 500, 505, 618, 622–623, 626, 634, 636–637, 711–712, 722, 740
   _grub.py41712869%216, 264–267, 273–277, 314–315, 322–327, 330–336, 339, 342–343, 348, 350–352, 361–367, 369–370, 372–374, 383–385, 387–389, 468–469, 473–474, 526, 532, 558, 580, 584–585, 600–602, 626–629, 641, 645–647, 649–651, 710–713, 738–741, 764–767, 779–780, 783–784, 819, 825, 845–846, 848, 860, 863, 866, 869, 873–875, 893–896, 924–927, 932–940, 945–953
   _jetson_cboot.py27121421%72–73, 80–81, 99–108, 120, 127–128, 140, 146–147, 157–159, 171–172, 183–184, 187–188, 191–192, 195–199, 202–203, 207–208, 213–214, 216–220, 222–228, 230–231, 236, 239, 242–243, 246, 250–251, 255–256, 260, 263, 266, 270–276, 278–280, 285, 288, 291, 295, 302, 304–307, 320, 323, 327, 329–331, 335, 342, 344, 347, 353–354, 359, 367, 375–377, 386–387, 389–391, 397, 400–402, 406–407, 409, 412, 421–423, 426, 429, 432–437, 439–441, 444, 447, 451–456, 460–462, 467–468, 472–473, 476, 479, 482–483, 486, 489, 494, 497, 500–501, 503, 505, 508, 511, 513–514, 517–521, 526–527, 529, 537–541, 543, 546, 549, 560–561, 566, 576, 579–587, 592–600, 605–613, 619–621, 624, 627
   _jetson_common.py1416653%50, 74, 129–134, 136, 141–143, 148–151, 159–160, 167–168, 173–174, 190–191, 193–195, 198–200, 203, 207, 211, 215–217, 223–224, 226, 259, 285–286, 288–290, 294–297, 299–300, 302–306, 308, 315–316, 319, 321, 331, 334–335, 338, 340
   _rpi_boot.py25812252%88–90, 96–97, 99–101, 103, 106–107, 112–113, 122–123, 127, 129, 133, 137–140, 145–147, 151–154, 178–180, 186–188, 201–203, 209–211, 224–231, 233, 237–239, 242–245, 248–249, 254, 258, 262, 266, 300, 327–329, 339–342, 346–352, 392–394, 436–440, 459–462, 467, 470, 494–497, 502–510, 515–523, 537–540, 546–548, 551
   configs.py460100% 
   protocol.py40100% 
   selecter.py382631%44–46, 49–50, 54–55, 58–60, 63, 65, 69, 77–79, 81–82, 84–85, 89, 91–93, 95, 97
src/otaclient/app/create_standby
   __init__.py12558%28–30, 32, 34
   common.py2194380%63, 66–67, 71–73, 75, 79–80, 82, 128, 176–178, 180–182, 184, 187–190, 194, 205, 279–280, 282–287, 300, 355, 358–360, 376–377, 391, 395, 417–418
   interface.py50100% 
   rebuild_mode.py90198%105
src/otaclient/configs
   _common.py80100% 
   ecu_info.py57198%107
   proxy_info.py52296%88, 90
src/otaclient_api/v2
   __init__.py140100% 
   api_caller.py39684%45–47, 83–85
   api_stub.py170100% 
   types.py2562391%86, 89–92, 131, 209–210, 212, 259, 262–263, 506–508, 512–513, 515, 518–519, 522–523, 586
src/otaclient_common
   __init__.py34876%42–44, 59, 61, 67, 74–75
   common.py1581987%44, 126, 232, 235–237, 252, 259–261, 327–329, 339, 348–350, 396, 400
   downloader.py2694483%72, 85–86, 301, 306, 310, 328–329, 379–383, 402–404, 407–408, 411–412, 433–436, 440–441, 445–446, 450–451, 460, 535–537, 553, 573–575, 579, 581, 584, 589–591
   linux.py471176%45–47, 53, 63, 68, 70, 102–103, 127–128
   logging.py29196%55
   persist_file_handling.py1131884%112, 114, 146–148, 150, 176–179, 184, 188–192, 218–219
   proto_streamer.py42880%33, 48, 66–67, 72, 81–82, 100
   proto_wrapper.py3984588%87, 165, 172, 184–186, 205, 210, 221, 257, 263, 268, 299, 303, 307, 402, 462, 469, 472, 492, 499, 501, 526, 532, 535, 537, 562, 568, 571, 573, 605, 609, 611, 625, 642, 669, 672, 676, 707, 713, 760–763, 765
   retry_task_map.py123298%129, 141
   typing.py250100% 
TOTAL6001135877% 

Tests Skipped Failures Errors Time
179 0 💤 0 ❌ 0 🔥 4m 54s ⏱️

Please sign in to comment.