From 4210459014ae0d17ebe4d8cdd0a9d41202c6eb1a Mon Sep 17 00:00:00 2001
From: Jens Nolte <jens@nightmarestudio.de>
Date: Tue, 10 Dec 2019 21:49:33 +0100
Subject: [PATCH] Move installation scripts and plumbing to dotfiles repo

---
 bin/deploy                                 | 147 +++++++++++++
 bin/format.nix                             | 243 +++++++++++++++++++++
 bin/install                                | 106 +++++++++
 bin/install-mounted                        |  47 ++++
 bin/message                                |  36 +++
 bin/sshd-allow-connection                  |   8 +
 bin/util.zsh                               |  11 +
 nixos/channels/nixos-19.09/channel.json    |   7 +
 nixos/channels/nixos-19.09/default.nix     |  11 +
 nixos/channels/nixos-19.09/update          |   4 +
 nixos/channels/nixos-unstable/channel.json |   7 +
 nixos/channels/nixos-unstable/default.nix  |  11 +
 nixos/channels/nixos-unstable/update       |   4 +
 nixos/configuration.nix                    |  39 ++++
 nixos/default.nix                          |  62 +++++-
 nixos/helpers.nix                          | 102 +++++++++
 16 files changed, 833 insertions(+), 12 deletions(-)
 create mode 100755 bin/deploy
 create mode 100644 bin/format.nix
 create mode 100755 bin/install
 create mode 100755 bin/install-mounted
 create mode 100755 bin/message
 create mode 100644 bin/sshd-allow-connection
 create mode 100644 bin/util.zsh
 create mode 100644 nixos/channels/nixos-19.09/channel.json
 create mode 100644 nixos/channels/nixos-19.09/default.nix
 create mode 100755 nixos/channels/nixos-19.09/update
 create mode 100644 nixos/channels/nixos-unstable/channel.json
 create mode 100644 nixos/channels/nixos-unstable/default.nix
 create mode 100755 nixos/channels/nixos-unstable/update
 create mode 100644 nixos/configuration.nix
 create mode 100644 nixos/helpers.nix

diff --git a/bin/deploy b/bin/deploy
new file mode 100755
index 0000000..01101ca
--- /dev/null
+++ b/bin/deploy
@@ -0,0 +1,147 @@
+#!/usr/bin/env zsh
+
+set -e
+# fail on undeclared variable
+set -u
+set -o pipefail
+
+readonly cmdname=$(basename $0)
+
+
+nixos_system_file=$MACHINES_PATH/nixos.nix
+
+# This script cannot run without the nixos configuration entry point
+if [[ ! -f "$nixos_system_file" ]]
+then
+  print -P "%B%F{red}Error: %F{orange}nixos.nix%F{red} not found%b%f" >&2
+  exit 2
+fi
+
+
+source $DOTFILES_PATH/bin/util.zsh
+
+usage() {
+  print "Usage: $cmdname [--via via_hostname] <hostname> [switch|boot|reboot|test|dry-activate|build]" >&2
+}
+
+positional=()
+via_hostname=""
+while [[ $# -gt 0 ]]
+do
+  case "$1" in
+    --help|-h)
+      usage
+      exit 0
+      ;;
+    --via)
+      via_hostname="$2"
+      shift
+      ;;
+    *)
+      positional+=("$1")
+      ;;
+  esac
+  shift
+done
+
+if [[ ${#positional[@]} -ne 1 && ${#positional[@]} -ne 2 ]]
+then
+  print "Invalid number of arguments." >&2
+  usage
+  exit 2
+fi
+
+readonly hostname="${positional[1]}"
+if [[ ${#positional[@]} -ge 2 ]]
+then
+  readonly original_operation="${positional[2]}"
+else
+  # default operation
+  readonly original_operation=switch
+fi
+
+if [[ -z "$via_hostname" ]]
+then
+  via_hostname="$hostname"
+fi
+
+operation=$original_operation
+set_profile=""
+reboot=""
+
+if [[ "$operation" = "switch" || "$operation" = "boot" ]]
+then
+  set_profile=1
+elif [[ "$operation" = "reboot" ]]
+then
+  operation="boot"
+  set_profile=1
+  reboot=1
+elif [[ "$operation" = "test" || "$operation" = "dry-activate" || "$operation" = "build" ]]
+then
+  # pass
+else
+  print_error "Invalid operation: $operation"
+  usage
+  exit 2
+fi
+
+if [[ "$(hostname)" = "$hostname" ]]
+then
+  readonly is_target_host=1
+else
+  readonly is_target_host=""
+fi
+
+
+readonly local_temp_dir=$(mktemp --tmpdir --directory phoenix-deploy.XXXXXXXXXX)
+trap "rm -rf $local_temp_dir" EXIT INT HUP TERM
+
+print_info "Building target system configuration"
+nix build --file "$nixos_system_file" --argstr hostname "$hostname" --out-link "$local_temp_dir/nixos-config-$hostname"
+readonly nixos_config_path=$(realpath "$local_temp_dir/nixos-config-$hostname")
+
+if [[ "$operation" = "build" ]]
+then
+  print_info "Build completed"
+  print $nixos_config_path
+  exit 0
+fi
+
+print_info "Deploying target system configuration"
+if [[ "$is_target_host" ]]
+then
+  # local deploy
+
+  if [[ -n "$set_profile" ]]
+  then
+    sudo nix-env --profile /nix/var/nix/profiles/system --set $nixos_config_path
+  fi
+  sudo $nixos_config_path/bin/switch-to-configuration $operation
+  sync
+
+  if [[ -n "$reboot" ]]
+  then
+    sudo systemctl reboot
+  fi
+else
+  # remote deploy
+
+  nix copy --file "$nixos_system_file" --argstr hostname "$hostname" --to "ssh://root@$via_hostname"
+
+  # The manual way to do it (this is in theory also supported by nixos-rebuild by using '-I')
+
+  if [[ -n "$set_profile" ]]
+  then
+    ssh root@$via_hostname "nix-env --profile /nix/var/nix/profiles/system --set $nixos_config_path"
+  fi
+  ssh root@$via_hostname "$nixos_config_path/bin/switch-to-configuration $operation && sync"
+
+  if [[ -n "$reboot" ]]
+  then
+    ssh root@$via_hostname "systemctl reboot"
+  fi
+fi
+
+
+print_info "Update completed"
diff --git a/bin/format.nix b/bin/format.nix
new file mode 100644
index 0000000..4d8cf59
--- /dev/null
+++ b/bin/format.nix
@@ -0,0 +1,243 @@
+# This script formats a host, mounts the partitions, runs nixos-generate-config
+# and then returns a summary that can be used to specialize the system
+# configuration for the machine.
+
+{ pkgs ? import <nixpkgs> {}, hostname, template }:
+
+with builtins; with pkgs;
+let
+  zsh-bin = "${zsh}/bin/zsh";
+  realpath-bin = "${coreutils}/bin/realpath";
+  lsblk-bin = "${utillinux}/bin/lsblk";
+  blkid-bin = "${utillinux}/bin/blkid";
+  blkdiscard-bin = "${utillinux}/bin/blkdiscard";
+  sfdisk-bin = "${utillinux}/bin/sfdisk";
+  mkswap-bin = "${utillinux}/bin/mkswap";
+  swapon-bin = "${utillinux}/bin/swapon";
+  mount-bin = "${utillinux}/bin/mount";
+  umount-bin = "${utillinux}/bin/umount";
+  cryptsetup-bin = "${cryptsetup}/bin/cryptsetup";
+  pvcreate-bin = "${lvm2}/bin/pvcreate";
+  lvcreate-bin = "${lvm2}/bin/lvcreate";
+  vgcreate-bin = "${lvm2}/bin/vgcreate";
+  mkfs-fat-bin = "${dosfstools}/bin/mkfs.fat";
+  mkfs-ext4-bin = "${e2fsprogs}/bin/mkfs.ext4";
+  mkfs-btrfs-bin = "${btrfsProgs}/bin/mkfs.btrfs";
+  btrfs-bin = "${btrfsProgs}/bin/btrfs";
+  fzf-bin = "${fzf}/bin/fzf";
+  jq-bin = "${jq}/bin/jq";
+
+  swap = (if template ? swap then template.swap else "8G");
+  luks = template.luks;
+
+in
+assert (typeOf luks) == "bool";
+assert (typeOf swap) == "string";
+{
+  format = writeScriptBin "format_${hostname}" ''
+    #!${zsh-bin}
+    set -e
+    set -u
+    set -o pipefail
+    set -x
+
+    source ${./util.zsh}
+
+    cmdname=$0
+    usage() {
+      print "Usage: $cmdname <CONFIG_FILE> <OUTPUT_FILE> [<MESSAGE_PROGRAM>]" >&2
+    }
+
+    print_info() {
+      if [[ $# -ge 1 ]]
+      then
+        print -P "%B%F{blue}$1%b%f" >&2
+      else
+        print >&2
+      fi
+    }
+
+    print_warning() {
+      print -P "%B%F{yellow}$1%b%f" >&2
+    }
+
+    print_error() {
+      print -P "%B%F{red}$1%b%f" >&2
+    }
+
+    if [ "$1" = "--help" -o "$1" = "-h" ]
+    then
+        usage
+        exit 0
+    fi
+
+    if [ $# -ne 2 -a $# -ne 3 ]
+    then
+      print "Invalid number of arguments." >&2
+      usage
+      exit 2
+    fi
+
+    config_file="$1"
+    output_file="$2"
+    message_program="$3"
+
+    # Before doing anything that could fail:
+    # Set up tmpdir controlled by this script (and trap to remove it) and move the
+    # installation config there to make sure it is deleted if the script fails in
+    # any way.
+    temp_dir=$(mktemp --tmpdir --directory install.nix.XXXXXXXXXX)
+    trap "rm -rf $temp_dir" EXIT INT HUP TERM
+    mv $config_file $temp_dir/config
+    config_file=$temp_dir/config
+
+    block_device=$(${jq-bin} --raw-output .blockDevice $config_file)
+    if [ "$block_device" = null ]
+    then
+      block_device=$(${lsblk-bin} --nodeps --output PATH,NAME,SIZE,TYPE,MODEL,VENDOR | ${fzf-bin} --layout=reverse --header-lines=1 --nth=1 | awk '{print $1;}')
+    fi
+
+    ${if luks then ''
+      luks_keyfile=$temp_dir/luksKey
+      luks_key=$(${jq-bin} -e --raw-output .luksKey $config_file)
+      print -n "$luks_key" > $luks_keyfile
+    '' else "" }
+
+    if [ ! -b "$block_device" ]
+    then
+      print_info "error: $block_device is not a block device."
+      exit 1
+    fi
+
+    print_info "Selected block device: $block_device"
+
+    stable_block_device=$(for i in /dev/disk/by-id/*; do [ "$(${realpath-bin} "$i")" = "$(${realpath-bin} "$block_device")" ] && echo "$i" && return; done)
+
+    print_info "Stable block device name is: $stable_block_device"
+    print_info
+
+    print_info "Printing current layout:"
+    print_info "$(${lsblk-bin} --output name,size,type,mountpoint,model,vendor "$block_device")"
+    print_info
+
+
+    print_info "You are about to install the configuration for host '${hostname}' to $block_device (this is executed on host '$(hostname)')."
+    if read -q "?Do you want to wipe all data on $block_device? (y/n) "
+    then
+      print >&2
+    else
+      print >&2
+      exit 3
+    fi
+
+    if [ -x $message_program ]
+    then
+      $message_program &
+    fi
+
+    print_info "Discarding disk contents..."
+    if ${blkdiscard-bin} $block_device
+    then
+      ssd=true
+    else
+      ssd=false
+      print_warning "Discard failed"
+    fi
+
+    print_info "Creating partition table for bootloader ${template.bootloader}"
+
+    ${if template.bootloader == "efi" then ''
+      ${sfdisk-bin} "$block_device" <<EOF
+        label: gpt
+        start=2048, size=512MiB, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name="esp"
+        type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="system"
+      EOF
+      esp_partition="$block_device"1
+      system_partition="$block_device"2
+    '' else if template.bootloader == "bios" then ''
+      ${sfdisk-bin} "$block_device" <<EOF
+        label: gpt
+        size=1MiB, type=21686148-6449-6E6F-744E-656564454649, name="bios_grub"
+        size=512MiB, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="boot"
+        type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="system"
+      EOF
+      esp_partition="$block_device"2
+      system_partition="$block_device"3
+    '' else abort "Invalid bootloader configured in template: ${template.bootloader}" }
+
+    print_info "Creating partitions..."
+
+    ${mkfs-fat-bin} -F32 -n ESP "$esp_partition"
+
+    ${if luks then ''
+      ${cryptsetup-bin} --batch-mode --key-file $luks_keyfile luksFormat --type luks2 $system_partition
+
+      luks_partition_uuid=$(${blkid-bin} --match-tag UUID --output value $system_partition)
+      if [[ -z $luks_partition_uuid ]]
+      then
+        print_error "Cound not detect uuid of luks partition" >&2
+        exit 1
+      fi
+
+      crypt_volume_name=cryptvol_${hostname}
+
+      if $ssd
+      then
+        ${cryptsetup-bin} --batch-mode --key-file $luks_keyfile --allow-discards --persistent open $system_partition $crypt_volume_name
+      else
+        ${cryptsetup-bin} --batch-mode --key-file $luks_keyfile open $system_partition $crypt_volume_name
+      fi
+
+      rm $luks_keyfile
+
+      lvm_partition=/dev/mapper/$crypt_volume_name
+    '' else ''
+      lvm_partition=$system_partition
+    ''}
+    vg_name=vg_${hostname}
+
+    ${pvcreate-bin} $lvm_partition
+    ${vgcreate-bin} $vg_name $lvm_partition
+
+    ${lvcreate-bin} --size "${swap}" --name swap --yes $vg_name
+    swap_partition="/dev/$vg_name/swap"
+    ${mkswap-bin} -L swap $swap_partition
+    ${swapon-bin} $swap_partition
+
+    ${lvcreate-bin} --extents "100%FREE" --name btrfs --yes $vg_name
+    root_partition="/dev/$vg_name/btrfs"
+    ${mkfs-btrfs-bin} -L "btrfs_${hostname}" "$root_partition"
+
+    mount_point=/mnt
+
+    # Create subvolumes
+    ${mount-bin} -o noatime,compress=zstd:1 $root_partition $mount_point
+    ${btrfs-bin} subvolume create $mount_point/${hostname}
+    ${btrfs-bin} subvolume create $mount_point/${hostname}/nix
+    ${umount-bin} $mount_point
+
+    # Remount
+    ${mount-bin} -o subvol=/${hostname},noatime,compress=zstd:2 $root_partition $mount_point
+
+    mkdir -p $mount_point/boot
+    ${mount-bin} -o noatime $esp_partition $mount_point/boot
+
+    print_info "Generating NixOS hardware config..."
+    nixos-generate-config --root $mount_point
+
+    print_info "Writing output..."
+    > "$output_file" <<EOF
+    {
+      "installedBlockDevice": "$stable_block_device",
+      "luks": ${toJSON luks},
+      ${if luks then ''
+        "luksPartitionUuid": "$luks_partition_uuid",
+      '' else ""}
+      "ssd": $ssd,
+      "bootloader": "${template.bootloader}"
+    }
+    EOF
+
+    print_info "Installation stage 1 completed"
+  '';
+}
\ No newline at end of file
diff --git a/bin/install b/bin/install
new file mode 100755
index 0000000..ed02a0a
--- /dev/null
+++ b/bin/install
@@ -0,0 +1,106 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i zsh -p zsh -p nix
+
+set -e
+set -u
+set -o pipefail
+
+source $DOTFILES_PATH/bin/util.zsh
+
+cmdname=$(basename $0)
+usage() {
+  print "Usage: $cmdname <via_host> <hostname>" >&2
+}
+
+if [[ $# -ge 1 ]]
+then
+  if [[ "$1" = "--help" || "$1" = "-h" ]]
+  then
+      usage
+      exit 0
+  fi
+fi
+
+if [ $# -ne 2 ]
+then
+  print "Invalid number of arguments." >&2
+  usage
+  exit 2
+fi
+
+via_host="$1"
+hostname="$2"
+
+
+nixos_system_file=$MACHINES_PATH/nixos.nix
+
+# This script cannot run without the nixos configuration entry point
+if [[ ! -f "$nixos_system_file" ]]
+then
+  print -P "%B%F{red}Error: %F{orange}nixos.nix%F{red} not found%b%f" >&2
+  exit 2
+fi
+
+
+local_temp_dir=$(mktemp --tmpdir --directory install-via.XXXXXXXXXX)
+trap "rm -rf $local_temp_dir" EXIT INT HUP TERM
+
+# Pre-build installation helper
+nix build --file $DOTFILES_PATH/bin/format.nix --argstr hostname "$hostname" --arg template "(import $MACHINES_PATH).machineTemplates.$hostname" --out-link "$local_temp_dir/format_$hostname"
+
+if ! ssh -o VisualHostKey=yes root@$via_host true
+then
+  print "Cannot connect to host '$via_host'" >&2
+  exit 1
+fi
+
+if ! nix ping-store --store ssh://root@$via_host
+then
+  print "Cannot connect to nix store on '$via_host'" >&2
+  exit 1
+fi
+
+local_config_file=$local_temp_dir/config
+
+# Prepare config
+#luks_key=$(pass hosts/$hostname/luks)
+luks_key=""
+> $local_config_file <<EOF
+{
+  "blockDevice": null,
+  "luksKey": "$luks_key"
+}
+EOF
+luks_key=""
+
+nix copy --file $DOTFILES_PATH/bin/format.nix --argstr hostname "$hostname" --arg template "(import $MACHINES_PATH).machineTemplates.$hostname" --to ssh://root@$via_host
+
+remote_temp_dir=$(ssh root@$via_host mktemp --tmpdir --directory install-via.XXXXXXXXXX)
+# copy install-helper and config
+scp -r $local_temp_dir/* root@$via_host:$remote_temp_dir/
+scp $DOTFILES_PATH/bin/message root@$via_host:$remote_temp_dir/
+
+# -t: Force pseudo-terminal allocation
+ssh -t root@$via_host "$remote_temp_dir/format_$hostname/bin/format_$hostname" "$remote_temp_dir/config" "$remote_temp_dir/output.json" "$remote_temp_dir/message"
+scp "root@$via_host:$remote_temp_dir/output.json" "$MACHINES_PATH/machines/$hostname/install-result.json"
+
+# TODO: ensure the working directory is set correctly
+print_info "Gathering hardware information..."
+scp root@$via_host:/mnt/etc/nixos/hardware-configuration.nix "$MACHINES_PATH/machines/$hostname/"
+
+print_info "Building target system configuration..."
+nix build --file "$nixos_system_file" --argstr hostname "$hostname" --out-link "$local_temp_dir/nixos-config-$hostname"
+
+print_info "Deploying target system configuration..."
+nix copy --file "$nixos_system_file" --argstr hostname "$hostname" --to ssh://root@$via_host
+nixos_config_path=$(realpath "$local_temp_dir/nixos-config-$hostname")
+
+# TODO: merge commands
+ssh root@$via_host "nixos-install --system $nixos_config_path && sync"
+ssh root@$via_host mkdir --mode u=rwx,g=,o= --parents /mnt/secrets/passwords
+
+# TODO: get host-specific password
+scp -r notThePassword root@$via_host:/mnt/secrets/passwords/root
+scp -r notThePassword root@$via_host:/mnt/secrets/passwords/jens
+
+print_info "Installation completed"
diff --git a/bin/install-mounted b/bin/install-mounted
new file mode 100755
index 0000000..cd37bb4
--- /dev/null
+++ b/bin/install-mounted
@@ -0,0 +1,47 @@
+#!/usr/bin/env nix-shell
+#!nix-shell --pure -i zsh -p zsh -p nix
+
+set -e
+
+source util.zsh
+
+cmdname=$(basename $0)
+usage() {
+  print "Usage: $cmdname <via_host> <hostname>" >&2
+}
+
+if [ "$1" = "--help" -o "$1" = "-h" ]
+then
+    usage
+    exit 0
+fi
+
+if [ $# -ne 2 ]
+then
+  print "Invalid number of arguments." >&2
+  usage
+  exit 2
+fi
+
+via_host="$1"
+hostname="$2"
+
+local_temp_dir=$(mktemp --tmpdir --directory install-via.XXXXXXXXXX)
+trap "rm -rf $local_temp_dir" EXIT INT HUP TERM
+
+print_info "Building target system configuration..."
+nix build --file ../nixos.nix --argstr hostname "$hostname" --out-link "$local_temp_dir/nixos-config-$hostname"
+
+print_info "Deploying target system configuration..."
+nix copy --file ../nixos.nix --argstr hostname "$hostname" --to ssh://root@$via_host
+nixos_config_path=$(realpath "$local_temp_dir/nixos-config-$hostname")
+
+# TODO: merge commands
+ssh root@$via_host "nixos-install --system $nixos_config_path && sync"
+ssh root@$via_host mkdir --mode u=rwx,g=,o= --parents /mnt/secrets/passwords
+
+# TODO: get host-specific password
+#scp -r notThePassword root@$via_host:/mnt/secrets/passwords/root
+#scp -r notThePassword root@$via_host:/mnt/secrets/passwords/jens
+
+print_info "Installation completed"
diff --git a/bin/message b/bin/message
new file mode 100755
index 0000000..8a57bcf
--- /dev/null
+++ b/bin/message
@@ -0,0 +1,36 @@
+#!/usr/bin/env zsh
+
+message="We are experiencing technical difficulties
+Please stand by..."
+
+tty_num=13
+
+tty="/dev/tty$tty_num"
+
+# Change tty
+chvt "$tty_num"
+
+reset() {
+	# Clear screen
+	printf "\e[3J" > $tty
+	# Reset cursor position
+	printf "\033[0;0H" > $tty
+}
+
+reset
+sleep 1
+
+# print character char by char
+for (( i=0; i<${#message}; i++ ))
+do
+	print -n "${message:$i:1}" > $tty
+	sleep 0.1
+done
+
+# keep screen clear of error messages
+while true
+do
+	sleep 10
+	reset
+	print -n "${message}" > $tty
+done
diff --git a/bin/sshd-allow-connection b/bin/sshd-allow-connection
new file mode 100644
index 0000000..2f53679
--- /dev/null
+++ b/bin/sshd-allow-connection
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+mkdir -p /root/.ssh
+>> /root/.ssh/authorized_keys <EOF
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOgQ0W/0pH5A0G8eyx2CQCQshsUb/23f2/mWyPWIvEXT jens@serenity
+EOF
+
+systemctl start sshd.service
\ No newline at end of file
diff --git a/bin/util.zsh b/bin/util.zsh
new file mode 100644
index 0000000..ef03cdf
--- /dev/null
+++ b/bin/util.zsh
@@ -0,0 +1,11 @@
+print_info() {
+  print -P "%B%F{blue}$1%b%f" >&2
+}
+
+print_warning() {
+  print -P "%B%F{yellow}$1%b%f" >&2
+}
+
+print_error() {
+  print -P "%B%F{red}$1%b%f" >&2
+}
diff --git a/nixos/channels/nixos-19.09/channel.json b/nixos/channels/nixos-19.09/channel.json
new file mode 100644
index 0000000..8d34a51
--- /dev/null
+++ b/nixos/channels/nixos-19.09/channel.json
@@ -0,0 +1,7 @@
+{
+  "url": "https://github.com/NixOS/nixpkgs-channels/",
+  "rev": "093faad9684796975520d9d88503e76ab539b8ef",
+  "date": "2019-12-03T20:28:01-05:00",
+  "sha256": "0v3an5f5anvqqfpihp9sgrhnzv68qvjihq16mjhfycglsz758z6p",
+  "fetchSubmodules": false
+}
diff --git a/nixos/channels/nixos-19.09/default.nix b/nixos/channels/nixos-19.09/default.nix
new file mode 100644
index 0000000..5370bfc
--- /dev/null
+++ b/nixos/channels/nixos-19.09/default.nix
@@ -0,0 +1,11 @@
+with builtins;
+
+let
+
+  channelDef = fromJSON ( readFile ./channel.json );
+
+in fetchGit {
+  name = "nixpkgs-19.09";
+  ref = "nixos-19.09";
+  inherit (channelDef) url rev;
+}
diff --git a/nixos/channels/nixos-19.09/update b/nixos/channels/nixos-19.09/update
new file mode 100755
index 0000000..4f11e78
--- /dev/null
+++ b/nixos/channels/nixos-19.09/update
@@ -0,0 +1,4 @@
+#!/usr/bin/env nix-shell
+#! nix-shell -i sh -p nix-prefetch-git
+
+nix-prefetch-git --rev refs/heads/nixos-19.09 --no-deepClone https://github.com/NixOS/nixpkgs-channels/ > channel.json
diff --git a/nixos/channels/nixos-unstable/channel.json b/nixos/channels/nixos-unstable/channel.json
new file mode 100644
index 0000000..2cf445c
--- /dev/null
+++ b/nixos/channels/nixos-unstable/channel.json
@@ -0,0 +1,7 @@
+{
+  "url": "https://github.com/NixOS/nixpkgs-channels/",
+  "rev": "e89b21504f3e61e535229afa0b121defb52d2a50",
+  "date": "2019-11-19T07:59:43-05:00",
+  "sha256": "0jqcv3rfki3mwda00g66d27k6q2y7ca5mslrnshfpbdm7j8ya0kj",
+  "fetchSubmodules": false
+}
diff --git a/nixos/channels/nixos-unstable/default.nix b/nixos/channels/nixos-unstable/default.nix
new file mode 100644
index 0000000..405068d
--- /dev/null
+++ b/nixos/channels/nixos-unstable/default.nix
@@ -0,0 +1,11 @@
+with builtins;
+
+let
+
+  channelDef = fromJSON ( readFile ./channel.json );
+
+in fetchGit {
+  name = "nixpkgs-unstable";
+  ref = "nixos-unstable";
+  inherit (channelDef) url rev;
+}
diff --git a/nixos/channels/nixos-unstable/update b/nixos/channels/nixos-unstable/update
new file mode 100755
index 0000000..747d585
--- /dev/null
+++ b/nixos/channels/nixos-unstable/update
@@ -0,0 +1,4 @@
+#!/usr/bin/env nix-shell
+#! nix-shell -i sh -p nix-prefetch-git
+
+nix-prefetch-git --rev refs/heads/nixos-unstable --no-deepClone https://github.com/NixOS/nixpkgs-channels/ > channel.json
diff --git a/nixos/configuration.nix b/nixos/configuration.nix
new file mode 100644
index 0000000..1ee0649
--- /dev/null
+++ b/nixos/configuration.nix
@@ -0,0 +1,39 @@
+# This is the entry point for my NixOS configuration.
+{ name, path, channel }:
+{ lib, config, pkgs, ... }:
+
+let
+  installResult = builtins.fromJSON (builtins.readFile (path + "/install-result.json"));
+  dotfilesConfig = import (path + "/dotfiles.nix");
+  layerImports = map (l: ./layers + "/${l}.nix") dotfilesConfig.layers;
+in
+{
+  imports = [
+    ./modules
+    (path + "/configuration.nix")
+    (path + "/hardware-configuration.nix")
+  ] ++ layerImports;
+
+  nixpkgs.config = {
+    packageOverrides = ( import ./pkgs ) { inherit lib config; } ;
+  };
+
+  # Pin channel in nix path
+  nix.nixPath = [ "nixpkgs=${channel}" ];
+
+  # Bootloader
+  boot.loader.systemd-boot.enable = (installResult.bootloader == "efi");
+  boot.loader.efi.canTouchEfiVariables = (installResult.bootloader == "efi");
+  boot.loader.grub.enable = (installResult.bootloader == "grub");
+  boot.loader.grub.device = installResult.installedBlockDevice;
+
+  # Default hostname ist machine directory name
+  networking.hostName = lib.mkDefault name;
+
+  boot.initrd.luks.devices = if installResult.luks then {
+    cryptvol = {
+      device = "/dev/disk/by-uuid/" + installResult.luksPartitionUuid;
+      allowDiscards = true;
+    };
+  } else {};
+}
diff --git a/nixos/default.nix b/nixos/default.nix
index a5de97f..c338ccc 100644
--- a/nixos/default.nix
+++ b/nixos/default.nix
@@ -1,16 +1,54 @@
-# This is the entry point for my NixOS configuration.
-{ layers ? [] }:
-{ lib, config, pkgs, ... }:
+# entry point for machine configurations:
+# (import <repo-path> { machinesDir=./machines }).<netname>.configurations.<hostname>
 
+{ machinesDir }:
+
+with builtins;
 let
-  layerImports = map (l: ./layers + "/${l}.nix") layers;
+  defaultChannel = (import channels/nixos-unstable);
+
+  # helpers :: { *: ? }
+  helpers = import ./helpers.nix;
+
+  # channelsDir :: path
+  channelsDir = ./channels;
+  # allChannels :: { *: path }
+  allChannels = with helpers; keysToAttrs (channelname: import (channelsDir + "/${channelname}")) (readFilterDir (filterAnd [(not filterDirHidden) filterDirDirs]) channelsDir);
+  # getMachineChannel :: string -> path
+  getMachineChannel = { name, path }:
+    let
+      channelFile = path + "/channel.nix";
+    in
+      if (pathExists channelFile)
+        then (import channelFile) allChannels
+        else defaultChannel;
+  # machineChannels :: { *: path }
+  machineChannels = withMachines getMachineChannel;
+
+  machinesDirContents = readDir machinesDir;
+  machineNames = filter (p: machinesDirContents.${p} == "directory") (attrNames machinesDirContents);
+  withMachines = lambda: listToAttrs (map (m: {name = m; value = lambda { name = m; path = (machinesDir + "/${m}"); }; }) machineNames);
+  mkMachineConfig = { name, path }: (
+    import ./configuration.nix {
+      inherit name path;
+      channel = machineChannels.${name};
+    }
+  );
+  mkNixosSystemDerivation = { name, path }:
+    let
+      channel = machineChannels.${name};
+      configuration = mkMachineConfig { inherit name path; };
+      # Importing <nixpkgs/nixos> results in a nixos system closure
+      nixos = import "${channel}/nixos" {
+        system = "x86_64-linux";
+        inherit configuration;
+      };
+    in
+      nixos.system;
 in
 {
-  imports = [
-    ./modules
-  ] ++ layerImports;
-
-  nixpkgs.config = {
-    packageOverrides = ( import ./pkgs ) { inherit lib config; } ;
-  };
-}
+  configurations = withMachines mkMachineConfig;
+  nixosSystemDerivations = withMachines mkNixosSystemDerivation;
+  machineTemplates = withMachines ({name, path}: import (path + /template.nix));
+  channels = machineChannels;
+}
\ No newline at end of file
diff --git a/nixos/helpers.nix b/nixos/helpers.nix
new file mode 100644
index 0000000..ffc8b9e
--- /dev/null
+++ b/nixos/helpers.nix
@@ -0,0 +1,102 @@
+rec {
+
+  # id :: a -> a
+  id = value: value;
+
+  # not :: Bool -> Bool
+  # not :: (a -> Bool) -> a -> Bool
+  not = value: with builtins; 
+    if isBool value then
+      ! value
+    else if isFunction value then
+      x: ! (value x)
+    else
+      throw ("value is a " + typeOf value + " while a Boolean or a Function was expected");
+
+
+  # filterAnd :: [ (a -> Bool) ] -> a -> Bool
+  filterAnd = lambdas: value: with builtins; 
+    all (lambda: lambda value) lambdas;
+
+
+  # filterOr :: [ (a -> Bool) ] -> a -> Bool
+  filterOr = lambdas: value: with builtins;
+    any (lambda: lambda value) lambdas;
+
+
+  # readFilterDir :: ({name:String, path:Path, type:String, ...} -> Bool) -> Path -> [ String ]
+  readFilterDir = lambda: path: with builtins;
+  let
+    dirContents = readDir path;
+    filterFunc = name: lambda rec {
+      inherit name;
+      path = path + "/${name}";
+      type = dirContents.${name};
+    };
+  in filter filterFunc (attrNames dirContents);
+
+
+  # filterDirHidden :: {name:String, ...} -> Bool
+  filterDirHidden = { name, ... }:
+    (builtins.substring 0 1 name) == ".";
+
+
+  # filterDirDirs :: {type:String, ...} -> Bool
+  filterDirDirs = { type, ... }:
+    type == "directory";
+
+
+  # filterDirFiles :: {type:String, ...} -> Bool
+  filterDirFiles = { type, ... }:
+    type == "regular";
+
+
+  # filterDirSymlinks :: {type:String, ...} -> Bool
+  filterDirSymlinks = { type, ... }:
+    type == "symlink";
+
+  # keysToAttrs :: ( String -> a ) -> [ String ] -> { *: a }
+  keysToAttrs = lambda: strings:
+    builtins.listToAttrs (map (k: {
+      name = k;
+      value = lambda k;
+    }) strings);
+
+  # isMaybe :: a -> Bool
+  isMaybe = m: with builtins;
+    if not isAttrs m then false else
+    if not (hasAttr "isMaybe") m then false else
+    if not isBool m.isMaybe then false else
+    m.isMaybe;
+
+  # Just :: a -> Maybe a
+  Just = a: {isMaybe = true; hasValue = true; value = a;};
+
+  # Nothing :: Maybe a
+  Nothing = {isMaybe = true; hasValue = false;};
+
+  # maybe :: b -> (a -> b) -> Maybe a -> b
+  maybe = default: transform: m:
+    if not isMaybe m then
+      throw "maybe: ${m} is not a Maybe."
+    else
+      if m.hasValue then
+        transform m.value
+      else
+        default
+    ;
+
+  # maybeToList :: Maybe a -> [ a ]
+  maybeToList = m:
+    if not isMaybe m then
+      throw "maybeToList: ${m} is not a Maybe."
+    else if m.hasValue then
+      [ m.value ]
+    else
+      [];
+
+  # toExistingPath :: Path -> Maybe Path
+  toExistingPath = path: with builtins;
+    if pathExists path then Just path else Nothing;
+
+}
-- 
GitLab