{ pkgs , fullName ? "John Doe" , organization ? "KVM Authority" , administratorPassword ? "123456" , uiLanguage ? "en-US" , inputLocale ? "en-US" , userLocale ? "en-US" , systemLocale ? "en-US" , users ? {} , productKey ? null , defaultUser ? "wfvm" , setupCommands ? [] , timeZone ? "UTC" , services ? {} , impureShellCommands ? [] , efi ? true , driveLetter ? if efi then "E:" else "D:" , ... }: let lib = pkgs.lib; serviceCommands = lib.mapAttrsToList ( serviceName: attrs: "powershell Set-Service -Name ${serviceName} " + ( lib.concatStringsSep " " ( ( lib.mapAttrsToList ( n: v: if builtins.typeOf v != "bool" then "-${n} ${v}" else "-${n}" ) ) ( # Always run without interaction { Force = true; } // attrs ) ) ) ) services; sshSetupCommands = # let # makeDirs = lib.mapAttrsToList (n: v: ''mkdir C:\Users\${n}\.ssh'') users; # writeKeys = lib.flatten (lib.mapAttrsToList (n: v: builtins.map (key: let # commands = [ # ''powershell.exe Set-Content -Path C:\Users\${n}\.ssh\authorized_keys -Value '${key}' '' # ]; # in lib.concatStringsSep "\n" commands) (v.sshKeys or [])) users); # mkDirsDesc = builtins.map (c: {Path = c; Description = "Make SSH key dir";}) makeDirs; # writeKeysDesc = builtins.map (c: {Path = c; Description = "Add SSH key";}) writeKeys; # in # mkDirsDesc ++ writeKeysDesc ++ [ { Path = ''powershell.exe Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 -Source ${driveLetter}\fod -LimitAccess''; Description = "Add OpenSSH service."; } { Path = ''powershell.exe Set-Service -Name sshd -StartupType Automatic''; Description = "Enable SSH by default."; } ]; assertCommand = c: builtins.typeOf c == "string" || builtins.typeOf c == "set" && builtins.hasAttr "Path" c && builtins.hasAttr "Description" c; commands = builtins.map (x: assert assertCommand x; if builtins.typeOf x == "string" then { Path = x; Description = x; } else x) ( [ { Path = "powershell.exe Set-ExecutionPolicy -Force Unrestricted"; Description = "Allow unsigned powershell scripts."; } ] ++ [ { Path = ''powershell.exe ${driveLetter}\win-bundle-installer.exe''; Description = "Install any declared packages."; } ] ++ setupCommands ++ [ { Path = ''powershell.exe ${driveLetter}\ssh-setup.ps1''; Description = "Setup SSH and keys"; } ] ++ serviceCommands ++ impureShellCommands ); mkCommand = attrs: '' ${lib.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (n: v: "<${n}>${v}") attrs)} ''; mkCommands = commands: ( builtins.foldl' ( acc: v: rec { i = acc.i + 1; values = acc.values ++ [ (mkCommand (v // { Order = builtins.toString i; })) ]; } ) { i = 0; values = []; } commands ).values; mkUser = { name , password , description ? "" , displayName ? "" , groups ? [] # , sshKeys ? [] # Handled in scripts }: '' ${password} true</PlainText> </Password> <Description>${description}</Description> <DisplayName>${displayName}</DisplayName> <Group>${builtins.concatStringsSep ";" (lib.unique ([ "Users" ] ++ groups))}</Group> <Name>${name}</Name> </LocalAccount> ''; # Windows expects a flat list of users while we want to manage them as a set flatUsers = builtins.attrValues (builtins.mapAttrs (name: s: s // { inherit name; }) users); autounattendXML = pkgs.writeText "autounattend.xml" '' <?xml version="1.0" encoding="utf-8"?> <unattend xmlns="urn:schemas-microsoft-com:unattend"> <settings pass="windowsPE"> <component name="Microsoft-Windows-PnpCustomizationsWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <DriverPaths> <PathAndCredentials wcm:action="add" wcm:keyValue="1"> <Path>D:\</Path> </PathAndCredentials> <PathAndCredentials wcm:action="add" wcm:keyValue="2"> <Path>E:\</Path> </PathAndCredentials> </DriverPaths> </component> <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <DiskConfiguration> <Disk wcm:action="add"> <CreatePartitions> <CreatePartition wcm:action="add"> <Order>1</Order> <Type>${if efi then "EFI" else "Primary"}</Type> <Size>300</Size> </CreatePartition> <CreatePartition wcm:action="add"> <Order>2</Order> <Type>${if efi then "MSR" else "Primary"}</Type> <Size>16</Size> </CreatePartition> <CreatePartition wcm:action="add"> <Order>3</Order> <Type>Primary</Type> <Extend>true</Extend> </CreatePartition> </CreatePartitions> <ModifyPartitions> <ModifyPartition wcm:action="add"> <Order>1</Order> <Format>${if efi then "FAT32" else "NTFS"}</Format> <Label>System</Label> <PartitionID>1</PartitionID> </ModifyPartition> <ModifyPartition wcm:action="add"> <Order>2</Order> <PartitionID>2</PartitionID> </ModifyPartition> <ModifyPartition wcm:action="add"> <Order>3</Order> <Format>NTFS</Format> <Label>Windows</Label> <Letter>C</Letter> <PartitionID>3</PartitionID> </ModifyPartition> </ModifyPartitions> <DiskID>0</DiskID> <WillWipeDisk>true</WillWipeDisk> </Disk> </DiskConfiguration> <ImageInstall> <OSImage> <InstallTo> <DiskID>0</DiskID> <PartitionID>3</PartitionID> </InstallTo> <InstallFrom> <MetaData wcm:action="add"> <Key>/IMAGE/INDEX</Key> <Value>1</Value> </MetaData> </InstallFrom> </OSImage> </ImageInstall> <UserData> <ProductKey> ${if productKey != null then "<Key>${productKey}</Key>" else ""} <WillShowUI>OnError</WillShowUI> </ProductKey> <AcceptEula>true</AcceptEula> <FullName>${fullName}</FullName> <Organization>${organization}</Organization> </UserData> </component> <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SetupUILanguage> <UILanguage>${uiLanguage}</UILanguage> </SetupUILanguage> <InputLocale>${inputLocale}</InputLocale> <SystemLocale>${systemLocale}</SystemLocale> <UILanguage>${uiLanguage}</UILanguage> <UILanguageFallback>en-US</UILanguageFallback> <UserLocale>${userLocale}</UserLocale> </component> </settings> <settings pass="oobeSystem"> <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <InputLocale>${inputLocale}</InputLocale> <SystemLocale>${systemLocale}</SystemLocale> <UILanguage>${uiLanguage}</UILanguage> <UILanguageFallback>en-US</UILanguageFallback> <UserLocale>${userLocale}</UserLocale> </component> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <OOBE> <HideEULAPage>true</HideEULAPage> <HideLocalAccountScreen>true</HideLocalAccountScreen> <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> <HideOnlineAccountScreens>true</HideOnlineAccountScreens> <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> <ProtectYourPC>1</ProtectYourPC> </OOBE> <TimeZone>${timeZone}</TimeZone> <UserAccounts> ${if administratorPassword != null then '' <AdministratorPassword> <Value>${administratorPassword}</Value> <PlainText>true</PlainText> </AdministratorPassword> '' else ""} <LocalAccounts> ${builtins.concatStringsSep "\n" (builtins.map mkUser flatUsers)} </LocalAccounts> </UserAccounts> ${if defaultUser == null then "" else '' <AutoLogon> <Password> <Value>${(builtins.getAttr defaultUser users).password}</Value> <PlainText>true</PlainText> </Password> <Enabled>true</Enabled> <Username>${defaultUser}</Username> </AutoLogon> ''} <FirstLogonCommands> <SynchronousCommand wcm:action="add"> <Order>1</Order> <CommandLine>cmd /C shutdown /s /f /t 00</CommandLine> <Description>ChangeHideFiles</Description> </SynchronousCommand> </FirstLogonCommands> </component> </settings> <settings pass="specialize"> <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <RunSynchronous> ${lib.concatStringsSep "\n" (mkCommands commands)} </RunSynchronous> </component> <component name="Microsoft-Windows-SQMApi" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="NonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <CEIPEnabled>0</CEIPEnabled> </component> </settings> <!-- Disable Windows UAC --> <settings pass="offlineServicing"> <component name="Microsoft-Windows-LUA-Settings" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <EnableLUA>false</EnableLUA> </component> </settings> <cpi:offlineImage cpi:source="wim:c:/wim/windows-10/install.wim#Windows 10 Enterprise LTSC 2019 Evaluation" xmlns:cpi="urn:schemas-microsoft-com:cpi" /> </unattend> ''; in { # Lint and format as a sanity check autounattendXML = pkgs.runCommandNoCC "autounattend.xml" {} '' ${pkgs.libxml2}/bin/xmllint --format ${autounattendXML} > $out ''; # autounattend.xml is _super_ picky about quotes and other things setupScript = pkgs.writeText "ssh-setup.ps1" ( '' # Setup SSH and keys '' + lib.concatStrings ( builtins.map (c: '' # ${c.Description} ${c.Path} '') sshSetupCommands ) ); }