Skip to content

Commit

Permalink
feat: add jetson-uefi boot control support, refine jetson-cboot boot …
Browse files Browse the repository at this point in the history
…control implementation (tier4#300)

This PR introduces the OTA support for NVIDIA Jetson device with UEFI(BSP >= R34) and UEFI Capsule firmware update(BSP >=R35.2). Together with tier4#376, this PR also implements the firmware update protocol for both jetson-uefi and jetson-cboot boot control.

Other Major changes includes:
1. refactor jetson-common module.
2. fully refactor the firmware update implementation in jetson-cboot module, with the newly refactored jetson-common module.
3. refine firmware_package to better meet the usages in jetson-uefi and jetson-cboot.

Other Minor changes includes:
1. minor fix on boot_control._common _load_status_file.
2. otaclient_common: implement file_digest, use file_digest to implement file_sha256.
  • Loading branch information
Bodong-Yang authored Sep 3, 2024
1 parent 9c86cd4 commit 855ec23
Show file tree
Hide file tree
Showing 15 changed files with 2,442 additions and 469 deletions.
2 changes: 1 addition & 1 deletion src/otaclient/boot_control/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ def _load_status_file(self):
_loaded_ota_status = None

# initialize ota_status files if not presented/incompleted/invalid
if not _loaded_ota_status:
if _loaded_ota_status is None:
logger.info(
"ota_status files incompleted/not presented, "
f"initializing and set/store status to {api_types.StatusOta.INITIALIZED.name}..."
Expand Down
112 changes: 78 additions & 34 deletions src/otaclient/boot_control/_firmware_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,43 +42,49 @@

from __future__ import annotations

import logging
import re
from enum import Enum
from typing import Any, List, Literal
from pathlib import Path
from typing import Any, List, Literal, Union

from pydantic import BaseModel, BeforeValidator, GetCoreSchemaHandler
from pydantic_core import CoreSchema, core_schema
import yaml
from pydantic import BaseModel, BeforeValidator
from typing_extensions import Annotated

from otaclient_common.typing import gen_strenum_validator
from otaclient_common.typing import StrOrPath, gen_strenum_validator

logger = logging.getLogger(__name__)


class PayloadType(str, Enum):
UEFI_CAPSULE = "UEFI-CAPSULE"
UEFI_BOOT_APP = "UEFI-BOOT-APP"
BUP = "BUP"


DIGEST_PATTERN = re.compile(r"^(?P<algorithm>[\w\-+ ]+):(?P<digest>[a-zA-Z0-9]+)$")


def _pydantic_str_schema(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
"""Pydantic schema adapter for str."""
return core_schema.no_info_after_validator_function(cls, handler(str))


class DigestValue(str):
class DigestValue(BaseModel):
"""Implementation of digest value schema <algorithm>:<hexstr>."""

def __init__(self, _in: str) -> None:
_in = _in.strip()
if not (ma := DIGEST_PATTERN.match(_in)):
raise ValueError(f"invalid digest value: {_in}")
self.algorithm = ma.group("algorithm")
self.digest = ma.group("digest")
algorithm: str
digest: str

__get_pydantic_core_schema__ = classmethod(_pydantic_str_schema)
@classmethod
def parse(cls, _in: str | DigestValue | Any) -> DigestValue:
if isinstance(_in, DigestValue):
return _in

if isinstance(_in, str):
_in = _in.strip()
if not (ma := DIGEST_PATTERN.match(_in)):
raise ValueError(f"invalid digest value: {_in}")
return DigestValue(
algorithm=ma.group("algorithm"), digest=ma.group("digest")
)
raise TypeError(f"expect instance of str or {cls}, get {type(_in)}")


class NVIDIAFirmwareCompat(BaseModel):
Expand All @@ -95,13 +101,13 @@ def check_compat(self, _tnspec: str) -> bool:
)


class NVIDIAUEFIFirmwareSpec(BaseModel):
# NOTE: let the jetson-uefi module parse the bsp_version
class NVIDIAFirmwareSpec(BaseModel):
# NOTE: let the boot control module parse BSP version
bsp_version: str
firmware_compat: NVIDIAFirmwareCompat


class PayloadFileLocation(str):
class PayloadFileLocation(BaseModel):
"""Specifying the payload file location.
It supports file URL or digest value.
Expand All @@ -113,26 +119,33 @@ class PayloadFileLocation(str):
"""

location_type: Literal["blob", "file"]
location_path: str | DigestValue
location_path: Union[str, DigestValue]

def __init__(self, _in: str) -> None:
if _in.startswith("file://"):
self.location_type = "file"
self.location_path = _in.replace("file://", "", 1)
else:
self.location_type = "blob"
self.location_path = DigestValue(_in)
@classmethod
def parse(cls, _in: str | PayloadFileLocation | Any) -> PayloadFileLocation:
if isinstance(_in, PayloadFileLocation):
return _in

__get_pydantic_core_schema__ = classmethod(_pydantic_str_schema)
if isinstance(_in, str):
if _in.startswith("file://"):
location_type = "file"
location_path = _in.replace("file://", "", 1)
else:
location_type = "blob"
location_path = DigestValue.parse(_in)
return cls(location_type=location_type, location_path=location_path)
raise TypeError(f"expect instance of str of {cls}, get {type(_in)}")


class FirmwarePackage(BaseModel):
"""Metadata of a firmware update package payload."""

payload_name: str
file_location: Annotated[PayloadFileLocation, BeforeValidator(PayloadFileLocation)]
file_location: Annotated[
PayloadFileLocation, BeforeValidator(PayloadFileLocation.parse)
]
type: Annotated[PayloadType, BeforeValidator(gen_strenum_validator(PayloadType))]
digest: DigestValue
digest: Annotated[DigestValue, BeforeValidator(DigestValue.parse)]


class HardwareType(str, Enum):
Expand All @@ -156,7 +169,7 @@ class FirmwareManifest(BaseModel):
]
hardware_series: str
hardware_model: str
firmware_spec: NVIDIAUEFIFirmwareSpec
firmware_spec: NVIDIAFirmwareSpec
firmware_packages: List[FirmwarePackage]

def check_compat(self, _tnspec: str) -> bool:
Expand Down Expand Up @@ -186,3 +199,34 @@ class FirmwareUpdateRequest(BaseModel):

format_version: Literal[1] = 1
firmware_list: List[str]


def load_firmware_package(
*,
firmware_update_request_fpath: StrOrPath,
firmware_manifest_fpath: StrOrPath,
) -> tuple[FirmwareUpdateRequest, FirmwareManifest] | None:
"""Parse firmware update package."""
try:
firmware_update_request = FirmwareUpdateRequest.model_validate(
yaml.safe_load(Path(firmware_update_request_fpath).read_text())
)
except FileNotFoundError:
logger.info("firmware update request file not found, skip firmware update")
return
except Exception as e:
logger.warning(f"invalid request file: {e!r}")
return

try:
firmware_manifest = FirmwareManifest.model_validate(
yaml.safe_load(Path(firmware_manifest_fpath).read_text())
)
except FileNotFoundError:
logger.warning("no firmware manifest file presented, skip firmware update!")
return
except Exception as e:
logger.warning(f"invalid manifest file: {e!r}")
return

return firmware_update_request, firmware_manifest
Loading

1 comment on commit 855ec23

@github-actions
Copy link

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.py3353888%100, 156, 161, 197–198, 208–209, 212, 224, 282, 292–295, 334–337, 417, 420, 428–430, 443, 452–453, 456–457, 669–670, 673, 700–702, 752, 755–757
   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_header.py68494%71, 91, 113, 121
   cache_streaming.py1421291%211, 225, 229–230, 265–266, 268, 348, 366–369
   config.py170100% 
   db.py731875%109, 115, 153, 159–160, 163, 169, 171, 192–199, 201–202
   errors.py50100% 
   lru_cache_helper.py47295%84–85
   ota_cache.py2155972%70–71, 140, 151–152, 184–185, 202, 239–243, 247–249, 251, 253–260, 262–264, 267–268, 272–273, 277, 324, 332–334, 413–416, 430, 433–434, 448–449, 451–453, 457–458, 464–465, 496, 502, 529, 581–583
   server_app.py1393971%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.py140100% 
src/otaclient
   __init__.py5260%17, 19
   __main__.py110%16
   log_setting.py52590%53, 55, 64–66
src/otaclient/app
   __main__.py110%16
   configs.py760100% 
   errors.py1200100% 
   interface.py50100% 
   main.py46589%52–53, 75–77
   ota_client.py37310871%79, 87, 108, 135, 137–138, 140, 144, 148–149, 154–155, 161, 163, 201–204, 210, 214, 220, 339, 351–352, 354, 363, 366, 371–372, 375, 381, 383–387, 406–409, 412–419, 447–450, 496–497, 501, 503–504, 534–535, 544–551, 558, 561–567, 612–615, 623, 659–661, 666–668, 671–672, 674–675, 677, 735–736, 739, 747–748, 751, 762–763, 766, 774–775, 778, 789, 808, 835, 854, 872
   ota_client_stub.py39310972%75–77, 79–80, 88–91, 94–96, 100, 105–106, 108–109, 112, 114–115, 118–120, 123–124, 127–129, 134–139, 143, 146–150, 152–153, 161–163, 166, 203–205, 210, 246, 271, 274, 277, 381, 405, 407, 431, 477, 534, 604–605, 644, 663–665, 671–674, 678–680, 687–689, 692, 696–699, 752, 841–843, 850, 880–881, 884–888, 897–906, 913, 919, 922–923, 927, 930
   update_stats.py104991%57, 103, 105, 114, 116, 125, 127, 148, 179
src/otaclient/boot_control
   __init__.py40100% 
   _common.py24811254%74–75, 96–98, 114–115, 135–136, 155–156, 175–176, 195–196, 218–220, 235–236, 260–266, 287, 295, 313, 321, 340–341, 344–345, 368, 370–379, 381–390, 392–394, 413, 416, 424, 432, 448–450, 452–457, 550, 555, 560, 673, 677–678, 681, 689, 691–692, 718–719, 721–724, 729, 735–736, 739–740, 742, 749–750, 761–767, 777–779, 783–784, 787–788, 791, 797
   _firmware_package.py942276%83, 87, 137, 181, 187, 210–211, 214–219, 221–222, 225–230, 232
   _grub.py41712869%217, 265–268, 274–278, 315–316, 323–328, 331–337, 340, 343–344, 349, 351–353, 362–368, 370–371, 373–375, 384–386, 388–390, 469–470, 474–475, 527, 533, 559, 581, 585–586, 601–603, 627–630, 642, 646–648, 650–652, 711–714, 739–742, 765–768, 780–781, 784–785, 820, 826, 846–847, 849, 861, 864, 867, 870, 874–876, 894–897, 925–928, 933–941, 946–954
   _jetson_cboot.py2612610%20, 22–25, 27–29, 35–38, 40–41, 57–58, 60, 62–63, 69, 73, 132, 135, 137–138, 141, 148–149, 157–158, 161, 167–168, 176, 185–189, 191, 197, 200–201, 207, 210–211, 216–217, 219, 225–226, 229–230, 233–235, 237, 243, 248–250, 252–254, 259, 261–264, 266–267, 276–277, 280–281, 286–287, 290–294, 297–298, 303–304, 307, 310–314, 319–322, 325, 328–329, 332, 335–336, 339, 343–348, 352–353, 357, 360–361, 364, 367–370, 372, 375–376, 380, 383, 386–389, 391, 398, 402–403, 406–407, 413–414, 420, 422–423, 427, 429, 431–433, 436, 440, 443, 446–447, 449, 452, 460–461, 468, 478, 481, 489–490, 495–498, 500, 507, 509–511, 517–518, 522–523, 527, 530, 532, 539–543, 545, 557–560, 563, 566, 568, 575, 579–580, 582–583, 585–587, 589, 591, 594, 597, 600, 602–603, 606–610, 614–616, 618, 626–630, 632, 635, 639, 642, 653–654, 659, 669, 672–680, 684–693, 697–706, 710, 712–714, 716–717, 719–720
   _jetson_common.py1724573%132, 140, 288–291, 294, 311, 319, 354, 359–364, 382, 408–409, 411–413, 417–420, 422–423, 425–429, 431, 438–439, 442–443, 453, 456–457, 460, 462, 506–507
   _jetson_uefi.py39727131%127–129, 134–135, 154–156, 161–164, 331, 449, 451–454, 458, 462–463, 465–473, 475, 487–488, 491–492, 495–496, 499–501, 505–506, 511–513, 517, 521–522, 525–526, 529–530, 534, 537–538, 540, 545–546, 550, 553–554, 559, 563–565, 569–571, 573, 577–580, 582–583, 605–606, 610–611, 613, 617, 621–622, 625–626, 633, 636–638, 641, 643–644, 649–650, 653–656, 658–659, 664, 666–667, 675, 678–681, 683–684, 686, 690–691, 695, 703–707, 710–711, 713, 716–720, 723, 726–730, 734–735, 738–743, 746–747, 750–753, 755–756, 763–764, 774–777, 780, 783–786, 789–793, 796–797, 800, 803–806, 809, 811, 816–817, 820, 823–826, 828, 834, 839–840, 859–860, 863, 871–872, 879, 889, 892, 899–900, 905–908, 916–919, 927–928, 940–943, 945, 948, 951, 959, 970–972, 974–976, 978–982, 987–988, 990, 1003, 1007, 1010, 1020, 1025, 1033–1034, 1037, 1041, 1043–1045, 1051–1052, 1057, 1065–1072, 1077–1085, 1090–1098, 1104–1106
   _rpi_boot.py28713453%55, 58, 122–123, 127, 135–138, 152–155, 162–163, 165–166, 171–172, 175–176, 185–186, 224, 230–234, 237, 255–257, 261–263, 268–270, 274–276, 286–287, 290, 293, 295–296, 298–299, 301–303, 309, 312–313, 323–326, 334–338, 340, 342–343, 348–349, 356–362, 393, 395–398, 408–411, 415–416, 418–422, 450–453, 472–475, 480, 483, 501–504, 509–517, 522–530, 547–550, 556–558, 561, 564
   configs.py550100% 
   protocol.py40100% 
   selecter.py412929%45–47, 50–51, 55–56, 59–61, 64, 66, 70, 78–80, 82–83, 85–86, 90, 92, 94–95, 97, 99–100, 102, 104
src/otaclient/configs
   _common.py80100% 
   ecu_info.py58198%108
   proxy_info.py52296%88, 90
src/otaclient/create_standby
   __init__.py12558%29–31, 33, 35
   common.py2244480%62, 65–66, 70–72, 74, 78–79, 81, 127, 175–177, 179–181, 183, 186–189, 193, 204, 278–279, 281–286, 298, 335, 363, 366–368, 384–385, 399, 403, 425–426
   interface.py50100% 
   rebuild_mode.py97990%93–95, 107–112
src/otaclient_api/v2
   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, 61, 63, 69, 76–77
   common.py1561888%47, 202, 205–207, 222, 229–231, 297–299, 309, 318–320, 366, 370
   downloader.py2001095%110–111, 129, 156, 372, 426, 430, 518–519, 528
   linux.py611575%51–53, 59, 69, 74, 76, 108–109, 133–134, 190, 195–196, 198
   logging.py29196%55
   persist_file_handling.py1181884%113, 118, 150–152, 163, 192–193, 228–232, 242–244, 246–247
   proto_streamer.py42880%33, 48, 66–67, 72, 81–82, 100
   proto_wrapper.py3984887%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, 803–805
   retry_task_map.py81791%164–165, 167, 179–182
   typing.py25388%69–70, 72
TOTAL6276167073% 

Tests Skipped Failures Errors Time
216 0 💤 0 ❌ 0 🔥 13m 34s ⏱️

Please sign in to comment.