This commit is contained in:
Niklas Gollenstede
2022-05-09 13:20:43 +02:00
parent 5e9cc7ce34
commit 1f72d9bf26
34 changed files with 1714 additions and 0 deletions

View File

@ -0,0 +1,34 @@
# Host Setup Scripts
This is a library of bash functions, mostly for NixOS system installation.
The (paths to these) scripts are meant to me passed in the `scripts` argument to [`mkSystemsFalke`](../flakes.nix#mkSystemsFalke), which makes their functions available in the per-host `devShells`/`apps`.
Host-specific nix variables are available to the bash functions as `@{...}` through [`substituteImplicit`](../scripts.nix#substituteImplicit) with the respective host as root context.
With the functions from here, adding a simple three-liner can be enough to do a completely automated NixOS installation:
```bash
function install-system {( set -eu # 1: blockDev
prepare-installer "$@"
do-disk-setup "$1"
install-system-to $mnt prompt=true @{config.th.minify.topLevel:-}
)}
```
# `install-system` Documentation
The above function performs the mostly automated installation of any `$HOST` from [`../../hosts/`](../../hosts/) to the local disk(s) (or image file(s)) `$DISK`.
On a NixOS host, this script can be run by root as: `#` `( cd /etc/nixos/ && nix run .#"$HOST" -- install-system "$DISK" )`.
Doing an installation on non-NixOS (but Linux), where nix isn't installed for root, is a bit of a hack, but works as well.
In this case, all `nix` commands will be run as `$SUDO_USER`, but this script and some other user-owned (or user-generated) code will (need to) be run as root.
If that is acceptable, run with `sudo` as first argument: `$` `( cd /etc/nixos/ && nix run .#"$HOST" -- sudo install-system "$DISK" )` (And then maybe `sudo bash -c 'chown $SUDO_USER: '"$DISK"` afterwards.)
The `nix run` in the above commands substitutes a number of `@{`-prefixed variables based on the `$HOST` name and its configuration from [`../hosts/`](../hosts/), and then sources this script and calls the `install-system` function.
If `$DISK` points to something in `/dev/`, then it is directly formatted and written to as block device, otherwise `$DISK` is (re-)created as raw image and then used as loop device.
For hosts that install to multiple disks, pass a `:`-separated list of `<disk-name>=<path>` pairs (the name may be omitted only for the `default` disk).
Once done, the disk can be transferred -- or the image be copied -- to the final system, and should boot there.
If the host's hardware target allows, a resulting image can also be passed to [`register-vbox`](../lib/setup-scripts/maintenance.sh#register-vbox) to create a bootable VirtualBox instance for the current user.
The "Installation" section of each host's documentation should contain host specific details, if any.

View File

@ -0,0 +1,19 @@
dirname: inputs: let
getNamedScriptFiles = dir: builtins.removeAttrs (builtins.listToAttrs (map (name: let
match = builtins.match ''^(.*)[.]sh([.]md)?$'' name;
in if (match != null) then {
name = builtins.head match; value = "${dir}/${name}";
} else { name = ""; value = null; }) (builtins.attrNames (builtins.readDir dir)))) [ "" ];
inherit (inputs.config) prefix;
replacePrefix = if prefix == "wip" then (x: x) else (builtins.mapAttrs (name: path: (
builtins.toFile name (builtins.replaceStrings
[ "@{config.wip." "@{#config.wip." "@{!config.wip." ]
[ "@{config.${prefix}." "@{#config.${prefix}." "@{!config.${prefix}." ]
(builtins.readFile path)
)
)));
in replacePrefix (getNamedScriptFiles dirname)

154
lib/setup-scripts/disk.sh Normal file
View File

@ -0,0 +1,154 @@
##
# Disk Partitioning and Formatting
##
## Prepares the disks of the target system for the copying of files.
function do-disk-setup { # 1: diskPaths
mnt=/tmp/nixos-install-@{config.networking.hostName} ; mkdir -p "$mnt" ; prepend_trap "rmdir $mnt" EXIT # »mnt=/run/user/0/...« would be more appropriate, but »nixos-install« does not like the »700« permissions on »/run/user/0«
partition-disks "$1"
# ... block layers would go here ...
source @{config.wip.installer.postPartitionCommands!writeText.postPartitionCommands}
format-partitions
source @{config.wip.installer.postFormatCommands!writeText.postFormatCommands}
prepend_trap "unmount-system $mnt" EXIT ; mount-system $mnt
source @{config.wip.installer.postMountCommands!writeText.postMountCommands}
}
## Partitions all »config.installer.disks« to ensure that all (correctly) specified »{config.installer.partitions« exist.
function partition-disks { { # 1: diskPaths
beQuiet=/dev/null ; if [[ ${debug:=} ]] ; then beQuiet=/dev/stdout ; fi
declare -g -A blockDevs=( ) # this ends up in the caller's scope
local path ; for path in ${1/:/ } ; do
name=${path/=*/} ; if [[ $name != "$path" ]] ; then path=${path/$name=/} ; else name=primary ; fi
if [[ ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name specified more than once. Duplicate definition: $path" ; exit 1 ; fi
blockDevs[$name]=$path
done
local name ; for name in "@{!config.wip.installer.disks!attrsAsBashEvalSets[@]}" ; do
if [[ ! ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name not provided" ; exit 1 ; fi
if [[ ! ${blockDevs[$name]} =~ ^(/dev/.*)$ ]] ; then
local outFile=${blockDevs[$name]} ; ( set -eu
eval "@{config.wip.installer.disks!attrsAsBashEvalSets[$name]}" # _size
install -o root -g root -m 640 -T /dev/null "$outFile" && fallocate -l "$_size" "$outFile"
) && blockDevs[$name]=$(losetup --show -f "$outFile") && prepend_trap "losetup -d ${blockDevs[$name]}" EXIT # NOTE: this must not be inside a sub-shell!
else
if [[ ! "$(blockdev --getsize64 "${blockDevs[$name]}")" ]] ; then echo "Block device $name does not exist at ${blockDevs[$name]}" ; exit 1 ; fi
blockDevs[$name]=$(realpath "${blockDevs[$name]}")
fi
done
} ; ( set -eu
for name in "@{!config.wip.installer.disks!attrsAsBashEvalSets[@]}" ; do (
eval "@{config.wip.installer.disks!attrsAsBashEvalSets[$name]}" # _name ; _size ; _serial ; _alignment ; _mbrParts ; _extraFDiskCommands
if [[ $_serial ]] ; then
actual=$(udevadm info --query=property --name="${blockDevs[$name]}" | grep -oP 'ID_SERIAL_SHORT=\K.*')
if [[ $_serial != "$actual" ]] ; then echo "Block device ${blockDevs[$name]} does not match the serial declared for $name" ; exit 1 ; fi
fi
sgdisk=( --zap-all ) # delete existing part tables
for partDecl in "@{config.wip.installer.partitionList!listAsBashEvalSets[@]}" ; do
eval "$partDecl" # _name ; _disk ; _type ; _size ; _index
if [[ $_disk != "$name" ]] ; then exit ; fi # i.e. continue
if [[ $_position =~ ^[0-9]+$ ]] ; then alignment=1 ; else alignment=$_alignment ; fi # if position is an absolute number, start precisely there
sgdisk+=( -a "$alignment" -n "${_index:-0}":"$_position":+"$_size" -t 0:"$_type" -c 0:"$_name" )
done
if [[ $_mbrParts ]] ; then
sgdisk+=( --hybrid "$_mbrParts" ) # --hybrid: create MBR in addition to GPT; $_mbrParts: make these GPT part 1 MBR parts 2[3[4]]
fi
sgdisk "${sgdisk[@]}" "${blockDevs[$name]}" >$beQuiet # running all at once is much faster
if [[ $_mbrParts ]] ; then
printf "
M # edit hybrid MBR
d;1 # delete parts 1 (GPT)
# move the selected »mbrParts« to slots 1[2[3]] instead of 2[3[4]] (by re-creating part1 in the last sector, then sorting)
n;p;1 # new ; primary ; part1
$(( $(blockSectorCount "${blockDevs[$name]}") - 1)) # start (size 1sec)
x;f;r # expert mode ; fix order ; return
d;$(( (${#_mbrParts} + 1) / 2 + 1 )) # delete ; part(last)
# create GPT part (spanning primary GPT area) as last part
n;p;4 # new ; primary ; part4
1;33 # start ; end
t;4;ee # type ; part4 ; GPT
${_extraFDiskCommands}
p;w;q # print ; write ; quit
" | perl -pe 's/^ *| *(#.*)?$//g' | perl -pe 's/\n\n+| *; */\n/g' | fdisk "${blockDevs[$name]}" &>$beQuiet
fi
partprobe "${blockDevs[$name]}"
) ; done
sleep 1 # sometimes partitions aren't quite made available yet (TODO: wait "for udev to settle" instead?)
)}
## For each filesystem in »config.fileSystems« whose ».device« is in »/dev/disk/by-partlabel/«, this creates the specified file system on that partition.
function format-partitions {( set -eu
beQuiet=/dev/null ; if [[ ${debug:=} ]] ; then beQuiet=/dev/stdout ; fi
for fsDecl in "@{config.fileSystems!attrsAsBashEvalSets[@]}" ; do (
eval "$fsDecl" # _name ; _device ; _fsType ; _formatOptions ; ...
if [[ $_device != /dev/disk/by-partlabel/* ]] ; then exit ; fi # i.e. continue
blockDev=$(realpath "$_device") ; if [[ $blockDev == /dev/sd* ]] ; then
blockDev=$( shopt -s extglob ; echo "${blockDev%%+([0-9])}" )
else
blockDev=$( shopt -s extglob ; echo "${blockDev%%p+([0-9])}" )
fi
if [[ ' '"${blockDevs[@]}"' ' != *' '"$blockDev"' '* ]] ; then echo "Partition alias $_device does not point at one of the target disks ${blockDevs[@]}" ; exit 1 ; fi
mkfs.${_fsType} ${_formatOptions} "${_device}" >$beQuiet
partprobe "${_device}"
) ; done
)}
## Mounts all file systems as it would happen during boot, but at path prefix »$mnt«.
function mount-system {( set -eu # 1: mnt, 2?: fstabPath
# mount --all --fstab @{config.system.build.toplevel.outPath}/etc/fstab --target-prefix "$1" -o X-mount.mkdir # (»--target-prefix« is not supported on Ubuntu 20.04)
mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel.outPath}/etc/fstab"}
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
if [[ ! $target || $target == none ]] ; then continue ; fi
options=,$options, ; options=${options//,ro,/,}
if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then continue ; fi
if ! mountpoint -q "$mnt"/"$target" ; then
mkdir -p "$mnt"/"$target"
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target"
fi
done
# Since bind mounts may depend on other mounts not only for the target (which the sort takes care of) but also for the source, do all bind mounts last. This would break if there was a different bind mountpoint within a bind-mounted target.
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
if [[ ! $target || $target == none ]] ; then continue ; fi
options=,$options, ; options=${options//,ro,/,}
if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then : ; else continue ; fi
if ! mountpoint -q "$mnt"/"$target" ; then
mkdir -p "$mnt"/"$target"
if [[ $type == overlay ]] ; then
options=${options//,workdir=/,workdir=$mnt\/} ; options=${options//,upperdir=/,upperdir=$mnt\/} # work and upper dirs must be in target, lower dirs are probably store paths
workdir=$(<<<"$options" grep -o -P ',workdir=\K[^,]+' || true) ; if [[ $workdir ]] ; then mkdir -p "$workdir" ; fi
upperdir=$(<<<"$options" grep -o -P ',upperdir=\K[^,]+' || true) ; if [[ $upperdir ]] ; then mkdir -p "$upperdir" ; fi
else
source=$mnt/$source ; if [[ ! -e $source ]] ; then mkdir -p "$source" ; fi
fi
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target"
fi
done
)}
## Unmounts all file systems (that would be mounted during boot / by »mount-system«).
function unmount-system {( set -eu # 1: mnt, 2?: fstabPath
mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel.outPath}/etc/fstab"}
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 -r | while read source target rest ; do
if [[ ! $target || $target == none ]] ; then continue ; fi
if mountpoint -q "$mnt"/"$target" ; then
umount "$mnt"/"$target"
fi
done
)}
## Given a block device path, returns the number of 512byte sectors it can hold.
function blockSectorCount { printf %s "$(( $(blockdev --getsize64 "$1") / 512 ))" ; }

View File

@ -0,0 +1,57 @@
##
# NixOS Installation
##
## Ensures that the installer gets called by root and with an argument, includes a hack to make installation work when nix isn't installed for root, and enables debugging (if requested).
function prepare-installer { # ...
beQuiet=/dev/null ; if [[ ${debug:=} ]] ; then set -x ; beQuiet=/dev/stdout ; fi
if [[ "$(id -u)" != '0' ]] ; then echo 'Script must be run in a root (e.g. in a »sudo --preserve-env=SSH_AUTH_SOCK -i«) shell.' ; exit ; fi
if [[ ${SUDO_USER:-} ]] ; then function nix {( args=("$@") ; su - "$SUDO_USER" -c "$(declare -p args)"' ; nix "${args[@]}"' )} ; fi
: ${1:?"Required: Target disk or image paths."}
if [[ $debug ]] ; then set +e ; set -E ; trap 'code= ; bash -l || code=$? ; if [[ $code ]] ; then exit $code ; fi' ERR ; fi # On error, instead of exiting straight away, open a shell to allow diagnosing/fixing the issue. Only exit if that shell reports failure (e.g. CtrlC + CtrlD). Unfortunately, the exiting has to be repeated for level of each nested sub-shells.
}
## Copies the system's dependencies to the disks mounted at »$mnt« and installs the bootloader. If »$inspect« is set, a root shell will be opened in »$mnt« afterwards.
# »$topLevel« may point to an alternative top-level dependency to install.
function install-system-to {( # 1: mnt, 2?: inspect, 3?: topLevel
mnt=$1 ; inspect=${2:-} ; topLevel=${3:-}
targetSystem=@{config.system.build.toplevel}
trap - EXIT # start with empty traps for sub-shell
for dir in dev/ sys/ run/ ; do mkdir -p $mnt/$dir ; mount tmpfs -t tmpfs $mnt/$dir ; prepend_trap "while umount -l $mnt/$dir 2>$beQuiet ; do : ; done" EXIT ; done # proc/ run/
mkdir -p -m 755 $mnt/nix/var ; mkdir -p -m 1775 $mnt/nix/store
if [[ ${SUDO_USER:-} ]] ; then chown $SUDO_USER: $mnt/nix/store $mnt/nix/var ; fi
( set -x ; time nix copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} )
ln -sT $(realpath $targetSystem) $mnt/run/current-system
mkdir -p -m 755 $mnt/nix/var/nix/profiles ; ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system
mkdir -p $mnt/etc/ ; [[ -e $mnt/etc/NIXOS ]] || touch $mnt/etc/NIXOS
if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{config.preface.hardware}"-linux ]] ; then # cross architecture installation
mkdir -p $mnt/run/binfmt ; cp -a {,$mnt}/run/binfmt/"@{config.preface.hardware}"-linux || true
# Ubuntu (by default) expects the "interpreter" at »/usr/bin/qemu-@{config.preface.hardware}-static«.
fi
if [[ ${SUDO_USER:-} ]] ; then chown -R root:root $mnt/nix ; chown :30000 $mnt/nix/store ; fi
mount -o bind /nix/store $mnt/nix/store # all the things required to _run_ the system are copied, but (may) need some more things to initially install it
code=0 ; TMPDIR=/tmp LC_ALL=C nixos-install --system ${topLevel:-$targetSystem} --no-root-passwd --no-channel-copy --root $mnt || code=$? #--debug
umount -l $mnt/nix/store
if [[ $inspect ]] ; then
if (( code != 0 )) ; then
( set +x ; echo "Something went wrong in the last step of the installation. Inspect the output above and the system mounted in CWD to decide whether it is critical. Exit the shell with 0 to proceed, or non-zero to abort." )
else
( set +x ; echo "Installation done, but the system is still mounted in CWD for inspection. Exit the shell to unmount it." )
fi
( cd $mnt ; mnt=$mnt bash -l )
fi
( mkdir -p $mnt/var/lib/systemd/timesync ; touch $mnt/var/lib/systemd/timesync/clock ) || true # save current time
)}

View File

@ -0,0 +1,40 @@
##
# NixOS Maintenance
##
## On the host and for the user it is called by, creates/registers a VirtualBox VM meant to run the shells target host. Requires the path to the target host's »diskImage« as the result of running the install script. The image file may not be deleted or moved. If »bridgeTo« is set (to a host interface name, e.g. as »eth0«), it is added as bridged network "Adapter 2" (which some hosts need).
function register-vbox {( set -eu # 1: diskImage, 2?: bridgeTo
diskImage=$1 ; bridgeTo=${2:-}
vmName="nixos-@{config.networking.hostName}"
if [[ ! -e $diskImage.vmdk ]] ; then
VBoxManage internalcommands createrawvmdk -filename $diskImage.vmdk -rawdisk $diskImage # pass-through
fi
VBoxManage createvm --name "$vmName" --register --ostype Linux26_64
VBoxManage modifyvm "$vmName" --memory 2048 --pae off --firmware efi
VBoxManage storagectl "$vmName" --name SATA --add sata --portcount 4 --bootable on --hostiocache on
VBoxManage storageattach "$vmName" --storagectl SATA --port 0 --device 0 --type hdd --medium $diskImage.vmdk
if [[ $bridgeTo ]] ; then # VBoxManage list bridgedifs
VBoxManage modifyvm "$vmName" --nic2 bridged --bridgeadapter2 $bridgeTo
fi
VBoxManage modifyvm "$vmName" --uart1 0x3F8 4 --uartmode1 server /run/user/$(id -u)/$vmName.socket # (guest sets speed)
set +x # avoid double-echoing
echo '# VM info:'
echo " VBoxManage showvminfo $vmName"
echo '# start VM:'
echo " VBoxManage startvm $vmName --type headless"
echo '# kill VM:'
echo " VBoxManage controlvm $vmName poweroff"
echo '# create TTY:'
echo " socat UNIX-CONNECT:/run/user/$(id -u)/$vmName.socket PTY,link=/run/user/$(id -u)/$vmName.pty"
echo '# connect TTY:'
echo " screen /run/user/$(id -u)/$vmName.pty"
echo '# screenshot:'
echo " ssh $(hostname) VBoxManage controlvm $vmName screenshotpng /dev/stdout | display"
)}

View File

@ -0,0 +1,17 @@
##
# Utilities
##
## Prepends a command to a trap. Especially useful fo define »finally« commands via »prepend_trap '<command>' EXIT«.
# NOTE: When calling this in a sub-shell whose parents already has traps installed, make sure to do »trap - trapName« first. On a new shell, this should be a no-op, but without it, the parent shell's traps will be added to the sub-shell as well (due to strange behavior of »trap -p« (in bash ~5.1.8)).
prepend_trap() { # 1: command, ...: trapNames
fatal() { printf "ERROR: $@\n" >&2 ; return 1 ; }
local cmd=$1 ; shift || fatal "${FUNCNAME} usage error"
local name ; for name in "$@" ; do
trap -- "$( set +x
printf '%s\n' "( ${cmd} ) || true ; "
p3() { printf '%s\n' "${3:-}" ; } ; eval "p3 $(trap -p "${name}")"
)" "${name}" || fatal "unable to add to trap ${name}"
done
} ; declare -f -t prepend_trap # required to modify DEBUG or RETURN traps