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
Willem Melching 2021-06-30 14:20:58 +02:00 committed by GitHub
parent 86630effcf
commit 7c6bf89e04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 163 additions and 92 deletions

View File

@ -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
}

View File

@ -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
}
]

View File

@ -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)

View File

@ -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)