0
0
Fork 0
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-07-15 22:50:42 +03:00
nixpkgs/nixos/modules/tasks/filesystems/bcachefs.nix

351 lines
12 KiB
Nix

{
config,
lib,
pkgs,
utils,
...
}:
let
cfgScrub = config.services.bcachefs.autoScrub;
bootFs = lib.filterAttrs (
n: fs: (fs.fsType == "bcachefs") && (utils.fsNeededForBoot fs)
) config.fileSystems;
commonFunctions = ''
prompt() {
local name="$1"
printf "enter passphrase for $name: "
}
tryUnlock() {
local name="$1"
local path="$2"
local success=false
local target
local uuid=$(echo -n $path | sed -e 's,UUID=\(.*\),\1,g')
printf "waiting for device to appear $path"
for try in $(seq 10); do
if [ -e $path ]; then
target=$(readlink -f $path)
success=true
break
else
target=$(blkid --uuid $uuid)
if [ $? == 0 ]; then
success=true
break
fi
fi
echo -n "."
sleep 1
done
printf "\n"
if [ $success == true ]; then
path=$target
fi
if bcachefs unlock -c $path > /dev/null 2> /dev/null; then # test for encryption
prompt $name
until bcachefs unlock $path 2> /dev/null; do # repeat until successfully unlocked
printf "unlocking failed!\n"
prompt $name
done
printf "unlocking successful.\n"
else
echo "Cannot unlock device $uuid with path $path" >&2
fi
}
'';
# we need only unlock one device manually, and cannot pass multiple at once
# remove this adaptation when bcachefs implements mounting by filesystem uuid
# also, implement automatic waiting for the constituent devices when that happens
# bcachefs does not support mounting devices with colons in the path, ergo we don't (see #49671)
firstDevice = fs: lib.head (lib.splitString ":" fs.device);
useClevis =
fs:
config.boot.initrd.clevis.enable
&& (lib.hasAttr (firstDevice fs) config.boot.initrd.clevis.devices);
openCommand =
name: fs:
if useClevis fs then
''
if clevis decrypt < /etc/clevis/${firstDevice fs}.jwe | bcachefs unlock ${firstDevice fs}
then
printf "unlocked ${name} using clevis\n"
else
printf "falling back to interactive unlocking...\n"
tryUnlock ${name} ${firstDevice fs}
fi
''
else
''
tryUnlock ${name} ${firstDevice fs}
'';
mkUnits =
prefix: name: fs:
let
mountUnit = "${utils.escapeSystemdPath (prefix + (lib.removeSuffix "/" fs.mountPoint))}.mount";
device = firstDevice fs;
mkDeviceUnit = device: "${utils.escapeSystemdPath device}.device";
deviceUnit = mkDeviceUnit device;
extractProperty =
prop: options: (map (lib.removePrefix "${prop}=") (builtins.filter (lib.hasPrefix prop) options));
mkMountUnit = path: "${utils.escapeSystemdPath path}.mount";
normalizeUnits =
unit:
if lib.hasPrefix "/dev/" unit then
mkDeviceUnit unit
else if lib.hasPrefix "/" unit then
mkMountUnit unit
else
unit;
requiredUnits = map normalizeUnits (extractProperty "x-systemd.requires" fs.options);
wantedUnits = map normalizeUnits (extractProperty "x-systemd.wants" fs.options);
in
{
name = "unlock-bcachefs-${utils.escapeSystemdPath fs.mountPoint}";
value = {
description = "Unlock bcachefs for ${fs.mountPoint}";
requiredBy = [ mountUnit ];
after = [ deviceUnit ] ++ requiredUnits ++ wantedUnits;
before = [
mountUnit
"shutdown.target"
];
bindsTo = [ deviceUnit ];
requires = requiredUnits;
wants = wantedUnits;
conflicts = [ "shutdown.target" ];
unitConfig.DefaultDependencies = false;
serviceConfig = {
Type = "oneshot";
ExecCondition = "${pkgs.bcachefs-tools}/bin/bcachefs unlock -c \"${device}\"";
Restart = "on-failure";
RestartMode = "direct";
# Ideally, this service would lock the key on stop.
# As is, RemainAfterExit doesn't accomplish anything.
RemainAfterExit = true;
};
script =
let
unlock = ''${pkgs.bcachefs-tools}/bin/bcachefs unlock "${device}"'';
unlockInteractively = ''${config.boot.initrd.systemd.package}/bin/systemd-ask-password --timeout=0 "enter passphrase for ${name}" | exec ${unlock}'';
in
if useClevis fs then
''
if ${config.boot.initrd.clevis.package}/bin/clevis decrypt < "/etc/clevis/${device}.jwe" | ${unlock}
then
printf "unlocked ${name} using clevis\n"
else
printf "falling back to interactive unlocking...\n"
${unlockInteractively}
fi
''
else
''
${unlockInteractively}
'';
};
};
assertions = [
{
assertion =
let
kernel = config.boot.kernelPackages.kernel;
in
(
kernel.kernelAtLeast "6.7"
|| (lib.elem (kernel.structuredExtraConfig.BCACHEFS_FS or null) [
lib.kernel.module
lib.kernel.yes
(lib.kernel.option lib.kernel.yes)
])
);
message = "Linux 6.7-rc1 at minimum or a custom linux kernel with bcachefs support is required";
}
];
in
{
options.services.bcachefs.autoScrub = {
enable = lib.mkEnableOption "regular bcachefs scrub";
fileSystems = lib.mkOption {
type = lib.types.listOf lib.types.path;
example = [ "/" ];
description = ''
List of paths to bcachefs filesystems to regularly call {command}`bcachefs scrub` on.
Defaults to all mount points with bcachefs filesystems.
'';
};
interval = lib.mkOption {
default = "monthly";
type = lib.types.str;
example = "weekly";
description = ''
Systemd calendar expression for when to scrub bcachefs filesystems.
The recommended period is a month but could be less.
See
{manpage}`systemd.time(7)`
for more information on the syntax.
'';
};
};
config = lib.mkIf (config.boot.supportedFilesystems.bcachefs or false) (
lib.mkMerge [
{
inherit assertions;
# needed for systemd-remount-fs
system.fsPackages = [ pkgs.bcachefs-tools ];
# FIXME: Remove this line when the LTS (default) kernel is at least version 6.7
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
services.udev.packages = [ pkgs.bcachefs-tools ];
systemd = {
packages = [ pkgs.bcachefs-tools ];
services = lib.mapAttrs' (mkUnits "") (
lib.filterAttrs (n: fs: (fs.fsType == "bcachefs") && (!utils.fsNeededForBoot fs)) config.fileSystems
);
};
}
(lib.mkIf ((config.boot.initrd.supportedFilesystems.bcachefs or false) || (bootFs != { })) {
inherit assertions;
boot.initrd.availableKernelModules =
[
"bcachefs"
"sha256"
]
++ lib.optionals (config.boot.kernelPackages.kernel.kernelOlder "6.15") [
# chacha20 and poly1305 are required only for decryption attempts
# kernel 6.15 uses kernel api libraries for poly1305/chacha20: 4bf4b5046de0ef7f9dc50f3a9ef8a6dcda178a6d
# kernel 6.16 removes poly1305: ceef731b0e22df80a13d67773ae9afd55a971f9e
"poly1305"
"chacha20"
];
boot.initrd.systemd.extraBin = {
# do we need this? boot/systemd.nix:566 & boot/systemd/initrd.nix:357
"bcachefs" = "${pkgs.bcachefs-tools}/bin/bcachefs";
"mount.bcachefs" = "${pkgs.bcachefs-tools}/bin/mount.bcachefs";
};
boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
copy_bin_and_libs ${pkgs.bcachefs-tools}/bin/bcachefs
copy_bin_and_libs ${pkgs.bcachefs-tools}/bin/mount.bcachefs
'';
boot.initrd.extraUtilsCommandsTest = lib.mkIf (!config.boot.initrd.systemd.enable) ''
$out/bin/bcachefs version
'';
boot.initrd.postDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable) (
commonFunctions + lib.concatStrings (lib.mapAttrsToList openCommand bootFs)
);
boot.initrd.systemd.services = lib.mapAttrs' (mkUnits "/sysroot") bootFs;
})
(lib.mkIf (cfgScrub.enable) {
assertions = [
{
assertion = lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.14";
message = "Bcachefs scrubbing is supported from kernel version 6.14 or later.";
}
{
assertion = cfgScrub.enable -> (cfgScrub.fileSystems != [ ]);
message = ''
If 'services.bcachefs.autoScrub' is enabled, you need to have at least one
bcachefs file system mounted via 'fileSystems' or specify a list manually
in 'services.bcachefs.autoScrub.fileSystems'.
'';
}
];
# This will remove duplicated units from either having a filesystem mounted multiple
# time, or additionally mounted subvolumes, as well as having a filesystem span
# multiple devices (provided the same device is used to mount said filesystem).
services.bcachefs.autoScrub.fileSystems =
let
isDeviceInList = list: device: builtins.filter (e: e.device == device) list != [ ];
uniqueDeviceList = lib.foldl' (
acc: e: if isDeviceInList acc e.device then acc else acc ++ [ e ]
) [ ];
in
lib.mkDefault (
map (e: e.mountPoint) (
uniqueDeviceList (
lib.mapAttrsToList (name: fs: {
mountPoint = fs.mountPoint;
device = fs.device;
}) (lib.filterAttrs (name: fs: fs.fsType == "bcachefs") config.fileSystems)
)
)
);
systemd.timers =
let
scrubTimer =
fs:
let
fs' = utils.escapeSystemdPath fs;
in
lib.nameValuePair "bcachefs-scrub-${fs'}" {
description = "regular bcachefs scrub timer on ${fs}";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfgScrub.interval;
AccuracySec = "1d";
Persistent = true;
};
};
in
lib.listToAttrs (map scrubTimer cfgScrub.fileSystems);
systemd.services =
let
scrubService =
fs:
let
fs' = utils.escapeSystemdPath fs;
in
lib.nameValuePair "bcachefs-scrub-${fs'}" {
description = "bcachefs scrub on ${fs}";
# scrub prevents suspend2ram or proper shutdown
conflicts = [
"shutdown.target"
"sleep.target"
];
before = [
"shutdown.target"
"sleep.target"
];
script = "${lib.getExe pkgs.bcachefs-tools} data scrub ${fs}";
serviceConfig = {
Type = "oneshot";
Nice = 19;
IOSchedulingClass = "idle";
};
};
in
lib.listToAttrs (map scrubService cfgScrub.fileSystems);
})
]
);
meta = {
inherit (pkgs.bcachefs-tools.meta) maintainers;
};
}