193 lines
9.0 KiB
Nix
193 lines
9.0 KiB
Nix
{...}: {
|
|
# Every filesystem on shadow lives here, hand-maintained.
|
|
#
|
|
# hardware.nix is generated with `--no-filesystems` precisely so it never
|
|
# emits fileSystems/swapDevices entries of its own. That keeps regeneration
|
|
# (`nix run .#sync-hardware`) safe: re-running it can never clobber these
|
|
# definitions, nor produce duplicates for whichever data drives happened to
|
|
# be mounted at the time it ran.
|
|
flake.nixosModules.shadowDisks = {pkgs, ...}: let
|
|
# Secondary data drives, addressed by filesystem label.
|
|
#
|
|
# nofail on all of them: it stops a missing or dying disk from becoming a
|
|
# failed boot dependency that drops the machine into emergency mode.
|
|
#
|
|
# On-disk ownership is already ahmza mode 755 on all three, so no uid mapping
|
|
# is needed — ext4 stores ownership in the filesystem and ignores uid=/gid=.
|
|
|
|
# Spinning disks (sda/sdc, both WD, rotational=1). noauto +
|
|
# x-systemd.automount makes systemd generate an .automount unit and mount on
|
|
# first access to the path rather than at boot, so they stay parked until
|
|
# something actually touches them; idle-timeout unmounts them again once
|
|
# quiet so they can spin back down.
|
|
#
|
|
# Note the idle unmount fails with "target is busy" while any shell has its
|
|
# cwd inside the mount. That is harmless — it simply stays mounted and retries.
|
|
spinningDrive = label: {
|
|
device = "/dev/disk/by-label/${label}";
|
|
fsType = "ext4";
|
|
options = [
|
|
"nofail"
|
|
"noauto"
|
|
"x-systemd.automount"
|
|
"x-systemd.idle-timeout=600"
|
|
"x-systemd.device-timeout=10"
|
|
];
|
|
};
|
|
|
|
# Always-on drive. Used for the games library: it is an SSD (Samsung 870 QVO,
|
|
# rotational=0) so there is no spin-down to gain, and a Steam library is
|
|
# better off never sitting behind an automount trigger.
|
|
#
|
|
# Unrelated but worth knowing, since it looks like a mount problem: Steam's
|
|
# FHS sandbox builds its bind list at launch by iterating /*, so a top-level
|
|
# directory that does not exist when Steam starts is invisible to that
|
|
# instance for its whole lifetime, and Steam reports it as "not writable".
|
|
# /mnt only comes into existence when these entries are activated, so Steam
|
|
# must be restarted after a fresh boot-time change here.
|
|
eagerDrive = label: {
|
|
device = "/dev/disk/by-label/${label}";
|
|
fsType = "ext4";
|
|
options = [
|
|
"nofail"
|
|
"x-systemd.device-timeout=10"
|
|
];
|
|
};
|
|
|
|
# Pin the NFS server until nas.hfx.ahmza.com points at 10.10.100.4.
|
|
# Automounting keeps an unavailable NAS out of the boot path.
|
|
nfsShare = export: {
|
|
device = "10.10.100.4:${export}";
|
|
fsType = "nfs";
|
|
options = [
|
|
"nfsvers=4.2"
|
|
"noauto"
|
|
"nofail"
|
|
"_netdev"
|
|
"x-systemd.automount"
|
|
"x-systemd.idle-timeout=600"
|
|
"x-systemd.mount-timeout=10"
|
|
];
|
|
};
|
|
in {
|
|
fileSystems = {
|
|
# Boot essentials, by UUID. UUIDs rather than labels here on purpose:
|
|
# these two must resolve unambiguously, and a label is only as unique as
|
|
# the last disk someone plugged in.
|
|
"/" = {
|
|
device = "/dev/disk/by-uuid/deff63fc-1738-4cfc-a79c-06e58c78471a";
|
|
fsType = "ext4";
|
|
};
|
|
|
|
"/boot" = {
|
|
device = "/dev/disk/by-uuid/5873-AD05";
|
|
fsType = "vfat";
|
|
options = ["fmask=0077" "dmask=0077"];
|
|
};
|
|
|
|
"/mnt/dump" = spinningDrive "DUMP";
|
|
"/mnt/bak" = spinningDrive "BAK";
|
|
"/mnt/games" = eagerDrive "GAMES";
|
|
"/mnt/nas" = nfsShare "/srv/nas";
|
|
"/mnt/media" = nfsShare "/srv/media";
|
|
};
|
|
|
|
# Swap exists for hibernation, not for memory pressure — 31GiB of RAM on a
|
|
# desktop rarely needs paging. The hibernation image must fit in swap, so the
|
|
# floor is "as big as RAM"; 40GiB leaves headroom above that.
|
|
#
|
|
# NVIDIA video memory does NOT land here: with
|
|
# NVreg_PreserveVideoMemoryAllocations=1 (set by
|
|
# hardware.nvidia.powerManagement.enable) the driver spools the 3090's VRAM
|
|
# to NVreg_TemporaryFilePath, which is unset and so defaults to /tmp. That is
|
|
# only safe because /tmp is on this ext4 root — if /tmp ever becomes a tmpfs
|
|
# (boot.tmp.useTmpfs) that VRAM dump goes into RAM and hibernation breaks.
|
|
#
|
|
# On non-btrfs filesystems NixOS creates this with `dd if=/dev/zero`, so the
|
|
# file is fully allocated rather than sparse. That matters because the kernel
|
|
# writes swap through the block layer using the extent map swapon built,
|
|
# bypassing the filesystem — so the mapping has to be stable and complete.
|
|
#
|
|
# The failure is loud, not silent, though: swapon refuses a file with holes
|
|
# outright ("skipping - it appears to have holes"), so a sparse file would
|
|
# simply leave the system with no swap rather than corrupt anything. Genuine
|
|
# silent corruption is the copy-on-write case, which is why btrfs needs
|
|
# chattr +C and a NOCOW-from-creation file.
|
|
swapDevices = [
|
|
{
|
|
device = "/var/lib/swapfile";
|
|
size = 40 * 1024; # MiB
|
|
}
|
|
];
|
|
|
|
# Resuming from hibernation needs the kernel to be told where the image is,
|
|
# and for a swap *file* that means both halves below.
|
|
#
|
|
# boot.initrd.systemd.enable is true on this host, which makes this
|
|
# mandatory rather than optional: under systemd initrd the `resume=` kernel
|
|
# param is emitted ONLY from boot.resumeDevice. The legacy stage-1 script
|
|
# that scans swapDevices to find a resume device is not in play. Without
|
|
# this, hibernate succeeds and the machine then cold-boots, silently
|
|
# discarding the session.
|
|
#
|
|
# resume_offset is the physical block offset of the swapfile's first extent,
|
|
# because the kernel addresses the file by raw block, not by path. Recompute
|
|
# it with the following if the swapfile is ever recreated, resized, or moved
|
|
# — a stale value resumes from the wrong blocks:
|
|
#
|
|
# sudo filefrag -v /var/lib/swapfile | awk '$1=="0:" {gsub(/\.\./,"",$4); print $4}'
|
|
#
|
|
# filefrag reports in 4096-byte filesystem blocks and the kernel wants
|
|
# PAGE_SIZE units; on x86_64 both are 4096, so the number transfers directly.
|
|
boot.resumeDevice = "/dev/disk/by-uuid/deff63fc-1738-4cfc-a79c-06e58c78471a";
|
|
boot.kernelParams = ["resume_offset=499355648"];
|
|
|
|
# A stale resume_offset fails *silently*: the kernel finds no swsusp
|
|
# signature at the given block, logs `PM: Image not found (code -22)`, and
|
|
# cold-boots — the hibernated session is simply gone, with no error at
|
|
# hibernate time. Since the value above is hand-copied, check it on every
|
|
# activation so drift surfaces as a warning instead of as a lost session.
|
|
#
|
|
# Two known limits, both acceptable:
|
|
# * nixpkgs only recreates the swapfile when `size` changes, so on the one
|
|
# rebuild that resizes it this check still sees the old file and passes;
|
|
# it fires on the next rebuild instead. It is a drift detector, not a
|
|
# pre-flight gate.
|
|
# * Warn rather than fail: activation must not be blocked over this, and it
|
|
# is expected to read nothing at all on a fresh install where
|
|
# mkswap-var-lib-swapfile.service has not run yet.
|
|
system.activationScripts.checkResumeOffset.text = ''
|
|
swapfile=/var/lib/swapfile
|
|
want=499355648
|
|
if [ -f "$swapfile" ]; then
|
|
got=$(${pkgs.e2fsprogs}/bin/filefrag -v "$swapfile" 2>/dev/null \
|
|
| ${pkgs.gawk}/bin/awk '$1=="0:" {gsub(/\.\./,"",$4); print $4; exit}') || got=""
|
|
if [ -n "$got" ] && [ "$got" != "$want" ]; then
|
|
echo "WARNING: resume_offset is pinned to $want in modules/hosts/shadow/disks.nix," >&2
|
|
echo " but $swapfile now starts at block $got. Hibernation will cold-boot" >&2
|
|
echo " and lose the session until boot.kernelParams is updated to $got." >&2
|
|
fi
|
|
fi
|
|
'';
|
|
|
|
# This host sleeps via suspend-then-hibernate (hypridle in the dotfiles repo
|
|
# is what invokes it): S3 first so a quick return is instant, then a real
|
|
# suspend-to-disk once the machine has clearly been abandoned.
|
|
#
|
|
# Deliberately NOT hybrid-sleep, which writes the disk image up front on
|
|
# every single sleep. Measured here that cost ~59s of filesystem sync plus an
|
|
# 11GiB write before the screens even went off, and because an S3 wake
|
|
# invalidates the image the next boot always reported `PM: Image not found
|
|
# (code -22)` — full hibernation price paid every time, redeemed only on a
|
|
# power cut. suspend-then-hibernate defers that write until it is actually
|
|
# worth doing, and unlike hybrid-sleep it is a mode NVIDIA explicitly
|
|
# supports (see the system-sleep hook installed by the gpuNvidia module).
|
|
#
|
|
# HibernateDelaySec is set explicitly because systemd otherwise derives the
|
|
# handoff from battery discharge estimation, and this desktop has no system
|
|
# battery — the sole /sys/class/power_supply entry is a Logitech mouse
|
|
# reporting scope=Device, which is ignored for that purpose.
|
|
systemd.sleep.settings.Sleep.HibernateDelaySec = "30min";
|
|
};
|
|
}
|