diff --git a/pkgs/clan-cli/clan_cli/flash.py b/pkgs/clan-cli/clan_cli/flash.py index 3c9df9616..f50c1a37b 100644 --- a/pkgs/clan-cli/clan_cli/flash.py +++ b/pkgs/clan-cli/clan_cli/flash.py @@ -24,11 +24,18 @@ from .nix import nix_build, nix_shell log = logging.getLogger(__name__) +@dataclass +class WifiConfig: + ssid: str + password: str + + @dataclass class SystemConfig: language: str | None = field(default=None) keymap: str | None = field(default=None) ssh_keys_path: list[str] | None = field(default=None) + wifi_settings: list[WifiConfig] | None = field(default=None) @API.register @@ -91,6 +98,12 @@ def flash_machine( ) -> None: system_config_nix: dict[str, Any] = {} + if system_config.wifi_settings: + wifi_settings = {} + for wifi in system_config.wifi_settings: + wifi_settings[wifi.ssid] = {"password": wifi.password} + system_config_nix["clan"] = {"iwd": {"networks": wifi_settings}} + if system_config.language: if system_config.language not in list_possible_languages(): raise ClanError( @@ -214,6 +227,7 @@ def flash_command(args: argparse.Namespace) -> None: language=args.language, keymap=args.keymap, ssh_keys_path=args.ssh_pubkey, + wifi_settings=None, ), write_efi_boot_entries=args.write_efi_boot_entries, nix_options=args.option, @@ -229,6 +243,12 @@ def flash_command(args: argparse.Namespace) -> None: print(keymap) return + if args.wifi: + opts.system_config.wifi_settings = [ + WifiConfig(ssid=ssid, password=password) + for ssid, password in args.wifi.items() + ] + machine = Machine(opts.machine, flake=opts.flake) if opts.confirm and not opts.dry_run: disk_str = ", ".join(f"{name}={device}" for name, device in opts.disks.items()) @@ -277,6 +297,15 @@ def register_parser(parser: argparse.ArgumentParser) -> None: Mount is useful for updating an existing system without losing data. """ ) + parser.add_argument( + "--wifi", + type=str, + nargs=2, + metavar=("ssid", "password"), + action=AppendDiskAction, + help="wifi network to connect to", + default={}, + ) parser.add_argument( "--mode", type=str, diff --git a/pkgs/installer/base64.nix b/pkgs/installer/base64.nix new file mode 100644 index 000000000..588d1dfd1 --- /dev/null +++ b/pkgs/installer/base64.nix @@ -0,0 +1,60 @@ +{ lib, ... }: +{ + toBase64 = + text: + let + inherit (lib) + sublist + mod + stringToCharacters + concatMapStrings + ; + inherit (lib.strings) charToInt; + inherit (builtins) + substring + foldl' + genList + elemAt + length + concatStringsSep + stringLength + ; + lookup = stringToCharacters "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + sliceN = + size: list: n: + sublist (n * size) size list; + pows = [ + (64 * 64 * 64) + (64 * 64) + 64 + 1 + ]; + intSextets = i: map (j: mod (i / j) 64) pows; + compose = + f: g: x: + f (g x); + intToChar = elemAt lookup; + convertTripletInt = sliceInt: concatMapStrings intToChar (intSextets sliceInt); + sliceToInt = foldl' (acc: val: acc * 256 + val) 0; + convertTriplet = compose convertTripletInt sliceToInt; + join = concatStringsSep ""; + convertLastSlice = + slice: + let + len = length slice; + in + if len == 1 then + (substring 0 2 (convertTripletInt ((sliceToInt slice) * 256 * 256))) + "==" + else if len == 2 then + (substring 0 3 (convertTripletInt ((sliceToInt slice) * 256))) + "=" + else + ""; + len = stringLength text; + nFullSlices = len / 3; + bytes = map charToInt (stringToCharacters text); + tripletAt = sliceN 3 bytes; + head = genList (compose convertTriplet tripletAt) nFullSlices; + tail = convertLastSlice (tripletAt nFullSlices); + in + join (head ++ [ tail ]); +} diff --git a/pkgs/installer/flake-module.nix b/pkgs/installer/flake-module.nix index def3b2b98..29c25e370 100644 --- a/pkgs/installer/flake-module.nix +++ b/pkgs/installer/flake-module.nix @@ -6,7 +6,7 @@ let { config, ... }: { imports = [ - self.clanModules.iwd + ./iwd.nix self.nixosModules.installer ]; diff --git a/pkgs/installer/iwd.nix b/pkgs/installer/iwd.nix new file mode 100644 index 000000000..8717a1c38 --- /dev/null +++ b/pkgs/installer/iwd.nix @@ -0,0 +1,67 @@ +{ + lib, + pkgs, + config, + ... +}: + +let + cfg = config.clan.iwd; + toBase64 = (pkgs.callPackage ./base64.nix { inherit lib; }).toBase64; + wifi_config = password: '' + [Security] + Passphrase=${password} + ''; +in +{ + options.clan.iwd = { + networks = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + ssid = lib.mkOption { + type = lib.types.strMatching "^[a-zA-Z0-9._-]+$"; + default = name; + description = "The name of the wifi network"; + }; + password = lib.mkOption { + type = lib.types.str; + description = "The password of the wifi network"; + }; + }; + } + ) + ); + default = { }; + description = "Wifi networks to predefine"; + }; + }; + config = lib.mkMerge [ + (lib.mkIf (cfg.networks != { }) { + # Systemd tmpfiles rule to create /var/lib/iwd/example.psk file + systemd.tmpfiles.rules = lib.mapAttrsToList ( + _: value: + "f+~ /var/lib/iwd/${value.ssid}.psk 0600 root root - ${toBase64 (wifi_config value.password)}" + ) cfg.networks; + + }) + { + # disable wpa supplicant + networking.wireless.enable = false; + + # Use iwd instead of wpa_supplicant. It has a user friendly CLI + networking.wireless.iwd = { + enable = true; + settings = { + Network = { + EnableIPv6 = true; + RoutePriorityOffset = 300; + }; + Settings.AutoConnect = true; + }; + }; + } + ]; +} diff --git a/pkgs/webview-ui/app/src/components/TextInput.tsx b/pkgs/webview-ui/app/src/components/TextInput.tsx index c1146ec8f..21cee203a 100644 --- a/pkgs/webview-ui/app/src/components/TextInput.tsx +++ b/pkgs/webview-ui/app/src/components/TextInput.tsx @@ -4,10 +4,11 @@ import { type JSX } from "solid-js"; interface TextInputProps { formStore: FormStore; value: string; - inputProps: JSX.HTMLAttributes; + inputProps: JSX.InputHTMLAttributes; label: JSX.Element; error?: string; required?: boolean; + type?: string; inlineLabel?: JSX.Element; } @@ -36,7 +37,7 @@ export function TextInput( { }, }); + /* ==== WIFI NETWORK ==== */ + const [wifiNetworks, setWifiNetworks] = createSignal([]); + const [passwordVisibility, setPasswordVisibility] = createSignal( + [], + ); + + createEffect(() => { + const formWifi = getValue(formStore, "wifi"); + if (formWifi !== undefined) { + setWifiNetworks(formWifi as Wifi[]); + setPasswordVisibility(new Array(formWifi.length).fill(false)); + } + }); + + const addWifiNetwork = () => { + const updatedNetworks = [...wifiNetworks(), { ssid: "", password: "" }]; + setWifiNetworks(updatedNetworks); + setPasswordVisibility([...passwordVisibility(), false]); + setValue(formStore, "wifi", updatedNetworks); + }; + + const removeWifiNetwork = (index: number) => { + const updatedNetworks = wifiNetworks().filter((_, i) => i !== index); + setWifiNetworks(updatedNetworks); + const updatedVisibility = passwordVisibility().filter( + (_, i) => i !== index, + ); + setPasswordVisibility(updatedVisibility); + setValue(formStore, "wifi", updatedNetworks); + }; + + const togglePasswordVisibility = (index: number) => { + const updatedVisibility = [...passwordVisibility()]; + updatedVisibility[index] = !updatedVisibility[index]; + setPasswordVisibility(updatedVisibility); + }; + /* ==== END OF WIFI NETWORK ==== */ + const deviceQuery = createQuery(() => ({ queryKey: ["block_devices"], queryFn: async () => { @@ -86,6 +136,7 @@ export const Flash = () => { }; const handleSubmit = async (values: FlashFormValues) => { + console.log("Submit WiFi Networks:", values.wifi); console.log( "Submit SSH Keys:", values.sshKeys.map((file) => file.name), @@ -104,6 +155,10 @@ export const Flash = () => { language: values.language, keymap: values.keymap, ssh_keys_path: values.sshKeys.map((file) => file.name), + wifi_settings: values.wifi.map((network) => ({ + ssid: network.ssid, + password: network.password, + })), }, dry_run: false, write_efi_boot_entries: false, @@ -250,6 +305,78 @@ export const Flash = () => { )} + + {/* WiFi Networks */} +
+

WiFi Networks

+ + {(network, index) => ( +
+ + {(field, props) => ( + + )} + + + {(field, props) => ( +
+ + +
+ )} +
+ +
+ )} +
+ +
+