Compare commits
	
		
			2 Commits
		
	
	
		
			550445e0f9
			...
			f1dae0d3bf
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f1dae0d3bf | |||
| 631e228bd5 | 
							
								
								
									
										2
									
								
								firmware/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								firmware/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,2 +1,2 @@
 | 
				
			|||||||
/result
 | 
					/result
 | 
				
			||||||
/*.img
 | 
					/*.img*
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								firmware/app.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								firmware/app.nix
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					{ lib, pkgs, config, ... }:
 | 
				
			||||||
 | 
					let
 | 
				
			||||||
 | 
					  configurer = pkgs.substituteAll {
 | 
				
			||||||
 | 
					    src = ./configurer.py;
 | 
				
			||||||
 | 
					    isExecutable = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    python = pkgs.python3.withPackages (ps: with ps; [ pyyaml ]);
 | 
				
			||||||
 | 
					    utilLinux = pkgs.util-linux;
 | 
				
			||||||
 | 
					    wireguardTools = pkgs.wireguard-tools;
 | 
				
			||||||
 | 
					    systemd = config.systemd.package;
 | 
				
			||||||
 | 
					    inherit (pkgs) iwd;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					in
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  config = {
 | 
				
			||||||
 | 
					    systemd = {
 | 
				
			||||||
 | 
					      services = {
 | 
				
			||||||
 | 
					        qclk-configurer = {
 | 
				
			||||||
 | 
					          description = "qclk dynamic configurer";
 | 
				
			||||||
 | 
					          after = [ "network.target" ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          serviceConfig = {
 | 
				
			||||||
 | 
					            Type = "notify-reload";
 | 
				
			||||||
 | 
					            ExecStart = "${configurer} serve";
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          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"
 | 
				
			||||||
 | 
					      '';
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -11,7 +11,7 @@ in
 | 
				
			|||||||
      };
 | 
					      };
 | 
				
			||||||
      name = "qclk";
 | 
					      name = "qclk";
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    documentation.nixos.enable = false;
 | 
					    documentation.enable = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    time.timeZone = "Europe/Dublin";
 | 
					    time.timeZone = "Europe/Dublin";
 | 
				
			||||||
    i18n.defaultLocale = "en_IE.UTF-8";
 | 
					    i18n.defaultLocale = "en_IE.UTF-8";
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										229
									
								
								firmware/configurer.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										229
									
								
								firmware/configurer.py
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,229 @@
 | 
				
			|||||||
 | 
					#! @python@/bin/python -B
 | 
				
			||||||
 | 
					import argparse
 | 
				
			||||||
 | 
					import base64
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import shutil
 | 
				
			||||||
 | 
					import signal
 | 
				
			||||||
 | 
					import socket
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					import tomllib
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONF_FILE = '/etc/qclk/config.yaml'
 | 
				
			||||||
 | 
					WG_KEY_FILE = '/etc/qclk/wg.key'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					IWD_PATH = '/var/lib/iwd'
 | 
				
			||||||
 | 
					IWD_CONF = '''[Settings]
 | 
				
			||||||
 | 
					Hidden={hidden}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Security]
 | 
				
			||||||
 | 
					Passphrase={psk}
 | 
				
			||||||
 | 
					'''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MANAGEMENT_NET = '''[Match]
 | 
				
			||||||
 | 
					Name=management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Network]
 | 
				
			||||||
 | 
					Address={ip}/32
 | 
				
			||||||
 | 
					'''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def log(m):
 | 
				
			||||||
 | 
					  print(m, file=sys.stderr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def iwd_ssid_basename(ssid: str) -> str:
 | 
				
			||||||
 | 
					  if ssid.isalnum() and not any(c in ssid for c in '-_ '):
 | 
				
			||||||
 | 
					    return ssid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return f"={ssid.encode('utf-8').hex()}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Configurer:
 | 
				
			||||||
 | 
					  tmpdir = '/run/qclk'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def __init__(self, conf_file: str, wg_key_file: str):
 | 
				
			||||||
 | 
					    self.conf_file = conf_file
 | 
				
			||||||
 | 
					    self.load_config()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with open(wg_key_file, 'rb') as f:
 | 
				
			||||||
 | 
					      output = subprocess.check_output(['@wireguardTools@/bin/wg', 'pubkey'], input=f.read())
 | 
				
			||||||
 | 
					    pubkey = base64.b64decode(output)
 | 
				
			||||||
 | 
					    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.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):
 | 
				
			||||||
 | 
					    hostname = f'qclk-{self.id}'
 | 
				
			||||||
 | 
					    log(f"Setting hostname to '{hostname}'")
 | 
				
			||||||
 | 
					    socket.sethostname(hostname)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def _setup_wifi(self):
 | 
				
			||||||
 | 
					    os.makedirs(IWD_CONF, exist_ok=True)
 | 
				
			||||||
 | 
					    if 'wifi' in self.config:
 | 
				
			||||||
 | 
					      conf = self.config['wifi']
 | 
				
			||||||
 | 
					      tmp = os.path.join(self.tmpdir, 'wifi.psk')
 | 
				
			||||||
 | 
					      with open(tmp, 'w') as f:
 | 
				
			||||||
 | 
					        print(
 | 
				
			||||||
 | 
					          IWD_CONF.format(
 | 
				
			||||||
 | 
					            hidden=str(conf['hidden']).lower(),
 | 
				
			||||||
 | 
					            psk=conf['password']),
 | 
				
			||||||
 | 
					          file=f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fname = f"{iwd_ssid_basename(conf['ssid'])}.psk"
 | 
				
			||||||
 | 
					      log(f"Writing IWD config file '{fname}' for network '{conf['ssid']}'")
 | 
				
			||||||
 | 
					      shutil.move(tmp, os.path.join(IWD_PATH, fname))
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					      fname = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for f in os.listdir(IWD_PATH):
 | 
				
			||||||
 | 
					      if (fname is not None and f != fname) and f.endswith('.psk'):
 | 
				
			||||||
 | 
					        log(f"Cleaning up old IWD config '{f}'")
 | 
				
			||||||
 | 
					        os.remove(os.path.join(IWD_PATH, f))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    subprocess.call(['@iwd@/bin/iwctl', 'station', 'wifi', 'scan'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def _setup_mgmt(self):
 | 
				
			||||||
 | 
					    conf = self.config['management']
 | 
				
			||||||
 | 
					    tmp = os.path.join(self.tmpdir, 'management.network')
 | 
				
			||||||
 | 
					    with open(tmp, 'w') as f:
 | 
				
			||||||
 | 
					      print(MANAGEMENT_NET.format(ip=conf['ip']), file=f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log(f"Configuring management IP {conf['ip']}")
 | 
				
			||||||
 | 
					    os.makedirs('/run/systemd/network', exist_ok=True)
 | 
				
			||||||
 | 
					    shutil.move(tmp, '/run/systemd/network/20-management.network')
 | 
				
			||||||
 | 
					    subprocess.check_call(['@systemd@/bin/networkctl', 'reload'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def reconcile(self):
 | 
				
			||||||
 | 
					    if self.sd_sock is not None:
 | 
				
			||||||
 | 
					      self.sd_sock.sendall(b'STATUS=Reconciling configuration...')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    self._setup_hostname()
 | 
				
			||||||
 | 
					    self._setup_wifi()
 | 
				
			||||||
 | 
					    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():
 | 
				
			||||||
 | 
					  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)
 | 
				
			||||||
 | 
					  if not hasattr(args, 'func'):
 | 
				
			||||||
 | 
					    c.serve(args)
 | 
				
			||||||
 | 
					  else:
 | 
				
			||||||
 | 
					    args.func(c, args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					  main()
 | 
				
			||||||
@@ -26,6 +26,7 @@ let
 | 
				
			|||||||
      ./base.nix
 | 
					      ./base.nix
 | 
				
			||||||
      ./disk.nix
 | 
					      ./disk.nix
 | 
				
			||||||
      ./network.nix
 | 
					      ./network.nix
 | 
				
			||||||
 | 
					      ./app.nix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      target
 | 
					      target
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
@@ -36,49 +37,107 @@ in
 | 
				
			|||||||
    qclk-rpi3 = mkSystem target/rpi3.nix;
 | 
					    qclk-rpi3 = mkSystem target/rpi3.nix;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  perSystem = { libMy, pkgs, ... }: {
 | 
					  perSystem = { lib, libMy, pkgs, ... }:
 | 
				
			||||||
 | 
					  let
 | 
				
			||||||
 | 
					    inherit (lib) concatMapStringsSep;
 | 
				
			||||||
 | 
					  in
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
    devenv.shells.firmware = libMy.withRootdir {
 | 
					    devenv.shells.firmware = libMy.withRootdir {
 | 
				
			||||||
      packages = with pkgs; [
 | 
					      packages = with pkgs; [
 | 
				
			||||||
        nixos-rebuild
 | 
					        nixos-rebuild
 | 
				
			||||||
        nixVersions.latest
 | 
					        nixVersions.latest
 | 
				
			||||||
 | 
					        wireguard-tools
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      scripts = {
 | 
					      scripts =
 | 
				
			||||||
 | 
					      let
 | 
				
			||||||
 | 
					        shellUtils = ''
 | 
				
			||||||
 | 
					          die() {
 | 
				
			||||||
 | 
					            echo "$1" >&2
 | 
				
			||||||
 | 
					            exit 1
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        '';
 | 
				
			||||||
 | 
					        exportPath = ps:
 | 
				
			||||||
 | 
					          let pkgsPath = concatMapStringsSep ":" (p: "${p}/bin") ps; in ''export PATH="$PATH:${pkgsPath}"'';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        buildImgRootHelper = pkgs.writeShellScript "fixup-perms" ''
 | 
				
			||||||
 | 
					          pushd "$1"
 | 
				
			||||||
 | 
					          # systemd-network GID
 | 
				
			||||||
 | 
					          chgrp 152 etc/qclk/wg.key
 | 
				
			||||||
 | 
					          chmod 640 etc/qclk/wg.key
 | 
				
			||||||
 | 
					          popd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          mkfs.ext4 -L qclkos-persist -d "$1" "$2"
 | 
				
			||||||
 | 
					        '';
 | 
				
			||||||
 | 
					      in
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
        build.exec = ''
 | 
					        build.exec = ''
 | 
				
			||||||
          nix build "..#nixosConfigurations.qclk-$1.config.system.build.toplevel"
 | 
					          nix build "..#nixosConfigurations.qclk-$1.config.system.build.toplevel"
 | 
				
			||||||
        '';
 | 
					        '';
 | 
				
			||||||
        build-image.exec = ''
 | 
					        build-image.exec = ''
 | 
				
			||||||
          set -e
 | 
					          set -e
 | 
				
			||||||
          export PATH="$PATH:${pkgs.util-linux}/bin:${pkgs.fakeroot}/bin:${pkgs.e2fsprogs}/bin"
 | 
					          ${exportPath (with pkgs; [ util-linux fakeroot e2fsprogs zstd python3 ])}
 | 
				
			||||||
          die() {
 | 
					          ${shellUtils}
 | 
				
			||||||
            echo "$1" >&2
 | 
					          usage() {
 | 
				
			||||||
            exit 1
 | 
					            die "usage: $0 <target> <IP>"
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          [ -z "$1" ] && die "Need to set target"
 | 
					          populate_persist() {
 | 
				
			||||||
 | 
					            mkdir -p etc/qclk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            old_umask="$(umask)"
 | 
				
			||||||
 | 
					            umask 077
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            wg genkey > etc/qclk/wg.key
 | 
				
			||||||
 | 
					            wg_pubkey="$(wg pubkey < etc/qclk/wg.key)"
 | 
				
			||||||
 | 
					            id="$(python3 -c "import base64; print(base64.b64decode('$wg_pubkey')[:4].hex())")"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            pin="$(python3 -c "import random; print('''.join(str(random.randint(0, 9)) for _ in range(6)))")"
 | 
				
			||||||
 | 
					            cat << EOF > etc/qclk/config.yaml
 | 
				
			||||||
 | 
					          management:
 | 
				
			||||||
 | 
					            ip: $ip
 | 
				
			||||||
 | 
					            pin: '$pin'
 | 
				
			||||||
 | 
					          EOF
 | 
				
			||||||
 | 
					            umask "$old_umask"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          [ $# -eq 2 ] || usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          target=$1
 | 
					          target=$1
 | 
				
			||||||
          out=qclkos-$target.img
 | 
					          ip=$2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          nix build "..#nixosConfigurations.qclk-$target.config.my.disk.image"
 | 
					          nix build "..#nixosConfigurations.qclk-$target.config.my.disk.image"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          persistRoot=$(mktemp --tmpdir -d qclkos-persist-XXXXX)
 | 
					          persistRoot=$(mktemp --tmpdir -d qclkos-persist-XXXXX)
 | 
				
			||||||
          # TODO: bless with unique stuff (e.g. keys)
 | 
					          pushd "$persistRoot"
 | 
				
			||||||
          touch "$persistRoot"/test.txt
 | 
					          populate_persist
 | 
				
			||||||
 | 
					          popd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          cp --sparse=always result/$out $out
 | 
					          out=qclkos-$target-$id.img
 | 
				
			||||||
 | 
					          cp --sparse=always result/qclkos-$target.img $out
 | 
				
			||||||
          chmod u+w $out
 | 
					          chmod u+w $out
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          eval $(partx $out -o START,SECTORS --nr 2 --pairs)
 | 
					          eval $(partx $out -o START,SECTORS --nr 2 --pairs)
 | 
				
			||||||
          persistImg=$(mktemp --tmpdir qclkos-persist-XXXXX.img)
 | 
					          persistImg=$(mktemp --tmpdir qclkos-persist-XXXXX.img)
 | 
				
			||||||
          truncate -s $((SECTORS * 512)) $persistImg
 | 
					          truncate -s $((SECTORS * 512)) $persistImg
 | 
				
			||||||
          fakeroot mkfs.ext4 -L qclkos-persist -d $persistRoot $persistImg
 | 
					          fakeroot ${buildImgRootHelper} $persistRoot $persistImg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          dd conv=notrunc if=$persistImg of=$out seek=$START count=$SECTORS
 | 
					          dd conv=notrunc if=$persistImg of=$out seek=$START count=$SECTORS
 | 
				
			||||||
          rm -r "$persistRoot" "$persistImg"
 | 
					          rm -r "$persistRoot" "$persistImg"
 | 
				
			||||||
 | 
					          [ -z "$NO_COMPRESS" ] && zstd -7 -T0 --rm -f $out
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          echo "====== DONE! ======"
 | 
				
			||||||
 | 
					          echo "WireGuard pubkey: $wg_pubkey"
 | 
				
			||||||
 | 
					          echo "Control PIN: $pin"
 | 
				
			||||||
        '';
 | 
					        '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        push-config.exec = ''
 | 
					        push-config.exec = ''
 | 
				
			||||||
 | 
					          ${shellUtils}
 | 
				
			||||||
 | 
					          usage() {
 | 
				
			||||||
 | 
					            die "usage: $0 <host> <target> <verb>"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          [ $# -eq 3 ] || usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          host=$1; shift
 | 
					          host=$1; shift
 | 
				
			||||||
          target=$1; shift
 | 
					          target=$1; shift
 | 
				
			||||||
          verb=$1; shift
 | 
					          verb=$1; shift
 | 
				
			||||||
@@ -86,6 +145,9 @@ in
 | 
				
			|||||||
          export NIX_SSHOPTS="-i .keys/management.key"
 | 
					          export NIX_SSHOPTS="-i .keys/management.key"
 | 
				
			||||||
          nixos-rebuild $verb --flake ..#qclk-$target --target-host root@"$host" --use-substitutes "$@"
 | 
					          nixos-rebuild $verb --flake ..#qclk-$target --target-host root@"$host" --use-substitutes "$@"
 | 
				
			||||||
        '';
 | 
					        '';
 | 
				
			||||||
 | 
					        clean.exec = ''
 | 
				
			||||||
 | 
					          rm -f qclkos-*.img*
 | 
				
			||||||
 | 
					        '';
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,15 +12,16 @@ let
 | 
				
			|||||||
    compressImage = false;
 | 
					    compressImage = false;
 | 
				
			||||||
  }).overrideAttrs (o: {
 | 
					  }).overrideAttrs (o: {
 | 
				
			||||||
    buildCommand = ''
 | 
					    buildCommand = ''
 | 
				
			||||||
      # HACK: `populateImageCommands` is executed in a subshell _before_ the paths are copied in...
 | 
					      # We need to move the store up a level since we're not using this as a rootfs but as /nix
 | 
				
			||||||
 | 
					      # HACK: `populateImageCommands` is executed in a subshell _before_ the store paths are copied in...
 | 
				
			||||||
      shopt -s expand_aliases
 | 
					      shopt -s expand_aliases
 | 
				
			||||||
      nixUpThenMkfs() {
 | 
					      nixUpThenFaketime() {
 | 
				
			||||||
        mv rootImage/{nix/store,}
 | 
					        mv rootImage/{nix/store,}
 | 
				
			||||||
        rmdir rootImage/nix
 | 
					        rmdir rootImage/nix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        faketime "$@"
 | 
					        faketime "$@"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      alias faketime=nixUpThenMkfs
 | 
					      alias faketime=nixUpThenFaketime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      ${o.buildCommand}
 | 
					      ${o.buildCommand}
 | 
				
			||||||
    '';
 | 
					    '';
 | 
				
			||||||
@@ -123,6 +124,8 @@ in
 | 
				
			|||||||
        directories = [
 | 
					        directories = [
 | 
				
			||||||
          "/var/lib/nixos"
 | 
					          "/var/lib/nixos"
 | 
				
			||||||
          "/var/lib/systemd"
 | 
					          "/var/lib/systemd"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          "/etc/qclk"
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
        files = [
 | 
					        files = [
 | 
				
			||||||
          "/etc/machine-id"
 | 
					          "/etc/machine-id"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,27 +1,52 @@
 | 
				
			|||||||
{ lib, config, ... }: {
 | 
					{ lib, config, pkgs, ... }: {
 | 
				
			||||||
 | 
					  environment = {
 | 
				
			||||||
 | 
					    systemPackages = with pkgs; [
 | 
				
			||||||
 | 
					      wireguard-tools
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  networking = {
 | 
					  networking = {
 | 
				
			||||||
    hostName = config.system.name;
 | 
					    hostName = config.system.name;
 | 
				
			||||||
    useDHCP = false;
 | 
					    useDHCP = false;
 | 
				
			||||||
    useNetworkd = true;
 | 
					    useNetworkd = true;
 | 
				
			||||||
    wireless.iwd = {
 | 
					    wireless.iwd = {
 | 
				
			||||||
      enable = true;
 | 
					      enable = true;
 | 
				
			||||||
      settings.DriverQuirks.DefaultInterface = "*";
 | 
					      settings = {
 | 
				
			||||||
 | 
					        # systemd-networkd gets confused if we hop between networks and doesn't redo DHCP
 | 
				
			||||||
 | 
					        General.EnableNetworkConfiguration = true;
 | 
				
			||||||
 | 
					        DriverQuirks.DefaultInterface = "*";
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  systemd = {
 | 
					  systemd = {
 | 
				
			||||||
    network = {
 | 
					    network = {
 | 
				
			||||||
      wait-online.enable = false;
 | 
					      wait-online.enable = false;
 | 
				
			||||||
 | 
					      netdevs = {
 | 
				
			||||||
 | 
					        "10-management" = {
 | 
				
			||||||
 | 
					          netdevConfig = {
 | 
				
			||||||
 | 
					            Name = "management";
 | 
				
			||||||
 | 
					            Kind = "wireguard";
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					          wireguardConfig = {
 | 
				
			||||||
 | 
					            PrivateKeyFile = "/etc/qclk/wg.key";
 | 
				
			||||||
 | 
					            RouteTable = "main";
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					          wireguardPeers = [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              Endpoint = "94.142.240.44:51821";
 | 
				
			||||||
 | 
					              PublicKey = "itMQ2DlPEMdJFlIZRQkwa+Mv7cLc9d4zgfzlljEtLn4=";
 | 
				
			||||||
 | 
					              AllowedIPs = [ "10.100.4.1/32" ];
 | 
				
			||||||
 | 
					              PersistentKeepalive = 15;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          ];
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
      networks = {
 | 
					      networks = {
 | 
				
			||||||
        "10-ethernet" = {
 | 
					        "10-ethernet" = {
 | 
				
			||||||
          matchConfig.Name = "ethernet";
 | 
					          matchConfig.Name = "ethernet";
 | 
				
			||||||
          DHCP = "yes";
 | 
					          DHCP = "yes";
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        "10-wifi" = {
 | 
					 | 
				
			||||||
          matchConfig.Name = "wifi";
 | 
					 | 
				
			||||||
          DHCP = "yes";
 | 
					 | 
				
			||||||
          networkConfig.IgnoreCarrierLoss = "3s";
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user