Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
100 lines
4.3 KiB
Python
100 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
"""Read a Linux phone inventory through an already authenticated SSH master."""
|
|
import argparse
|
|
import ipaddress
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
|
|
from tundra import ROOT, now, save
|
|
|
|
READS = {
|
|
'identity': ['id'],
|
|
'os-release': ['cat', '/etc/os-release'],
|
|
'kernel': ['uname', '-a'],
|
|
'cmdline': ['cat', '/proc/cmdline'],
|
|
'partitions': ['lsblk', '-b', '-J', '-o', 'NAME,TYPE,SIZE,FSTYPE,PARTLABEL,MOUNTPOINTS'],
|
|
'partition-links': ['ls', '-l', '/dev/disk/by-partlabel', '/dev/block/by-name'],
|
|
'mounts': ['cat', '/proc/mounts'],
|
|
'fstab': ['cat', '/etc/fstab'],
|
|
'deviceinfo': ['ls', '-la', '/etc/deviceinfo'],
|
|
'boot-files': ['ls', '-l', '/boot'],
|
|
'devices': ['ls', '-l', '/dev/dri', '/dev/input', '/dev/video0', '/dev/binder'],
|
|
'services': ['systemctl', 'list-units', '--type=service', '--state=running', '--no-pager', '--plain'],
|
|
'packages': ['dpkg-query', '-W', '-f', '${binary:Package}\t${Version}\n'],
|
|
'android-properties': ['getprop'],
|
|
}
|
|
|
|
|
|
def identify(model, compatible, android_device=''):
|
|
model = model.replace('\0', '').strip()
|
|
tokens = compatible.strip().split('\0')
|
|
android_device = android_device.strip()
|
|
if model == 'Google Pixel 3a XL' or 'google,bonito' in tokens or android_device == 'bonito':
|
|
return 'wrong-device-pixel-3a-xl'
|
|
if model == 'Google Pixel 3a' or 'google,sargo' in tokens or android_device == 'sargo':
|
|
return 'sargo'
|
|
return 'unverified'
|
|
|
|
|
|
def ssh_base(socket, host, user):
|
|
# ProxyCommand=false prevents a new connection if the master disappears.
|
|
return ['ssh', '-F', '/dev/null', '-S', str(socket),
|
|
'-o', 'BatchMode=yes', '-o', 'ProxyCommand=false',
|
|
'-o', 'ForwardAgent=no', '-o', 'ClearAllForwardings=yes',
|
|
f'{user}@{host}']
|
|
|
|
|
|
def capture(base, command):
|
|
try:
|
|
result = subprocess.run(base + [shlex.join(command)], capture_output=True,
|
|
text=True, errors='replace', timeout=20)
|
|
output = result.stdout
|
|
if command == READS['packages']:
|
|
output = '\n'.join(line for line in output.splitlines() if re.search(
|
|
r'android|droidian|hybris|halium|linux-|phosh|phoc|wayland|mesa|firmware|modem|pulse|pipewire',
|
|
line, re.I))
|
|
return {'command': command, 'returncode': result.returncode,
|
|
'stdout': output[:65536], 'stderr': result.stderr[:4096],
|
|
'truncated': len(output) > 65536}
|
|
except subprocess.TimeoutExpired:
|
|
return {'command': command, 'returncode': None, 'error': 'timeout'}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--control-socket', type=Path, required=True)
|
|
parser.add_argument('--host', default='10.15.19.82')
|
|
parser.add_argument('--user', default='droidian')
|
|
args = parser.parse_args()
|
|
ipaddress.ip_address(args.host)
|
|
if not re.fullmatch(r'[a-z_][a-z0-9_-]*', args.user):
|
|
parser.error('Invalid SSH user')
|
|
socket = args.control_socket.resolve()
|
|
if not socket.is_socket():
|
|
parser.error('Open the authenticated SSH master in your terminal first')
|
|
os.umask(0o077)
|
|
base = ssh_base(socket, args.host, args.user)
|
|
subprocess.run(base[:-1] + ['-O', 'check', base[-1]], check=True)
|
|
records = {name: capture(base, ['cat', path]) for name, path in (
|
|
('model', '/proc/device-tree/model'),
|
|
('compatible', '/proc/device-tree/compatible'))}
|
|
records['android-device'] = capture(base, ['getprop', 'ro.product.device'])
|
|
value = lambda name: records[name].get('stdout', '') if records[name].get('returncode') == 0 else ''
|
|
identity = identify(value('model'), value('compatible'), value('android-device'))
|
|
if identity == 'sargo':
|
|
for name, command in READS.items():
|
|
records[name] = capture(base, command)
|
|
path = ROOT/'logs/device'/('ssh-'+now().replace(':', '-')+'.json')
|
|
save(path, {'recordedAt': now(), 'transport': 'ssh-existing-master',
|
|
'identity': identity, 'readOnly': True,
|
|
'flashAuthorizedByThisReport': False, 'records': records})
|
|
print(f'Inventory: {path}\nIdentity: {identity}')
|
|
return 0 if identity == 'sargo' else 2
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|