Prepare Sargo installations with private per-device workspaces
Bind backups, storage layout, RAM builds, file staging and recovery to each phone. Verify atomic transfers without replacing existing state and record the unresolved Android HAL source provenance. Validated with 53 native tooling tests, 5 changelog tests and two RAM boots with persistent Gaia state on the development Sargo. No new flash or native binary release.
This commit is contained in:
parent
978542bc87
commit
d04824ec89
16 changed files with 707 additions and 7 deletions
80
tundra/tools/collect-sargo-installation.py
Normal file
80
tundra/tools/collect-sargo-installation.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Run as root on Sargo to collect boot backups and its existing ext4 layout.
|
||||
|
||||
Only regular output files are written. No partition, slot, filesystem, service
|
||||
or network configuration is changed. Transfer the private output to your PC.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import fcntl
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import struct
|
||||
|
||||
from boot_backup import SIZES, verify
|
||||
|
||||
|
||||
def identity(proc=Path('/proc')):
|
||||
compatible = (proc/'device-tree/compatible').read_bytes().split(b'\0')
|
||||
model = (proc/'device-tree/model').read_text().strip('\0')
|
||||
if b'google,b4s4-sdm670' not in compatible or ' S4 ' not in model:
|
||||
raise ValueError('Expected the Sargo device tree')
|
||||
cmdline = dict(x.split('=', 1) for x in (proc/'cmdline').read_text().split() if '=' in x)
|
||||
serial = cmdline.get('androidboot.serialno', '')
|
||||
if not serial or cmdline.get('androidboot.slot_suffix') != '_a':
|
||||
raise ValueError('Expected a device serial and active slot A')
|
||||
return {'serialSha256': hashlib.sha256(serial.encode()).hexdigest(), 'activeSlot': 'a'}
|
||||
|
||||
|
||||
def collect(mountpoint, output):
|
||||
if os.geteuid() != 0:
|
||||
raise ValueError('Raw partition backup requires root on the phone')
|
||||
device = identity()
|
||||
spec = importlib.util.spec_from_file_location('storage', Path(__file__).with_name('inventory-storage.py'))
|
||||
storage = importlib.util.module_from_spec(spec); spec.loader.exec_module(storage)
|
||||
layout = {**storage.inventory(mountpoint), **device}
|
||||
output = Path(output).absolute()
|
||||
output.mkdir(mode=0o700)
|
||||
(output/'.incomplete').touch()
|
||||
backup = output/'backup'; backup.mkdir(mode=0o700)
|
||||
manifest = {'device': 'sargo', 'rawPartitions': True, **device,
|
||||
'recordedAt': datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
'files': {}, 'scope': 'Boot, DTBO and vbmeta only; not a backup of user data'}
|
||||
for name, size in SIZES.items():
|
||||
path = (Path('/dev/disk/by-partlabel')/name.removesuffix('.img')).resolve(strict=True)
|
||||
fd = os.open(path, os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW)
|
||||
with os.fdopen(fd, 'rb', buffering=0) as source:
|
||||
if not stat.S_ISBLK(os.fstat(source.fileno()).st_mode):
|
||||
raise ValueError('Expected a partition block device')
|
||||
actual = struct.unpack('Q', fcntl.ioctl(source.fileno(), 0x80081272, bytes(8)))[0]
|
||||
if actual != size:
|
||||
raise ValueError('Unexpected partition size: '+name)
|
||||
sha = hashlib.sha256(); left = size
|
||||
with (backup/name).open('xb') as target:
|
||||
while left:
|
||||
block = source.read(min(left, 1024*1024))
|
||||
if not block:
|
||||
raise ValueError('Truncated partition read: '+name)
|
||||
target.write(block); sha.update(block); left -= len(block)
|
||||
target.flush(); os.fsync(target.fileno())
|
||||
manifest['files'][name] = {'bytes': size, 'sha256': sha.hexdigest()}
|
||||
if identity() != device or storage.inventory(mountpoint) != {k:v for k,v in layout.items() if k not in device}:
|
||||
raise ValueError('Device identity or storage changed during collection')
|
||||
(backup/'manifest.json').write_text(json.dumps(manifest, indent=2)+'\n')
|
||||
(output/'layout.json').write_text(json.dumps(layout, indent=2)+'\n')
|
||||
verify(backup)
|
||||
(output/'.incomplete').unlink()
|
||||
return output
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--mountpoint', required=True)
|
||||
parser.add_argument('--output', required=True, type=Path)
|
||||
args = parser.parse_args(); os.umask(0o077)
|
||||
print(collect(args.mountpoint, args.output))
|
||||
Loading…
Reference in a new issue