Add reproducible Radxa AirPlay and Bluetooth mixer setup
This commit is contained in:
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Snapshot/restore mixer-managed files and service states, not apt packages."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
UNITS = ['shairport-sync', 'nqptp', 'bluealsa', 'bluealsa-aplay',
|
||||
'radxa-bt-output', 'radxa-bose-output', 'avahi-daemon', 'bluetooth']
|
||||
PATHS = [
|
||||
'/etc/asound.conf', '/etc/shairport-sync.conf',
|
||||
'/etc/radxa-audio/mixer.json', '/etc/radxa-audio/asound-dac.conf',
|
||||
'/etc/radxa-audio/asound-bluetooth.conf',
|
||||
'/etc/systemd/system/shairport-sync.service', '/etc/systemd/system/nqptp.service',
|
||||
'/etc/systemd/system/bluealsa.service.d/10-radxa-sink.conf',
|
||||
'/etc/systemd/system/bluealsa-aplay.service.d/10-radxa-mixer.conf',
|
||||
'/etc/systemd/system/radxa-bt-output.service',
|
||||
'/etc/modules-load.d/radxa-audio-loopback.conf',
|
||||
'/etc/modprobe.d/radxa-audio-loopback.conf',
|
||||
'/etc/modprobe.d/90-radxa-airplay-wifi.conf',
|
||||
'/usr/local/bin/shairport-sync', '/usr/local/bin/nqptp',
|
||||
'/usr/local/bin/radxa-audio', '/usr/local/bin/radxa-audio-output',
|
||||
]
|
||||
|
||||
|
||||
def ctl(*args):
|
||||
return subprocess.run(['systemctl', *args], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True)
|
||||
|
||||
|
||||
def snapshot(directory):
|
||||
directory.mkdir(parents=True, exist_ok=False, mode=0o700)
|
||||
files = {}
|
||||
for name in PATHS:
|
||||
path = Path(name)
|
||||
files[name] = os.path.lexists(path)
|
||||
if files[name]:
|
||||
target = directory / 'files' / name.lstrip('/')
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(path, target, follow_symlinks=False)
|
||||
states = {u: {'active': ctl('is-active', '--quiet', u).returncode == 0,
|
||||
'enabled': ctl('is-enabled', '--quiet', u).returncode == 0} for u in UNITS}
|
||||
(directory / 'manifest.json').write_text(json.dumps({'files': files, 'units': states}, indent=2))
|
||||
|
||||
|
||||
def restore(directory):
|
||||
manifest = json.loads((directory / 'manifest.json').read_text())
|
||||
# Only restore allowlisted paths from our own snapshot.
|
||||
if set(manifest['files']) != set(PATHS) or set(manifest['units']) != set(UNITS):
|
||||
raise ValueError('알 수 없는 백업 형식입니다.')
|
||||
for unit in UNITS[:6]: ctl('stop', unit)
|
||||
for name, existed in manifest['files'].items():
|
||||
target = Path(name)
|
||||
if existed:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.unlink(missing_ok=True)
|
||||
shutil.copy2(directory / 'files' / name.lstrip('/'), target, follow_symlinks=False)
|
||||
else:
|
||||
target.unlink(missing_ok=True)
|
||||
ctl('daemon-reload')
|
||||
for unit, state in manifest['units'].items():
|
||||
ctl('enable' if state['enabled'] else 'disable', unit)
|
||||
# Bluetooth and Avahi may have been started by apt during installation.
|
||||
for unit in reversed(UNITS):
|
||||
state = manifest['units'][unit]
|
||||
result = ctl('restart' if state['active'] else 'stop', unit)
|
||||
if state['active'] and result.returncode:
|
||||
print(result.stdout, file=sys.stderr)
|
||||
raise ValueError('서비스 복원에 실패했습니다: ' + unit)
|
||||
print('복구 완료:', directory)
|
||||
print('apt로 설치한 의존 패키지는 유지합니다. 블루투스 페어링 기록은 삭제하지 않습니다.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if os.geteuid() != 0: sys.exit('sudo로 실행해 주세요.')
|
||||
if len(sys.argv) != 3 or sys.argv[1] not in ('save', 'restore'):
|
||||
sys.exit('Usage: backup.py save|restore DIRECTORY')
|
||||
if sys.argv[1] == 'save': snapshot(Path(sys.argv[2]))
|
||||
else: restore(Path(sys.argv[2]))
|
||||
Reference in New Issue
Block a user