Skip to content

Commit

Permalink
vm: Introduce new Windows VM installer for Cygwin.
Browse files Browse the repository at this point in the history
After quite a lot of fighting against Windows and its various
limitations, this new is the base architecture for installing and
accessing the Windows VM and thus the Cygwin environment inside it:

                .------------.
          .---> | vde_switch |
          |     `-[#]----[#]-'
          |        |      |
         ,'       .'      `---.___
       ,'    192.168.0.1          `.
       |          |            192.168.0.2
      ,'    _____[#]____           |
    ,'     |            |   ______[#]______
    |      | Windows VM |  |    .--'       |
    |      |____________|  |    |          |
    |             |  /|\   |  .-|          |
    | .---------. |   |    |  | |          |
  .-|-| manager |-'   |    |  | |          |
  | | `---------'     |    |  | |          |
  | |                 |    |  | |          |
  | | .-------------. |    | Samba         |
  | | | BOOTSTRAP   | |    |  | |          |
  | | |-------------| |    |  | |   .------|
  | `-| spawn VMs   |-+--> |  | `---| xchg | <-------.
  |   |-------------|      |  | .---^------|         |
  |   | install     |---.  |  `-| nixstore | <----.  |
  |   |-------------|   |  |    `----------|      |  |
  |---| suspend VM  |   |  |               |      |  |
  |   `------.------'   |  | Controller VM |      |  |
  |          |          |  |_______________|      |  |
  |       .--'          |         /|\            VirtIO
  |       |           __|__________:____________  |  |
  |      \|/         |  |          `.           | |  |
  | .------------.   |  |           :           | |  |
  | | REAL BUILD |   |  |   .-------^--------.  | |  |
  | |------------|   |  `-> | serial console |  | |  |
  `-| revive VM  |   |      `----------------'  | |  |
    |------------|   |------------.             | |  |
    | build      |-->| /nix/store >>>-----------|-'  |
    |------------|   |------------|             |    |
    | collect    |<--| xchg       >>>-----------|----'
    `-----.------'   |------------'             |
          |          |                          |
         \|/         |    |  |  __   ___  |     |
                     |    |--| |  | (__  -|-    |
    F I N I S H E D  |    |  | |__| ___)  |     |
                     |__________________________|

This might look a bit overwhelming, but let me try to explain:

We're starting at the base derivation ("BOOTSTRAP" above), where we
actually install the Cygwin envirenment. Over there we basically fire up
a vde_switch process and two virtual machines: One is the Windows
machine, the other is a NixOS machine, which serves as some kind of
proxy between the host and the Windows machine.

The reason we're doing this, is because we don't have a lot of options
for sharing files between a stock Windows machine and the host. In
earlier experiments, I've tried to communicate with the Windows guest by
using pipes and OpenSSH, but obviously this wasn't a big speed rush (or
to say it bluntly: It was fucking slow).

Using TCP/IP directly for accessing the guest would have been another
option, but it could lead to possible errors when the port or a range of
ports are in use at the Host system. Also, we would need to punch a hole
into the sandbox of the Nix builder (as it doesn't allow networking),
which in turn will possibly undermine deterministic builds/runs (well,
at least as deterministic as it can be, we're running Windows,
remember?).

So, let's continue: The responsibility of the NixOS (controller) VM is
to just wait until an SSH port becomes available on the Windows VM,
whereas the Windows VM itself is installed using an unattended
installation file provided via a virtual floppy image.

With the installation of the basic Windows OS, we directly install
Cygwin and start up an OpenSSH service.

At this point the bootstrapping is almost finished and as soon as the
port is available, the controller VM sets up Samba shares and makes it
available as drive letters within Windows and as bind mounts (for
example /nix/store) within Cygwin.

Finally we're making a snapshot of the memory of the Windows VM in order
to revive it within a few seconds when we want to build something.

Now, the build process itself is fairly straightforward: Revive VM and
build based on existing store derivations and collect the result _and_
the exit code from the xchg share/directory.

Conclusion: This architecture may sound a bit complicated, but we're
trying to achieve deterministic and reproducable builds and/or test
runs.

Signed-off-by: aszlig <aszlig@redmoonstudios.org>
  • Loading branch information
aszlig committed Feb 26, 2014
1 parent 83c98e4 commit 276b72f
Show file tree
Hide file tree
Showing 2 changed files with 426 additions and 0 deletions.
348 changes: 348 additions & 0 deletions pkgs/build-support/vm/windows/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
with import <nixpkgs> {};

with import <nixpkgs/nixos/lib/build-vms.nix> {
inherit system;
minimal = false;
};

let
winISO = /path/to/iso/XXX;

bootstrapAfterLogin = runCommand "bootstrap.sh" {} ''
cat > "$out" <<EOF
mkdir -p ~/.ssh
cat > ~/.ssh/authorized_keys <<PUBKEY
$(cat "${snakeOilSSH}/key.pub")
PUBKEY
ssh-host-config -y -c 'binmode ntsec' -w dummy
cygrunsrv -S sshd
net use S: '\\192.168.0.2\nixstore'
mkdir -p /nix/store
mount -o bind /cygdrives/s /nix/store
EOF
'';

productKey = "XXX";

unattended = /* productKey: */ let
installCygwin = [ "openssh" ];
cygwinRoot = "C:\\cygwin";
afterSetup = [
"E:\\setup.exe"
"-L -n -q"
"-l E:\\"
"-R ${cygwinRoot}"
"-C base"
] ++ map (p: "-P ${p}") installCygwin;
runCygShell = args: "${cygwinRoot}\\bin\\bash -l ${args}";
in writeText "winnt.sif" ''
[Data]
AutoPartition = 1
AutomaticUpdates = 0
MsDosInitiated = 0
UnattendedInstall = Yes
[Unattended]
DUDisable = Yes
DriverSigningPolicy = Ignore
Hibernation = No
OemPreinstall = No
OemSkipEula = Yes
Repartition = Yes
TargetPath = \WINDOWS
UnattendMode = FullUnattended
UnattendSwitch = Yes
WaitForReboot = No
[GuiUnattended]
AdminPassword = "nopasswd"
AutoLogon = Yes
AutoLogonCount = 1
OEMSkipRegional = 1
OemSkipWelcome = 1
ServerWelcome = No
TimeZone = 85
[UserData]
ComputerName = "cygwin"
FullName = "cygwin"
OrgName = ""
ProductKey = "${productKey}"
[Networking]
InstallDefaultComponents = Yes
[Identification]
JoinWorkgroup = cygwin
[NetAdapters]
PrimaryAdapter = params.PrimaryAdapter
[params.PrimaryAdapter]
InfID = *
[params.MS_MSClient]
[NetProtocols]
MS_TCPIP = params.MS_TCPIP
[params.MS_TCPIP]
AdapterSections=params.MS_TCPIP.PrimaryAdapter
[params.MS_TCPIP.PrimaryAdapter]
DHCP = No
IPAddress = 192.168.0.1
SpecificTo = PrimaryAdapter
SubnetMask = 255.255.255.0
WINS = No
; Turn off all components
[Components]
${lib.concatMapStrings (comp: "${comp} = Off\n") [
"AccessOpt" "Appsrv_console" "Aspnet" "BitsServerExtensionsISAPI"
"BitsServerExtensionsManager" "Calc" "Certsrv" "Certsrv_client"
"Certsrv_server" "Charmap" "Chat" "Clipbook" "Cluster" "Complusnetwork"
"Deskpaper" "Dialer" "Dtcnetwork" "Fax" "Fp_extensions" "Fp_vdir_deploy"
"Freecell" "Hearts" "Hypertrm" "IEAccess" "IEHardenAdmin" "IEHardenUser"
"Iis_asp" "Iis_common" "Iis_ftp" "Iis_inetmgr" "Iis_internetdataconnector"
"Iis_nntp" "Iis_serversideincludes" "Iis_smtp" "Iis_webdav" "Iis_www"
"Indexsrv_system" "Inetprint" "Licenseserver" "Media_clips" "Media_utopia"
"Minesweeper" "Mousepoint" "Msmq_ADIntegrated" "Msmq_Core"
"Msmq_HTTPSupport" "Msmq_LocalStorage" "Msmq_MQDSService"
"Msmq_RoutingSupport" "Msmq_TriggersService" "Msnexplr" "Mswordpad"
"Netcis" "Netoc" "OEAccess" "Objectpkg" "Paint" "Pinball" "Pop3Admin"
"Pop3Service" "Pop3Srv" "Rec" "Reminst" "Rootautoupdate" "Rstorage" "SCW"
"Sakit_web" "Solitaire" "Spider" "TSWebClient" "Templates"
"TerminalServer" "UDDIAdmin" "UDDIDatabase" "UDDIWeb" "Vol" "WMAccess"
"WMPOCM" "WbemMSI" "Wms" "Wms_admin_asp" "Wms_admin_mmc" "Wms_isapi"
"Wms_server" "Zonegames"
]}
[WindowsFirewall]
Profiles = WindowsFirewall.TurnOffFirewall
[WindowsFirewall.TurnOffFirewall]
Mode = 0
[SetupParams]
UserExecute = "${lib.concatStringsSep " " afterSetup}"
[GuiRunOnce]
Command0 = "${cygwinRoot}\bin\bash -l E:\bootstrap.sh"
'';

floppyImg = stdenv.mkDerivation {
name = "unattended-floppy.img";
buildCommand = ''
dd if=/dev/zero of="$out" count=1440 bs=1024
${dosfstools}/sbin/mkfs.msdos "$out"
${mtools}/bin/mcopy -i "$out" "${unattended}" ::winnt.sif
${mtools}/bin/mcopy -i "$out" "${snakeOilSSH}/key.pub" ::ssh.pub
'';
};

qemuCommandWindows = ''
${vmTools.qemuProg} \
${lib.optionalString (stdenv.system == "x86_64-linux") "-cpu kvm64"} \
-nographic -no-reboot \
-virtfs local,path=/nix/store,security_model=none,mount_tag=store \
-virtfs local,path=$TMPDIR/xchg,security_model=none,mount_tag=xchg \
-drive file=$diskImage,if=virtio,cache=writeback,werror=report \
$QEMU_OPTS
'';

cygwinMirror = "http://ftp.gwdg.de/pub/linux/sources.redhat.com/cygwin";

cygPkgList = fetchurl {
url = "${cygwinMirror}/x86_64/setup.ini";
sha256 = "0d54pli0gnm3010w9iq2bar3r2sc4syyblg62q75inc2cq341bi3";
};

makeCygwinClosure = { packages, packageList }: let
expr = import (runCommand "cygwin.nix" { buildInputs = [ python ]; } ''
python ${./mkclosure.py} "${packages}" ${toString packageList} > "$out"
'');
gen = { url, md5 }: {
source = fetchurl {
url = "${cygwinMirror}/${url}";
inherit md5;
};
target = url;
};
in map gen expr;

cygiso = import <nixpkgs/nixos/lib/make-iso9660-image.nix> {
inherit (pkgs) stdenv perl cdrkit pathsFromGraph;
contents = [
{ source = bootstrapAfterLogin;
target = "bootstrap.sh";
}
{ source = fetchurl {
url = "http://cygwin.com/setup-x86_64.exe";
sha256 = "1bjmq9h1p6mmiqp6f1kvmg94jbsdi1pxfa07a5l497zzv9dsfivm";
};
target = "setup.exe";
}
{ source = cygPkgList;
target = "setup.ini";
}
] ++ makeCygwinClosure {
packages = cygPkgList;
packageList = [ "openssh" ];
};
};

maybeKvm64 = lib.optional (stdenv.system == "x86_64-linux") "-cpu kvm64";

cygwinQemuArgs = lib.concatStringsSep " " (maybeKvm64 ++ [
"-monitor unix:$MONITOR_SOCKET,server,nowait"
"-nographic"
"-boot order=c,once=d"
"-drive file=${floppyImg},readonly,index=0,if=floppy"
"-drive file=winvm.img,index=0,media=disk"
"-drive file=${winISO},index=1,media=cdrom"
"-drive file=${cygiso}/iso/cd.iso,index=2,media=cdrom"
"-net nic,vlan=0,macaddr=52:54:00:12:01:01"
"-net vde,vlan=0,sock=$QEMU_VDE_SOCKET"
"-rtc base=2010-01-01,clock=vm"
]);

modulesClosure = lib.overrideDerivation vmTools.modulesClosure (o: {
rootModules = o.rootModules ++ lib.singleton "virtio_net";
});

snakeOilSSH = stdenv.mkDerivation {
name = "snakeoil-ssh-cygwin";
buildCommand = ''
ensureDir "$out"
${openssh}/bin/ssh-keygen -t ecdsa -f "$out/key" -N ""
'';
};

controllerQemuArgs = cmd: let
preInitScript = writeScript "preinit.sh" ''
#!${vmTools.initrdUtils}/bin/ash -e
export PATH=${vmTools.initrdUtils}/bin
mount -t proc none /proc
mount -t sysfs none /sys
for arg in $(cat /proc/cmdline); do
if [ "x''${arg#command=}" != "x$arg" ]; then
command="''${arg#command=}"
fi
done
for i in $(cat ${modulesClosure}/insmod-list); do
insmod $i
done
mkdir -p /tmp /dev
mknod /dev/null c 1 3
mknod /dev/zero c 1 5
mknod /dev/random c 1 8
mknod /dev/urandom c 1 9
mknod /dev/tty c 5 0
ifconfig lo up
ifconfig eth0 up 192.168.0.2
mkdir -p /nix/store /etc /var/run /var/log
cat > /etc/passwd <<PASSWD
root:x:0:0::/root:/bin/false
nobody:x:65534:65534::/var/empty:/bin/false
PASSWD
mount -t 9p \
-o trans=virtio,version=9p2000.L,msize=262144,cache=loose \
store /nix/store
exec "$command"
'';
initrd = makeInitrd {
contents = lib.singleton {
object = preInitScript;
symlink = "/init";
};
};
initScript = writeScript "init.sh" ''
#!${stdenv.shell}
${coreutils}/bin/mkdir -p /etc/samba /etc/samba/private /var/lib/samba
${coreutils}/bin/cat > /etc/samba/smb.conf <<CONFIG
[global]
security = user
map to guest = Bad User
workgroup = cygwin
netbios name = controller
server string = %h
log level = 1
max log size = 1000
log file = /var/log/samba.log
[nixstore]
path = /nix/store
read only = no
guest ok = yes
CONFIG
${samba}/sbin/nmbd -D
${samba}/sbin/smbd -D
${coreutils}/bin/cp -L "${snakeOilSSH}/key" /ssh.key
${coreutils}/bin/chmod 600 /ssh.key
echo -n "Waiting for Windows VM to become ready"
while ! ${netcat}/bin/netcat -z 192.168.0.1 22; do
echo -n .
${coreutils}/bin/sleep 1
done
echo " ready."
${openssh}/bin/ssh \
-o UserKnownHostsFile=/dev/null \
-o StrictHostKeyChecking=no \
-i /ssh.key \
-l Administrator \
192.168.0.1 -- "${cmd}"
${busybox}/sbin/poweroff -f
'';
kernelAppend = lib.concatStringsSep " " [
"panic=1"
"loglevel=4"
"console=tty1"
"console=ttyS0"
"command=${initScript}"
];
in lib.concatStringsSep " " (maybeKvm64 ++ [
"-nographic"
"-no-reboot"
"-virtfs local,path=/nix/store,security_model=none,mount_tag=store"
"-kernel ${modulesClosure.kernel}/bzImage"
"-initrd ${initrd}/initrd"
"-append \"${kernelAppend}\""
"-net nic,vlan=0,macaddr=52:54:00:12:01:02,model=virtio"
"-net vde,vlan=0,sock=$QEMU_VDE_SOCKET"
]);

bootstrap = stdenv.mkDerivation {
name = "windown-vm";

buildCommand = ''
${qemu}/bin/qemu-img create -f qcow2 winvm.img 2G
QEMU_VDE_SOCKET="$(pwd)/vde.ctl"
MONITOR_SOCKET="$(pwd)/monitor"
${vde2}/bin/vde_switch -s "$QEMU_VDE_SOCKET" &
${vmTools.qemuProg} ${cygwinQemuArgs} &
${vmTools.qemuProg} ${controllerQemuArgs "sync"}
ensureDir "$out"
${socat}/bin/socat - UNIX-CONNECT:$MONITOR_SOCKET <<CMD
stop
migrate_set_speed 4095m
migrate "exec:${gzip}/bin/gzip -c > '$out/state.gz'"
CMD
cp winvm.img "$out/disk.img"
'';
};

in bootstrap
Loading

0 comments on commit 276b72f

Please sign in to comment.