Skip to content

Commit

Permalink
refactor: rpi_boot: detects slot by partition tables, not by checking…
Browse files Browse the repository at this point in the history
… slot fslabel (#321)

This PR refines the rpi_boot module to detect slot by examining the partition layout, not relying on reading the fslabel, and explicitly requires the expected partition tables(but support extra partitions after partition ID 3).

Also this PR implements the feature that rpi_boot will correct the active slot's fslabel with correct slot_id after slot detection.
  • Loading branch information
Bodong-Yang authored Jun 19, 2024
1 parent 5366b13 commit 0773108
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 58 deletions.
26 changes: 25 additions & 1 deletion src/otaclient/app/boot_control/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,30 @@ def get_parent_dev(cls, child_device: str, *, raise_exception: bool = True) -> s
cmd = ["lsblk", "-idpno", "PKNAME", child_device]
return subprocess_check_output(cmd, raise_exception=raise_exception)

@classmethod
def get_device_tree(
cls, parent_dev: str, *, raise_exception: bool = True
) -> list[str]:
"""Get the device tree of a parent device.
For example, for sda with 3 partitions, we will get:
["/dev/sda", "/dev/sda1", "/dev/sda2", "/dev/sda3"]
This function is implemented by calling:
lsblk -lnpo NAME <parent_dev>
Args:
parent_dev (str): The parent device to be checked.
raise_exception (bool, optional): raise exception on subprocess call failed.
Defaults to True.
Returns:
str: _description_
"""
cmd = ["lsblk", "-lnpo", "NAME", parent_dev]
raw_res = subprocess_check_output(cmd, raise_exception=raise_exception)
return raw_res.splitlines()

@classmethod
def set_ext4_fslabel(cls, dev: str, fslabel: str, *, raise_exception: bool = True):
"""Set <fslabel> to ext4 formatted <dev>.
Expand Down Expand Up @@ -724,7 +748,7 @@ def prepare_standby_dev(
# TODO: in the future if in-place update mode is implemented, do a
# fschck over the standby slot file system.
if fslabel:
CMDHelperFuncs.set_ext4_fslabel(self.active_slot_dev, fslabel=fslabel)
CMDHelperFuncs.set_ext4_fslabel(self.standby_slot_dev, fslabel=fslabel)

def umount_all(self, *, ignore_error: bool = True):
logger.debug("unmount standby slot and active slot mount point...")
Expand Down
131 changes: 74 additions & 57 deletions src/otaclient/app/boot_control/_rpi_boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import logging
import os
import re
import subprocess
from pathlib import Path
from string import Template
from typing import Generator
Expand All @@ -33,11 +34,7 @@
from otaclient.app.boot_control.configs import rpi_boot_cfg as cfg
from otaclient.app.boot_control.protocol import BootControllerProtocol
from otaclient_api.v2 import types as api_types
from otaclient_common.common import (
replace_atomic,
subprocess_call,
subprocess_check_output,
)
from otaclient_common.common import replace_atomic, subprocess_call

logger = logging.getLogger(__name__)

Expand All @@ -54,12 +51,13 @@ class _RPIBootControllerError(Exception):
class _RPIBootControl:
"""Boot control helper for rpi4 support.
Expected partition layout:
/dev/sda:
- sda1: fat32, fslabel=systemb-boot
- sda2: ext4, fslabel=slot_a
- sda3: ext4, fslabel=slot_b
Supported partition layout:
/dev/sd<x>:
- sd<x>1: fat32, fslabel=systemb-boot
- sd<x>2: ext4, fslabel=slot_a
- sd<x>3: ext4, fslabel=slot_b
slot is the fslabel for each AB rootfs.
NOTE that we allow extra partitions with ID after 3.
This class provides the following features:
1. AB partition detection,
Expand Down Expand Up @@ -90,60 +88,79 @@ def __init__(self) -> None:
raise ValueError(_err_msg)
self._init_slots_info()
self._init_boot_files()
self._check_active_slot_id()

def _init_slots_info(self):
"""Get current/standby slots info."""
logger.debug("checking and initializing slots info...")
try:
# detect active slot
_active_slot_dev = CMDHelperFuncs.get_current_rootfs_dev()
assert _active_slot_dev
self._active_slot_dev = _active_slot_dev
def _check_active_slot_id(self):
"""Check whether the active slot fslabel is matching the slot id.
_active_slot = CMDHelperFuncs.get_attrs_by_dev(
"LABEL", str(self._active_slot_dev)
If mismatched, try to correct the problem.
"""
fslabel = self.active_slot
actual_fslabel = CMDHelperFuncs.get_attrs_by_dev(
"LABEL", self.active_slot_dev, raise_exception=False
)
if actual_fslabel == fslabel:
return

logger.warning(
(
f"current active slot is {fslabel}, but its fslabel is {actual_fslabel}, "
f"try to correct the fslabel with slot id {fslabel}..."
)
assert _active_slot
self._active_slot = _active_slot

# detect standby slot
# NOTE: using the similar logic like grub, detect the silibing dev
# of the active slot as standby slot
_parent = CMDHelperFuncs.get_parent_dev(str(self._active_slot_dev))
assert _parent

# list children device file from parent device
# exclude parent dev(always in the front)
# expected raw result from lsblk:
# NAME="/dev/sdx"
# NAME="/dev/sdx1" # system-boot
# NAME="/dev/sdx2" # slot_a
# NAME="/dev/sdx3" # slot_b
_check_dev_family_cmd = ["lsblk", "-Ppo", "NAME", _parent]
_raw_child_partitions = subprocess_check_output(
_check_dev_family_cmd, raise_exception=True
)
try:
CMDHelperFuncs.set_ext4_fslabel(self.active_slot_dev, fslabel)
os.sync()
except subprocess.CalledProcessError as e:
logger.error(
f"failed to correct the fslabel mismatched: {e!r}, {e.stderr.decode()}"
)
logger.error("this might cause problem on future OTA update!")

def _init_slots_info(self):
"""Get current/standby slots info."""
logger.debug("checking and initializing slots info...")
try:
# ------ detect active slot ------ #
active_slot_dev = CMDHelperFuncs.get_current_rootfs_dev()
assert active_slot_dev
self._active_slot_dev = active_slot_dev

# detect the parent device of boot device
# i.e., for /dev/sda2 here we get /dev/sda
parent_dev = CMDHelperFuncs.get_parent_dev(str(self._active_slot_dev))
assert parent_dev

# get device tree, for /dev/sda device, we will get:
# ["/dev/sda", "/dev/sda1", "/dev/sda2", "/dev/sda3"]
_device_tree = CMDHelperFuncs.get_device_tree(parent_dev)
# remove the parent dev itself and system-boot partition
device_tree = _device_tree[2:]

# Now we should only have two partitions in the device_tree list:
# /dev/sda2, /dev/sda3
# NOTE that we allow extra partitions presented after sd<x>3.
assert (
len(device_tree) >= 2
), f"unexpected partition layout: {_device_tree=}"

# get the active slot ID by its position in the disk
try:
# NOTE: exclude the first 2 lines(parent and system-boot)
_child_partitions = [
raw.split("=")[-1].strip('"')
for raw in _raw_child_partitions.splitlines()[2:]
]
if (
len(_child_partitions) != 2
or self._active_slot_dev not in _child_partitions
):
raise ValueError
_child_partitions.remove(self._active_slot_dev)
except Exception:
idx = device_tree.index(active_slot_dev)
except ValueError:
raise ValueError(
f"unexpected partition layout: {_raw_child_partitions}"
) from None
# it is OK if standby_slot dev doesn't have fslabel or fslabel != standby_slot_id
# we will always set the fslabel
self._standby_slot = self.AB_FLIPS[self._active_slot]
self._standby_slot_dev = _child_partitions[0]
f"active lost is not in the device tree: {active_slot_dev=}, {device_tree=}"
)

if idx == 0: # slot_a
self._active_slot = self.SLOT_A
self._standby_slot = self.SLOT_B
self._standby_slot_dev = device_tree[1]
elif idx == 1: # slot_b
self._active_slot = self.SLOT_B
self._standby_slot = self.SLOT_A
self._standby_slot_dev = device_tree[0]

logger.info(
f"rpi_boot: active_slot: {self._active_slot}({self._active_slot_dev}), "
f"standby_slot: {self._standby_slot}({self._standby_slot_dev})"
Expand Down

1 comment on commit 0773108

@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.py3263589%145, 150, 186–187, 197–198, 201, 213, 271, 281–284, 323–326, 406, 409, 417–419, 432, 441–442, 445–446, 720, 723, 735–740
   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.py4018678%98–99, 218, 229, 256–258, 278, 294–295, 297, 320–321, 327, 331, 333, 360–362, 378, 439–440, 482–483, 553, 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
   log_setting.py52590%53, 55, 64–66
src/otaclient/app
   __main__.py110%16
   configs.py750100% 
   errors.py1120100% 
   interface.py50100% 
   main.py46589%52–53, 75–77
   ota_client.py39213166%67, 75, 96, 201–203, 214, 260–263, 275–278, 281–284, 294–297, 302–303, 305, 314, 317, 322–323, 326, 332, 334, 337, 379–382, 387, 391, 394, 410–413, 416–423, 426–433, 439–442, 471, 474–475, 477, 480–483, 485–486, 491–492, 495, 509–516, 523, 526–532, 579–582, 590, 626, 631–634, 639–641, 644–645, 647–648, 650–651, 653, 713–714, 717, 725–726, 729, 740–741, 744, 752–753, 756, 767, 786, 813, 832, 850
   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.py2387966%73–74, 95–97, 113–114, 134–135, 154–155, 174–175, 194–195, 217–219, 234–235, 256, 264, 282, 290, 309–310, 313–314, 337, 339–348, 350–359, 361–363, 382, 385, 393, 401, 417–419, 421–426, 519, 524, 529, 642, 646–647, 650, 658, 660–661, 735–736, 746, 764
   _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.py27021420%69–70, 77–78, 96–105, 117, 124–125, 137, 143–144, 154–156, 168–169, 180–181, 184–185, 188–189, 192–196, 199–200, 204–205, 210–211, 213–217, 219–225, 227–228, 233, 236, 239–240, 243, 247–248, 252–253, 257, 260, 263, 267–273, 275–277, 282, 285, 288, 292, 299, 301–304, 317, 320, 324, 326–328, 332, 339, 341, 344, 350–351, 356, 364, 372–374, 383–384, 386–388, 394, 397–399, 403–404, 406, 409, 418–420, 423, 426, 429–434, 436–438, 441, 444, 448–453, 457–459, 464–465, 469–470, 473, 476, 479–480, 483, 486, 491, 494, 497–498, 500, 502, 505, 508, 510–511, 514–518, 523–524, 526, 534–538, 540, 543, 546, 557–558, 563, 573, 576–584, 589–597, 602–610, 616–618, 621, 624
   _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.py27312753%86–88, 103, 114–115, 118, 122–123, 125–127, 131–132, 136, 138, 143, 148–151, 155–162, 164, 168–171, 195–197, 203–205, 218–220, 226–228, 241–248, 250, 254–256, 259–262, 265–266, 271, 275, 279, 283, 317, 344–346, 356–359, 363–369, 409–411, 453–457, 476–479, 484, 487, 511–514, 519–527, 532–540, 554–557, 563–565, 568
   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.py99990%94–96, 108–113
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.py1582087%40, 44, 126, 232, 235–237, 252, 259–261, 327–329, 339, 348–350, 396, 400
   downloader.py2694384%72, 85–86, 301, 306, 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.py84396%141, 143, 158
   typing.py250100% 
TOTAL5987136277% 

Tests Skipped Failures Errors Time
179 0 💤 0 ❌ 0 🔥 5m 8s ⏱️

Please sign in to comment.