firmware: Add ability to load config from USB
This commit is contained in:
parent
631e228bd5
commit
8496ec73ad
@ -5,8 +5,10 @@ let
|
|||||||
isExecutable = true;
|
isExecutable = true;
|
||||||
|
|
||||||
python = pkgs.python3.withPackages (ps: with ps; [ pyyaml ]);
|
python = pkgs.python3.withPackages (ps: with ps; [ pyyaml ]);
|
||||||
|
utilLinux = pkgs.util-linux;
|
||||||
wireguardTools = pkgs.wireguard-tools;
|
wireguardTools = pkgs.wireguard-tools;
|
||||||
systemd = config.systemd.package;
|
systemd = config.systemd.package;
|
||||||
|
iwd = config.networking.wireless.iwd.package;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@ -18,14 +20,19 @@ in
|
|||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "notify-reload";
|
||||||
RemainAfterExit = true;
|
ExecStart = "${configurer} serve";
|
||||||
ExecStart = "${configurer}";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
udev.extraRules = ''
|
||||||
|
SUBSYSTEM=="block", ACTION=="add", ENV{ID_BUS}=="usb", ENV{ID_FS_TYPE}=="vfat", RUN+="${configurer} ext-load %E{ID_PATH_TAG} %N"
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
#! @python@/bin/python -B
|
#! @python@/bin/python -B
|
||||||
|
import argparse
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import signal
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
import tomllib
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@ -37,18 +41,34 @@ def iwd_ssid_basename(ssid: str) -> str:
|
|||||||
return f"={ssid.encode('utf-8').hex()}"
|
return f"={ssid.encode('utf-8').hex()}"
|
||||||
|
|
||||||
class Configurer:
|
class Configurer:
|
||||||
tmpdir = '/tmp/qclk'
|
tmpdir = '/run/qclk'
|
||||||
|
|
||||||
def __init__(self, conf_file: str, wg_key_file: str):
|
def __init__(self, conf_file: str, wg_key_file: str):
|
||||||
with open(conf_file) as f:
|
self.conf_file = conf_file
|
||||||
self.config = yaml.safe_load(f)
|
self.load_config()
|
||||||
|
|
||||||
with open(wg_key_file, 'rb') as f:
|
with open(wg_key_file, 'rb') as f:
|
||||||
output = subprocess.check_output(['@wireguardTools@/bin/wg', 'pubkey'], input=f.read())
|
output = subprocess.check_output(['@wireguardTools@/bin/wg', 'pubkey'], input=f.read())
|
||||||
pubkey = base64.b64decode(output)
|
pubkey = base64.b64decode(output)
|
||||||
self.id = pubkey[:4].hex()
|
self.id = pubkey[:4].hex()
|
||||||
|
|
||||||
|
self.load_dir = os.path.join(self.tmpdir, 'load')
|
||||||
|
self.sd_sock = None
|
||||||
|
|
||||||
os.makedirs(self.tmpdir, exist_ok=True)
|
os.makedirs(self.tmpdir, exist_ok=True)
|
||||||
|
os.makedirs(self.load_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
with open(self.conf_file) as f:
|
||||||
|
self.config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
def write_config(self):
|
||||||
|
log(f'Updaing config')
|
||||||
|
tmp = os.path.join(self.tmpdir, 'new-config.yaml')
|
||||||
|
with open(tmp, 'w') as f:
|
||||||
|
yaml.dump(self.config, f)
|
||||||
|
|
||||||
|
shutil.move(tmp, self.conf_file)
|
||||||
|
|
||||||
def _setup_hostname(self):
|
def _setup_hostname(self):
|
||||||
hostname = f'qclk-{self.id}'
|
hostname = f'qclk-{self.id}'
|
||||||
@ -78,6 +98,8 @@ class Configurer:
|
|||||||
log(f"Cleaning up old IWD config '{f}'")
|
log(f"Cleaning up old IWD config '{f}'")
|
||||||
os.remove(os.path.join(IWD_PATH, f))
|
os.remove(os.path.join(IWD_PATH, f))
|
||||||
|
|
||||||
|
subprocess.call(['@iwd@/bin/iwctl', 'station', 'wifi', 'scan'])
|
||||||
|
|
||||||
def _setup_mgmt(self):
|
def _setup_mgmt(self):
|
||||||
conf = self.config['management']
|
conf = self.config['management']
|
||||||
tmp = os.path.join(self.tmpdir, 'management.network')
|
tmp = os.path.join(self.tmpdir, 'management.network')
|
||||||
@ -90,13 +112,118 @@ class Configurer:
|
|||||||
subprocess.check_call(['@systemd@/bin/networkctl', 'reload'])
|
subprocess.check_call(['@systemd@/bin/networkctl', 'reload'])
|
||||||
|
|
||||||
def reconcile(self):
|
def reconcile(self):
|
||||||
|
if self.sd_sock is not None:
|
||||||
|
self.sd_sock.sendall(b'STATUS=Reconciling configuration...')
|
||||||
|
|
||||||
self._setup_hostname()
|
self._setup_hostname()
|
||||||
self._setup_wifi()
|
self._setup_wifi()
|
||||||
self._setup_mgmt()
|
self._setup_mgmt()
|
||||||
|
|
||||||
|
if self.sd_sock is not None:
|
||||||
|
self.sd_sock.sendall(b'STATUS=OK\nREADY=1')
|
||||||
|
|
||||||
|
def _ext_load(self, identifier: str, device: str):
|
||||||
|
mountpoint = os.path.join(self.tmpdir, 'mnt', identifier)
|
||||||
|
os.makedirs(mountpoint, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
log(f'Mounting {device} -> {mountpoint}')
|
||||||
|
subprocess.check_call(['@utilLinux@/bin/mount', '-t', 'vfat', '-o', 'ro', device, mountpoint])
|
||||||
|
try:
|
||||||
|
path = os.path.join(mountpoint, 'qclk.toml')
|
||||||
|
if not os.path.exists(path):
|
||||||
|
log(f'No config file found on {device}')
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
ext_conf = tomllib.load(f)
|
||||||
|
finally:
|
||||||
|
subprocess.check_call(['@utilLinux@/bin/umount', mountpoint])
|
||||||
|
finally:
|
||||||
|
os.rmdir(mountpoint)
|
||||||
|
|
||||||
|
if 'wifi' in ext_conf:
|
||||||
|
log(f'Loading WiFi settings from {device}')
|
||||||
|
self.config['wifi'] = {
|
||||||
|
'ssid': ext_conf['wifi']['ssid'],
|
||||||
|
'password': ext_conf['wifi'].get('password', ''),
|
||||||
|
'hidden': ext_conf['wifi'].get('hidden', False),
|
||||||
|
}
|
||||||
|
self._setup_wifi()
|
||||||
|
self.write_config()
|
||||||
|
|
||||||
|
def serve(self, args: argparse.Namespace):
|
||||||
|
os.makedirs(os.path.join(self.tmpdir, 'load'), exist_ok=True)
|
||||||
|
|
||||||
|
addr = os.getenv('NOTIFY_SOCKET')
|
||||||
|
if addr is not None:
|
||||||
|
self.sd_sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
|
if addr[0] == '@':
|
||||||
|
addr = '\0' + addr[1:]
|
||||||
|
self.sd_sock.connect(addr)
|
||||||
|
|
||||||
|
running = True
|
||||||
|
def sighandler(sig, frame):
|
||||||
|
nonlocal running
|
||||||
|
if sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
running = False
|
||||||
|
return
|
||||||
|
|
||||||
|
if sig == signal.SIGHUP:
|
||||||
|
if self.sd_sock is not None:
|
||||||
|
t = time.clock_gettime_ns(time.CLOCK_MONOTONIC) // 1000
|
||||||
|
self.sd_sock.sendall(f'RELOADING=1\nMONOTONIC_USEC={t}'.encode('ascii'))
|
||||||
|
self.load_config()
|
||||||
|
self.reconcile()
|
||||||
|
elif sig == signal.SIGUSR1:
|
||||||
|
for identifier in os.listdir(self.load_dir):
|
||||||
|
fname = os.path.join(self.load_dir, identifier)
|
||||||
|
with open(fname) as f:
|
||||||
|
device = f.read().strip()
|
||||||
|
os.remove(fname)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._ext_load(identifier, device)
|
||||||
|
except Exception as ex:
|
||||||
|
log(f'Failed to load config from {device}: {ex}')
|
||||||
|
|
||||||
|
for s in [signal.SIGINT, signal.SIGTERM, signal.SIGHUP, signal.SIGUSR1]:
|
||||||
|
signal.signal(s, sighandler)
|
||||||
|
|
||||||
|
self.reconcile()
|
||||||
|
while running:
|
||||||
|
signal.pause()
|
||||||
|
|
||||||
|
if self.sd_sock is not None:
|
||||||
|
self.sd_sock.close()
|
||||||
|
|
||||||
|
def ext_load(self, args: argparse.Namespace):
|
||||||
|
with open(os.path.join(self.load_dir, args.identifier), 'w') as f:
|
||||||
|
print(args.device, file=f)
|
||||||
|
output = subprocess.check_output(
|
||||||
|
['@systemd@/bin/systemctl', 'show', '--property', 'MainPID', '--value', 'qclk-configurer.service'], encoding='ascii')
|
||||||
|
pid = int(output.strip())
|
||||||
|
|
||||||
|
os.kill(pid, signal.SIGUSR1)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description='qclkOS configurer')
|
||||||
|
cmds = parser.add_subparsers(title='commands', help='reconcile')
|
||||||
|
|
||||||
|
rec_parser = cmds.add_parser('serve')
|
||||||
|
rec_parser.set_defaults(func=Configurer.serve)
|
||||||
|
|
||||||
|
eload_parser = cmds.add_parser('ext-load')
|
||||||
|
eload_parser.set_defaults(func=Configurer.ext_load)
|
||||||
|
eload_parser.add_argument('identifier', help='Unique device name')
|
||||||
|
eload_parser.add_argument('device', help='Block device path')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
c = Configurer(CONF_FILE, WG_KEY_FILE)
|
c = Configurer(CONF_FILE, WG_KEY_FILE)
|
||||||
c.reconcile()
|
if not hasattr(args, 'func'):
|
||||||
|
c.serve(args)
|
||||||
|
else:
|
||||||
|
args.func(c, args)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
Loading…
Reference in New Issue
Block a user