tici: flash bootloader partitions from manifest (#21399)
* move swapping to python * only create downloader if needed * typo * number * add sanity check * boot full check to test * manifest is required argument * implement full hash check * off by one * new manifest * only write tag for system * bump splash * review comments part 1 * trigger update Co-authored-by: Robbe Derks <robbe.derks@gmail.com>pull/21343/head
parent
86630effcf
commit
7c6bf89e04
|
@ -104,62 +104,11 @@ function tici_init {
|
|||
|
||||
# Check if AGNOS update is required
|
||||
if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then
|
||||
# Get number of slot to switch to
|
||||
CUR_SLOT=$(abctl --boot_slot)
|
||||
if [[ "$CUR_SLOT" == "_a" ]]; then
|
||||
OTHER_SLOT="_b"
|
||||
OTHER_SLOT_NUMBER="1"
|
||||
else
|
||||
OTHER_SLOT="_a"
|
||||
OTHER_SLOT_NUMBER="0"
|
||||
fi
|
||||
echo "Cur slot $CUR_SLOT, target $OTHER_SLOT"
|
||||
|
||||
# Get expected hashes from manifest
|
||||
MANIFEST="$DIR/selfdrive/hardware/tici/agnos.json"
|
||||
SYSTEM_HASH_EXPECTED=$(jq -r ".[] | select(.name == \"system\") | .hash_raw" $MANIFEST)
|
||||
SYSTEM_SIZE=$(jq -r ".[] | select(.name == \"system\") | .size" $MANIFEST)
|
||||
BOOT_HASH_EXPECTED=$(jq -r ".[] | select(.name == \"boot\") | .hash_raw" $MANIFEST)
|
||||
BOOT_SIZE=$(jq -r ".[] | select(.name == \"boot\") | .size" $MANIFEST)
|
||||
echo "Expected hashes:"
|
||||
echo "System: $SYSTEM_HASH_EXPECTED"
|
||||
echo "Boot: $BOOT_HASH_EXPECTED"
|
||||
$DIR/selfdrive/hardware/tici/agnos.py --swap $MANIFEST
|
||||
|
||||
# Read hashes from alternate partitions, should already be flashed by updated
|
||||
SYSTEM_HASH=$(dd if="/dev/disk/by-partlabel/system$OTHER_SLOT" bs=1 skip="$SYSTEM_SIZE" count=64 2>/dev/null)
|
||||
BOOT_HASH=$(dd if="/dev/disk/by-partlabel/boot$OTHER_SLOT" bs=1 skip="$BOOT_SIZE" count=64 2>/dev/null)
|
||||
echo "Found hashes:"
|
||||
echo "System: $SYSTEM_HASH"
|
||||
echo "Boot: $BOOT_HASH"
|
||||
|
||||
if [[ "$SYSTEM_HASH" == "$SYSTEM_HASH_EXPECTED" && "$BOOT_HASH" == "$BOOT_HASH_EXPECTED" ]]; then
|
||||
echo "Swapping active slot to $OTHER_SLOT_NUMBER"
|
||||
|
||||
# Clean hashes before swapping to prevent looping
|
||||
dd if=/dev/zero of="/dev/disk/by-partlabel/system$OTHER_SLOT" bs=1 seek="$SYSTEM_SIZE" count=64
|
||||
dd if=/dev/zero of="/dev/disk/by-partlabel/boot$OTHER_SLOT" bs=1 seek="$BOOT_SIZE" count=64
|
||||
sync
|
||||
|
||||
abctl --set_active "$OTHER_SLOT_NUMBER"
|
||||
|
||||
sleep 1
|
||||
sudo reboot
|
||||
else
|
||||
echo "Hash mismatch, downloading agnos"
|
||||
if $DIR/selfdrive/hardware/tici/agnos.py $MANIFEST; then
|
||||
echo "Download done, swapping active slot to $OTHER_SLOT_NUMBER"
|
||||
|
||||
# Clean hashes before swapping to prevent looping
|
||||
dd if=/dev/zero of="/dev/disk/by-partlabel/system$OTHER_SLOT" bs=1 seek="$SYSTEM_SIZE" count=64
|
||||
dd if=/dev/zero of="/dev/disk/by-partlabel/boot$OTHER_SLOT" bs=1 seek="$BOOT_SIZE" count=64
|
||||
sync
|
||||
|
||||
abctl --set_active "$OTHER_SLOT_NUMBER"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
sudo reboot
|
||||
fi
|
||||
sleep 1
|
||||
sudo reboot
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,63 @@
|
|||
[
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-96524a331ee8bea6132127fc82fd22bb9cc4b2ec424a81607c73b681c510a467.img.xz",
|
||||
"hash": "13842404eaceb5466a028a2eb91db938a20c8ff5bc541599c6b9729a4bb608f5",
|
||||
"hash_raw": "96524a331ee8bea6132127fc82fd22bb9cc4b2ec424a81607c73b681c510a467",
|
||||
"size": 10737418240,
|
||||
"sparse": true
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-1d31f0b0afafb27157c26f13226d884aa6f58abf6a6ddc21d928140bef0f539c.img.xz",
|
||||
"hash": "1d31f0b0afafb27157c26f13226d884aa6f58abf6a6ddc21d928140bef0f539c",
|
||||
"hash_raw": "1d31f0b0afafb27157c26f13226d884aa6f58abf6a6ddc21d928140bef0f539c",
|
||||
"size": 14772224,
|
||||
"sparse": false
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true
|
||||
},
|
||||
{
|
||||
"name": "abl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-f73aae9e302eeccebc234c456d059394fb01b2a0307eab19afe3adadf1f2796c.img.xz",
|
||||
"hash": "f73aae9e302eeccebc234c456d059394fb01b2a0307eab19afe3adadf1f2796c",
|
||||
"hash_raw": "f73aae9e302eeccebc234c456d059394fb01b2a0307eab19afe3adadf1f2796c",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true
|
||||
},
|
||||
{
|
||||
"name": "xbl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl-2b1b67aa918cd127f2b0b4ed0a372f3a93676cf9d270bd3e56329516efdc5a35.img.xz",
|
||||
"hash": "2b1b67aa918cd127f2b0b4ed0a372f3a93676cf9d270bd3e56329516efdc5a35",
|
||||
"hash_raw": "2b1b67aa918cd127f2b0b4ed0a372f3a93676cf9d270bd3e56329516efdc5a35",
|
||||
"size": 3670016,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true
|
||||
},
|
||||
{
|
||||
"name": "xbl_config",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-3aa926394b4cec464300bfc0e7ab77d50889b38041138c60cd84c397930b38ad.img.xz",
|
||||
"hash": "3aa926394b4cec464300bfc0e7ab77d50889b38041138c60cd84c397930b38ad",
|
||||
"hash_raw": "3aa926394b4cec464300bfc0e7ab77d50889b38041138c60cd84c397930b38ad",
|
||||
"size": 364544,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true
|
||||
},
|
||||
{
|
||||
"name": "splash",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/splash-5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08.img.xz",
|
||||
"hash": "5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08",
|
||||
"hash_raw": "5c61260048f22ede6e6343fabb27f6ff73f9271f4751a01aaf7abf097afc1f08",
|
||||
"size": 34226176,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": false
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-96524a331ee8bea6132127fc82fd22bb9cc4b2ec424a81607c73b681c510a467.img.xz",
|
||||
"hash": "13842404eaceb5466a028a2eb91db938a20c8ff5bc541599c6b9729a4bb608f5",
|
||||
"hash_raw": "96524a331ee8bea6132127fc82fd22bb9cc4b2ec424a81607c73b681c510a467",
|
||||
"size": 10737418240,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ from typing import Generator
|
|||
|
||||
from common.spinner import Spinner
|
||||
|
||||
SPARSE_CHUNK_FMT = struct.Struct('H2xI4x')
|
||||
|
||||
|
||||
class StreamingDecompressor:
|
||||
def __init__(self, url: str) -> None:
|
||||
|
@ -39,7 +41,7 @@ class StreamingDecompressor:
|
|||
self.sha256.update(result)
|
||||
return result
|
||||
|
||||
SPARSE_CHUNK_FMT = struct.Struct('H2xI4x')
|
||||
|
||||
def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]:
|
||||
# https://source.android.com/devices/bootloader/images#sparse-format
|
||||
magic = struct.unpack("I", f.read(4))[0]
|
||||
|
@ -74,25 +76,75 @@ def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]:
|
|||
raise Exception("Unhandled sparse chunk type")
|
||||
|
||||
|
||||
def flash_partition(cloudlog, spinner, target_slot, partition):
|
||||
cloudlog.info(f"Downloading and writing {partition['name']}")
|
||||
def get_target_slot_number():
|
||||
current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip()
|
||||
return 1 if current_slot == "_a" else 0
|
||||
|
||||
downloader = StreamingDecompressor(partition['url'])
|
||||
with open(f"/dev/disk/by-partlabel/{partition['name']}{target_slot}", 'wb+') as out:
|
||||
|
||||
def slot_number_to_suffix(slot_number):
|
||||
assert slot_number in (0, 1)
|
||||
return '_a' if slot_number == 0 else '_b'
|
||||
|
||||
|
||||
def get_partition_path(target_slot_number, partition):
|
||||
path = f"/dev/disk/by-partlabel/{partition['name']}"
|
||||
|
||||
if partition.get('has_ab', True):
|
||||
path += slot_number_to_suffix(target_slot_number)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def verify_partition(target_slot_number, partition):
|
||||
full_check = partition['full_check']
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
partition_size = partition['size']
|
||||
|
||||
with open(path, 'rb+') as out:
|
||||
if full_check:
|
||||
raw_hash = hashlib.sha256()
|
||||
|
||||
pos = 0
|
||||
chunk_size = 1024 * 1024
|
||||
while pos < partition_size:
|
||||
n = min(chunk_size, partition_size - pos)
|
||||
raw_hash.update(out.read(n))
|
||||
pos += n
|
||||
|
||||
return raw_hash.hexdigest().lower() == partition['hash_raw'].lower()
|
||||
else:
|
||||
out.seek(partition_size)
|
||||
return out.read(64) == partition['hash_raw'].lower().encode()
|
||||
|
||||
|
||||
def clear_partition_hash(target_slot_number, partition):
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
with open(path, 'wb+') as out:
|
||||
partition_size = partition['size']
|
||||
|
||||
# Check if partition is already flashed
|
||||
out.seek(partition_size)
|
||||
if out.read(64) == partition['hash_raw'].lower().encode():
|
||||
cloudlog.info(f"Already flashed {partition['name']}")
|
||||
return
|
||||
|
||||
# Clear hash before flashing
|
||||
out.seek(partition_size)
|
||||
out.write(b"\x00" * 64)
|
||||
out.seek(0)
|
||||
os.sync()
|
||||
|
||||
|
||||
def flash_partition(target_slot_number, partition, cloudlog, spinner=None):
|
||||
cloudlog.info(f"Downloading and writing {partition['name']}")
|
||||
|
||||
if verify_partition(target_slot_number, partition):
|
||||
cloudlog.info(f"Already flashed {partition['name']}")
|
||||
return
|
||||
|
||||
downloader = StreamingDecompressor(partition['url'])
|
||||
|
||||
# Clear hash before flashing in case we get interrupted
|
||||
full_check = partition['full_check']
|
||||
if not full_check:
|
||||
clear_partition_hash(target_slot_number, partition)
|
||||
|
||||
path = get_partition_path(target_slot_number, partition)
|
||||
with open(path, 'wb+') as out:
|
||||
partition_size = partition['size']
|
||||
|
||||
# Flash partition
|
||||
if partition['sparse']:
|
||||
raw_hash = hashlib.sha256()
|
||||
|
@ -120,17 +172,23 @@ def flash_partition(cloudlog, spinner, target_slot, partition):
|
|||
|
||||
# Write hash after successfull flash
|
||||
os.sync()
|
||||
out.write(partition['hash_raw'].lower().encode())
|
||||
if not full_check:
|
||||
out.write(partition['hash_raw'].lower().encode())
|
||||
|
||||
|
||||
def flash_agnos_update(manifest_path, cloudlog, spinner=None):
|
||||
def swap(manifest_path, target_slot_number):
|
||||
update = json.load(open(manifest_path))
|
||||
for partition in update:
|
||||
if not partition.get('full_check', False):
|
||||
clear_partition_hash(target_slot_number, partition)
|
||||
|
||||
subprocess.check_output(f"abctl --set_active {target_slot_number}", shell=True)
|
||||
|
||||
|
||||
def flash_agnos_update(manifest_path, target_slot_number, cloudlog, spinner=None):
|
||||
update = json.load(open(manifest_path))
|
||||
|
||||
current_slot = subprocess.check_output(["abctl", "--boot_slot"], encoding='utf-8').strip()
|
||||
target_slot = "_b" if current_slot == "_a" else "_a"
|
||||
target_slot_number = "0" if target_slot == "_a" else "1"
|
||||
|
||||
cloudlog.info(f"Current slot {current_slot}, target slot {target_slot}")
|
||||
cloudlog.info(f"Target slot {target_slot_number}")
|
||||
|
||||
# set target slot as unbootable
|
||||
os.system(f"abctl --set_unbootable {target_slot_number}")
|
||||
|
@ -140,7 +198,7 @@ def flash_agnos_update(manifest_path, cloudlog, spinner=None):
|
|||
|
||||
for retries in range(10):
|
||||
try:
|
||||
flash_partition(cloudlog, spinner, target_slot, partition)
|
||||
flash_partition(target_slot_number, partition, cloudlog, spinner)
|
||||
success = True
|
||||
break
|
||||
|
||||
|
@ -154,21 +212,39 @@ def flash_agnos_update(manifest_path, cloudlog, spinner=None):
|
|||
cloudlog.info(f"Failed to flash {partition['name']}, aborting")
|
||||
raise Exception("Maximum retries exceeded")
|
||||
|
||||
cloudlog.info(f"AGNOS ready on slot {target_slot}")
|
||||
cloudlog.info(f"AGNOS ready on slot {target_slot_number}")
|
||||
|
||||
|
||||
def verify_agnos_update(manifest_path, target_slot_number):
|
||||
update = json.load(open(manifest_path))
|
||||
return all(verify_partition(target_slot_number, partition) for partition in update)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import logging
|
||||
import time
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: ./agnos.py <manifest.json>")
|
||||
exit(1)
|
||||
parser = argparse.ArgumentParser(description="Flash and verify AGNOS update",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
|
||||
parser.add_argument("--swap", action="store_true", help="Verify and perform swap, downloads if necessary")
|
||||
parser.add_argument("manifest", help="Manifest json")
|
||||
args = parser.parse_args()
|
||||
|
||||
spinner = Spinner()
|
||||
spinner.update("Updating AGNOS")
|
||||
time.sleep(5)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
flash_agnos_update(sys.argv[1], logging, spinner)
|
||||
|
||||
target_slot_number = get_target_slot_number()
|
||||
if args.swap:
|
||||
while not verify_agnos_update(args.manifest, target_slot_number):
|
||||
logging.error("Verification failed. Flashing AGNOS")
|
||||
flash_agnos_update(args.manifest, target_slot_number, logging, spinner)
|
||||
|
||||
logging.warning(f"Verification succeeded. Swapping to slot {target_slot_number}")
|
||||
swap(args.manifest, target_slot_number)
|
||||
else:
|
||||
flash_agnos_update(args.manifest, target_slot_number, logging, spinner)
|
||||
|
|
|
@ -219,7 +219,7 @@ def finalize_update() -> None:
|
|||
|
||||
|
||||
def handle_agnos_update(wait_helper):
|
||||
from selfdrive.hardware.tici.agnos import flash_agnos_update
|
||||
from selfdrive.hardware.tici.agnos import flash_agnos_update, get_target_slot_number
|
||||
|
||||
cur_version = HARDWARE.get_os_version()
|
||||
updated_version = run(["bash", "-c", r"unset AGNOS_VERSION && source launch_env.sh && \
|
||||
|
@ -236,7 +236,8 @@ def handle_agnos_update(wait_helper):
|
|||
set_offroad_alert("Offroad_NeosUpdate", True)
|
||||
|
||||
manifest_path = os.path.join(OVERLAY_MERGED, "selfdrive/hardware/tici/agnos.json")
|
||||
flash_agnos_update(manifest_path, cloudlog)
|
||||
target_slot_number = get_target_slot_number()
|
||||
flash_agnos_update(manifest_path, target_slot_number, cloudlog)
|
||||
set_offroad_alert("Offroad_NeosUpdate", False)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue