Last active
August 16, 2019 18:13
-
-
Save dredozubov/1cd277a9131a3c0fc9d69262b95f5537 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# Installs NixOS on a Hetzner server, wiping the server. | |
# | |
# This is for a specific server configuration; adjust where needed. | |
# | |
# Prerequisites: | |
# * Update the script to adjust SSH pubkeys, hostname, NixOS version etc. | |
# | |
# Usage: | |
# ssh root@YOUR_SERVERS_IP "$(< hetzner-dedicated-wipe-and-install-nixos.sh)" | |
# | |
# When the script is done, make sure to boot the server from HD, not rescue mode again. | |
# Explanations: | |
# | |
# * Adapted from https://gist.githubusercontent.com/nh2/ebc27311731f53ee623ae781ca25103f/raw/8fd9d659022a458b85e9d2db3e20542c7914da70/hetzner-dedicated-wipe-and-install-nixos.sh | |
# which was Adapted from https://gist.github.com/nh2/78d1c65e33806e7728622dbe748c2b6a | |
# * Following largely https://nixos.org/nixos/manual/index.html#sec-installing-from-other-distro. | |
# * **Important:** We boot in legacy-BIOS mode, not UEFI, because that's what Hetzner uses. | |
# * NVMe devices aren't supported for booting (those require EFI boot) | |
# * We set a custom `configuration.nix` so that we can connect to the machine afterwards, | |
# inspired by https://nixos.wiki/wiki/Install_NixOS_on_Hetzner_Online | |
# * This server has 2 HDDs. | |
# We put everything on RAID1. | |
# Storage scheme: `partitions -> RAID -> LVM -> ext4`. | |
# * A root user with empty password is created, so that you can just login | |
# as root and press enter when using the Hetzner spider KVM. | |
# Of course that empty-password login isn't exposed to the Internet. | |
# Change the password afterwards to avoid anyone with physical access | |
# being able to login without any authentication. | |
# * The script reboots at the end. | |
set -eu | |
set -o pipefail | |
set -x | |
# Inspect existing disks | |
lsblk | |
# Undo existing setups to allow running the script multiple times to iterate on it. | |
# We allow these operations to fail for the case the script runs the first time. | |
set +e | |
umount /mnt | |
vgchange -an vg0 | |
set -e | |
# Stop all mdadm arrays that the boot may have activated. | |
mdadm --stop --scan | |
DISK0="/dev/nvme0n1" | |
DISK1="/dev/nvme1n1" | |
PART1="p1" | |
PART2="p2" | |
# Create partition tables (--script to not ask) | |
parted --script "$DISK0" mklabel gpt | |
parted --script "$DISK1" mklabel gpt | |
# Create partitions (--script to not ask) | |
# | |
# We create the 1MB BIOS boot partition at the front. | |
# | |
# Note we use "MB" instead of "MiB" because otherwise `--align optimal` has no effect; | |
# as per documentation https://www.gnu.org/software/parted/manual/html_node/unit.html#unit: | |
# > Note that as of parted-2.4, when you specify start and/or end values using IEC | |
# > binary units like "MiB", "GiB", "TiB", etc., parted treats those values as exact | |
# | |
# Note: When using `mkpart` on GPT, as per | |
# https://www.gnu.org/software/parted/manual/html_node/mkpart.html#mkpart | |
# the first argument to `mkpart` is not a `part-type`, but the GPT partition name: | |
# ... part-type is one of 'primary', 'extended' or 'logical', and may be specified only with 'msdos' or 'dvh' partition tables. | |
# A name must be specified for a 'gpt' partition table. | |
# GPT partition names are limited to 36 UTF-16 chars, see https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_entries_(LBA_2-33). | |
parted --script --align optimal "$DISK0" -- mklabel gpt mkpart 'BIOS-boot-partition' 1MB 2MB set 1 bios_grub on mkpart 'data-partition' 2MB '100%' | |
parted --script --align optimal "$DISK1" -- mklabel gpt mkpart 'BIOS-boot-partition' 1MB 2MB set 1 bios_grub on mkpart 'data-partition' 2MB '100%' | |
# Relaod partitions | |
partprobe | |
# Wait for all devices to exist | |
udevadm settle --timeout=5 --exit-if-exists="$DISK0$PART1" | |
udevadm settle --timeout=5 --exit-if-exists="$DISK0$PART2" | |
udevadm settle --timeout=5 --exit-if-exists="$DISK0$PART1" | |
udevadm settle --timeout=5 --exit-if-exists="$DISK0$PART2" | |
# Wipe any previous RAID signatures | |
mdadm --zero-superblock "$DISK0$PART2" | |
mdadm --zero-superblock "$DISK1$PART2" | |
# Create RAIDs | |
# Note that during creating and boot-time assembly, mdadm cares about the | |
# host name, and the existence and contents of `mdadm.conf`! | |
# This also affects the names appearing in /dev/md/ being different | |
# before and after reboot in general (but we take extra care here | |
# to pass explicit names, and set HOMEHOST for the rebooting system further | |
# down, so that the names appear the same). | |
# Almost all details of this are explained in | |
# https://bugzilla.redhat.com/show_bug.cgi?id=606481#c14 | |
# and the followup comments by Doug Ledford. | |
mdadm --create --run --verbose /dev/md0 --level=1 --raid-devices=2 --homehost=hetzner --name=root0 "$DISK0$PART2" "$DISK1$PART2" | |
# Wipe filesystem signatures that might be on the RAID from some | |
# possibly existing older use of the disks (RAID creation does not do that). | |
# See https://serverfault.com/questions/911370/why-does-mdadm-zero-superblock-preserve-file-system-information | |
wipefs -a /dev/md0 | |
# Disable RAID recovery. We don't want this to slow down machine provisioning | |
# in the rescue mode. It can run in normal operation after reboot. | |
echo 0 > /proc/sys/dev/raid/speed_limit_max | |
# LVM | |
# PVs | |
pvcreate /dev/md0 | |
# VGs | |
vgcreate vg0 /dev/md0 | |
# LVs (--yes to automatically wipe detected file system signatures) | |
lvcreate --yes --extents 95%FREE -n root0 vg0 # 5% slack space | |
# Filesystems (-F to not ask on preexisting FS) | |
mkfs.ext4 -F -L root /dev/mapper/vg0-root0 | |
# Creating file systems changes their UUIDs. | |
# Trigger udev so that the entries in /dev/disk/by-uuid get refreshed. | |
# `nixos-generate-config` depends on those being up-to-date. | |
# See https://github.com/NixOS/nixpkgs/issues/62444 | |
udevadm trigger | |
# Wait for FS labels to appear | |
udevadm settle --timeout=5 --exit-if-exists=/dev/disk/by-label/root | |
# NixOS pre-installation mounts | |
# Mount target root partition | |
mount /dev/disk/by-label/root /mnt | |
# Installing nix | |
# disable IPv6 for apt-get, because it gets stuck on 0% | |
# see https://unix.stackexchange.com/questions/9940/convince-apt-get-not-to-use-ipv6-method | |
echo 'Acquire::ForceIPv4 "true";' | tee /etc/apt/apt.conf.d/99force-ipv4 | |
# Install nix requires `sudo`; the Hetzner rescue mode doesn't have it. | |
apt-get install -y sudo | |
# Allow installing nix as root, see | |
# https://github.com/NixOS/nix/issues/936#issuecomment-475795730 | |
mkdir -p /etc/nix | |
echo "build-users-group =" > /etc/nix/nix.conf | |
curl https://nixos.org/nix/install | sh | |
set +u +x # sourcing this may refer to unset variables that we have no control over | |
. $HOME/.nix-profile/etc/profile.d/nix.sh | |
set -u -x | |
nix-channel --add https://nixos.org/channels/nixos-19.03 nixpkgs | |
nix-channel --update | |
# Getting NixOS installation tools | |
nix-env -iE "_: with import <nixpkgs/nixos> { configuration = {}; }; with config.system.build; [ nixos-generate-config nixos-install nixos-enter manual.manpages ]" | |
nixos-generate-config --root /mnt | |
# On the Hetzner rescue mode, the default Internet interface is called `eth0`. | |
# Find what its name will be under NixOS, which uses stable interface names. | |
# See https://major.io/2015/08/21/understanding-systemds-predictable-network-device-names/#comment-545626 | |
INTERFACE=$(udevadm info -e | grep -A 11 ^P.*eth0 | grep -o -E 'ID_NET_NAME_PATH=\w+' | cut -d= -f2) | |
echo "Determined INTERFACE as $INTERFACE" | |
# Determine our Internet IP by checking which route would be taken. | |
# The `ip route get` output on Hetzner looks like: | |
# # ip route get 8.8.8.8 | |
# 8.8.8.8 via 1.2.3.161 dev eth0 src 1.2.3.165 | |
# cache | |
IP_V4=$(ip route get 8.8.8.8 | head -1 | cut -d' ' -f7) | |
echo "Determined IP_V4 as $IP_V4" | |
# Determine Internet IPv6 by checking route, and using ::1 | |
# (because Hetzner rescue mode uses ::2 by default). | |
# The `ip -6 route get` output on Hetzner looks like: | |
# # ip -6 route get 2001:4860:4860:0:0:0:0:8888 | |
# 2001:4860:4860::8888 via fe80::1 dev eth0 src 2a01:4f8:151:62aa::2 metric 1024 pref medium | |
IP_V6="$(ip route get 2001:4860:4860:0:0:0:0:8888 | head -1 | cut -d' ' -f7 | cut -d: -f1-4)::1" | |
echo "Determined IP_V6 as $IP_V6" | |
# From https://stackoverflow.com/questions/1204629/how-do-i-get-the-default-gateway-in-linux-given-the-destination/15973156#15973156 | |
read _ _ DEFAULT_GATEWAY _ < <(ip route list match 0/0); echo "$DEFAULT_GATEWAY" | |
echo "Determined DEFAULT_GATEWAY as $DEFAULT_GATEWAY" | |
# Generate `configuration.nix`. Note that we splice in shell variables. | |
cat > /mnt/etc/nixos/configuration.nix <<EOF | |
{ config, pkgs, ... }: | |
{ | |
imports = | |
[ # Include the results of the hardware scan. | |
./hardware-configuration.nix | |
]; | |
# Use GRUB2 as the boot loader. | |
# We don't use systemd-boot because Hetzner uses BIOS legacy boot. | |
boot.loader.systemd-boot.enable = true; | |
boot.loader.grub = { | |
enable = true; | |
efiSupport = false; | |
devices = [ "$DISK0" "$DISK1" ]; | |
}; | |
networking.hostName = "hetzner"; | |
# The mdadm RAID1s were created with 'mdadm --create ... --homehost=hetzner', | |
# but the hostname for each machine may be different, and mdadm's HOMEHOST | |
# setting defaults to '<system>' (using the system hostname). | |
# This results mdadm considering such disks as "foreign" as opposed to | |
# "local", and showing them as e.g. '/dev/md/hetzner:data0' | |
# instead of '/dev/md/data0'. | |
# This is mdadm's protection against accidentally putting a RAID disk | |
# into the wrong machine and corrupting data by accidental sync, see | |
# https://bugzilla.redhat.com/show_bug.cgi?id=606481#c14 and onward. | |
# We set the HOMEHOST manually go get the short '/dev/md' names, | |
# and so that things look and are configured the same on all such | |
# machines irrespective of host names. | |
# We do not worry about plugging disks into the wrong machine because | |
# we will never exchange disks between machines. | |
environment.etc."mdadm.conf".text = '' | |
HOMEHOST hetzner | |
''; | |
# The RAIDs are assembled in stage1, so we need to make the config | |
# available there. | |
boot.initrd.mdadmConf = config.environment.etc."mdadm.conf".text; | |
# Network (Hetzner uses static IP assignments, and we don't use HDCP here) | |
networking.useDHCP = false; | |
networking.interfaces."$INTERFACE".ipv4.addresses = [ | |
{ | |
address = "$IP_V4"; | |
prefixLength = 24; | |
} | |
]; | |
networking.interfaces."$INTERFACE".ipv6.addresses = [ | |
{ | |
address = "$IP_V6"; | |
prefixLength = 64; | |
} | |
]; | |
networking.defaultGateway = "$DEFAULT_GATEWAY"; | |
networking.defaultGateway6 = { address = "fe80::1"; interface = "$INTERFACE"; }; | |
networking.nameservers = [ "8.8.8.8" ]; | |
# Initial empty root password for easy login: | |
users.users.root.initialHashedPassword = ""; | |
services.openssh.permitRootLogin = "prohibit-password"; | |
users.users.root.openssh.authorizedKeys.keys = [ | |
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDpAECn6zU302e63SStkz96MnuW6EVu26H/JrzNZg7DNBwTXVoWiQZS56U7UYktVu8s5fTpUgjQD3Vr11nU9rx5hvDqMcEa3IDtXHEU++tVkdEYK27lP7+i3H6NWUJUs9yVJUfKqrXYwJbAEJ/uOlnE0exFALZo7cESMQvN2S3c8+uOWbp/edTmrUWRtRMszln6TCWes4bRDoto9LZWtLbskoMOQnednXfv/94LrX5L7EVb59p5U6XRQAujTHJ96m4HEmjCen2sKV7epoBVw8p6o0hNXn7idITVI3iupKRv0wHQdbWTI9YHDmm3TiSM0kDQ+eR7g9ZwVgnhN8MLFDfr [email protected]" | |
]; | |
services.openssh.enable = true; | |
# This value determines the NixOS release with which your system is to be | |
# compatible, in order to avoid breaking some software such as database | |
# servers. You should change this only after NixOS release notes say you | |
# should. | |
system.stateVersion = "19.03"; # Did you read the comment? | |
} | |
EOF | |
# Install NixOS | |
PATH="$PATH" NIX_PATH="$NIX_PATH" `which nixos-install` --no-root-passwd --root /mnt --max-jobs 40 | |
reboot |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment