diff --git a/artiq-fast/windows/autounattend.nix b/artiq-fast/windows/autounattend.nix index 302a3de..5c2d038 100644 --- a/artiq-fast/windows/autounattend.nix +++ b/artiq-fast/windows/autounattend.nix @@ -14,7 +14,7 @@ , timeZone ? "UTC" , services ? {} , impureShellCommands ? [] -, driveLetter ? "F:" +, driveLetter ? "E:" , ... }: diff --git a/artiq-fast/windows/build.nix b/artiq-fast/windows/build.nix index 9cac0d0..2a64d8f 100644 --- a/artiq-fast/windows/build.nix +++ b/artiq-fast/windows/build.nix @@ -42,6 +42,26 @@ win.makeWindowsImage { # Auto login defaultUser = "artiq"; + # Imperative installation commands, to be installed incrementally + installCommands = [ + + { + name = "Anaconda3"; + script = let + Anaconda3 = pkgs.fetchurl { + name = "Anaconda3.exe"; + url = "https://repo.anaconda.com/archive/Anaconda3-2019.03-Windows-x86_64.exe"; + sha256 = "1f9icm5rwab6l1f23a70dw0qixzrl62wbglimip82h4zhxlh3jfj"; + }; + in '' + cp ${Anaconda3} ./Anaconda3.exe + win put Anaconda3.exe 'C:\Users\artiq' + win exec 'start /wait "" .\Anaconda3.exe /S /D=%UserProfile%\Anaconda3' + ''; + } + + ]; + # services = { # # Enable remote management # WinRm = { diff --git a/artiq-fast/windows/win.nix b/artiq-fast/windows/win.nix index 77d8942..960975f 100644 --- a/artiq-fast/windows/win.nix +++ b/artiq-fast/windows/win.nix @@ -7,6 +7,8 @@ , packages ? [] , impureMode ? false , baseRtc ? "2020-04-20T14:21:42" +, installCommands ? [] +, users ? {} , ... }@attrs: @@ -103,6 +105,7 @@ let cp ${bundleInstaller} pkgs/"$(stripHash "${bundleInstaller}")" # Install optional windows features + cp ${openSshServerPackage} pkgs/fod/OpenSSH-Server-Package~31bf3856ad364e35~amd64~~.cab # SSH setup script goes here because windows XML parser sucks @@ -131,6 +134,10 @@ let # "-cdrom" "${fodIso}" # Set the base clock inside the VM "-rtc base=${baseRtc}" + # Always enable SSH port forward + # It's not really required for the initial setup but we do it here anyway + "-netdev user,id=n1,net=192.168.1.0/24,restrict=off,hostfwd=tcp::2022-:22" + "-device e1000,netdev=n1" ] ++ lib.optional (!impureMode) "-nographic" ++ extraFlags; installScript = pkgs.writeScript "windows-install-script" ( @@ -179,8 +186,116 @@ let '' ); + baseImage = pkgs.runCommandNoCC "windows.img" {} '' + ${installScript} + mv c.img $out + ''; + + # Use Paramiko instead of OpenSSH + # + # OpenSSH goes out of it's way to make password logins hard + # and Windows goes out of it's way to make key authentication hard + # so we're in a pretty tough spot + # + # Luckily the usage patterns are quite simple and easy to reimplement with paramiko + paramikoClient = pkgs.writeScriptBin "win" '' + #!${pkgs.python3.withPackages(ps: [ ps.paramiko ])}/bin/python + import paramiko + import os.path + import sys + + + def w_join(*args): + # Like os.path.join but for windows paths + return "\\".join(args) + + + if __name__ == '__main__': + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.client.AutoAddPolicy) + + + cmd = sys.argv[1] + + try: + client.connect(hostname="127.0.0.1", port=2022, username="artiq", password="${users.artiq.password}", timeout=1) + + if cmd == "put": + sftp = client.open_sftp() + src = sys.argv[2] + dst = sys.argv[3] + sftp.put(src, w_join(dst, os.path.basename(src))) + + elif cmd == "exec": + _, stdout, stderr = client.exec_command(sys.argv[2]) + + sys.stdout.write(stdout.read().strip().decode()) + sys.stdout.flush() + + sys.stderr.write(stderr.read().strip().decode()) + sys.stderr.flush() + + else: + raise ValueError(f"Unhandled command: {cmd}") + except (EOFError, paramiko.ssh_exception.SSHException): + exit(1) + ''; + + finalImage = builtins.foldl' (acc: v: pkgs.runCommandNoCC "${v.name}.img" { + buildInputs = [ + paramikoClient + qemu + ]; + } (let + script = pkgs.writeScript "${v.name}-script" v.script; + qemuParams = mkQemuFlags [ + # Output image + "-drive" + "file=c.img,index=0,media=disk,cache=unsafe" + ]; + + in '' + export HOME=$(mktemp -d) + + # Create an image referencing the previous image in the chain + qemu-img create -f qcow2 -b ${acc} c.img + + qemu-system-x86_64 ${lib.concatStringsSep " " qemuParams} & + + # If the machine is not up within 10 minutes it's likely never coming up + timeout=600 + + # Wait for VM to be accessible + sleep 20 + echo "Waiting for SSH" + while true; do + if test "$timeout" -eq 0; then + echo "SSH connection timed out" + exit 1 + fi + + output=$(win exec 'echo Ran command' || echo "") + if test "$output" = "Ran command"; then + break + fi + + echo "Retrying in 1 second, timing out in $timeout seconds" + + ((timeout=$timeout-1)) + + sleep 1 + done + + echo "Executing user script to build layer" + + ${script} + + win exec 'shutdown /s' + + mv c.img $out + '')) baseImage installCommands; + in -if impureMode then installScript else pkgs.runCommandNoCC "windows.img" {} '' - ${installScript} - mv c.img $out -'' + +# impureMode is meant for debugging the base image, not the full incremental build process +if !(impureMode) then finalImage else assert installCommands == []; installScript