diff options
Diffstat (limited to 'nixos/modules/tasks')
33 files changed, 5376 insertions, 0 deletions
diff --git a/nixos/modules/tasks/auto-upgrade.nix b/nixos/modules/tasks/auto-upgrade.nix new file mode 100644 index 00000000000..1404dcbaf7c --- /dev/null +++ b/nixos/modules/tasks/auto-upgrade.nix @@ -0,0 +1,228 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let cfg = config.system.autoUpgrade; + +in { + + options = { + + system.autoUpgrade = { + + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to periodically upgrade NixOS to the latest + version. If enabled, a systemd timer will run + <literal>nixos-rebuild switch --upgrade</literal> once a + day. + ''; + }; + + flake = mkOption { + type = types.nullOr types.str; + default = null; + example = "github:kloenk/nix"; + description = '' + The Flake URI of the NixOS configuration to build. + Disables the option <option>system.autoUpgrade.channel</option>. + ''; + }; + + channel = mkOption { + type = types.nullOr types.str; + default = null; + example = "https://nixos.org/channels/nixos-14.12-small"; + description = '' + The URI of the NixOS channel to use for automatic + upgrades. By default, this is the channel set using + <command>nix-channel</command> (run <literal>nix-channel + --list</literal> to see the current value). + ''; + }; + + flags = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "-I" + "stuff=/home/alice/nixos-stuff" + "--option" + "extra-binary-caches" + "http://my-cache.example.org/" + ]; + description = '' + Any additional flags passed to <command>nixos-rebuild</command>. + + If you are using flakes and use a local repo you can add + <command>[ "--update-input" "nixpkgs" "--commit-lock-file" ]</command> + to update nixpkgs. + ''; + }; + + dates = mkOption { + default = "04:40"; + type = types.str; + description = '' + Specification (in the format described by + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum></citerefentry>) of the time at + which the update will occur. + ''; + }; + + allowReboot = mkOption { + default = false; + type = types.bool; + description = '' + Reboot the system into the new generation instead of a switch + if the new generation uses a different kernel, kernel modules + or initrd than the booted system. + See <option>rebootWindow</option> for configuring the times at which a reboot is allowed. + ''; + }; + + randomizedDelaySec = mkOption { + default = "0"; + type = types.str; + example = "45min"; + description = '' + Add a randomized delay before each automatic upgrade. + The delay will be chozen between zero and this value. + This value must be a time span in the format specified by + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum></citerefentry> + ''; + }; + + rebootWindow = mkOption { + description = '' + Define a lower and upper time value (in HH:MM format) which + constitute a time window during which reboots are allowed after an upgrade. + This option only has an effect when <option>allowReboot</option> is enabled. + The default value of <literal>null</literal> means that reboots are allowed at any time. + ''; + default = null; + example = { lower = "01:00"; upper = "05:00"; }; + type = with types; nullOr (submodule { + options = { + lower = mkOption { + description = "Lower limit of the reboot window"; + type = types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}"; + example = "01:00"; + }; + + upper = mkOption { + description = "Upper limit of the reboot window"; + type = types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}"; + example = "05:00"; + }; + }; + }); + }; + + }; + + }; + + config = lib.mkIf cfg.enable { + + assertions = [{ + assertion = !((cfg.channel != null) && (cfg.flake != null)); + message = '' + The options 'system.autoUpgrade.channels' and 'system.autoUpgrade.flake' cannot both be set. + ''; + }]; + + system.autoUpgrade.flags = (if cfg.flake == null then + [ "--no-build-output" ] ++ optionals (cfg.channel != null) [ + "-I" + "nixpkgs=${cfg.channel}/nixexprs.tar.xz" + ] + else + [ "--flake ${cfg.flake}" ]); + + systemd.services.nixos-upgrade = { + description = "NixOS Upgrade"; + + restartIfChanged = false; + unitConfig.X-StopOnRemoval = false; + + serviceConfig.Type = "oneshot"; + + environment = config.nix.envVars // { + inherit (config.environment.sessionVariables) NIX_PATH; + HOME = "/root"; + } // config.networking.proxy.envVars; + + path = with pkgs; [ + coreutils + gnutar + xz.bin + gzip + gitMinimal + config.nix.package.out + config.programs.ssh.package + ]; + + script = let + nixos-rebuild = "${config.system.build.nixos-rebuild}/bin/nixos-rebuild"; + date = "${pkgs.coreutils}/bin/date"; + readlink = "${pkgs.coreutils}/bin/readlink"; + shutdown = "${pkgs.systemd}/bin/shutdown"; + upgradeFlag = optional (cfg.channel == null) "--upgrade"; + in if cfg.allowReboot then '' + ${nixos-rebuild} boot ${toString (cfg.flags ++ upgradeFlag)} + booted="$(${readlink} /run/booted-system/{initrd,kernel,kernel-modules})" + built="$(${readlink} /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})" + + ${optionalString (cfg.rebootWindow != null) '' + current_time="$(${date} +%H:%M)" + + lower="${cfg.rebootWindow.lower}" + upper="${cfg.rebootWindow.upper}" + + if [[ "''${lower}" < "''${upper}" ]]; then + if [[ "''${current_time}" > "''${lower}" ]] && \ + [[ "''${current_time}" < "''${upper}" ]]; then + do_reboot="true" + else + do_reboot="false" + fi + else + # lower > upper, so we are crossing midnight (e.g. lower=23h, upper=6h) + # we want to reboot if cur > 23h or cur < 6h + if [[ "''${current_time}" < "''${upper}" ]] || \ + [[ "''${current_time}" > "''${lower}" ]]; then + do_reboot="true" + else + do_reboot="false" + fi + fi + ''} + + if [ "''${booted}" = "''${built}" ]; then + ${nixos-rebuild} switch ${toString cfg.flags} + ${optionalString (cfg.rebootWindow != null) '' + elif [ "''${do_reboot}" != true ]; then + echo "Outside of configured reboot window, skipping." + ''} + else + ${shutdown} -r +1 + fi + '' else '' + ${nixos-rebuild} switch ${toString (cfg.flags ++ upgradeFlag)} + ''; + + startAt = cfg.dates; + }; + + systemd.timers.nixos-upgrade.timerConfig.RandomizedDelaySec = + cfg.randomizedDelaySec; + + }; + +} + diff --git a/nixos/modules/tasks/bcache.nix b/nixos/modules/tasks/bcache.nix new file mode 100644 index 00000000000..41fb7664f3d --- /dev/null +++ b/nixos/modules/tasks/bcache.nix @@ -0,0 +1,13 @@ +{ pkgs, ... }: + +{ + + environment.systemPackages = [ pkgs.bcache-tools ]; + + services.udev.packages = [ pkgs.bcache-tools ]; + + boot.initrd.extraUdevRulesCommands = '' + cp -v ${pkgs.bcache-tools}/lib/udev/rules.d/*.rules $out/ + ''; + +} diff --git a/nixos/modules/tasks/cpu-freq.nix b/nixos/modules/tasks/cpu-freq.nix new file mode 100644 index 00000000000..f1219c07c50 --- /dev/null +++ b/nixos/modules/tasks/cpu-freq.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cpupower = config.boot.kernelPackages.cpupower; + cfg = config.powerManagement; +in + +{ + ###### interface + + options.powerManagement = { + + # TODO: This should be aliased to powerManagement.cpufreq.governor. + # https://github.com/NixOS/nixpkgs/pull/53041#commitcomment-31825338 + cpuFreqGovernor = mkOption { + type = types.nullOr types.str; + default = null; + example = "ondemand"; + description = '' + Configure the governor used to regulate the frequency of the + available CPUs. By default, the kernel configures the + performance governor, although this may be overwritten in your + hardware-configuration.nix file. + + Often used values: "ondemand", "powersave", "performance" + ''; + }; + + cpufreq = { + + max = mkOption { + type = types.nullOr types.ints.unsigned; + default = null; + example = 2200000; + description = '' + The maximum frequency the CPU will use. Defaults to the maximum possible. + ''; + }; + + min = mkOption { + type = types.nullOr types.ints.unsigned; + default = null; + example = 800000; + description = '' + The minimum frequency the CPU will use. + ''; + }; + }; + + }; + + + ###### implementation + + config = + let + governorEnable = cfg.cpuFreqGovernor != null; + maxEnable = cfg.cpufreq.max != null; + minEnable = cfg.cpufreq.min != null; + enable = + !config.boot.isContainer && + (governorEnable || maxEnable || minEnable); + in + mkIf enable { + + boot.kernelModules = optional governorEnable "cpufreq_${cfg.cpuFreqGovernor}"; + + environment.systemPackages = [ cpupower ]; + + systemd.services.cpufreq = { + description = "CPU Frequency Setup"; + after = [ "systemd-modules-load.service" ]; + wantedBy = [ "multi-user.target" ]; + path = [ cpupower pkgs.kmod ]; + unitConfig.ConditionVirtualization = false; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = "yes"; + ExecStart = "${cpupower}/bin/cpupower frequency-set " + + optionalString governorEnable "--governor ${cfg.cpuFreqGovernor} " + + optionalString maxEnable "--max ${toString cfg.cpufreq.max} " + + optionalString minEnable "--min ${toString cfg.cpufreq.min} "; + SuccessExitStatus = "0 237"; + }; + }; + + }; +} diff --git a/nixos/modules/tasks/encrypted-devices.nix b/nixos/modules/tasks/encrypted-devices.nix new file mode 100644 index 00000000000..06117d19af4 --- /dev/null +++ b/nixos/modules/tasks/encrypted-devices.nix @@ -0,0 +1,87 @@ +{ config, lib, ... }: + +with lib; + +let + fileSystems = config.system.build.fileSystems ++ config.swapDevices; + encDevs = filter (dev: dev.encrypted.enable) fileSystems; + keyedEncDevs = filter (dev: dev.encrypted.keyFile != null) encDevs; + keylessEncDevs = filter (dev: dev.encrypted.keyFile == null) encDevs; + anyEncrypted = + foldr (j: v: v || j.encrypted.enable) false encDevs; + + encryptedFSOptions = { + + options.encrypted = { + enable = mkOption { + default = false; + type = types.bool; + description = "The block device is backed by an encrypted one, adds this device as a initrd luks entry."; + }; + + blkDev = mkOption { + default = null; + example = "/dev/sda1"; + type = types.nullOr types.str; + description = "Location of the backing encrypted device."; + }; + + label = mkOption { + default = null; + example = "rootfs"; + type = types.nullOr types.str; + description = "Label of the unlocked encrypted device. Set <literal>fileSystems.<name?>.device</literal> to <literal>/dev/mapper/<label></literal> to mount the unlocked device."; + }; + + keyFile = mkOption { + default = null; + example = "/mnt-root/root/.swapkey"; + type = types.nullOr types.str; + description = '' + Path to a keyfile used to unlock the backing encrypted + device. At the time this keyfile is accessed, the + <literal>neededForBoot</literal> filesystems (see + <literal>fileSystems.<name?>.neededForBoot</literal>) + will have been mounted under <literal>/mnt-root</literal>, + so the keyfile path should usually start with "/mnt-root/". + ''; + }; + }; + }; +in + +{ + + options = { + fileSystems = mkOption { + type = with lib.types; attrsOf (submodule encryptedFSOptions); + }; + swapDevices = mkOption { + type = with lib.types; listOf (submodule encryptedFSOptions); + }; + }; + + config = mkIf anyEncrypted { + assertions = map (dev: { + assertion = dev.encrypted.label != null; + message = '' + The filesystem for ${dev.mountPoint} has encrypted.enable set to true, but no encrypted.label set + ''; + }) encDevs; + + boot.initrd = { + luks = { + devices = + builtins.listToAttrs (map (dev: { + name = dev.encrypted.label; + value = { device = dev.encrypted.blkDev; }; + }) keylessEncDevs); + forceLuksSupportInInitrd = true; + }; + postMountCommands = + concatMapStrings (dev: + "cryptsetup luksOpen --key-file ${dev.encrypted.keyFile} ${dev.encrypted.blkDev} ${dev.encrypted.label};\n" + ) keyedEncDevs; + }; + }; +} diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix new file mode 100644 index 00000000000..f3da6771197 --- /dev/null +++ b/nixos/modules/tasks/filesystems.nix @@ -0,0 +1,385 @@ +{ config, lib, pkgs, utils, ... }: + +with lib; +with utils; + +let + + addCheckDesc = desc: elemType: check: types.addCheck elemType check + // { description = "${elemType.description} (with check: ${desc})"; }; + + isNonEmpty = s: (builtins.match "[ \t\n]*" s) == null; + nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty; + + fileSystems' = toposort fsBefore (attrValues config.fileSystems); + + fileSystems = if fileSystems' ? result + then # use topologically sorted fileSystems everywhere + fileSystems'.result + else # the assertion below will catch this, + # but we fall back to the original order + # anyway so that other modules could check + # their assertions too + (attrValues config.fileSystems); + + specialFSTypes = [ "proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts" ]; + + nonEmptyWithoutTrailingSlash = addCheckDesc "non-empty without trailing slash" types.str + (s: isNonEmpty s && (builtins.match ".+/" s) == null); + + coreFileSystemOpts = { name, config, ... }: { + + options = { + mountPoint = mkOption { + example = "/mnt/usb"; + type = nonEmptyWithoutTrailingSlash; + description = "Location of the mounted the file system."; + }; + + device = mkOption { + default = null; + example = "/dev/sda"; + type = types.nullOr nonEmptyStr; + description = "Location of the device."; + }; + + fsType = mkOption { + default = "auto"; + example = "ext3"; + type = nonEmptyStr; + description = "Type of the file system."; + }; + + options = mkOption { + default = [ "defaults" ]; + example = [ "data=journal" ]; + description = "Options used to mount the file system."; + type = types.listOf nonEmptyStr; + }; + + depends = mkOption { + default = [ ]; + example = [ "/persist" ]; + type = types.listOf nonEmptyWithoutTrailingSlash; + description = '' + List of paths that should be mounted before this one. This filesystem's + <option>device</option> and <option>mountPoint</option> are always + checked and do not need to be included explicitly. If a path is added + to this list, any other filesystem whose mount point is a parent of + the path will be mounted before this filesystem. The paths do not need + to actually be the <option>mountPoint</option> of some other filesystem. + ''; + }; + + }; + + config = { + mountPoint = mkDefault name; + device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType); + }; + + }; + + fileSystemOpts = { config, ... }: { + + options = { + + label = mkOption { + default = null; + example = "root-partition"; + type = types.nullOr nonEmptyStr; + description = "Label of the device (if any)."; + }; + + autoFormat = mkOption { + default = false; + type = types.bool; + description = '' + If the device does not currently contain a filesystem (as + determined by <command>blkid</command>, then automatically + format it with the filesystem type specified in + <option>fsType</option>. Use with caution. + ''; + }; + + formatOptions = mkOption { + default = ""; + type = types.str; + description = '' + If <option>autoFormat</option> option is set specifies + extra options passed to mkfs. + ''; + }; + + autoResize = mkOption { + default = false; + type = types.bool; + description = '' + If set, the filesystem is grown to its maximum size before + being mounted. (This is typically the size of the containing + partition.) This is currently only supported for ext2/3/4 + filesystems that are mounted during early boot. + ''; + }; + + noCheck = mkOption { + default = false; + type = types.bool; + description = "Disable running fsck on this filesystem."; + }; + + }; + + config = let + defaultFormatOptions = + # -F needed to allow bare block device without partitions + if (builtins.substring 0 3 config.fsType) == "ext" then "-F" + # -q needed for non-interactive operations + else if config.fsType == "jfs" then "-q" + # (same here) + else if config.fsType == "reiserfs" then "-q" + else null; + in { + options = mkIf config.autoResize [ "x-nixos.autoresize" ]; + formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions); + }; + + }; + + # Makes sequence of `specialMount device mountPoint options fsType` commands. + # `systemMount` should be defined in the sourcing script. + makeSpecialMounts = mounts: + pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: '' + specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}" + '') mounts); + +in + +{ + + ###### interface + + options = { + + fileSystems = mkOption { + default = {}; + example = literalExpression '' + { + "/".device = "/dev/hda1"; + "/data" = { + device = "/dev/hda2"; + fsType = "ext3"; + options = [ "data=journal" ]; + }; + "/bigdisk".label = "bigdisk"; + } + ''; + type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]); + description = '' + The file systems to be mounted. It must include an entry for + the root directory (<literal>mountPoint = "/"</literal>). Each + entry in the list is an attribute set with the following fields: + <literal>mountPoint</literal>, <literal>device</literal>, + <literal>fsType</literal> (a file system type recognised by + <command>mount</command>; defaults to + <literal>"auto"</literal>), and <literal>options</literal> + (the mount options passed to <command>mount</command> using the + <option>-o</option> flag; defaults to <literal>[ "defaults" ]</literal>). + + Instead of specifying <literal>device</literal>, you can also + specify a volume label (<literal>label</literal>) for file + systems that support it, such as ext2/ext3 (see <command>mke2fs + -L</command>). + ''; + }; + + system.fsPackages = mkOption { + internal = true; + default = [ ]; + description = "Packages supplying file system mounters and checkers."; + }; + + boot.supportedFilesystems = mkOption { + default = [ ]; + example = [ "btrfs" ]; + type = types.listOf types.str; + description = "Names of supported filesystem types."; + }; + + boot.specialFileSystems = mkOption { + default = {}; + type = types.attrsOf (types.submodule coreFileSystemOpts); + internal = true; + description = '' + Special filesystems that are mounted very early during boot. + ''; + }; + + }; + + + ###### implementation + + config = { + + assertions = let + ls = sep: concatMapStringsSep sep (x: x.mountPoint); + notAutoResizable = fs: fs.autoResize && !(hasPrefix "ext" fs.fsType || fs.fsType == "f2fs"); + in [ + { assertion = ! (fileSystems' ? cycle); + message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}"; + } + { assertion = ! (any notAutoResizable fileSystems); + message = let + fs = head (filter notAutoResizable fileSystems); + in + "Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = \"${fs.fsType}\"':${if fs.fsType == "auto" then " fsType has to be explicitly set and" else ""} only the ext filesystems and f2fs support it."; + } + ]; + + # Export for use in other modules + system.build.fileSystems = fileSystems; + system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result; + + boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; + + # Add the mount helpers to the system path so that `mount' can find them. + system.fsPackages = [ pkgs.dosfstools ]; + + environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages; + + environment.etc.fstab.text = + let + fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" "apfs" ]; + skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck; + # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces + escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string; + swapOptions = sw: concatStringsSep "," ( + sw.options + ++ optional (sw.priority != null) "pri=${toString sw.priority}" + ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}" + ); + in '' + # This is a generated file. Do not edit! + # + # To make changes, edit the fileSystems and swapDevices NixOS options + # in your /etc/nixos/configuration.nix file. + # + # <file system> <mount point> <type> <options> <dump> <pass> + + # Filesystems. + ${concatMapStrings (fs: + (if fs.device != null then escape fs.device + else if fs.label != null then "/dev/disk/by-label/${escape fs.label}" + else throw "No device specified for mount point ‘${fs.mountPoint}’.") + + " " + escape fs.mountPoint + + " " + fs.fsType + + " " + builtins.concatStringsSep "," fs.options + + " 0" + + " " + (if skipCheck fs then "0" else + if fs.mountPoint == "/" then "1" else "2") + + "\n" + ) fileSystems} + + # Swap devices. + ${flip concatMapStrings config.swapDevices (sw: + "${sw.realDevice} none swap ${swapOptions sw}\n" + )} + ''; + + # Provide a target that pulls in all filesystems. + systemd.targets.fs = + { description = "All File Systems"; + wants = [ "local-fs.target" "remote-fs.target" ]; + }; + + systemd.services = + + # Emit systemd services to format requested filesystems. + let + formatDevice = fs: + let + mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount"; + device' = escapeSystemdPath fs.device; + device'' = "${device'}.device"; + in nameValuePair "mkfs-${device'}" + { description = "Initialisation of Filesystem ${fs.device}"; + wantedBy = [ mountPoint' ]; + before = [ mountPoint' "systemd-fsck@${device'}.service" ]; + requires = [ device'' ]; + after = [ device'' ]; + path = [ pkgs.util-linux ] ++ config.system.fsPackages; + script = + '' + if ! [ -e "${fs.device}" ]; then exit 1; fi + # FIXME: this is scary. The test could be more robust. + type=$(blkid -p -s TYPE -o value "${fs.device}" || true) + if [ -z "$type" ]; then + echo "creating ${fs.fsType} filesystem on ${fs.device}..." + mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}" + fi + ''; + unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ]; + unitConfig.DefaultDependencies = false; # needed to prevent a cycle + serviceConfig.Type = "oneshot"; + }; + in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems)) // { + # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore. + # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then. + "mount-pstore" = { + serviceConfig = { + Type = "oneshot"; + # skip on kernels without the pstore module + ExecCondition = "${pkgs.kmod}/bin/modprobe -b pstore"; + ExecStart = pkgs.writeShellScript "mount-pstore.sh" '' + set -eu + # if the pstore module is builtin it will have mounted the persistent store automatically. it may also be already mounted for other reasons. + ${pkgs.util-linux}/bin/mountpoint -q /sys/fs/pstore || ${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore + # wait up to 1.5 seconds for the backend to be registered and the files to appear. a systemd path unit cannot detect this happening; and succeeding after a restart would not start dependent units. + TRIES=15 + while [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do + if (( $TRIES )); then + sleep 0.1 + TRIES=$((TRIES-1)) + else + echo "Persistent Storage backend was not registered in time." >&2 + break + fi + done + ''; + RemainAfterExit = true; + }; + unitConfig = { + ConditionVirtualization = "!container"; + DefaultDependencies = false; # needed to prevent a cycle + }; + before = [ "systemd-pstore.service" ]; + wantedBy = [ "systemd-pstore.service" ]; + }; + }; + + systemd.tmpfiles.rules = [ + "d /run/keys 0750 root ${toString config.ids.gids.keys}" + "z /run/keys 0750 root ${toString config.ids.gids.keys}" + ]; + + # Sync mount options with systemd's src/core/mount-setup.c: mount_table. + boot.specialFileSystems = { + "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; }; + "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; }; + "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; }; + "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; }; + "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; }; + + # To hold secrets that shouldn't be written to disk + "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; }; + } // optionalAttrs (!config.boot.isContainer) { + # systemd-nspawn populates /sys by itself, and remounting it causes all + # kinds of weird issues (most noticeably, waiting for host disk device + # nodes). + "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; }; + }; + + }; + +} diff --git a/nixos/modules/tasks/filesystems/apfs.nix b/nixos/modules/tasks/filesystems/apfs.nix new file mode 100644 index 00000000000..2f2be351df6 --- /dev/null +++ b/nixos/modules/tasks/filesystems/apfs.nix @@ -0,0 +1,22 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + inInitrd = any (fs: fs == "apfs") config.boot.initrd.supportedFilesystems; + +in + +{ + config = mkIf (any (fs: fs == "apfs") config.boot.supportedFilesystems) { + + system.fsPackages = [ pkgs.apfsprogs ]; + + boot.extraModulePackages = [ config.boot.kernelPackages.apfs ]; + + boot.initrd.kernelModules = mkIf inInitrd [ "apfs" ]; + + # Don't copy apfsck into the initramfs since it does not support repairing the filesystem + }; +} diff --git a/nixos/modules/tasks/filesystems/bcachefs.nix b/nixos/modules/tasks/filesystems/bcachefs.nix new file mode 100644 index 00000000000..ac41ba5f93a --- /dev/null +++ b/nixos/modules/tasks/filesystems/bcachefs.nix @@ -0,0 +1,65 @@ +{ config, lib, pkgs, utils, ... }: + +with lib; + +let + + bootFs = 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" + 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 sucessfully unlocked + printf "unlocking failed!\n" + prompt $name + done + printf "unlocking successful.\n" + fi + } + ''; + + openCommand = name: fs: + let + # 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 = head (splitString ":" fs.device); + in + '' + tryUnlock ${name} ${firstDevice} + ''; + +in + +{ + config = mkIf (elem "bcachefs" config.boot.supportedFilesystems) (mkMerge [ + { + system.fsPackages = [ pkgs.bcachefs-tools ]; + + # use kernel package with bcachefs support until it's in mainline + boot.kernelPackages = pkgs.linuxPackages_testing_bcachefs; + } + + (mkIf ((elem "bcachefs" config.boot.initrd.supportedFilesystems) || (bootFs != {})) { + # chacha20 and poly1305 are required only for decryption attempts + boot.initrd.availableKernelModules = [ "bcachefs" "sha256" "chacha20" "poly1305" ]; + + boot.initrd.extraUtilsCommands = '' + copy_bin_and_libs ${pkgs.bcachefs-tools}/bin/bcachefs + ''; + boot.initrd.extraUtilsCommandsTest = '' + $out/bin/bcachefs version + ''; + + boot.initrd.postDeviceCommands = commonFunctions + concatStrings (mapAttrsToList openCommand bootFs); + }) + ]); +} diff --git a/nixos/modules/tasks/filesystems/btrfs.nix b/nixos/modules/tasks/filesystems/btrfs.nix new file mode 100644 index 00000000000..ae1dab5b8d8 --- /dev/null +++ b/nixos/modules/tasks/filesystems/btrfs.nix @@ -0,0 +1,149 @@ +{ config, lib, pkgs, utils, ... }: + +with lib; + +let + + inInitrd = any (fs: fs == "btrfs") config.boot.initrd.supportedFilesystems; + inSystem = any (fs: fs == "btrfs") config.boot.supportedFilesystems; + + cfgScrub = config.services.btrfs.autoScrub; + + enableAutoScrub = cfgScrub.enable; + enableBtrfs = inInitrd || inSystem || enableAutoScrub; + +in + +{ + options = { + # One could also do regular btrfs balances, but that shouldn't be necessary + # during normal usage and as long as the filesystems aren't filled near capacity + services.btrfs.autoScrub = { + enable = mkEnableOption "regular btrfs scrub"; + + fileSystems = mkOption { + type = types.listOf types.path; + example = [ "/" ]; + description = '' + List of paths to btrfs filesystems to regularily call <command>btrfs scrub</command> on. + Defaults to all mount points with btrfs filesystems. + If you mount a filesystem multiple times or additionally mount subvolumes, + you need to manually specify this list to avoid scrubbing multiple times. + ''; + }; + + interval = mkOption { + default = "monthly"; + type = types.str; + example = "weekly"; + description = '' + Systemd calendar expression for when to scrub btrfs filesystems. + The recommended period is a month but could be less + (<citerefentry><refentrytitle>btrfs-scrub</refentrytitle> + <manvolnum>8</manvolnum></citerefentry>). + See + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum></citerefentry> + for more information on the syntax. + ''; + }; + + }; + }; + + config = mkMerge [ + (mkIf enableBtrfs { + system.fsPackages = [ pkgs.btrfs-progs ]; + + boot.initrd.kernelModules = mkIf inInitrd [ "btrfs" ]; + boot.initrd.availableKernelModules = mkIf inInitrd ( + [ "crc32c" ] + ++ optionals (config.boot.kernelPackages.kernel.kernelAtLeast "5.5") [ + # Needed for mounting filesystems with new checksums + "xxhash_generic" + "blake2b_generic" + "sha256_generic" # Should be baked into our kernel, just to be sure + ] + ); + + boot.initrd.extraUtilsCommands = mkIf inInitrd + '' + copy_bin_and_libs ${pkgs.btrfs-progs}/bin/btrfs + ln -sv btrfs $out/bin/btrfsck + ln -sv btrfsck $out/bin/fsck.btrfs + ''; + + boot.initrd.extraUtilsCommandsTest = mkIf inInitrd + '' + $out/bin/btrfs --version + ''; + + boot.initrd.postDeviceCommands = mkIf inInitrd + '' + btrfs device scan + ''; + }) + + (mkIf enableAutoScrub { + assertions = [ + { + assertion = cfgScrub.enable -> (cfgScrub.fileSystems != []); + message = '' + If 'services.btrfs.autoScrub' is enabled, you need to have at least one + btrfs file system mounted via 'fileSystems' or specify a list manually + in 'services.btrfs.autoScrub.fileSystems'. + ''; + } + ]; + + # This will yield duplicated units if the user mounts a filesystem multiple times + # or additionally mounts subvolumes, but going the other way around via devices would + # yield duplicated units when a filesystem spans multiple devices. + # This way around seems like the more sensible default. + services.btrfs.autoScrub.fileSystems = mkDefault (mapAttrsToList (name: fs: fs.mountPoint) + (filterAttrs (name: fs: fs.fsType == "btrfs") config.fileSystems)); + + # TODO: Did not manage to do it via the usual btrfs-scrub@.timer/.service + # template units due to problems enabling the parameterized units, + # so settled with many units and templating via nix for now. + # https://github.com/NixOS/nixpkgs/pull/32496#discussion_r156527544 + systemd.timers = let + scrubTimer = fs: let + fs' = utils.escapeSystemdPath fs; + in nameValuePair "btrfs-scrub-${fs'}" { + description = "regular btrfs scrub timer on ${fs}"; + + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = cfgScrub.interval; + AccuracySec = "1d"; + Persistent = true; + }; + }; + in listToAttrs (map scrubTimer cfgScrub.fileSystems); + + systemd.services = let + scrubService = fs: let + fs' = utils.escapeSystemdPath fs; + in nameValuePair "btrfs-scrub-${fs'}" { + description = "btrfs scrub on ${fs}"; + # scrub prevents suspend2ram or proper shutdown + conflicts = [ "shutdown.target" "sleep.target" ]; + before = [ "shutdown.target" "sleep.target" ]; + + serviceConfig = { + # simple and not oneshot, otherwise ExecStop is not used + Type = "simple"; + Nice = 19; + IOSchedulingClass = "idle"; + ExecStart = "${pkgs.btrfs-progs}/bin/btrfs scrub start -B ${fs}"; + # if the service is stopped before scrub end, cancel it + ExecStop = pkgs.writeShellScript "btrfs-scrub-maybe-cancel" '' + (${pkgs.btrfs-progs}/bin/btrfs scrub status ${fs} | ${pkgs.gnugrep}/bin/grep finished) || ${pkgs.btrfs-progs}/bin/btrfs scrub cancel ${fs} + ''; + }; + }; + in listToAttrs (map scrubService cfgScrub.fileSystems); + }) + ]; +} diff --git a/nixos/modules/tasks/filesystems/cifs.nix b/nixos/modules/tasks/filesystems/cifs.nix new file mode 100644 index 00000000000..47ba0c03c56 --- /dev/null +++ b/nixos/modules/tasks/filesystems/cifs.nix @@ -0,0 +1,25 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + inInitrd = any (fs: fs == "cifs") config.boot.initrd.supportedFilesystems; + +in + +{ + config = { + + system.fsPackages = mkIf (any (fs: fs == "cifs") config.boot.supportedFilesystems) [ pkgs.cifs-utils ]; + + boot.initrd.availableKernelModules = mkIf inInitrd + [ "cifs" "nls_utf8" "hmac" "md4" "ecb" "des_generic" "sha256" ]; + + boot.initrd.extraUtilsCommands = mkIf inInitrd + '' + copy_bin_and_libs ${pkgs.cifs-utils}/sbin/mount.cifs + ''; + + }; +} diff --git a/nixos/modules/tasks/filesystems/ecryptfs.nix b/nixos/modules/tasks/filesystems/ecryptfs.nix new file mode 100644 index 00000000000..8138e659161 --- /dev/null +++ b/nixos/modules/tasks/filesystems/ecryptfs.nix @@ -0,0 +1,24 @@ +{ config, lib, pkgs, ... }: +# TODO: make ecryptfs work in initramfs? + +with lib; + +{ + config = mkIf (any (fs: fs == "ecryptfs") config.boot.supportedFilesystems) { + system.fsPackages = [ pkgs.ecryptfs ]; + security.wrappers = { + "mount.ecryptfs_private" = + { setuid = true; + owner = "root"; + group = "root"; + source = "${pkgs.ecryptfs.out}/bin/mount.ecryptfs_private"; + }; + "umount.ecryptfs_private" = + { setuid = true; + owner = "root"; + group = "root"; + source = "${pkgs.ecryptfs.out}/bin/umount.ecryptfs_private"; + }; + }; + }; +} diff --git a/nixos/modules/tasks/filesystems/exfat.nix b/nixos/modules/tasks/filesystems/exfat.nix new file mode 100644 index 00000000000..540b9b91c3e --- /dev/null +++ b/nixos/modules/tasks/filesystems/exfat.nix @@ -0,0 +1,13 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + config = mkIf (any (fs: fs == "exfat") config.boot.supportedFilesystems) { + system.fsPackages = if config.boot.kernelPackages.kernelOlder "5.7" then [ + pkgs.exfat # FUSE + ] else [ + pkgs.exfatprogs # non-FUSE + ]; + }; +} diff --git a/nixos/modules/tasks/filesystems/ext.nix b/nixos/modules/tasks/filesystems/ext.nix new file mode 100644 index 00000000000..a14a3ac3854 --- /dev/null +++ b/nixos/modules/tasks/filesystems/ext.nix @@ -0,0 +1,22 @@ +{ pkgs, ... }: + +{ + config = { + + system.fsPackages = [ pkgs.e2fsprogs ]; + + # As of kernel 4.3, there is no separate ext3 driver (they're also handled by ext4.ko) + boot.initrd.availableKernelModules = [ "ext2" "ext4" ]; + + boot.initrd.extraUtilsCommands = + '' + # Copy e2fsck and friends. + copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/e2fsck + copy_bin_and_libs ${pkgs.e2fsprogs}/sbin/tune2fs + ln -sv e2fsck $out/bin/fsck.ext2 + ln -sv e2fsck $out/bin/fsck.ext3 + ln -sv e2fsck $out/bin/fsck.ext4 + ''; + + }; +} diff --git a/nixos/modules/tasks/filesystems/f2fs.nix b/nixos/modules/tasks/filesystems/f2fs.nix new file mode 100644 index 00000000000..a305235979a --- /dev/null +++ b/nixos/modules/tasks/filesystems/f2fs.nix @@ -0,0 +1,25 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + inInitrd = any (fs: fs == "f2fs") config.boot.initrd.supportedFilesystems; + fileSystems = filter (x: x.fsType == "f2fs") config.system.build.fileSystems; +in +{ + config = mkIf (any (fs: fs == "f2fs") config.boot.supportedFilesystems) { + + system.fsPackages = [ pkgs.f2fs-tools ]; + + boot.initrd.availableKernelModules = mkIf inInitrd [ "f2fs" "crc32" ]; + + boot.initrd.extraUtilsCommands = mkIf inInitrd '' + copy_bin_and_libs ${pkgs.f2fs-tools}/sbin/fsck.f2fs + ${optionalString (any (fs: fs.autoResize) fileSystems) '' + # We need f2fs-tools' tools to resize filesystems + copy_bin_and_libs ${pkgs.f2fs-tools}/sbin/resize.f2fs + ''} + + ''; + }; +} diff --git a/nixos/modules/tasks/filesystems/glusterfs.nix b/nixos/modules/tasks/filesystems/glusterfs.nix new file mode 100644 index 00000000000..e8c7fa8efba --- /dev/null +++ b/nixos/modules/tasks/filesystems/glusterfs.nix @@ -0,0 +1,11 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + config = mkIf (any (fs: fs == "glusterfs") config.boot.supportedFilesystems) { + + system.fsPackages = [ pkgs.glusterfs ]; + + }; +} diff --git a/nixos/modules/tasks/filesystems/jfs.nix b/nixos/modules/tasks/filesystems/jfs.nix new file mode 100644 index 00000000000..fc3905c7dc2 --- /dev/null +++ b/nixos/modules/tasks/filesystems/jfs.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + inInitrd = any (fs: fs == "jfs") config.boot.initrd.supportedFilesystems; +in +{ + config = mkIf (any (fs: fs == "jfs") config.boot.supportedFilesystems) { + + system.fsPackages = [ pkgs.jfsutils ]; + + boot.initrd.kernelModules = mkIf inInitrd [ "jfs" ]; + + boot.initrd.extraUtilsCommands = mkIf inInitrd '' + copy_bin_and_libs ${pkgs.jfsutils}/sbin/fsck.jfs + ''; + }; +} diff --git a/nixos/modules/tasks/filesystems/nfs.nix b/nixos/modules/tasks/filesystems/nfs.nix new file mode 100644 index 00000000000..38c3920a78a --- /dev/null +++ b/nixos/modules/tasks/filesystems/nfs.nix @@ -0,0 +1,135 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + inInitrd = any (fs: fs == "nfs") config.boot.initrd.supportedFilesystems; + + nfsStateDir = "/var/lib/nfs"; + + rpcMountpoint = "${nfsStateDir}/rpc_pipefs"; + + format = pkgs.formats.ini {}; + + idmapdConfFile = format.generate "idmapd.conf" cfg.idmapd.settings; + nfsConfFile = pkgs.writeText "nfs.conf" cfg.extraConfig; + requestKeyConfFile = pkgs.writeText "request-key.conf" '' + create id_resolver * * ${pkgs.nfs-utils}/bin/nfsidmap -t 600 %k %d + ''; + + cfg = config.services.nfs; + +in + +{ + ###### interface + + options = { + services.nfs = { + idmapd.settings = mkOption { + type = format.type; + default = {}; + description = '' + libnfsidmap configuration. Refer to + <link xlink:href="https://linux.die.net/man/5/idmapd.conf"/> + for details. + ''; + example = literalExpression '' + { + Translation = { + GSS-Methods = "static,nsswitch"; + }; + Static = { + "root/hostname.domain.com@REALM.COM" = "root"; + }; + } + ''; + }; + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra nfs-utils configuration. + ''; + }; + }; + }; + + ###### implementation + + config = mkIf (any (fs: fs == "nfs" || fs == "nfs4") config.boot.supportedFilesystems) { + + services.rpcbind.enable = true; + + services.nfs.idmapd.settings = { + General = mkMerge [ + { Pipefs-Directory = rpcMountpoint; } + (mkIf (config.networking.domain != null) { Domain = config.networking.domain; }) + ]; + Mapping = { + Nobody-User = "nobody"; + Nobody-Group = "nogroup"; + }; + Translation = { + Method = "nsswitch"; + }; + }; + + system.fsPackages = [ pkgs.nfs-utils ]; + + boot.initrd.kernelModules = mkIf inInitrd [ "nfs" ]; + + systemd.packages = [ pkgs.nfs-utils ]; + + environment.systemPackages = [ pkgs.keyutils ]; + + environment.etc = { + "idmapd.conf".source = idmapdConfFile; + "nfs.conf".source = nfsConfFile; + "request-key.conf".source = requestKeyConfFile; + }; + + systemd.services.nfs-blkmap = + { restartTriggers = [ nfsConfFile ]; + }; + + systemd.targets.nfs-client = + { wantedBy = [ "multi-user.target" "remote-fs.target" ]; + }; + + systemd.services.nfs-idmapd = + { restartTriggers = [ idmapdConfFile ]; + }; + + systemd.services.nfs-mountd = + { restartTriggers = [ nfsConfFile ]; + enable = mkDefault false; + }; + + systemd.services.nfs-server = + { restartTriggers = [ nfsConfFile ]; + enable = mkDefault false; + }; + + systemd.services.auth-rpcgss-module = + { + unitConfig.ConditionPathExists = [ "" "/etc/krb5.keytab" ]; + }; + + systemd.services.rpc-gssd = + { restartTriggers = [ nfsConfFile ]; + unitConfig.ConditionPathExists = [ "" "/etc/krb5.keytab" ]; + }; + + systemd.services.rpc-statd = + { restartTriggers = [ nfsConfFile ]; + + preStart = + '' + mkdir -p /var/lib/nfs/{sm,sm.bak} + ''; + }; + + }; +} diff --git a/nixos/modules/tasks/filesystems/ntfs.nix b/nixos/modules/tasks/filesystems/ntfs.nix new file mode 100644 index 00000000000..c40d2a1a80b --- /dev/null +++ b/nixos/modules/tasks/filesystems/ntfs.nix @@ -0,0 +1,11 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + config = mkIf (any (fs: fs == "ntfs" || fs == "ntfs-3g") config.boot.supportedFilesystems) { + + system.fsPackages = [ pkgs.ntfs3g ]; + + }; +} diff --git a/nixos/modules/tasks/filesystems/reiserfs.nix b/nixos/modules/tasks/filesystems/reiserfs.nix new file mode 100644 index 00000000000..ab4c43e2ab8 --- /dev/null +++ b/nixos/modules/tasks/filesystems/reiserfs.nix @@ -0,0 +1,25 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + inInitrd = any (fs: fs == "reiserfs") config.boot.initrd.supportedFilesystems; + +in + +{ + config = mkIf (any (fs: fs == "reiserfs") config.boot.supportedFilesystems) { + + system.fsPackages = [ pkgs.reiserfsprogs ]; + + boot.initrd.kernelModules = mkIf inInitrd [ "reiserfs" ]; + + boot.initrd.extraUtilsCommands = mkIf inInitrd + '' + copy_bin_and_libs ${pkgs.reiserfsprogs}/sbin/reiserfsck + ln -s reiserfsck $out/bin/fsck.reiserfs + ''; + + }; +} diff --git a/nixos/modules/tasks/filesystems/unionfs-fuse.nix b/nixos/modules/tasks/filesystems/unionfs-fuse.nix new file mode 100644 index 00000000000..f54f3559c34 --- /dev/null +++ b/nixos/modules/tasks/filesystems/unionfs-fuse.nix @@ -0,0 +1,32 @@ +{ config, pkgs, lib, ... }: + +{ + config = lib.mkMerge [ + + (lib.mkIf (lib.any (fs: fs == "unionfs-fuse") config.boot.initrd.supportedFilesystems) { + boot.initrd.kernelModules = [ "fuse" ]; + + boot.initrd.extraUtilsCommands = '' + copy_bin_and_libs ${pkgs.fuse}/sbin/mount.fuse + copy_bin_and_libs ${pkgs.unionfs-fuse}/bin/unionfs + substitute ${pkgs.unionfs-fuse}/sbin/mount.unionfs-fuse $out/bin/mount.unionfs-fuse \ + --replace '${pkgs.bash}/bin/bash' /bin/sh \ + --replace '${pkgs.fuse}/sbin' /bin \ + --replace '${pkgs.unionfs-fuse}/bin' /bin + chmod +x $out/bin/mount.unionfs-fuse + ''; + + boot.initrd.postDeviceCommands = '' + # Hacky!!! fuse hard-codes the path to mount + mkdir -p /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.util-linux.name}-bin/bin + ln -s $(which mount) /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.util-linux.name}-bin/bin + ln -s $(which umount) /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-${pkgs.util-linux.name}-bin/bin + ''; + }) + + (lib.mkIf (lib.any (fs: fs == "unionfs-fuse") config.boot.supportedFilesystems) { + system.fsPackages = [ pkgs.unionfs-fuse ]; + }) + + ]; +} diff --git a/nixos/modules/tasks/filesystems/vboxsf.nix b/nixos/modules/tasks/filesystems/vboxsf.nix new file mode 100644 index 00000000000..5497194f6a8 --- /dev/null +++ b/nixos/modules/tasks/filesystems/vboxsf.nix @@ -0,0 +1,23 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + inInitrd = any (fs: fs == "vboxsf") config.boot.initrd.supportedFilesystems; + + package = pkgs.runCommand "mount.vboxsf" { preferLocalBuild = true; } '' + mkdir -p $out/bin + cp ${pkgs.linuxPackages.virtualboxGuestAdditions}/bin/mount.vboxsf $out/bin + ''; +in + +{ + config = mkIf (any (fs: fs == "vboxsf") config.boot.supportedFilesystems) { + + system.fsPackages = [ package ]; + + boot.initrd.kernelModules = mkIf inInitrd [ "vboxsf" ]; + + }; +} diff --git a/nixos/modules/tasks/filesystems/vfat.nix b/nixos/modules/tasks/filesystems/vfat.nix new file mode 100644 index 00000000000..958e27ae8a3 --- /dev/null +++ b/nixos/modules/tasks/filesystems/vfat.nix @@ -0,0 +1,25 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + inInitrd = any (fs: fs == "vfat") config.boot.initrd.supportedFilesystems; + +in + +{ + config = mkIf (any (fs: fs == "vfat") config.boot.supportedFilesystems) { + + system.fsPackages = [ pkgs.dosfstools ]; + + boot.initrd.kernelModules = mkIf inInitrd [ "vfat" "nls_cp437" "nls_iso8859-1" ]; + + boot.initrd.extraUtilsCommands = mkIf inInitrd + '' + copy_bin_and_libs ${pkgs.dosfstools}/sbin/dosfsck + ln -sv dosfsck $out/bin/fsck.vfat + ''; + + }; +} diff --git a/nixos/modules/tasks/filesystems/xfs.nix b/nixos/modules/tasks/filesystems/xfs.nix new file mode 100644 index 00000000000..98038701ca5 --- /dev/null +++ b/nixos/modules/tasks/filesystems/xfs.nix @@ -0,0 +1,30 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + inInitrd = any (fs: fs == "xfs") config.boot.initrd.supportedFilesystems; + +in + +{ + config = mkIf (any (fs: fs == "xfs") config.boot.supportedFilesystems) { + + system.fsPackages = [ pkgs.xfsprogs.bin ]; + + boot.initrd.availableKernelModules = mkIf inInitrd [ "xfs" "crc32c" ]; + + boot.initrd.extraUtilsCommands = mkIf inInitrd + '' + copy_bin_and_libs ${pkgs.xfsprogs.bin}/bin/fsck.xfs + copy_bin_and_libs ${pkgs.xfsprogs.bin}/bin/xfs_repair + ''; + + # Trick just to set 'sh' after the extraUtils nuke-refs. + boot.initrd.extraUtilsCommandsTest = mkIf inInitrd + '' + sed -i -e 's,^#!.*,#!'$out/bin/sh, $out/bin/fsck.xfs + ''; + }; +} diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix new file mode 100644 index 00000000000..3bc0dedec00 --- /dev/null +++ b/nixos/modules/tasks/filesystems/zfs.nix @@ -0,0 +1,795 @@ +{ config, lib, options, pkgs, utils, ... }: +# +# TODO: zfs tunables + +with utils; +with lib; + +let + + cfgZfs = config.boot.zfs; + optZfs = options.boot.zfs; + cfgExpandOnBoot = config.services.zfs.expandOnBoot; + cfgSnapshots = config.services.zfs.autoSnapshot; + cfgSnapFlags = cfgSnapshots.flags; + cfgScrub = config.services.zfs.autoScrub; + cfgTrim = config.services.zfs.trim; + cfgZED = config.services.zfs.zed; + + inInitrd = any (fs: fs == "zfs") config.boot.initrd.supportedFilesystems; + inSystem = any (fs: fs == "zfs") config.boot.supportedFilesystems; + + autosnapPkg = pkgs.zfstools.override { + zfs = cfgZfs.package; + }; + + zfsAutoSnap = "${autosnapPkg}/bin/zfs-auto-snapshot"; + + datasetToPool = x: elemAt (splitString "/" x) 0; + + fsToPool = fs: datasetToPool fs.device; + + zfsFilesystems = filter (x: x.fsType == "zfs") config.system.build.fileSystems; + + allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools); + + rootPools = unique (map fsToPool (filter fsNeededForBoot zfsFilesystems)); + + dataPools = unique (filter (pool: !(elem pool rootPools)) allPools); + + snapshotNames = [ "frequent" "hourly" "daily" "weekly" "monthly" ]; + + # When importing ZFS pools, there's one difficulty: These scripts may run + # before the backing devices (physical HDDs, etc.) of the pool have been + # scanned and initialized. + # + # An attempted import with all devices missing will just fail, and can be + # retried, but an import where e.g. two out of three disks in a three-way + # mirror are missing, will succeed. This is a problem: When the missing disks + # are later discovered, they won't be automatically set online, rendering the + # pool redundancy-less (and far slower) until such time as the system reboots. + # + # The solution is the below. poolReady checks the status of an un-imported + # pool, to see if *every* device is available -- in which case the pool will be + # in state ONLINE, as opposed to DEGRADED, FAULTED or MISSING. + # + # The import scripts then loop over this, waiting until the pool is ready or a + # sufficient amount of time has passed that we can assume it won't be. In the + # latter case it makes one last attempt at importing, allowing the system to + # (eventually) boot even with a degraded pool. + importLib = {zpoolCmd, awkCmd, cfgZfs}: '' + poolReady() { + pool="$1" + state="$("${zpoolCmd}" import 2>/dev/null | "${awkCmd}" "/pool: $pool/ { found = 1 }; /state:/ { if (found == 1) { print \$2; exit } }; END { if (found == 0) { print \"MISSING\" } }")" + if [[ "$state" = "ONLINE" ]]; then + return 0 + else + echo "Pool $pool in state $state, waiting" + return 1 + fi + } + poolImported() { + pool="$1" + "${zpoolCmd}" list "$pool" >/dev/null 2>/dev/null + } + poolImport() { + pool="$1" + "${zpoolCmd}" import -d "${cfgZfs.devNodes}" -N $ZFS_FORCE "$pool" + } + ''; + + zedConf = generators.toKeyValue { + mkKeyValue = generators.mkKeyValueDefault { + mkValueString = v: + if isInt v then toString v + else if isString v then "\"${v}\"" + else if true == v then "1" + else if false == v then "0" + else if isList v then "\"" + (concatStringsSep " " v) + "\"" + else err "this value is" (toString v); + } "="; + } cfgZED.settings; +in + +{ + + imports = [ + (mkRemovedOptionModule [ "boot" "zfs" "enableLegacyCrypto" ] "The corresponding package was removed from nixpkgs.") + ]; + + ###### interface + + options = { + boot.zfs = { + package = mkOption { + readOnly = true; + type = types.package; + default = if config.boot.zfs.enableUnstable then pkgs.zfsUnstable else pkgs.zfs; + defaultText = literalExpression "if config.boot.zfs.enableUnstable then pkgs.zfsUnstable else pkgs.zfs"; + description = "Configured ZFS userland tools package."; + }; + + enabled = mkOption { + readOnly = true; + type = types.bool; + default = inInitrd || inSystem; + defaultText = literalDocBook "<literal>true</literal> if ZFS filesystem support is enabled"; + description = "True if ZFS filesystem support is enabled"; + }; + + enableUnstable = mkOption { + type = types.bool; + default = false; + description = '' + Use the unstable zfs package. This might be an option, if the latest + kernel is not yet supported by a published release of ZFS. Enabling + this option will install a development version of ZFS on Linux. The + version will have already passed an extensive test suite, but it is + more likely to hit an undiscovered bug compared to running a released + version of ZFS on Linux. + ''; + }; + + extraPools = mkOption { + type = types.listOf types.str; + default = []; + example = [ "tank" "data" ]; + description = '' + Name or GUID of extra ZFS pools that you wish to import during boot. + + Usually this is not necessary. Instead, you should set the mountpoint property + of ZFS filesystems to <literal>legacy</literal> and add the ZFS filesystems to + NixOS's <option>fileSystems</option> option, which makes NixOS automatically + import the associated pool. + + However, in some cases (e.g. if you have many filesystems) it may be preferable + to exclusively use ZFS commands to manage filesystems. If so, since NixOS/systemd + will not be managing those filesystems, you will need to specify the ZFS pool here + so that NixOS automatically imports it on every boot. + ''; + }; + + devNodes = mkOption { + type = types.path; + default = "/dev/disk/by-id"; + description = '' + Name of directory from which to import ZFS devices. + + This should be a path under /dev containing stable names for all devices needed, as + import may fail if device nodes are renamed concurrently with a device failing. + ''; + }; + + forceImportRoot = mkOption { + type = types.bool; + default = true; + description = '' + Forcibly import the ZFS root pool(s) during early boot. + + This is enabled by default for backwards compatibility purposes, but it is highly + recommended to disable this option, as it bypasses some of the safeguards ZFS uses + to protect your ZFS pools. + + If you set this option to <literal>false</literal> and NixOS subsequently fails to + boot because it cannot import the root pool, you should boot with the + <literal>zfs_force=1</literal> option as a kernel parameter (e.g. by manually + editing the kernel params in grub during boot). You should only need to do this + once. + ''; + }; + + forceImportAll = mkOption { + type = types.bool; + default = false; + description = '' + Forcibly import all ZFS pool(s). + + If you set this option to <literal>false</literal> and NixOS subsequently fails to + import your non-root ZFS pool(s), you should manually import each pool with + "zpool import -f <pool-name>", and then reboot. You should only need to do + this once. + ''; + }; + + requestEncryptionCredentials = mkOption { + type = types.either types.bool (types.listOf types.str); + default = true; + example = [ "tank" "data" ]; + description = '' + If true on import encryption keys or passwords for all encrypted datasets + are requested. To only decrypt selected datasets supply a list of dataset + names instead. For root pools the encryption key can be supplied via both + an interactive prompt (keylocation=prompt) and from a file (keylocation=file://). + ''; + }; + }; + + services.zfs.autoSnapshot = { + enable = mkOption { + default = false; + type = types.bool; + description = '' + Enable the (OpenSolaris-compatible) ZFS auto-snapshotting service. + Note that you must set the <literal>com.sun:auto-snapshot</literal> + property to <literal>true</literal> on all datasets which you wish + to auto-snapshot. + + You can override a child dataset to use, or not use auto-snapshotting + by setting its flag with the given interval: + <literal>zfs set com.sun:auto-snapshot:weekly=false DATASET</literal> + ''; + }; + + flags = mkOption { + default = "-k -p"; + example = "-k -p --utc"; + type = types.str; + description = '' + Flags to pass to the zfs-auto-snapshot command. + + Run <literal>zfs-auto-snapshot</literal> (without any arguments) to + see available flags. + + If it's not too inconvenient for snapshots to have timestamps in UTC, + it is suggested that you append <literal>--utc</literal> to the list + of default options (see example). + + Otherwise, snapshot names can cause name conflicts or apparent time + reversals due to daylight savings, timezone or other date/time changes. + ''; + }; + + frequent = mkOption { + default = 4; + type = types.int; + description = '' + Number of frequent (15-minute) auto-snapshots that you wish to keep. + ''; + }; + + hourly = mkOption { + default = 24; + type = types.int; + description = '' + Number of hourly auto-snapshots that you wish to keep. + ''; + }; + + daily = mkOption { + default = 7; + type = types.int; + description = '' + Number of daily auto-snapshots that you wish to keep. + ''; + }; + + weekly = mkOption { + default = 4; + type = types.int; + description = '' + Number of weekly auto-snapshots that you wish to keep. + ''; + }; + + monthly = mkOption { + default = 12; + type = types.int; + description = '' + Number of monthly auto-snapshots that you wish to keep. + ''; + }; + }; + + services.zfs.trim = { + enable = mkOption { + description = "Whether to enable periodic TRIM on all ZFS pools."; + default = true; + example = false; + type = types.bool; + }; + + interval = mkOption { + default = "weekly"; + type = types.str; + example = "daily"; + description = '' + How often we run trim. For most desktop and server systems + a sufficient trimming frequency is once a week. + + The format is described in + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum></citerefentry>. + ''; + }; + }; + + services.zfs.autoScrub = { + enable = mkEnableOption "periodic scrubbing of ZFS pools"; + + interval = mkOption { + default = "Sun, 02:00"; + type = types.str; + example = "daily"; + description = '' + Systemd calendar expression when to scrub ZFS pools. See + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum></citerefentry>. + ''; + }; + + pools = mkOption { + default = []; + type = types.listOf types.str; + example = [ "tank" ]; + description = '' + List of ZFS pools to periodically scrub. If empty, all pools + will be scrubbed. + ''; + }; + }; + + services.zfs.expandOnBoot = mkOption { + type = types.either (types.enum [ "disabled" "all" ]) (types.listOf types.str); + default = "disabled"; + example = [ "tank" "dozer" ]; + description = '' + After importing, expand each device in the specified pools. + + Set the value to the plain string "all" to expand all pools on boot: + + services.zfs.expandOnBoot = "all"; + + or set the value to a list of pools to expand the disks of specific pools: + + services.zfs.expandOnBoot = [ "tank" "dozer" ]; + ''; + }; + + services.zfs.zed = { + enableMail = mkEnableOption "ZED's ability to send emails" // { + default = cfgZfs.package.enableMail; + defaultText = literalExpression "config.${optZfs.package}.enableMail"; + }; + + settings = mkOption { + type = with types; attrsOf (oneOf [ str int bool (listOf str) ]); + example = literalExpression '' + { + ZED_DEBUG_LOG = "/tmp/zed.debug.log"; + + ZED_EMAIL_ADDR = [ "root" ]; + ZED_EMAIL_PROG = "mail"; + ZED_EMAIL_OPTS = "-s '@SUBJECT@' @ADDRESS@"; + + ZED_NOTIFY_INTERVAL_SECS = 3600; + ZED_NOTIFY_VERBOSE = false; + + ZED_USE_ENCLOSURE_LEDS = true; + ZED_SCRUB_AFTER_RESILVER = false; + } + ''; + description = '' + ZFS Event Daemon /etc/zfs/zed.d/zed.rc content + + See + <citerefentry><refentrytitle>zed</refentrytitle><manvolnum>8</manvolnum></citerefentry> + for details on ZED and the scripts in /etc/zfs/zed.d to find the possible variables + ''; + }; + }; + }; + + ###### implementation + + config = mkMerge [ + (mkIf cfgZfs.enabled { + assertions = [ + { + assertion = cfgZED.enableMail -> cfgZfs.package.enableMail; + message = '' + To allow ZED to send emails, ZFS needs to be configured to enable + this. To do so, one must override the `zfs` package and set + `enableMail` to true. + ''; + } + { + assertion = config.networking.hostId != null; + message = "ZFS requires networking.hostId to be set"; + } + { + assertion = !cfgZfs.forceImportAll || cfgZfs.forceImportRoot; + message = "If you enable boot.zfs.forceImportAll, you must also enable boot.zfs.forceImportRoot"; + } + ]; + + boot = { + kernelModules = [ "zfs" ]; + + extraModulePackages = [ + (if config.boot.zfs.enableUnstable then + config.boot.kernelPackages.zfsUnstable + else + config.boot.kernelPackages.zfs) + ]; + }; + + boot.initrd = mkIf inInitrd { + kernelModules = [ "zfs" ] ++ optional (!cfgZfs.enableUnstable) "spl"; + extraUtilsCommands = + '' + copy_bin_and_libs ${cfgZfs.package}/sbin/zfs + copy_bin_and_libs ${cfgZfs.package}/sbin/zdb + copy_bin_and_libs ${cfgZfs.package}/sbin/zpool + ''; + extraUtilsCommandsTest = mkIf inInitrd + '' + $out/bin/zfs --help >/dev/null 2>&1 + $out/bin/zpool --help >/dev/null 2>&1 + ''; + postDeviceCommands = concatStringsSep "\n" (['' + ZFS_FORCE="${optionalString cfgZfs.forceImportRoot "-f"}" + + for o in $(cat /proc/cmdline); do + case $o in + zfs_force|zfs_force=1) + ZFS_FORCE="-f" + ;; + esac + done + ''] ++ [(importLib { + # See comments at importLib definition. + zpoolCmd = "zpool"; + awkCmd = "awk"; + inherit cfgZfs; + })] ++ (map (pool: '' + echo -n "importing root ZFS pool \"${pool}\"..." + # Loop across the import until it succeeds, because the devices needed may not be discovered yet. + if ! poolImported "${pool}"; then + for trial in `seq 1 60`; do + poolReady "${pool}" > /dev/null && msg="$(poolImport "${pool}" 2>&1)" && break + sleep 1 + echo -n . + done + echo + if [[ -n "$msg" ]]; then + echo "$msg"; + fi + poolImported "${pool}" || poolImport "${pool}" # Try one last time, e.g. to import a degraded pool. + fi + ${if isBool cfgZfs.requestEncryptionCredentials + then optionalString cfgZfs.requestEncryptionCredentials '' + zfs load-key -a + '' + else concatMapStrings (fs: '' + zfs load-key ${fs} + '') cfgZfs.requestEncryptionCredentials} + '') rootPools)); + }; + + # TODO FIXME See https://github.com/NixOS/nixpkgs/pull/99386#issuecomment-798813567. To not break people's bootloader and as probably not everybody would read release notes that thoroughly add inSystem. + boot.loader.grub = mkIf (inInitrd || inSystem) { + zfsSupport = true; + }; + + services.zfs.zed.settings = { + ZED_EMAIL_PROG = mkIf cfgZED.enableMail (mkDefault "${pkgs.mailutils}/bin/mail"); + PATH = lib.makeBinPath [ + cfgZfs.package + pkgs.coreutils + pkgs.curl + pkgs.gawk + pkgs.gnugrep + pkgs.gnused + pkgs.nettools + pkgs.util-linux + ]; + }; + + environment.etc = genAttrs + (map + (file: "zfs/zed.d/${file}") + [ + "all-syslog.sh" + "pool_import-led.sh" + "resilver_finish-start-scrub.sh" + "statechange-led.sh" + "vdev_attach-led.sh" + "zed-functions.sh" + "data-notify.sh" + "resilver_finish-notify.sh" + "scrub_finish-notify.sh" + "statechange-notify.sh" + "vdev_clear-led.sh" + ] + ) + (file: { source = "${cfgZfs.package}/etc/${file}"; }) + // { + "zfs/zed.d/zed.rc".text = zedConf; + "zfs/zpool.d".source = "${cfgZfs.package}/etc/zfs/zpool.d/"; + }; + + system.fsPackages = [ cfgZfs.package ]; # XXX: needed? zfs doesn't have (need) a fsck + environment.systemPackages = [ cfgZfs.package ] + ++ optional cfgSnapshots.enable autosnapPkg; # so the user can run the command to see flags + + services.udev.packages = [ cfgZfs.package ]; # to hook zvol naming, etc. + systemd.packages = [ cfgZfs.package ]; + + systemd.services = let + getPoolFilesystems = pool: + filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems; + + getPoolMounts = pool: + let + mountPoint = fs: escapeSystemdPath fs.mountPoint; + in + map (x: "${mountPoint x}.mount") (getPoolFilesystems pool); + + createImportService = pool: + nameValuePair "zfs-import-${pool}" { + description = "Import ZFS pool \"${pool}\""; + # we need systemd-udev-settle until https://github.com/zfsonlinux/zfs/pull/4943 is merged + requires = [ "systemd-udev-settle.service" ]; + after = [ + "systemd-udev-settle.service" + "systemd-modules-load.service" + "systemd-ask-password-console.service" + ]; + wantedBy = (getPoolMounts pool) ++ [ "local-fs.target" ]; + before = (getPoolMounts pool) ++ [ "local-fs.target" ]; + unitConfig = { + DefaultDependencies = "no"; + }; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + environment.ZFS_FORCE = optionalString cfgZfs.forceImportAll "-f"; + script = (importLib { + # See comments at importLib definition. + zpoolCmd = "${cfgZfs.package}/sbin/zpool"; + awkCmd = "${pkgs.gawk}/bin/awk"; + inherit cfgZfs; + }) + '' + poolImported "${pool}" && exit + echo -n "importing ZFS pool \"${pool}\"..." + # Loop across the import until it succeeds, because the devices needed may not be discovered yet. + for trial in `seq 1 60`; do + poolReady "${pool}" && poolImport "${pool}" && break + sleep 1 + done + poolImported "${pool}" || poolImport "${pool}" # Try one last time, e.g. to import a degraded pool. + if poolImported "${pool}"; then + ${optionalString (if isBool cfgZfs.requestEncryptionCredentials + then cfgZfs.requestEncryptionCredentials + else cfgZfs.requestEncryptionCredentials != []) '' + ${cfgZfs.package}/sbin/zfs list -rHo name,keylocation ${pool} | while IFS=$'\t' read ds kl; do + { + ${optionalString (!isBool cfgZfs.requestEncryptionCredentials) '' + if ! echo '${concatStringsSep "\n" cfgZfs.requestEncryptionCredentials}' | grep -qFx "$ds"; then + continue + fi + ''} + case "$kl" in + none ) + ;; + prompt ) + ${config.systemd.package}/bin/systemd-ask-password "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds" + ;; + * ) + ${cfgZfs.package}/sbin/zfs load-key "$ds" + ;; + esac + } < /dev/null # To protect while read ds kl in case anything reads stdin + done + ''} + echo "Successfully imported ${pool}" + else + exit 1 + fi + ''; + }; + + # This forces a sync of any ZFS pools prior to poweroff, even if they're set + # to sync=disabled. + createSyncService = pool: + nameValuePair "zfs-sync-${pool}" { + description = "Sync ZFS pool \"${pool}\""; + wantedBy = [ "shutdown.target" ]; + unitConfig = { + DefaultDependencies = false; + }; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + ${cfgZfs.package}/sbin/zfs set nixos:shutdown-time="$(date)" "${pool}" + ''; + }; + + createZfsService = serv: + nameValuePair serv { + after = [ "systemd-modules-load.service" ]; + wantedBy = [ "zfs.target" ]; + }; + + in listToAttrs (map createImportService dataPools ++ + map createSyncService allPools ++ + map createZfsService [ "zfs-mount" "zfs-share" "zfs-zed" ]); + + systemd.targets.zfs-import = + let + services = map (pool: "zfs-import-${pool}.service") dataPools; + in + { + requires = services; + after = services; + wantedBy = [ "zfs.target" ]; + }; + + systemd.targets.zfs.wantedBy = [ "multi-user.target" ]; + }) + + (mkIf (cfgZfs.enabled && cfgExpandOnBoot != "disabled") { + systemd.services."zpool-expand@" = { + description = "Expand ZFS pools"; + after = [ "zfs.target" ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + + scriptArgs = "%i"; + path = [ pkgs.gawk cfgZfs.package ]; + + # ZFS has no way of enumerating just devices in a pool in a way + # that 'zpool online -e' supports. Thus, we've implemented a + # bit of a strange approach of highlighting just devices. + # See: https://github.com/openzfs/zfs/issues/12505 + script = let + # This UUID has been chosen at random and is to provide a + # collision-proof, predictable token to search for + magicIdentifier = "NIXOS-ZFS-ZPOOL-DEVICE-IDENTIFIER-37108bec-aff6-4b58-9e5e-53c7c9766f05"; + zpoolScripts = pkgs.writeShellScriptBin "device-highlighter" '' + echo "${magicIdentifier}" + ''; + in '' + pool=$1 + + echo "Expanding all devices for $pool." + + # Put our device-highlighter script it to the PATH + export ZPOOL_SCRIPTS_PATH=${zpoolScripts}/bin + + # Enable running our precisely specified zpool script as root + export ZPOOL_SCRIPTS_AS_ROOT=1 + + devices() ( + zpool status -c device-highlighter "$pool" \ + | awk '($2 == "ONLINE" && $6 == "${magicIdentifier}") { print $1; }' + ) + + for device in $(devices); do + echo "Attempting to expand $device of $pool..." + if ! zpool online -e "$pool" "$device"; then + echo "Failed to expand '$device' of '$pool'." + fi + done + ''; + }; + + systemd.services."zpool-expand-pools" = + let + # Create a string, to be interpolated in a bash script + # which enumerates all of the pools to expand. + # If the `pools` option is `true`, we want to dynamically + # expand every pool. Otherwise we want to enumerate + # just the specifically provided list of pools. + poolListProvider = if cfgExpandOnBoot == "all" + then "$(zpool list -H | awk '{print $1}')" + else lib.escapeShellArgs cfgExpandOnBoot; + in + { + description = "Expand specified ZFS pools"; + wantedBy = [ "default.target" ]; + after = [ "zfs.target" ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + + path = [ pkgs.gawk cfgZfs.package ]; + + script = '' + for pool in ${poolListProvider}; do + systemctl start --no-block "zpool-expand@$pool" + done + ''; + }; + }) + + (mkIf (cfgZfs.enabled && cfgSnapshots.enable) { + systemd.services = let + descr = name: if name == "frequent" then "15 mins" + else if name == "hourly" then "hour" + else if name == "daily" then "day" + else if name == "weekly" then "week" + else if name == "monthly" then "month" + else throw "unknown snapshot name"; + numSnapshots = name: builtins.getAttr name cfgSnapshots; + in builtins.listToAttrs (map (snapName: + { + name = "zfs-snapshot-${snapName}"; + value = { + description = "ZFS auto-snapshotting every ${descr snapName}"; + after = [ "zfs-import.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${zfsAutoSnap} ${cfgSnapFlags} ${snapName} ${toString (numSnapshots snapName)}"; + }; + restartIfChanged = false; + }; + }) snapshotNames); + + systemd.timers = let + timer = name: if name == "frequent" then "*:0,15,30,45" else name; + in builtins.listToAttrs (map (snapName: + { + name = "zfs-snapshot-${snapName}"; + value = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = timer snapName; + Persistent = "yes"; + }; + }; + }) snapshotNames); + }) + + (mkIf (cfgZfs.enabled && cfgScrub.enable) { + systemd.services.zfs-scrub = { + description = "ZFS pools scrubbing"; + after = [ "zfs-import.target" ]; + serviceConfig = { + Type = "oneshot"; + }; + script = '' + ${cfgZfs.package}/bin/zpool scrub ${ + if cfgScrub.pools != [] then + (concatStringsSep " " cfgScrub.pools) + else + "$(${cfgZfs.package}/bin/zpool list -H -o name)" + } + ''; + }; + + systemd.timers.zfs-scrub = { + wantedBy = [ "timers.target" ]; + after = [ "multi-user.target" ]; # Apparently scrubbing before boot is complete hangs the system? #53583 + timerConfig = { + OnCalendar = cfgScrub.interval; + Persistent = "yes"; + }; + }; + }) + + (mkIf (cfgZfs.enabled && cfgTrim.enable) { + systemd.services.zpool-trim = { + description = "ZFS pools trim"; + after = [ "zfs-import.target" ]; + path = [ cfgZfs.package ]; + startAt = cfgTrim.interval; + # By default we ignore errors returned by the trim command, in case: + # - HDDs are mixed with SSDs + # - There is a SSDs in a pool that is currently trimmed. + # - There are only HDDs and we would set the system in a degraded state + serviceConfig.ExecStart = "${pkgs.runtimeShell} -c 'for pool in $(zpool list -H -o name); do zpool trim $pool; done || true' "; + }; + + systemd.timers.zpool-trim.timerConfig.Persistent = "yes"; + }) + ]; +} diff --git a/nixos/modules/tasks/lvm.nix b/nixos/modules/tasks/lvm.nix new file mode 100644 index 00000000000..35316603c38 --- /dev/null +++ b/nixos/modules/tasks/lvm.nix @@ -0,0 +1,84 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.lvm; +in { + options.services.lvm = { + package = mkOption { + type = types.package; + default = if cfg.dmeventd.enable then pkgs.lvm2_dmeventd else pkgs.lvm2; + internal = true; + defaultText = literalExpression "pkgs.lvm2"; + description = '' + This option allows you to override the LVM package that's used on the system + (udev rules, tmpfiles, systemd services). + Defaults to pkgs.lvm2, or pkgs.lvm2_dmeventd if dmeventd is enabled. + ''; + }; + dmeventd.enable = mkEnableOption "the LVM dmevent daemon"; + boot.thin.enable = mkEnableOption "support for booting from ThinLVs"; + }; + + config = mkMerge [ + ({ + # minimal configuration file to make lvmconfig/lvm2-activation-generator happy + environment.etc."lvm/lvm.conf".text = "config {}"; + }) + (mkIf (!config.boot.isContainer) { + systemd.tmpfiles.packages = [ cfg.package.out ]; + environment.systemPackages = [ cfg.package ]; + systemd.packages = [ cfg.package ]; + + # TODO: update once https://github.com/NixOS/nixpkgs/pull/93006 was merged + services.udev.packages = [ cfg.package.out ]; + }) + (mkIf cfg.dmeventd.enable { + systemd.sockets."dm-event".wantedBy = [ "sockets.target" ]; + systemd.services."lvm2-monitor".wantedBy = [ "sysinit.target" ]; + + environment.etc."lvm/lvm.conf".text = '' + dmeventd/executable = "${cfg.package}/bin/dmeventd" + ''; + }) + (mkIf cfg.boot.thin.enable { + boot.initrd = { + kernelModules = [ "dm-snapshot" "dm-thin-pool" ]; + + extraUtilsCommands = '' + for BIN in ${pkgs.thin-provisioning-tools}/bin/*; do + copy_bin_and_libs $BIN + done + ''; + + extraUtilsCommandsTest = '' + ls ${pkgs.thin-provisioning-tools}/bin/ | grep -v pdata_tools | while read BIN; do + $out/bin/$(basename $BIN) --help > /dev/null + done + ''; + }; + + environment.etc."lvm/lvm.conf".text = concatMapStringsSep "\n" + (bin: "global/${bin}_executable = ${pkgs.thin-provisioning-tools}/bin/${bin}") + [ "thin_check" "thin_dump" "thin_repair" "cache_check" "cache_dump" "cache_repair" ]; + }) + (mkIf (cfg.dmeventd.enable || cfg.boot.thin.enable) { + boot.initrd.preLVMCommands = '' + mkdir -p /etc/lvm + cat << EOF >> /etc/lvm/lvm.conf + ${optionalString cfg.boot.thin.enable ( + concatMapStringsSep "\n" + (bin: "global/${bin}_executable = $(command -v ${bin})") + [ "thin_check" "thin_dump" "thin_repair" "cache_check" "cache_dump" "cache_repair" ] + ) + } + ${optionalString cfg.dmeventd.enable '' + dmeventd/executable = "$(command -v false)" + activation/monitoring = 0 + ''} + EOF + ''; + }) + ]; + +} diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix new file mode 100644 index 00000000000..19f2be2c4a2 --- /dev/null +++ b/nixos/modules/tasks/network-interfaces-scripted.nix @@ -0,0 +1,625 @@ +{ config, lib, pkgs, utils, ... }: + +with utils; +with lib; + +let + + cfg = config.networking; + interfaces = attrValues cfg.interfaces; + + slaves = concatMap (i: i.interfaces) (attrValues cfg.bonds) + ++ concatMap (i: i.interfaces) (attrValues cfg.bridges) + ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches) + ++ concatMap (i: [i.interface]) (attrValues cfg.macvlans) + ++ concatMap (i: [i.interface]) (attrValues cfg.vlans); + + # We must escape interfaces due to the systemd interpretation + subsystemDevice = interface: + "sys-subsystem-net-devices-${escapeSystemdPath interface}.device"; + + interfaceIps = i: + i.ipv4.addresses + ++ optionals cfg.enableIPv6 i.ipv6.addresses; + + destroyBond = i: '' + while true; do + UPDATED=1 + SLAVES=$(ip link | grep 'master ${i}' | awk -F: '{print $2}') + for I in $SLAVES; do + UPDATED=0 + ip link set "$I" nomaster + done + [ "$UPDATED" -eq "1" ] && break + done + ip link set "${i}" down 2>/dev/null || true + ip link del "${i}" 2>/dev/null || true + ''; + + # warn that these attributes are deprecated (2017-2-2) + # Should be removed in the release after next + bondDeprecation = rec { + deprecated = [ "lacp_rate" "miimon" "mode" "xmit_hash_policy" ]; + filterDeprecated = bond: (filterAttrs (attrName: attr: + elem attrName deprecated && attr != null) bond); + }; + + bondWarnings = + let oneBondWarnings = bondName: bond: + mapAttrsToList (bondText bondName) (bondDeprecation.filterDeprecated bond); + bondText = bondName: optName: _: + "${bondName}.${optName} is deprecated, use ${bondName}.driverOptions"; + in { + warnings = flatten (mapAttrsToList oneBondWarnings cfg.bonds); + }; + + normalConfig = { + systemd.network.links = let + createNetworkLink = i: nameValuePair "40-${i.name}" { + matchConfig.OriginalName = i.name; + linkConfig = optionalAttrs (i.macAddress != null) { + MACAddress = i.macAddress; + } // optionalAttrs (i.mtu != null) { + MTUBytes = toString i.mtu; + } // optionalAttrs (i.wakeOnLan.enable == true) { + WakeOnLan = "magic"; + }; + }; + in listToAttrs (map createNetworkLink interfaces); + systemd.services = + let + + deviceDependency = dev: + # Use systemd service if we manage device creation, else + # trust udev when not in a container + if (hasAttr dev (filterAttrs (k: v: v.virtual) cfg.interfaces)) || + (hasAttr dev cfg.bridges) || + (hasAttr dev cfg.bonds) || + (hasAttr dev cfg.macvlans) || + (hasAttr dev cfg.sits) || + (hasAttr dev cfg.vlans) || + (hasAttr dev cfg.vswitches) + then [ "${dev}-netdev.service" ] + else optional (dev != null && dev != "lo" && !config.boot.isContainer) (subsystemDevice dev); + + hasDefaultGatewaySet = (cfg.defaultGateway != null && cfg.defaultGateway.address != "") + || (cfg.enableIPv6 && cfg.defaultGateway6 != null && cfg.defaultGateway6.address != ""); + + networkLocalCommands = { + after = [ "network-setup.service" ]; + bindsTo = [ "network-setup.service" ]; + }; + + networkSetup = + { description = "Networking Setup"; + + after = [ "network-pre.target" "systemd-udevd.service" "systemd-sysctl.service" ]; + before = [ "network.target" "shutdown.target" ]; + wants = [ "network.target" ]; + # exclude bridges from the partOf relationship to fix container networking bug #47210 + partOf = map (i: "network-addresses-${i.name}.service") (filter (i: !(hasAttr i.name cfg.bridges)) interfaces); + conflicts = [ "shutdown.target" ]; + wantedBy = [ "multi-user.target" ] ++ optional hasDefaultGatewaySet "network-online.target"; + + unitConfig.ConditionCapability = "CAP_NET_ADMIN"; + + path = [ pkgs.iproute2 ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + + unitConfig.DefaultDependencies = false; + + script = + '' + ${optionalString config.networking.resolvconf.enable '' + # Set the static DNS configuration, if given. + ${pkgs.openresolv}/sbin/resolvconf -m 1 -a static <<EOF + ${optionalString (cfg.nameservers != [] && cfg.domain != null) '' + domain ${cfg.domain} + ''} + ${optionalString (cfg.search != []) ("search " + concatStringsSep " " cfg.search)} + ${flip concatMapStrings cfg.nameservers (ns: '' + nameserver ${ns} + '')} + EOF + ''} + + # Set the default gateway. + ${optionalString (cfg.defaultGateway != null && cfg.defaultGateway.address != "") '' + ${optionalString (cfg.defaultGateway.interface != null) '' + ip route replace ${cfg.defaultGateway.address} dev ${cfg.defaultGateway.interface} ${optionalString (cfg.defaultGateway.metric != null) + "metric ${toString cfg.defaultGateway.metric}" + } proto static + ''} + ip route replace default ${optionalString (cfg.defaultGateway.metric != null) + "metric ${toString cfg.defaultGateway.metric}" + } via "${cfg.defaultGateway.address}" ${ + optionalString (cfg.defaultGatewayWindowSize != null) + "window ${toString cfg.defaultGatewayWindowSize}"} ${ + optionalString (cfg.defaultGateway.interface != null) + "dev ${cfg.defaultGateway.interface}"} proto static + ''} + ${optionalString (cfg.defaultGateway6 != null && cfg.defaultGateway6.address != "") '' + ${optionalString (cfg.defaultGateway6.interface != null) '' + ip -6 route replace ${cfg.defaultGateway6.address} dev ${cfg.defaultGateway6.interface} ${optionalString (cfg.defaultGateway6.metric != null) + "metric ${toString cfg.defaultGateway6.metric}" + } proto static + ''} + ip -6 route replace default ${optionalString (cfg.defaultGateway6.metric != null) + "metric ${toString cfg.defaultGateway6.metric}" + } via "${cfg.defaultGateway6.address}" ${ + optionalString (cfg.defaultGatewayWindowSize != null) + "window ${toString cfg.defaultGatewayWindowSize}"} ${ + optionalString (cfg.defaultGateway6.interface != null) + "dev ${cfg.defaultGateway6.interface}"} proto static + ''} + ''; + }; + + # For each interface <foo>, create a job ‘network-addresses-<foo>.service" + # that performs static address configuration. It has a "wants" + # dependency on ‘<foo>.service’, which is supposed to create + # the interface and need not exist (i.e. for hardware + # interfaces). It has a binds-to dependency on the actual + # network device, so it only gets started after the interface + # has appeared, and it's stopped when the interface + # disappears. + configureAddrs = i: + let + ips = interfaceIps i; + in + nameValuePair "network-addresses-${i.name}" + { description = "Address configuration of ${i.name}"; + wantedBy = [ + "network-setup.service" + "network.target" + ]; + # order before network-setup because the routes that are configured + # there may need ip addresses configured + before = [ "network-setup.service" ]; + bindsTo = deviceDependency i.name; + after = [ "network-pre.target" ] ++ (deviceDependency i.name); + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + # Restart rather than stop+start this unit to prevent the + # network from dying during switch-to-configuration. + stopIfChanged = false; + path = [ pkgs.iproute2 ]; + script = + '' + state="/run/nixos/network/addresses/${i.name}" + mkdir -p $(dirname "$state") + + ip link set "${i.name}" up + + ${flip concatMapStrings ips (ip: + let + cidr = "${ip.address}/${toString ip.prefixLength}"; + in + '' + echo "${cidr}" >> $state + echo -n "adding address ${cidr}... " + if out=$(ip addr add "${cidr}" dev "${i.name}" 2>&1); then + echo "done" + elif ! echo "$out" | grep "File exists" >/dev/null 2>&1; then + echo "'ip addr add "${cidr}" dev "${i.name}"' failed: $out" + exit 1 + fi + '' + )} + + state="/run/nixos/network/routes/${i.name}" + mkdir -p $(dirname "$state") + + ${flip concatMapStrings (i.ipv4.routes ++ i.ipv6.routes) (route: + let + cidr = "${route.address}/${toString route.prefixLength}"; + via = optionalString (route.via != null) ''via "${route.via}"''; + options = concatStrings (mapAttrsToList (name: val: "${name} ${val} ") route.options); + in + '' + echo "${cidr}" >> $state + echo -n "adding route ${cidr}... " + if out=$(ip route add "${cidr}" ${options} ${via} dev "${i.name}" proto static 2>&1); then + echo "done" + elif ! echo "$out" | grep "File exists" >/dev/null 2>&1; then + echo "'ip route add "${cidr}" ${options} ${via} dev "${i.name}"' failed: $out" + exit 1 + fi + '' + )} + ''; + preStop = '' + state="/run/nixos/network/routes/${i.name}" + if [ -e "$state" ]; then + while read cidr; do + echo -n "deleting route $cidr... " + ip route del "$cidr" dev "${i.name}" >/dev/null 2>&1 && echo "done" || echo "failed" + done < "$state" + rm -f "$state" + fi + + state="/run/nixos/network/addresses/${i.name}" + if [ -e "$state" ]; then + while read cidr; do + echo -n "deleting address $cidr... " + ip addr del "$cidr" dev "${i.name}" >/dev/null 2>&1 && echo "done" || echo "failed" + done < "$state" + rm -f "$state" + fi + ''; + }; + + createTunDevice = i: nameValuePair "${i.name}-netdev" + { description = "Virtual Network Interface ${i.name}"; + bindsTo = optional (!config.boot.isContainer) "dev-net-tun.device"; + after = optional (!config.boot.isContainer) "dev-net-tun.device" ++ [ "network-pre.target" ]; + wantedBy = [ "network-setup.service" (subsystemDevice i.name) ]; + partOf = [ "network-setup.service" ]; + before = [ "network-setup.service" ]; + path = [ pkgs.iproute2 ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + ip tuntap add dev "${i.name}" mode "${i.virtualType}" user "${i.virtualOwner}" + ''; + postStop = '' + ip link del ${i.name} || true + ''; + }; + + createBridgeDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = concatLists (map deviceDependency v.interfaces); + in + { description = "Bridge Interface ${n}"; + wantedBy = [ "network-setup.service" (subsystemDevice n) ]; + bindsTo = deps ++ optional v.rstp "mstpd.service"; + partOf = [ "network-setup.service" ] ++ optional v.rstp "mstpd.service"; + after = [ "network-pre.target" ] ++ deps ++ optional v.rstp "mstpd.service" + ++ map (i: "network-addresses-${i}.service") v.interfaces; + before = [ "network-setup.service" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute2 ]; + script = '' + # Remove Dead Interfaces + echo "Removing old bridge ${n}..." + ip link show "${n}" >/dev/null 2>&1 && ip link del "${n}" + + echo "Adding bridge ${n}..." + ip link add name "${n}" type bridge + + # Enslave child interfaces + ${flip concatMapStrings v.interfaces (i: '' + ip link set "${i}" master "${n}" + ip link set "${i}" up + '')} + # Save list of enslaved interfaces + echo "${flip concatMapStrings v.interfaces (i: '' + ${i} + '')}" > /run/${n}.interfaces + + ${optionalString config.virtualisation.libvirtd.enable '' + # Enslave dynamically added interfaces which may be lost on nixos-rebuild + # + # if `libvirtd.service` is not running, do not use `virsh` which would try activate it via 'libvirtd.socket' and thus start it out-of-order. + # `libvirtd.service` will set up bridge interfaces when it will start normally. + # + if /run/current-system/systemd/bin/systemctl --quiet is-active 'libvirtd.service'; then + for uri in qemu:///system lxc:///; do + for dom in $(${pkgs.libvirt}/bin/virsh -c $uri list --name); do + ${pkgs.libvirt}/bin/virsh -c $uri dumpxml "$dom" | \ + ${pkgs.xmlstarlet}/bin/xmlstarlet sel -t -m "//domain/devices/interface[@type='bridge'][source/@bridge='${n}'][target/@dev]" -v "concat('ip link set ',target/@dev,' master ',source/@bridge,';')" | \ + ${pkgs.bash}/bin/bash + done + done + fi + ''} + + # Enable stp on the interface + ${optionalString v.rstp '' + echo 2 >/sys/class/net/${n}/bridge/stp_state + ''} + + ip link set "${n}" up + ''; + postStop = '' + ip link set "${n}" down || true + ip link del "${n}" || true + rm -f /run/${n}.interfaces + ''; + reload = '' + # Un-enslave child interfaces (old list of interfaces) + for interface in `cat /run/${n}.interfaces`; do + ip link set "$interface" nomaster up + done + + # Enslave child interfaces (new list of interfaces) + ${flip concatMapStrings v.interfaces (i: '' + ip link set "${i}" master "${n}" + ip link set "${i}" up + '')} + # Save list of enslaved interfaces + echo "${flip concatMapStrings v.interfaces (i: '' + ${i} + '')}" > /run/${n}.interfaces + + # (Un-)set stp on the bridge + echo ${if v.rstp then "2" else "0"} > /sys/class/net/${n}/bridge/stp_state + ''; + reloadIfChanged = true; + }); + + createVswitchDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = concatLists (map deviceDependency (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces))); + internalConfigs = map (i: "network-addresses-${i}.service") (attrNames (filterAttrs (_: config: config.type == "internal") v.interfaces)); + ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules; + in + { description = "Open vSwitch Interface ${n}"; + wantedBy = [ "network-setup.service" (subsystemDevice n) ] ++ internalConfigs; + # before = [ "network-setup.service" ]; + # should work without internalConfigs dependencies because address/link configuration depends + # on the device, which is created by ovs-vswitchd with type=internal, but it does not... + before = [ "network-setup.service" ] ++ internalConfigs; + partOf = [ "network-setup.service" ]; # shutdown the bridge when network is shutdown + bindsTo = [ "ovs-vswitchd.service" ]; # requires ovs-vswitchd to be alive at all times + after = [ "network-pre.target" "ovs-vswitchd.service" ] ++ deps; # start switch after physical interfaces and vswitch daemon + wants = deps; # if one or more interface fails, the switch should continue to run + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute2 config.virtualisation.vswitch.package ]; + preStart = '' + echo "Resetting Open vSwitch ${n}..." + ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \ + -- set bridge ${n} protocols=${concatStringsSep "," v.supportedOpenFlowVersions} + ''; + script = '' + echo "Configuring Open vSwitch ${n}..." + ovs-vsctl ${concatStrings (mapAttrsToList (name: config: " -- add-port ${n} ${name}" + optionalString (config.vlan != null) " tag=${toString config.vlan}") v.interfaces)} \ + ${concatStrings (mapAttrsToList (name: config: optionalString (config.type != null) " -- set interface ${name} type=${config.type}") v.interfaces)} \ + ${concatMapStrings (x: " -- set-controller ${n} " + x) v.controllers} \ + ${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)} + + + echo "Adding OpenFlow rules for Open vSwitch ${n}..." + ovs-ofctl --protocols=${v.openFlowVersion} add-flows ${n} ${ofRules} + ''; + postStop = '' + echo "Cleaning Open vSwitch ${n}" + echo "Shuting down internal ${n} interface" + ip link set ${n} down || true + echo "Deleting flows for ${n}" + ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true + echo "Deleting Open vSwitch ${n}" + ovs-vsctl --if-exists del-br ${n} || true + ''; + }); + + createBondDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = concatLists (map deviceDependency v.interfaces); + in + { description = "Bond Interface ${n}"; + wantedBy = [ "network-setup.service" (subsystemDevice n) ]; + bindsTo = deps; + partOf = [ "network-setup.service" ]; + after = [ "network-pre.target" ] ++ deps + ++ map (i: "network-addresses-${i}.service") v.interfaces; + before = [ "network-setup.service" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute2 pkgs.gawk ]; + script = '' + echo "Destroying old bond ${n}..." + ${destroyBond n} + + echo "Creating new bond ${n}..." + ip link add name "${n}" type bond \ + ${let opts = (mapAttrs (const toString) + (bondDeprecation.filterDeprecated v)) + // v.driverOptions; + in concatStringsSep "\n" + (mapAttrsToList (set: val: " ${set} ${val} \\") opts)} + + # !!! There must be a better way to wait for the interface + while [ ! -d "/sys/class/net/${n}" ]; do sleep 0.1; done; + + # Bring up the bond and enslave the specified interfaces + ip link set "${n}" up + ${flip concatMapStrings v.interfaces (i: '' + ip link set "${i}" down + ip link set "${i}" master "${n}" + '')} + ''; + postStop = destroyBond n; + }); + + createMacvlanDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = deviceDependency v.interface; + in + { description = "Vlan Interface ${n}"; + wantedBy = [ "network-setup.service" (subsystemDevice n) ]; + bindsTo = deps; + partOf = [ "network-setup.service" ]; + after = [ "network-pre.target" ] ++ deps; + before = [ "network-setup.service" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute2 ]; + script = '' + # Remove Dead Interfaces + ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" + ip link add link "${v.interface}" name "${n}" type macvlan \ + ${optionalString (v.mode != null) "mode ${v.mode}"} + ip link set "${n}" up + ''; + postStop = '' + ip link delete "${n}" || true + ''; + }); + + createFouEncapsulation = n: v: nameValuePair "${n}-fou-encap" + (let + # if we have a device to bind to we can wait for its addresses to be + # configured, otherwise external sequencing is required. + deps = optionals (v.local != null && v.local.dev != null) + (deviceDependency v.local.dev ++ [ "network-addresses-${v.local.dev}.service" ]); + fouSpec = "port ${toString v.port} ${ + if v.protocol != null then "ipproto ${toString v.protocol}" else "gue" + } ${ + optionalString (v.local != null) "local ${escapeShellArg v.local.address} ${ + optionalString (v.local.dev != null) "dev ${escapeShellArg v.local.dev}" + }" + }"; + in + { description = "FOU endpoint ${n}"; + wantedBy = [ "network-setup.service" (subsystemDevice n) ]; + bindsTo = deps; + partOf = [ "network-setup.service" ]; + after = [ "network-pre.target" ] ++ deps; + before = [ "network-setup.service" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute2 ]; + script = '' + # always remove previous incarnation since show can't filter + ip fou del ${fouSpec} >/dev/null 2>&1 || true + ip fou add ${fouSpec} + ''; + postStop = '' + ip fou del ${fouSpec} || true + ''; + }); + + createSitDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = deviceDependency v.dev; + in + { description = "6-to-4 Tunnel Interface ${n}"; + wantedBy = [ "network-setup.service" (subsystemDevice n) ]; + bindsTo = deps; + partOf = [ "network-setup.service" ]; + after = [ "network-pre.target" ] ++ deps; + before = [ "network-setup.service" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute2 ]; + script = '' + # Remove Dead Interfaces + ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" + ip link add name "${n}" type sit \ + ${optionalString (v.remote != null) "remote \"${v.remote}\""} \ + ${optionalString (v.local != null) "local \"${v.local}\""} \ + ${optionalString (v.ttl != null) "ttl ${toString v.ttl}"} \ + ${optionalString (v.dev != null) "dev \"${v.dev}\""} \ + ${optionalString (v.encapsulation != null) + "encap ${v.encapsulation.type} encap-dport ${toString v.encapsulation.port} ${ + optionalString (v.encapsulation.sourcePort != null) + "encap-sport ${toString v.encapsulation.sourcePort}" + }"} + ip link set "${n}" up + ''; + postStop = '' + ip link delete "${n}" || true + ''; + }); + + createGreDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = deviceDependency v.dev; + in + { description = "GRE Tunnel Interface ${n}"; + wantedBy = [ "network-setup.service" (subsystemDevice n) ]; + bindsTo = deps; + partOf = [ "network-setup.service" ]; + after = [ "network-pre.target" ] ++ deps; + before = [ "network-setup.service" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute2 ]; + script = '' + # Remove Dead Interfaces + ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" + ip link add name "${n}" type ${v.type} \ + ${optionalString (v.remote != null) "remote \"${v.remote}\""} \ + ${optionalString (v.local != null) "local \"${v.local}\""} \ + ${optionalString (v.dev != null) "dev \"${v.dev}\""} + ip link set "${n}" up + ''; + postStop = '' + ip link delete "${n}" || true + ''; + }); + + createVlanDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = deviceDependency v.interface; + in + { description = "Vlan Interface ${n}"; + wantedBy = [ "network-setup.service" (subsystemDevice n) ]; + bindsTo = deps; + partOf = [ "network-setup.service" ]; + after = [ "network-pre.target" ] ++ deps; + before = [ "network-setup.service" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute2 ]; + script = '' + # Remove Dead Interfaces + ip link show "${n}" >/dev/null 2>&1 && ip link delete "${n}" + ip link add link "${v.interface}" name "${n}" type vlan id "${toString v.id}" + + # We try to bring up the logical VLAN interface. If the master + # interface the logical interface is dependent upon is not up yet we will + # fail to immediately bring up the logical interface. The resulting logical + # interface will brought up later when the master interface is up. + ip link set "${n}" up || true + ''; + postStop = '' + ip link delete "${n}" || true + ''; + }); + + in listToAttrs ( + map configureAddrs interfaces ++ + map createTunDevice (filter (i: i.virtual) interfaces)) + // mapAttrs' createBridgeDevice cfg.bridges + // mapAttrs' createVswitchDevice cfg.vswitches + // mapAttrs' createBondDevice cfg.bonds + // mapAttrs' createMacvlanDevice cfg.macvlans + // mapAttrs' createFouEncapsulation cfg.fooOverUDP + // mapAttrs' createSitDevice cfg.sits + // mapAttrs' createGreDevice cfg.greTunnels + // mapAttrs' createVlanDevice cfg.vlans + // { + network-setup = networkSetup; + network-local-commands = networkLocalCommands; + }; + + services.udev.extraRules = + '' + KERNEL=="tun", TAG+="systemd" + ''; + + + }; + +in + +{ + config = mkMerge [ + bondWarnings + (mkIf (!cfg.useNetworkd) normalConfig) + { # Ensure slave interfaces are brought up + networking.interfaces = genAttrs slaves (i: {}); + } + ]; +} diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix new file mode 100644 index 00000000000..8a5e1b5af11 --- /dev/null +++ b/nixos/modules/tasks/network-interfaces-systemd.nix @@ -0,0 +1,406 @@ +{ config, lib, utils, pkgs, ... }: + +with utils; +with lib; + +let + + cfg = config.networking; + interfaces = attrValues cfg.interfaces; + + interfaceIps = i: + i.ipv4.addresses + ++ optionals cfg.enableIPv6 i.ipv6.addresses; + + interfaceRoutes = i: + i.ipv4.routes + ++ optionals cfg.enableIPv6 i.ipv6.routes; + + dhcpStr = useDHCP: if useDHCP == true || useDHCP == null then "yes" else "no"; + + slaves = + concatLists (map (bond: bond.interfaces) (attrValues cfg.bonds)) + ++ concatLists (map (bridge: bridge.interfaces) (attrValues cfg.bridges)) + ++ map (sit: sit.dev) (attrValues cfg.sits) + ++ map (gre: gre.dev) (attrValues cfg.greTunnels) + ++ map (vlan: vlan.interface) (attrValues cfg.vlans) + # add dependency to physical or independently created vswitch member interface + # TODO: warn the user that any address configured on those interfaces will be useless + ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches); + +in + +{ + + config = mkIf cfg.useNetworkd { + + assertions = [ { + assertion = cfg.defaultGatewayWindowSize == null; + message = "networking.defaultGatewayWindowSize is not supported by networkd."; + } { + assertion = cfg.defaultGateway == null || cfg.defaultGateway.interface == null; + message = "networking.defaultGateway.interface is not supported by networkd."; + } { + assertion = cfg.defaultGateway6 == null || cfg.defaultGateway6.interface == null; + message = "networking.defaultGateway6.interface is not supported by networkd."; + } { + assertion = cfg.useDHCP == false; + message = '' + networking.useDHCP is not supported by networkd. + Please use per interface configuration and set the global option to false. + ''; + } ] ++ flip mapAttrsToList cfg.bridges (n: { rstp, ... }: { + assertion = !rstp; + message = "networking.bridges.${n}.rstp is not supported by networkd."; + }) ++ flip mapAttrsToList cfg.fooOverUDP (n: { local, ... }: { + assertion = local == null; + message = "networking.fooOverUDP.${n}.local is not supported by networkd."; + }); + + networking.dhcpcd.enable = mkDefault false; + + systemd.network = + let + domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain); + genericNetwork = override: + let gateway = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address + ++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address; + in optionalAttrs (gateway != [ ]) { + routes = override [ + { + routeConfig = { + Gateway = gateway; + GatewayOnLink = false; + }; + } + ]; + } // optionalAttrs (domains != [ ]) { + domains = override domains; + }; + in mkMerge [ { + enable = true; + } + (mkMerge (forEach interfaces (i: { + netdevs = mkIf i.virtual ({ + "40-${i.name}" = { + netdevConfig = { + Name = i.name; + Kind = i.virtualType; + }; + "${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) { + User = i.virtualOwner; + }; + }; + }); + networks."40-${i.name}" = mkMerge [ (genericNetwork id) { + name = mkDefault i.name; + DHCP = mkForce (dhcpStr + (if i.useDHCP != null then i.useDHCP else false)); + address = forEach (interfaceIps i) + (ip: "${ip.address}/${toString ip.prefixLength}"); + routes = forEach (interfaceRoutes i) + (route: { + # Most of these route options have not been tested. + # Please fix or report any mistakes you may find. + routeConfig = + optionalAttrs (route.prefixLength > 0) { + Destination = "${route.address}/${toString route.prefixLength}"; + } // + optionalAttrs (route.options ? fastopen_no_cookie) { + FastOpenNoCookie = route.options.fastopen_no_cookie; + } // + optionalAttrs (route.via != null) { + Gateway = route.via; + } // + optionalAttrs (route.options ? onlink) { + GatewayOnLink = true; + } // + optionalAttrs (route.options ? initrwnd) { + InitialAdvertisedReceiveWindow = route.options.initrwnd; + } // + optionalAttrs (route.options ? initcwnd) { + InitialCongestionWindow = route.options.initcwnd; + } // + optionalAttrs (route.options ? pref) { + IPv6Preference = route.options.pref; + } // + optionalAttrs (route.options ? mtu) { + MTUBytes = route.options.mtu; + } // + optionalAttrs (route.options ? metric) { + Metric = route.options.metric; + } // + optionalAttrs (route.options ? src) { + PreferredSource = route.options.src; + } // + optionalAttrs (route.options ? protocol) { + Protocol = route.options.protocol; + } // + optionalAttrs (route.options ? quickack) { + QuickAck = route.options.quickack; + } // + optionalAttrs (route.options ? scope) { + Scope = route.options.scope; + } // + optionalAttrs (route.options ? from) { + Source = route.options.from; + } // + optionalAttrs (route.options ? table) { + Table = route.options.table; + } // + optionalAttrs (route.options ? advmss) { + TCPAdvertisedMaximumSegmentSize = route.options.advmss; + } // + optionalAttrs (route.options ? ttl-propagate) { + TTLPropagate = route.options.ttl-propagate == "enabled"; + }; + }); + networkConfig.IPv6PrivacyExtensions = "kernel"; + linkConfig = optionalAttrs (i.macAddress != null) { + MACAddress = i.macAddress; + } // optionalAttrs (i.mtu != null) { + MTUBytes = toString i.mtu; + }; + }]; + }))) + (mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "bridge"; + }; + }; + networks = listToAttrs (forEach bridge.interfaces (bi: + nameValuePair "40-${bi}" (mkMerge [ (genericNetwork (mkOverride 999)) { + DHCP = mkOverride 0 (dhcpStr false); + networkConfig.Bridge = name; + } ]))); + }))) + (mkMerge (flip mapAttrsToList cfg.bonds (name: bond: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "bond"; + }; + bondConfig = let + # manual mapping as of 2017-02-03 + # man 5 systemd.netdev [BOND] + # to https://www.kernel.org/doc/Documentation/networking/bonding.txt + # driver options. + driverOptionMapping = let + trans = f: optName: { valTransform = f; optNames = [optName]; }; + simp = trans id; + ms = trans (v: v + "ms"); + in { + Mode = simp "mode"; + TransmitHashPolicy = simp "xmit_hash_policy"; + LACPTransmitRate = simp "lacp_rate"; + MIIMonitorSec = ms "miimon"; + UpDelaySec = ms "updelay"; + DownDelaySec = ms "downdelay"; + LearnPacketIntervalSec = simp "lp_interval"; + AdSelect = simp "ad_select"; + FailOverMACPolicy = simp "fail_over_mac"; + ARPValidate = simp "arp_validate"; + # apparently in ms for this value?! Upstream bug? + ARPIntervalSec = simp "arp_interval"; + ARPIPTargets = simp "arp_ip_target"; + ARPAllTargets = simp "arp_all_targets"; + PrimaryReselectPolicy = simp "primary_reselect"; + ResendIGMP = simp "resend_igmp"; + PacketsPerSlave = simp "packets_per_slave"; + GratuitousARP = { valTransform = id; + optNames = [ "num_grat_arp" "num_unsol_na" ]; }; + AllSlavesActive = simp "all_slaves_active"; + MinLinks = simp "min_links"; + }; + + do = bond.driverOptions; + assertNoUnknownOption = let + knownOptions = flatten (mapAttrsToList (_: kOpts: kOpts.optNames) + driverOptionMapping); + # options that apparently don’t exist in the networkd config + unknownOptions = [ "primary" ]; + assertTrace = bool: msg: if bool then true else builtins.trace msg false; + in assert all (driverOpt: assertTrace + (elem driverOpt (knownOptions ++ unknownOptions)) + "The bond.driverOption `${driverOpt}` cannot be mapped to the list of known networkd bond options. Please add it to the mapping above the assert or to `unknownOptions` should it not exist in networkd.") + (mapAttrsToList (k: _: k) do); ""; + # get those driverOptions that have been set + filterSystemdOptions = filterAttrs (sysDOpt: kOpts: + any (kOpt: do ? ${kOpt}) kOpts.optNames); + # build final set of systemd options to bond values + buildOptionSet = mapAttrs (_: kOpts: with kOpts; + # we simply take the first set kernel bond option + # (one option has multiple names, which is silly) + head (map (optN: valTransform (do.${optN})) + # only map those that exist + (filter (o: do ? ${o}) optNames))); + in seq assertNoUnknownOption + (buildOptionSet (filterSystemdOptions driverOptionMapping)); + + }; + + networks = listToAttrs (forEach bond.interfaces (bi: + nameValuePair "40-${bi}" (mkMerge [ (genericNetwork (mkOverride 999)) { + DHCP = mkOverride 0 (dhcpStr false); + networkConfig.Bond = name; + } ]))); + }))) + (mkMerge (flip mapAttrsToList cfg.macvlans (name: macvlan: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "macvlan"; + }; + macvlanConfig = optionalAttrs (macvlan.mode != null) { Mode = macvlan.mode; }; + }; + networks."40-${macvlan.interface}" = (mkMerge [ (genericNetwork (mkOverride 999)) { + macvlan = [ name ]; + } ]); + }))) + (mkMerge (flip mapAttrsToList cfg.fooOverUDP (name: fou: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "fou"; + }; + # unfortunately networkd cannot encode dependencies of netdevs on addresses/routes, + # so we cannot specify Local=, Peer=, PeerPort=. this looks like a missing feature + # in networkd. + fooOverUDPConfig = { + Port = fou.port; + Encapsulation = if fou.protocol != null then "FooOverUDP" else "GenericUDPEncapsulation"; + } // (optionalAttrs (fou.protocol != null) { + Protocol = fou.protocol; + }); + }; + }))) + (mkMerge (flip mapAttrsToList cfg.sits (name: sit: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "sit"; + }; + tunnelConfig = + (optionalAttrs (sit.remote != null) { + Remote = sit.remote; + }) // (optionalAttrs (sit.local != null) { + Local = sit.local; + }) // (optionalAttrs (sit.ttl != null) { + TTL = sit.ttl; + }) // (optionalAttrs (sit.encapsulation != null) ( + { + FooOverUDP = true; + Encapsulation = + if sit.encapsulation.type == "fou" + then "FooOverUDP" + else "GenericUDPEncapsulation"; + FOUDestinationPort = sit.encapsulation.port; + } // (optionalAttrs (sit.encapsulation.sourcePort != null) { + FOUSourcePort = sit.encapsulation.sourcePort; + }))); + }; + networks = mkIf (sit.dev != null) { + "40-${sit.dev}" = (mkMerge [ (genericNetwork (mkOverride 999)) { + tunnel = [ name ]; + } ]); + }; + }))) + (mkMerge (flip mapAttrsToList cfg.greTunnels (name: gre: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = gre.type; + }; + tunnelConfig = + (optionalAttrs (gre.remote != null) { + Remote = gre.remote; + }) // (optionalAttrs (gre.local != null) { + Local = gre.local; + }); + }; + networks = mkIf (gre.dev != null) { + "40-${gre.dev}" = (mkMerge [ (genericNetwork (mkOverride 999)) { + tunnel = [ name ]; + } ]); + }; + }))) + (mkMerge (flip mapAttrsToList cfg.vlans (name: vlan: { + netdevs."40-${name}" = { + netdevConfig = { + Name = name; + Kind = "vlan"; + }; + vlanConfig.Id = vlan.id; + }; + networks."40-${vlan.interface}" = (mkMerge [ (genericNetwork (mkOverride 999)) { + vlan = [ name ]; + } ]); + }))) + ]; + + # We need to prefill the slaved devices with networking options + # This forces the network interface creator to initialize slaves. + networking.interfaces = listToAttrs (map (i: nameValuePair i { }) slaves); + + systemd.services = let + # We must escape interfaces due to the systemd interpretation + subsystemDevice = interface: + "sys-subsystem-net-devices-${escapeSystemdPath interface}.device"; + # support for creating openvswitch switches + createVswitchDevice = n: v: nameValuePair "${n}-netdev" + (let + deps = map subsystemDevice (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces)); + ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules; + in + { description = "Open vSwitch Interface ${n}"; + wantedBy = [ "network.target" (subsystemDevice n) ]; + # and create bridge before systemd-networkd starts because it might create internal interfaces + before = [ "systemd-networkd.service" ]; + # shutdown the bridge when network is shutdown + partOf = [ "network.target" ]; + # requires ovs-vswitchd to be alive at all times + bindsTo = [ "ovs-vswitchd.service" ]; + # start switch after physical interfaces and vswitch daemon + after = [ "network-pre.target" "ovs-vswitchd.service" ] ++ deps; + wants = deps; # if one or more interface fails, the switch should continue to run + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + path = [ pkgs.iproute2 config.virtualisation.vswitch.package ]; + preStart = '' + echo "Resetting Open vSwitch ${n}..." + ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \ + -- set bridge ${n} protocols=${concatStringsSep "," v.supportedOpenFlowVersions} + ''; + script = '' + echo "Configuring Open vSwitch ${n}..." + ovs-vsctl ${concatStrings (mapAttrsToList (name: config: " -- add-port ${n} ${name}" + optionalString (config.vlan != null) " tag=${toString config.vlan}") v.interfaces)} \ + ${concatStrings (mapAttrsToList (name: config: optionalString (config.type != null) " -- set interface ${name} type=${config.type}") v.interfaces)} \ + ${concatMapStrings (x: " -- set-controller ${n} " + x) v.controllers} \ + ${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)} + + + echo "Adding OpenFlow rules for Open vSwitch ${n}..." + ovs-ofctl --protocols=${v.openFlowVersion} add-flows ${n} ${ofRules} + ''; + postStop = '' + echo "Cleaning Open vSwitch ${n}" + echo "Shuting down internal ${n} interface" + ip link set ${n} down || true + echo "Deleting flows for ${n}" + ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true + echo "Deleting Open vSwitch ${n}" + ovs-vsctl --if-exists del-br ${n} || true + ''; + }); + in mapAttrs' createVswitchDevice cfg.vswitches + // { + "network-local-commands" = { + after = [ "systemd-networkd.service" ]; + bindsTo = [ "systemd-networkd.service" ]; + }; + }; + }; + +} diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix new file mode 100644 index 00000000000..01980b80f1c --- /dev/null +++ b/nixos/modules/tasks/network-interfaces.nix @@ -0,0 +1,1537 @@ +{ config, options, lib, pkgs, utils, ... }: + +with lib; +with utils; + +let + + cfg = config.networking; + opt = options.networking; + interfaces = attrValues cfg.interfaces; + hasVirtuals = any (i: i.virtual) interfaces; + hasSits = cfg.sits != { }; + hasGres = cfg.greTunnels != { }; + hasBonds = cfg.bonds != { }; + hasFous = cfg.fooOverUDP != { } + || filterAttrs (_: s: s.encapsulation != null) cfg.sits != { }; + + slaves = concatMap (i: i.interfaces) (attrValues cfg.bonds) + ++ concatMap (i: i.interfaces) (attrValues cfg.bridges) + ++ concatMap (i: attrNames (filterAttrs (name: config: ! (config.type == "internal" || hasAttr name cfg.interfaces)) i.interfaces)) (attrValues cfg.vswitches); + + slaveIfs = map (i: cfg.interfaces.${i}) (filter (i: cfg.interfaces ? ${i}) slaves); + + rstpBridges = flip filterAttrs cfg.bridges (_: { rstp, ... }: rstp); + + needsMstpd = rstpBridges != { }; + + bridgeStp = optional needsMstpd (pkgs.writeTextFile { + name = "bridge-stp"; + executable = true; + destination = "/bin/bridge-stp"; + text = '' + #!${pkgs.runtimeShell} -e + export PATH="${pkgs.mstpd}/bin" + + BRIDGES=(${concatStringsSep " " (attrNames rstpBridges)}) + for BRIDGE in $BRIDGES; do + if [ "$BRIDGE" = "$1" ]; then + if [ "$2" = "start" ]; then + mstpctl addbridge "$BRIDGE" + exit 0 + elif [ "$2" = "stop" ]; then + mstpctl delbridge "$BRIDGE" + exit 0 + fi + exit 1 + fi + done + exit 1 + ''; + }); + + # We must escape interfaces due to the systemd interpretation + subsystemDevice = interface: + "sys-subsystem-net-devices-${escapeSystemdPath interface}.device"; + + addrOpts = v: + assert v == 4 || v == 6; + { options = { + address = mkOption { + type = types.str; + description = '' + IPv${toString v} address of the interface. Leave empty to configure the + interface using DHCP. + ''; + }; + + prefixLength = mkOption { + type = types.addCheck types.int (n: n >= 0 && n <= (if v == 4 then 32 else 128)); + description = '' + Subnet mask of the interface, specified as the number of + bits in the prefix (<literal>${if v == 4 then "24" else "64"}</literal>). + ''; + }; + }; + }; + + routeOpts = v: + { options = { + address = mkOption { + type = types.str; + description = "IPv${toString v} address of the network."; + }; + + prefixLength = mkOption { + type = types.addCheck types.int (n: n >= 0 && n <= (if v == 4 then 32 else 128)); + description = '' + Subnet mask of the network, specified as the number of + bits in the prefix (<literal>${if v == 4 then "24" else "64"}</literal>). + ''; + }; + + via = mkOption { + type = types.nullOr types.str; + default = null; + description = "IPv${toString v} address of the next hop."; + }; + + options = mkOption { + type = types.attrsOf types.str; + default = { }; + example = { mtu = "1492"; window = "524288"; }; + description = '' + Other route options. See the symbol <literal>OPTIONS</literal> + in the <literal>ip-route(8)</literal> manual page for the details. + You may also specify <literal>metric</literal>, + <literal>src</literal>, <literal>protocol</literal>, + <literal>scope</literal>, <literal>from</literal> + and <literal>table</literal>, which are technically + not route options, in the sense used in the manual. + ''; + }; + + }; + }; + + gatewayCoerce = address: { inherit address; }; + + gatewayOpts = { ... }: { + + options = { + + address = mkOption { + type = types.str; + description = "The default gateway address."; + }; + + interface = mkOption { + type = types.nullOr types.str; + default = null; + example = "enp0s3"; + description = "The default gateway interface."; + }; + + metric = mkOption { + type = types.nullOr types.int; + default = null; + example = 42; + description = "The default gateway metric/preference."; + }; + + }; + + }; + + interfaceOpts = { name, ... }: { + + options = { + name = mkOption { + example = "eth0"; + type = types.str; + description = "Name of the interface."; + }; + + tempAddress = mkOption { + type = types.enum (lib.attrNames tempaddrValues); + default = cfg.tempAddresses; + defaultText = literalExpression ''config.networking.tempAddresses''; + description = '' + When IPv6 is enabled with SLAAC, this option controls the use of + temporary address (aka privacy extensions) on this + interface. This is used to reduce tracking. + + See also the global option + <xref linkend="opt-networking.tempAddresses"/>, which + applies to all interfaces where this is not set. + + Possible values are: + ${tempaddrDoc} + ''; + }; + + useDHCP = mkOption { + type = types.nullOr types.bool; + default = null; + description = '' + Whether this interface should be configured with dhcp. + Null implies the old behavior which depends on whether ip addresses + are specified or not. + ''; + }; + + ipv4.addresses = mkOption { + default = [ ]; + example = [ + { address = "10.0.0.1"; prefixLength = 16; } + { address = "192.168.1.1"; prefixLength = 24; } + ]; + type = with types; listOf (submodule (addrOpts 4)); + description = '' + List of IPv4 addresses that will be statically assigned to the interface. + ''; + }; + + ipv6.addresses = mkOption { + default = [ ]; + example = [ + { address = "fdfd:b3f0:482::1"; prefixLength = 48; } + { address = "2001:1470:fffd:2098::e006"; prefixLength = 64; } + ]; + type = with types; listOf (submodule (addrOpts 6)); + description = '' + List of IPv6 addresses that will be statically assigned to the interface. + ''; + }; + + ipv4.routes = mkOption { + default = []; + example = [ + { address = "10.0.0.0"; prefixLength = 16; } + { address = "192.168.2.0"; prefixLength = 24; via = "192.168.1.1"; } + ]; + type = with types; listOf (submodule (routeOpts 4)); + description = '' + List of extra IPv4 static routes that will be assigned to the interface. + <warning><para>If the route type is the default <literal>unicast</literal>, then the scope + is set differently depending on the value of <option>networking.useNetworkd</option>: + the script-based backend sets it to <literal>link</literal>, while networkd sets + it to <literal>global</literal>.</para></warning> + If you want consistency between the two implementations, + set the scope of the route manually with + <literal>networking.interfaces.eth0.ipv4.routes = [{ options.scope = "global"; }]</literal> + for example. + ''; + }; + + ipv6.routes = mkOption { + default = []; + example = [ + { address = "fdfd:b3f0::"; prefixLength = 48; } + { address = "2001:1470:fffd:2098::"; prefixLength = 64; via = "fdfd:b3f0::1"; } + ]; + type = with types; listOf (submodule (routeOpts 6)); + description = '' + List of extra IPv6 static routes that will be assigned to the interface. + ''; + }; + + macAddress = mkOption { + default = null; + example = "00:11:22:33:44:55"; + type = types.nullOr (types.str); + description = '' + MAC address of the interface. Leave empty to use the default. + ''; + }; + + mtu = mkOption { + default = null; + example = 9000; + type = types.nullOr types.int; + description = '' + MTU size for packets leaving the interface. Leave empty to use the default. + ''; + }; + + virtual = mkOption { + default = false; + type = types.bool; + description = '' + Whether this interface is virtual and should be created by tunctl. + This is mainly useful for creating bridges between a host and a virtual + network such as VPN or a virtual machine. + ''; + }; + + virtualOwner = mkOption { + default = "root"; + type = types.str; + description = '' + In case of a virtual device, the user who owns it. + ''; + }; + + virtualType = mkOption { + default = if hasPrefix "tun" name then "tun" else "tap"; + defaultText = literalExpression ''if hasPrefix "tun" name then "tun" else "tap"''; + type = with types; enum [ "tun" "tap" ]; + description = '' + The type of interface to create. + The default is TUN for an interface name starting + with "tun", otherwise TAP. + ''; + }; + + proxyARP = mkOption { + default = false; + type = types.bool; + description = '' + Turn on proxy_arp for this device. + This is mainly useful for creating pseudo-bridges between a real + interface and a virtual network such as VPN or a virtual machine for + interfaces that don't support real bridging (most wlan interfaces). + As ARP proxying acts slightly above the link-layer, below-ip traffic + isn't bridged, so things like DHCP won't work. The advantage above + using NAT lies in the fact that no IP addresses are shared, so all + hosts are reachable/routeable. + + WARNING: turns on ip-routing, so if you have multiple interfaces, you + should think of the consequence and setup firewall rules to limit this. + ''; + }; + + wakeOnLan = { + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable wol on this interface."; + }; + }; + }; + + config = { + name = mkDefault name; + }; + + # Renamed or removed options + imports = + let + defined = x: x != "_mkMergedOptionModule"; + in [ + (mkChangedOptionModule [ "preferTempAddress" ] [ "tempAddress" ] + (config: + let bool = getAttrFromPath [ "preferTempAddress" ] config; + in if bool then "default" else "enabled" + )) + (mkRenamedOptionModule [ "ip4" ] [ "ipv4" "addresses"]) + (mkRenamedOptionModule [ "ip6" ] [ "ipv6" "addresses"]) + (mkRemovedOptionModule [ "subnetMask" ] '' + Supply a prefix length instead; use option + networking.interfaces.<name>.ipv{4,6}.addresses'') + (mkMergedOptionModule + [ [ "ipAddress" ] [ "prefixLength" ] ] + [ "ipv4" "addresses" ] + (cfg: with cfg; + optional (defined ipAddress && defined prefixLength) + { address = ipAddress; prefixLength = prefixLength; })) + (mkMergedOptionModule + [ [ "ipv6Address" ] [ "ipv6PrefixLength" ] ] + [ "ipv6" "addresses" ] + (cfg: with cfg; + optional (defined ipv6Address && defined ipv6PrefixLength) + { address = ipv6Address; prefixLength = ipv6PrefixLength; })) + + ({ options.warnings = options.warnings; options.assertions = options.assertions; }) + ]; + + }; + + vswitchInterfaceOpts = {name, ...}: { + + options = { + + name = mkOption { + description = "Name of the interface"; + example = "eth0"; + type = types.str; + }; + + vlan = mkOption { + description = "Vlan tag to apply to interface"; + example = 10; + type = types.nullOr types.int; + default = null; + }; + + type = mkOption { + description = "Openvswitch type to assign to interface"; + example = "internal"; + type = types.nullOr types.str; + default = null; + }; + }; + }; + + hexChars = stringToCharacters "0123456789abcdef"; + + isHexString = s: all (c: elem c hexChars) (stringToCharacters (toLower s)); + + tempaddrValues = { + disabled = { + sysctl = "0"; + description = "completely disable IPv6 temporary addresses"; + }; + enabled = { + sysctl = "1"; + description = "generate IPv6 temporary addresses but still use EUI-64 addresses as source addresses"; + }; + default = { + sysctl = "2"; + description = "generate IPv6 temporary addresses and use these as source addresses in routing"; + }; + }; + tempaddrDoc = '' + <itemizedlist> + ${concatStringsSep "\n" (mapAttrsToList (name: { description, ... }: '' + <listitem> + <para> + <literal>"${name}"</literal> to ${description}; + </para> + </listitem> + '') tempaddrValues)} + </itemizedlist> + ''; + +in + +{ + + ###### interface + + options = { + + networking.hostName = mkOption { + default = "nixos"; + # Only allow hostnames without the domain name part (i.e. no FQDNs, see + # e.g. "man 5 hostname") and require valid DNS labels (recommended + # syntax). Note: We also allow underscores for compatibility/legacy + # reasons (as undocumented feature): + type = types.strMatching + "^$|^[[:alnum:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$"; + description = '' + The name of the machine. Leave it empty if you want to obtain it from a + DHCP server (if using DHCP). The hostname must be a valid DNS label (see + RFC 1035 section 2.3.1: "Preferred name syntax", RFC 1123 section 2.1: + "Host Names and Numbers") and as such must not contain the domain part. + This means that the hostname must start with a letter or digit, + end with a letter or digit, and have as interior characters only + letters, digits, and hyphen. The maximum length is 63 characters. + Additionally it is recommended to only use lower-case characters. + If (e.g. for legacy reasons) a FQDN is required as the Linux kernel + network node hostname (uname --nodename) the option + boot.kernel.sysctl."kernel.hostname" can be used as a workaround (but + the 64 character limit still applies). + + WARNING: Do not use underscores (_) or you may run into unexpected issues. + ''; + # warning until the issues in https://github.com/NixOS/nixpkgs/pull/138978 + # are resolved + }; + + networking.fqdn = mkOption { + readOnly = true; + type = types.str; + default = if (cfg.hostName != "" && cfg.domain != null) + then "${cfg.hostName}.${cfg.domain}" + else throw '' + The FQDN is required but cannot be determined. Please make sure that + both networking.hostName and networking.domain are set properly. + ''; + defaultText = literalExpression ''"''${networking.hostName}.''${networking.domain}"''; + description = '' + The fully qualified domain name (FQDN) of this host. It is the result + of combining networking.hostName and networking.domain. Using this + option will result in an evaluation error if the hostname is empty or + no domain is specified. + ''; + }; + + networking.hostId = mkOption { + default = null; + example = "4e98920d"; + type = types.nullOr types.str; + description = '' + The 32-bit host ID of the machine, formatted as 8 hexadecimal characters. + + You should try to make this ID unique among your machines. You can + generate a random 32-bit ID using the following commands: + + <literal>head -c 8 /etc/machine-id</literal> + + (this derives it from the machine-id that systemd generates) or + + <literal>head -c4 /dev/urandom | od -A none -t x4</literal> + + The primary use case is to ensure when using ZFS that a pool isn't imported + accidentally on a wrong machine. + ''; + }; + + networking.enableIPv6 = mkOption { + default = true; + type = types.bool; + description = '' + Whether to enable support for IPv6. + ''; + }; + + networking.defaultGateway = mkOption { + default = null; + example = { + address = "131.211.84.1"; + interface = "enp3s0"; + }; + type = types.nullOr (types.coercedTo types.str gatewayCoerce (types.submodule gatewayOpts)); + description = '' + The default gateway. It can be left empty if it is auto-detected through DHCP. + It can be specified as a string or an option set along with a network interface. + ''; + }; + + networking.defaultGateway6 = mkOption { + default = null; + example = { + address = "2001:4d0:1e04:895::1"; + interface = "enp3s0"; + }; + type = types.nullOr (types.coercedTo types.str gatewayCoerce (types.submodule gatewayOpts)); + description = '' + The default ipv6 gateway. It can be left empty if it is auto-detected through DHCP. + It can be specified as a string or an option set along with a network interface. + ''; + }; + + networking.defaultGatewayWindowSize = mkOption { + default = null; + example = 524288; + type = types.nullOr types.int; + description = '' + The window size of the default gateway. It limits maximal data bursts that TCP peers + are allowed to send to us. + ''; + }; + + networking.nameservers = mkOption { + type = types.listOf types.str; + default = []; + example = ["130.161.158.4" "130.161.33.17"]; + description = '' + The list of nameservers. It can be left empty if it is auto-detected through DHCP. + ''; + }; + + networking.search = mkOption { + default = []; + example = [ "example.com" "home.arpa" ]; + type = types.listOf types.str; + description = '' + The list of search paths used when resolving domain names. + ''; + }; + + networking.domain = mkOption { + default = null; + example = "home.arpa"; + type = types.nullOr types.str; + description = '' + The domain. It can be left empty if it is auto-detected through DHCP. + ''; + }; + + networking.useHostResolvConf = mkOption { + type = types.bool; + default = false; + description = '' + In containers, whether to use the + <filename>resolv.conf</filename> supplied by the host. + ''; + }; + + networking.localCommands = mkOption { + type = types.lines; + default = ""; + example = "text=anything; echo You can put $text here."; + description = '' + Shell commands to be executed at the end of the + <literal>network-setup</literal> systemd service. Note that if + you are using DHCP to obtain the network configuration, + interfaces may not be fully configured yet. + ''; + }; + + networking.interfaces = mkOption { + default = {}; + example = + { eth0.ipv4.addresses = [ { + address = "131.211.84.78"; + prefixLength = 25; + } ]; + }; + description = '' + The configuration for each network interface. If + <option>networking.useDHCP</option> is true, then every + interface not listed here will be configured using DHCP. + ''; + type = with types; attrsOf (submodule interfaceOpts); + }; + + networking.vswitches = mkOption { + default = { }; + example = + { vs0.interfaces = { eth0 = { }; lo1 = { type="internal"; }; }; + vs1.interfaces = [ { name = "eth2"; } { name = "lo2"; type="internal"; } ]; + }; + description = + '' + This option allows you to define Open vSwitches that connect + physical networks together. The value of this option is an + attribute set. Each attribute specifies a vswitch, with the + attribute name specifying the name of the vswitch's network + interface. + ''; + + type = with types; attrsOf (submodule { + + options = { + + interfaces = mkOption { + description = "The physical network interfaces connected by the vSwitch."; + type = with types; attrsOf (submodule vswitchInterfaceOpts); + }; + + controllers = mkOption { + type = types.listOf types.str; + default = []; + example = [ "ptcp:6653:[::1]" ]; + description = '' + Specify the controller targets. For the allowed options see <literal>man 8 ovs-vsctl</literal>. + ''; + }; + + openFlowRules = mkOption { + type = types.lines; + default = ""; + example = '' + actions=normal + ''; + description = '' + OpenFlow rules to insert into the Open vSwitch. All <literal>openFlowRules</literal> are + loaded with <literal>ovs-ofctl</literal> within one atomic operation. + ''; + }; + + # TODO: custom "openflow version" type, with list from existing openflow protocols + supportedOpenFlowVersions = mkOption { + type = types.listOf types.str; + example = [ "OpenFlow10" "OpenFlow13" "OpenFlow14" ]; + default = [ "OpenFlow13" ]; + description = '' + Supported versions to enable on this switch. + ''; + }; + + # TODO: use same type as elements from supportedOpenFlowVersions + openFlowVersion = mkOption { + type = types.str; + default = "OpenFlow13"; + description = '' + Version of OpenFlow protocol to use when communicating with the switch internally (e.g. with <literal>openFlowRules</literal>). + ''; + }; + + extraOvsctlCmds = mkOption { + type = types.lines; + default = ""; + example = '' + set-fail-mode <switch_name> secure + set Bridge <switch_name> stp_enable=true + ''; + description = '' + Commands to manipulate the Open vSwitch database. Every line executed with <literal>ovs-vsctl</literal>. + All commands are bundled together with the operations for adding the interfaces + into one atomic operation. + ''; + }; + + }; + + }); + + }; + + networking.bridges = mkOption { + default = { }; + example = + { br0.interfaces = [ "eth0" "eth1" ]; + br1.interfaces = [ "eth2" "wlan0" ]; + }; + description = + '' + This option allows you to define Ethernet bridge devices + that connect physical networks together. The value of this + option is an attribute set. Each attribute specifies a + bridge, with the attribute name specifying the name of the + bridge's network interface. + ''; + + type = with types; attrsOf (submodule { + + options = { + + interfaces = mkOption { + example = [ "eth0" "eth1" ]; + type = types.listOf types.str; + description = + "The physical network interfaces connected by the bridge."; + }; + + rstp = mkOption { + default = false; + type = types.bool; + description = "Whether the bridge interface should enable rstp."; + }; + + }; + + }); + + }; + + networking.bonds = + let + driverOptionsExample = '' + { + miimon = "100"; + mode = "active-backup"; + } + ''; + in mkOption { + default = { }; + example = literalExpression '' + { + bond0 = { + interfaces = [ "eth0" "wlan0" ]; + driverOptions = ${driverOptionsExample}; + }; + anotherBond.interfaces = [ "enp4s0f0" "enp4s0f1" "enp5s0f0" "enp5s0f1" ]; + } + ''; + description = '' + This option allows you to define bond devices that aggregate multiple, + underlying networking interfaces together. The value of this option is + an attribute set. Each attribute specifies a bond, with the attribute + name specifying the name of the bond's network interface + ''; + + type = with types; attrsOf (submodule { + + options = { + + interfaces = mkOption { + example = [ "enp4s0f0" "enp4s0f1" "wlan0" ]; + type = types.listOf types.str; + description = "The interfaces to bond together"; + }; + + driverOptions = mkOption { + type = types.attrsOf types.str; + default = {}; + example = literalExpression driverOptionsExample; + description = '' + Options for the bonding driver. + Documentation can be found in + <link xlink:href="https://www.kernel.org/doc/Documentation/networking/bonding.txt" /> + ''; + + }; + + lacp_rate = mkOption { + default = null; + example = "fast"; + type = types.nullOr types.str; + description = '' + DEPRECATED, use `driverOptions`. + Option specifying the rate in which we'll ask our link partner + to transmit LACPDU packets in 802.3ad mode. + ''; + }; + + miimon = mkOption { + default = null; + example = 100; + type = types.nullOr types.int; + description = '' + DEPRECATED, use `driverOptions`. + Miimon is the number of millisecond in between each round of polling + by the device driver for failed links. By default polling is not + enabled and the driver is trusted to properly detect and handle + failure scenarios. + ''; + }; + + mode = mkOption { + default = null; + example = "active-backup"; + type = types.nullOr types.str; + description = '' + DEPRECATED, use `driverOptions`. + The mode which the bond will be running. The default mode for + the bonding driver is balance-rr, optimizing for throughput. + More information about valid modes can be found at + https://www.kernel.org/doc/Documentation/networking/bonding.txt + ''; + }; + + xmit_hash_policy = mkOption { + default = null; + example = "layer2+3"; + type = types.nullOr types.str; + description = '' + DEPRECATED, use `driverOptions`. + Selects the transmit hash policy to use for slave selection in + balance-xor, 802.3ad, and tlb modes. + ''; + }; + + }; + + }); + }; + + networking.macvlans = mkOption { + default = { }; + example = literalExpression '' + { + wan = { + interface = "enp2s0"; + mode = "vepa"; + }; + } + ''; + description = '' + This option allows you to define macvlan interfaces which should + be automatically created. + ''; + type = with types; attrsOf (submodule { + options = { + + interface = mkOption { + example = "enp4s0"; + type = types.str; + description = "The interface the macvlan will transmit packets through."; + }; + + mode = mkOption { + default = null; + type = types.nullOr types.str; + example = "vepa"; + description = "The mode of the macvlan device."; + }; + + }; + + }); + }; + + networking.fooOverUDP = mkOption { + default = { }; + example = + { + primary = { port = 9001; local = { address = "192.0.2.1"; dev = "eth0"; }; }; + backup = { port = 9002; }; + }; + description = '' + This option allows you to configure Foo Over UDP and Generic UDP Encapsulation + endpoints. See <citerefentry><refentrytitle>ip-fou</refentrytitle> + <manvolnum>8</manvolnum></citerefentry> for details. + ''; + type = with types; attrsOf (submodule { + options = { + port = mkOption { + type = port; + description = '' + Local port of the encapsulation UDP socket. + ''; + }; + + protocol = mkOption { + type = nullOr (ints.between 1 255); + default = null; + description = '' + Protocol number of the encapsulated packets. Specifying <literal>null</literal> + (the default) creates a GUE endpoint, specifying a protocol number will create + a FOU endpoint. + ''; + }; + + local = mkOption { + type = nullOr (submodule { + options = { + address = mkOption { + type = types.str; + description = '' + Local address to bind to. The address must be available when the FOU + endpoint is created, using the scripted network setup this can be achieved + either by setting <literal>dev</literal> or adding dependency information to + <literal>systemd.services.<name>-fou-encap</literal>; it isn't supported + when using networkd. + ''; + }; + + dev = mkOption { + type = nullOr str; + default = null; + example = "eth0"; + description = '' + Network device to bind to. + ''; + }; + }; + }); + default = null; + example = { address = "203.0.113.22"; }; + description = '' + Local address (and optionally device) to bind to using the given port. + ''; + }; + }; + }); + }; + + networking.sits = mkOption { + default = { }; + example = literalExpression '' + { + hurricane = { + remote = "10.0.0.1"; + local = "10.0.0.22"; + ttl = 255; + }; + msipv6 = { + remote = "192.168.0.1"; + dev = "enp3s0"; + ttl = 127; + }; + } + ''; + description = '' + This option allows you to define 6-to-4 interfaces which should be automatically created. + ''; + type = with types; attrsOf (submodule { + options = { + + remote = mkOption { + type = types.nullOr types.str; + default = null; + example = "10.0.0.1"; + description = '' + The address of the remote endpoint to forward traffic over. + ''; + }; + + local = mkOption { + type = types.nullOr types.str; + default = null; + example = "10.0.0.22"; + description = '' + The address of the local endpoint which the remote + side should send packets to. + ''; + }; + + ttl = mkOption { + type = types.nullOr types.int; + default = null; + example = 255; + description = '' + The time-to-live of the connection to the remote tunnel endpoint. + ''; + }; + + dev = mkOption { + type = types.nullOr types.str; + default = null; + example = "enp4s0f0"; + description = '' + The underlying network device on which the tunnel resides. + ''; + }; + + encapsulation = with types; mkOption { + type = nullOr (submodule { + options = { + type = mkOption { + type = enum [ "fou" "gue" ]; + description = '' + Selects encapsulation type. See + <citerefentry><refentrytitle>ip-link</refentrytitle> + <manvolnum>8</manvolnum></citerefentry> for details. + ''; + }; + + port = mkOption { + type = port; + example = 9001; + description = '' + Destination port for encapsulated packets. + ''; + }; + + sourcePort = mkOption { + type = nullOr types.port; + default = null; + example = 9002; + description = '' + Source port for encapsulated packets. Will be chosen automatically by + the kernel if unset. + ''; + }; + }; + }); + default = null; + example = { type = "fou"; port = 9001; }; + description = '' + Configures encapsulation in UDP packets. + ''; + }; + + }; + + }); + }; + + networking.greTunnels = mkOption { + default = { }; + example = literalExpression '' + { + greBridge = { + remote = "10.0.0.1"; + local = "10.0.0.22"; + dev = "enp4s0f0"; + type = "tap"; + }; + gre6Tunnel = { + remote = "fd7a:5634::1"; + local = "fd7a:5634::2"; + dev = "enp4s0f0"; + type = "tun6"; + }; + } + ''; + description = '' + This option allows you to define Generic Routing Encapsulation (GRE) tunnels. + ''; + type = with types; attrsOf (submodule { + options = { + + remote = mkOption { + type = types.nullOr types.str; + default = null; + example = "10.0.0.1"; + description = '' + The address of the remote endpoint to forward traffic over. + ''; + }; + + local = mkOption { + type = types.nullOr types.str; + default = null; + example = "10.0.0.22"; + description = '' + The address of the local endpoint which the remote + side should send packets to. + ''; + }; + + dev = mkOption { + type = types.nullOr types.str; + default = null; + example = "enp4s0f0"; + description = '' + The underlying network device on which the tunnel resides. + ''; + }; + + type = mkOption { + type = with types; enum [ "tun" "tap" "tun6" "tap6" ]; + default = "tap"; + example = "tap"; + apply = v: { + tun = "gre"; + tap = "gretap"; + tun6 = "ip6gre"; + tap6 = "ip6gretap"; + }.${v}; + description = '' + Whether the tunnel routes layer 2 (tap) or layer 3 (tun) traffic. + ''; + }; + }; + }); + }; + + networking.vlans = mkOption { + default = { }; + example = literalExpression '' + { + vlan0 = { + id = 3; + interface = "enp3s0"; + }; + vlan1 = { + id = 1; + interface = "wlan0"; + }; + } + ''; + description = + '' + This option allows you to define vlan devices that tag packets + on top of a physical interface. The value of this option is an + attribute set. Each attribute specifies a vlan, with the name + specifying the name of the vlan interface. + ''; + + type = with types; attrsOf (submodule { + + options = { + + id = mkOption { + example = 1; + type = types.int; + description = "The vlan identifier"; + }; + + interface = mkOption { + example = "enp4s0"; + type = types.str; + description = "The interface the vlan will transmit packets through."; + }; + + }; + + }); + + }; + + networking.wlanInterfaces = mkOption { + default = { }; + example = literalExpression '' + { + wlan-station0 = { + device = "wlp6s0"; + }; + wlan-adhoc0 = { + type = "ibss"; + device = "wlp6s0"; + mac = "02:00:00:00:00:01"; + }; + wlan-p2p0 = { + device = "wlp6s0"; + mac = "02:00:00:00:00:02"; + }; + wlan-ap0 = { + device = "wlp6s0"; + mac = "02:00:00:00:00:03"; + }; + } + ''; + description = + '' + Creating multiple WLAN interfaces on top of one physical WLAN device (NIC). + + The name of the WLAN interface corresponds to the name of the attribute. + A NIC is referenced by the persistent device name of the WLAN interface that + <literal>udev</literal> assigns to a NIC by default. + If a NIC supports multiple WLAN interfaces, then the one NIC can be used as + <literal>device</literal> for multiple WLAN interfaces. + If a NIC is used for creating WLAN interfaces, then the default WLAN interface + with a persistent device name form <literal>udev</literal> is not created. + A WLAN interface with the persistent name assigned from <literal>udev</literal> + would have to be created explicitly. + ''; + + type = with types; attrsOf (submodule { + + options = { + + device = mkOption { + type = types.str; + example = "wlp6s0"; + description = "The name of the underlying hardware WLAN device as assigned by <literal>udev</literal>."; + }; + + type = mkOption { + type = types.enum [ "managed" "ibss" "monitor" "mesh" "wds" ]; + default = "managed"; + example = "ibss"; + description = '' + The type of the WLAN interface. + The type has to be supported by the underlying hardware of the device. + ''; + }; + + meshID = mkOption { + type = types.nullOr types.str; + default = null; + description = "MeshID of interface with type <literal>mesh</literal>."; + }; + + flags = mkOption { + type = with types; nullOr (enum [ "none" "fcsfail" "control" "otherbss" "cook" "active" ]); + default = null; + example = "control"; + description = '' + Flags for interface of type <literal>monitor</literal>. + ''; + }; + + fourAddr = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether to enable <literal>4-address mode</literal> with type <literal>managed</literal>."; + }; + + mac = mkOption { + type = types.nullOr types.str; + default = null; + example = "02:00:00:00:00:01"; + description = '' + MAC address to use for the device. If <literal>null</literal>, then the MAC of the + underlying hardware WLAN device is used. + + INFO: Locally administered MAC addresses are of the form: + <itemizedlist> + <listitem><para>x2:xx:xx:xx:xx:xx</para></listitem> + <listitem><para>x6:xx:xx:xx:xx:xx</para></listitem> + <listitem><para>xA:xx:xx:xx:xx:xx</para></listitem> + <listitem><para>xE:xx:xx:xx:xx:xx</para></listitem> + </itemizedlist> + ''; + }; + + }; + + }); + + }; + + networking.useDHCP = mkOption { + type = types.bool; + default = true; + description = '' + Whether to use DHCP to obtain an IP address and other + configuration for all network interfaces that are not manually + configured. + + Using this option is highly discouraged and also incompatible with + <option>networking.useNetworkd</option>. Please use + <option>networking.interfaces.<name>.useDHCP</option> instead + and set this to false. + ''; + }; + + networking.useNetworkd = mkOption { + default = false; + type = types.bool; + description = '' + Whether we should use networkd as the network configuration backend or + the legacy script based system. Note that this option is experimental, + enable at your own risk. + ''; + }; + + networking.tempAddresses = mkOption { + default = if cfg.enableIPv6 then "default" else "disabled"; + defaultText = literalExpression '' + if ''${config.${opt.enableIPv6}} then "default" else "disabled" + ''; + type = types.enum (lib.attrNames tempaddrValues); + description = '' + Whether to enable IPv6 Privacy Extensions for interfaces not + configured explicitly in + <xref linkend="opt-networking.interfaces._name_.tempAddress" />. + + This sets the ipv6.conf.*.use_tempaddr sysctl for all + interfaces. Possible values are: + + ${tempaddrDoc} + ''; + }; + + }; + + + ###### implementation + + config = { + + warnings = concatMap (i: i.warnings) interfaces; + + assertions = + (forEach interfaces (i: { + # With the linux kernel, interface name length is limited by IFNAMSIZ + # to 16 bytes, including the trailing null byte. + # See include/linux/if.h in the kernel sources + assertion = stringLength i.name < 16; + message = '' + The name of networking.interfaces."${i.name}" is too long, it needs to be less than 16 characters. + ''; + })) ++ (forEach slaveIfs (i: { + assertion = i.ipv4.addresses == [ ] && i.ipv6.addresses == [ ]; + message = '' + The networking.interfaces."${i.name}" must not have any defined ips when it is a slave. + ''; + })) ++ (forEach interfaces (i: { + assertion = i.tempAddress != "disabled" -> cfg.enableIPv6; + message = '' + Temporary addresses are only needed when IPv6 is enabled. + ''; + })) ++ (forEach interfaces (i: { + assertion = (i.virtual && i.virtualType == "tun") -> i.macAddress == null; + message = '' + Setting a MAC Address for tun device ${i.name} isn't supported. + ''; + })) ++ [ + { + assertion = cfg.hostId == null || (stringLength cfg.hostId == 8 && isHexString cfg.hostId); + message = "Invalid value given to the networking.hostId option."; + } + ]; + + boot.kernelModules = [ ] + ++ optional hasVirtuals "tun" + ++ optional hasSits "sit" + ++ optional hasGres "gre" + ++ optional hasBonds "bonding" + ++ optional hasFous "fou"; + + boot.extraModprobeConfig = + # This setting is intentional as it prevents default bond devices + # from being created. + optionalString hasBonds "options bonding max_bonds=0"; + + boot.kernel.sysctl = { + "net.ipv4.conf.all.forwarding" = mkDefault (any (i: i.proxyARP) interfaces); + "net.ipv6.conf.all.disable_ipv6" = mkDefault (!cfg.enableIPv6); + "net.ipv6.conf.default.disable_ipv6" = mkDefault (!cfg.enableIPv6); + # networkmanager falls back to "/proc/sys/net/ipv6/conf/default/use_tempaddr" + "net.ipv6.conf.default.use_tempaddr" = tempaddrValues.${cfg.tempAddresses}.sysctl; + } // listToAttrs (flip concatMap (filter (i: i.proxyARP) interfaces) + (i: [(nameValuePair "net.ipv4.conf.${replaceChars ["."] ["/"] i.name}.proxy_arp" true)])) + // listToAttrs (forEach interfaces + (i: let + opt = i.tempAddress; + val = tempaddrValues.${opt}.sysctl; + in nameValuePair "net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr" val)); + + security.wrappers = { + ping = { + owner = "root"; + group = "root"; + capabilities = "cap_net_raw+p"; + source = "${pkgs.iputils.out}/bin/ping"; + }; + }; + security.apparmor.policies."bin.ping".profile = lib.mkIf config.security.apparmor.policies."bin.ping".enable (lib.mkAfter '' + /run/wrappers/bin/ping { + include <abstractions/base> + include <nixos/security.wrappers> + rpx /run/wrappers/wrappers.*/ping, + } + /run/wrappers/wrappers.*/ping { + include <abstractions/base> + include <nixos/security.wrappers> + r /run/wrappers/wrappers.*/ping.real, + mrpx ${config.security.wrappers.ping.source}, + capability net_raw, + capability setpcap, + } + ''); + + # Set the host and domain names in the activation script. Don't + # clear it if it's not configured in the NixOS configuration, + # since it may have been set by dhcpcd in the meantime. + system.activationScripts.hostname = + optionalString (cfg.hostName != "") '' + hostname "${cfg.hostName}" + ''; + system.activationScripts.domain = + optionalString (cfg.domain != null) '' + domainname "${cfg.domain}" + ''; + + environment.etc.hostid = mkIf (cfg.hostId != null) + { source = pkgs.runCommand "gen-hostid" { preferLocalBuild = true; } '' + hi="${cfg.hostId}" + ${if pkgs.stdenv.isBigEndian then '' + echo -ne "\x''${hi:0:2}\x''${hi:2:2}\x''${hi:4:2}\x''${hi:6:2}" > $out + '' else '' + echo -ne "\x''${hi:6:2}\x''${hi:4:2}\x''${hi:2:2}\x''${hi:0:2}" > $out + ''} + ''; + }; + + # static hostname configuration needed for hostnamectl and the + # org.freedesktop.hostname1 dbus service (both provided by systemd) + environment.etc.hostname = mkIf (cfg.hostName != "") + { + text = cfg.hostName + "\n"; + }; + + environment.systemPackages = + [ pkgs.host + pkgs.iproute2 + pkgs.iputils + pkgs.nettools + ] + ++ optionals config.networking.wireless.enable [ + pkgs.wirelesstools # FIXME: obsolete? + pkgs.iw + ] + ++ bridgeStp; + + # The network-interfaces target is kept for backwards compatibility. + # New modules must NOT use it. + systemd.targets.network-interfaces = + { description = "All Network Interfaces (deprecated)"; + wantedBy = [ "network.target" ]; + before = [ "network.target" ]; + after = [ "network-pre.target" ]; + unitConfig.X-StopOnReconfiguration = true; + }; + + systemd.services = { + network-local-commands = { + description = "Extra networking commands."; + before = [ "network.target" ]; + wantedBy = [ "network.target" ]; + after = [ "network-pre.target" ]; + unitConfig.ConditionCapability = "CAP_NET_ADMIN"; + path = [ pkgs.iproute2 ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + script = '' + # Run any user-specified commands. + ${cfg.localCommands} + ''; + }; + }; + services.mstpd = mkIf needsMstpd { enable = true; }; + + virtualisation.vswitch = mkIf (cfg.vswitches != { }) { enable = true; }; + + services.udev.packages = [ + (pkgs.writeTextFile rec { + name = "ipv6-privacy-extensions.rules"; + destination = "/etc/udev/rules.d/98-${name}"; + text = let + sysctl-value = tempaddrValues.${cfg.tempAddresses}.sysctl; + in '' + # enable and prefer IPv6 privacy addresses by default + ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.bash}/bin/sh -c 'echo ${sysctl-value} > /proc/sys/net/ipv6/conf/%k/use_tempaddr'" + ''; + }) + (pkgs.writeTextFile rec { + name = "ipv6-privacy-extensions.rules"; + destination = "/etc/udev/rules.d/99-${name}"; + text = concatMapStrings (i: + let + opt = i.tempAddress; + val = tempaddrValues.${opt}.sysctl; + msg = tempaddrValues.${opt}.description; + in + '' + # override to ${msg} for ${i.name} + ACTION=="add", SUBSYSTEM=="net", RUN+="${pkgs.procps}/bin/sysctl net.ipv6.conf.${replaceChars ["."] ["/"] i.name}.use_tempaddr=${val}" + '') (filter (i: i.tempAddress != cfg.tempAddresses) interfaces); + }) + ] ++ lib.optional (cfg.wlanInterfaces != {}) + (pkgs.writeTextFile { + name = "99-zzz-40-wlanInterfaces.rules"; + destination = "/etc/udev/rules.d/99-zzz-40-wlanInterfaces.rules"; + text = + let + # Collect all interfaces that are defined for a device + # as device:interface key:value pairs. + wlanDeviceInterfaces = + let + allDevices = unique (mapAttrsToList (_: v: v.device) cfg.wlanInterfaces); + interfacesOfDevice = d: filterAttrs (_: v: v.device == d) cfg.wlanInterfaces; + in + genAttrs allDevices (d: interfacesOfDevice d); + + # Convert device:interface key:value pairs into a list, and if it exists, + # place the interface which is named after the device at the beginning. + wlanListDeviceFirst = device: interfaces: + if hasAttr device interfaces + then mapAttrsToList (n: v: v//{_iName=n;}) (filterAttrs (n: _: n==device) interfaces) ++ mapAttrsToList (n: v: v//{_iName=n;}) (filterAttrs (n: _: n!=device) interfaces) + else mapAttrsToList (n: v: v // {_iName = n;}) interfaces; + + # Udev script to execute for the default WLAN interface with the persistend udev name. + # The script creates the required, new WLAN interfaces interfaces and configures the + # existing, default interface. + curInterfaceScript = device: current: new: pkgs.writeScript "udev-run-script-wlan-interfaces-${device}.sh" '' + #!${pkgs.runtimeShell} + # Change the wireless phy device to a predictable name. + ${pkgs.iw}/bin/iw phy `${pkgs.coreutils}/bin/cat /sys/class/net/$INTERFACE/phy80211/name` set name ${device} + + # Add new WLAN interfaces + ${flip concatMapStrings new (i: '' + ${pkgs.iw}/bin/iw phy ${device} interface add ${i._iName} type managed + '')} + + # Configure the current interface + ${pkgs.iw}/bin/iw dev ${device} set type ${current.type} + ${optionalString (current.type == "mesh" && current.meshID!=null) "${pkgs.iw}/bin/iw dev ${device} set meshid ${current.meshID}"} + ${optionalString (current.type == "monitor" && current.flags!=null) "${pkgs.iw}/bin/iw dev ${device} set monitor ${current.flags}"} + ${optionalString (current.type == "managed" && current.fourAddr!=null) "${pkgs.iw}/bin/iw dev ${device} set 4addr ${if current.fourAddr then "on" else "off"}"} + ${optionalString (current.mac != null) "${pkgs.iproute2}/bin/ip link set dev ${device} address ${current.mac}"} + ''; + + # Udev script to execute for a new WLAN interface. The script configures the new WLAN interface. + newInterfaceScript = new: pkgs.writeScript "udev-run-script-wlan-interfaces-${new._iName}.sh" '' + #!${pkgs.runtimeShell} + # Configure the new interface + ${pkgs.iw}/bin/iw dev ${new._iName} set type ${new.type} + ${optionalString (new.type == "mesh" && new.meshID!=null) "${pkgs.iw}/bin/iw dev ${new._iName} set meshid ${new.meshID}"} + ${optionalString (new.type == "monitor" && new.flags!=null) "${pkgs.iw}/bin/iw dev ${new._iName} set monitor ${new.flags}"} + ${optionalString (new.type == "managed" && new.fourAddr!=null) "${pkgs.iw}/bin/iw dev ${new._iName} set 4addr ${if new.fourAddr then "on" else "off"}"} + ${optionalString (new.mac != null) "${pkgs.iproute2}/bin/ip link set dev ${new._iName} address ${new.mac}"} + ''; + + # Udev attributes for systemd to name the device and to create a .device target. + systemdAttrs = n: ''NAME:="${n}", ENV{INTERFACE}="${n}", ENV{SYSTEMD_ALIAS}="/sys/subsystem/net/devices/${n}", TAG+="systemd"''; + in + flip (concatMapStringsSep "\n") (attrNames wlanDeviceInterfaces) (device: + let + interfaces = wlanListDeviceFirst device wlanDeviceInterfaces.${device}; + curInterface = elemAt interfaces 0; + newInterfaces = drop 1 interfaces; + in '' + # It is important to have that rule first as overwriting the NAME attribute also prevents the + # next rules from matching. + ${flip (concatMapStringsSep "\n") (wlanListDeviceFirst device wlanDeviceInterfaces.${device}) (interface: + ''ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", ENV{INTERFACE}=="${interface._iName}", ${systemdAttrs interface._iName}, RUN+="${newInterfaceScript interface}"'')} + + # Add the required, new WLAN interfaces to the default WLAN interface with the + # persistent, default name as assigned by udev. + ACTION=="add", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", NAME=="${device}", ${systemdAttrs curInterface._iName}, RUN+="${curInterfaceScript device curInterface newInterfaces}" + # Generate the same systemd events for both 'add' and 'move' udev events. + ACTION=="move", SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", NAME=="${device}", ${systemdAttrs curInterface._iName} + ''); + }); + }; + +} diff --git a/nixos/modules/tasks/powertop.nix b/nixos/modules/tasks/powertop.nix new file mode 100644 index 00000000000..e8064f9fa80 --- /dev/null +++ b/nixos/modules/tasks/powertop.nix @@ -0,0 +1,29 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.powerManagement.powertop; +in { + ###### interface + + options.powerManagement.powertop.enable = mkEnableOption "powertop auto tuning on startup"; + + ###### implementation + + config = mkIf (cfg.enable) { + systemd.services = { + powertop = { + wantedBy = [ "multi-user.target" ]; + after = [ "multi-user.target" ]; + description = "Powertop tunings"; + path = [ pkgs.kmod ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = "yes"; + ExecStart = "${pkgs.powertop}/bin/powertop --auto-tune"; + }; + }; + }; + }; +} diff --git a/nixos/modules/tasks/scsi-link-power-management.nix b/nixos/modules/tasks/scsi-link-power-management.nix new file mode 100644 index 00000000000..a9d987780ee --- /dev/null +++ b/nixos/modules/tasks/scsi-link-power-management.nix @@ -0,0 +1,54 @@ +{ config, lib, ... }: + +with lib; + +let + + cfg = config.powerManagement.scsiLinkPolicy; + + kernel = config.boot.kernelPackages.kernel; + + allowedValues = [ + "min_power" + "max_performance" + "medium_power" + "med_power_with_dipm" + ]; + +in + +{ + ###### interface + + options = { + + powerManagement.scsiLinkPolicy = mkOption { + default = null; + type = types.nullOr (types.enum allowedValues); + description = '' + SCSI link power management policy. The kernel default is + "max_performance". + </para><para> + "med_power_with_dipm" is supported by kernel versions + 4.15 and newer. + ''; + }; + + }; + + + ###### implementation + + config = mkIf (cfg != null) { + + assertions = singleton { + assertion = (cfg == "med_power_with_dipm") -> versionAtLeast kernel.version "4.15"; + message = "med_power_with_dipm is not supported for kernels older than 4.15"; + }; + + services.udev.extraRules = '' + SUBSYSTEM=="scsi_host", ACTION=="add", KERNEL=="host*", ATTR{link_power_management_policy}="${cfg}" + ''; + }; + +} diff --git a/nixos/modules/tasks/snapraid.nix b/nixos/modules/tasks/snapraid.nix new file mode 100644 index 00000000000..c8dde5b4899 --- /dev/null +++ b/nixos/modules/tasks/snapraid.nix @@ -0,0 +1,230 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let cfg = config.snapraid; +in +{ + options.snapraid = with types; { + enable = mkEnableOption "SnapRAID"; + dataDisks = mkOption { + default = { }; + example = { + d1 = "/mnt/disk1/"; + d2 = "/mnt/disk2/"; + d3 = "/mnt/disk3/"; + }; + description = "SnapRAID data disks."; + type = attrsOf str; + }; + parityFiles = mkOption { + default = [ ]; + example = [ + "/mnt/diskp/snapraid.parity" + "/mnt/diskq/snapraid.2-parity" + "/mnt/diskr/snapraid.3-parity" + "/mnt/disks/snapraid.4-parity" + "/mnt/diskt/snapraid.5-parity" + "/mnt/disku/snapraid.6-parity" + ]; + description = "SnapRAID parity files."; + type = listOf str; + }; + contentFiles = mkOption { + default = [ ]; + example = [ + "/var/snapraid.content" + "/mnt/disk1/snapraid.content" + "/mnt/disk2/snapraid.content" + ]; + description = "SnapRAID content list files."; + type = listOf str; + }; + exclude = mkOption { + default = [ ]; + example = [ "*.unrecoverable" "/tmp/" "/lost+found/" ]; + description = "SnapRAID exclude directives."; + type = listOf str; + }; + touchBeforeSync = mkOption { + default = true; + example = false; + description = + "Whether <command>snapraid touch</command> should be run before <command>snapraid sync</command>."; + type = bool; + }; + sync.interval = mkOption { + default = "01:00"; + example = "daily"; + description = "How often to run <command>snapraid sync</command>."; + type = str; + }; + scrub = { + interval = mkOption { + default = "Mon *-*-* 02:00:00"; + example = "weekly"; + description = "How often to run <command>snapraid scrub</command>."; + type = str; + }; + plan = mkOption { + default = 8; + example = 5; + description = + "Percent of the array that should be checked by <command>snapraid scrub</command>."; + type = int; + }; + olderThan = mkOption { + default = 10; + example = 20; + description = + "Number of days since data was last scrubbed before it can be scrubbed again."; + type = int; + }; + }; + extraConfig = mkOption { + default = ""; + example = '' + nohidden + blocksize 256 + hashsize 16 + autosave 500 + pool /pool + ''; + description = "Extra config options for SnapRAID."; + type = lines; + }; + }; + + config = + let + nParity = builtins.length cfg.parityFiles; + mkPrepend = pre: s: pre + s; + in + mkIf cfg.enable { + assertions = [ + { + assertion = nParity <= 6; + message = "You can have no more than six SnapRAID parity files."; + } + { + assertion = builtins.length cfg.contentFiles >= nParity + 1; + message = + "There must be at least one SnapRAID content file for each SnapRAID parity file plus one."; + } + ]; + + environment = { + systemPackages = with pkgs; [ snapraid ]; + + etc."snapraid.conf" = { + text = with cfg; + let + prependData = mkPrepend "data "; + prependContent = mkPrepend "content "; + prependExclude = mkPrepend "exclude "; + in + concatStringsSep "\n" + (map prependData + ((mapAttrsToList (name: value: name + " " + value)) dataDisks) + ++ zipListsWith (a: b: a + b) + ([ "parity " ] ++ map (i: toString i + "-parity ") (range 2 6)) + parityFiles ++ map prependContent contentFiles + ++ map prependExclude exclude) + "\n" + extraConfig; + }; + }; + + systemd.services = with cfg; { + snapraid-scrub = { + description = "Scrub the SnapRAID array"; + startAt = scrub.interval; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.snapraid}/bin/snapraid scrub -p ${ + toString scrub.plan + } -o ${toString scrub.olderThan}"; + Nice = 19; + IOSchedulingPriority = 7; + CPUSchedulingPolicy = "batch"; + + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = "none"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = "@system-service"; + SystemCallErrorNumber = "EPERM"; + CapabilityBoundingSet = "CAP_DAC_OVERRIDE"; + + ProtectSystem = "strict"; + ProtectHome = "read-only"; + ReadWritePaths = + # scrub requires access to directories containing content files + # to remove them if they are stale + let + contentDirs = map dirOf contentFiles; + in + unique ( + attrValues dataDisks ++ contentDirs + ); + }; + unitConfig.After = "snapraid-sync.service"; + }; + snapraid-sync = { + description = "Synchronize the state of the SnapRAID array"; + startAt = sync.interval; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.snapraid}/bin/snapraid sync"; + Nice = 19; + IOSchedulingPriority = 7; + CPUSchedulingPolicy = "batch"; + + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = "none"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = "@system-service"; + SystemCallErrorNumber = "EPERM"; + CapabilityBoundingSet = "CAP_DAC_OVERRIDE" + + lib.optionalString cfg.touchBeforeSync " CAP_FOWNER"; + + ProtectSystem = "strict"; + ProtectHome = "read-only"; + ReadWritePaths = + # sync requires access to directories containing content files + # to remove them if they are stale + let + contentDirs = map dirOf contentFiles; + in + unique ( + attrValues dataDisks ++ parityFiles ++ contentDirs + ); + } // optionalAttrs touchBeforeSync { + ExecStartPre = "${pkgs.snapraid}/bin/snapraid touch"; + }; + }; + }; + }; +} diff --git a/nixos/modules/tasks/swraid.nix b/nixos/modules/tasks/swraid.nix new file mode 100644 index 00000000000..8fa19194bed --- /dev/null +++ b/nixos/modules/tasks/swraid.nix @@ -0,0 +1,17 @@ +{ pkgs, ... }: + +{ + + environment.systemPackages = [ pkgs.mdadm ]; + + services.udev.packages = [ pkgs.mdadm ]; + + systemd.packages = [ pkgs.mdadm ]; + + boot.initrd.availableKernelModules = [ "md_mod" "raid0" "raid1" "raid10" "raid456" ]; + + boot.initrd.extraUdevRulesCommands = '' + cp -v ${pkgs.mdadm}/lib/udev/rules.d/*.rules $out/ + ''; + +} diff --git a/nixos/modules/tasks/trackpoint.nix b/nixos/modules/tasks/trackpoint.nix new file mode 100644 index 00000000000..029d8a00295 --- /dev/null +++ b/nixos/modules/tasks/trackpoint.nix @@ -0,0 +1,108 @@ +{ config, lib, ... }: + +with lib; + +{ + ###### interface + + options = { + + hardware.trackpoint = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + Enable sensitivity and speed configuration for trackpoints. + ''; + }; + + sensitivity = mkOption { + default = 128; + example = 255; + type = types.int; + description = '' + Configure the trackpoint sensitivity. By default, the kernel + configures 128. + ''; + }; + + speed = mkOption { + default = 97; + example = 255; + type = types.int; + description = '' + Configure the trackpoint speed. By default, the kernel + configures 97. + ''; + }; + + emulateWheel = mkOption { + default = false; + type = types.bool; + description = '' + Enable scrolling while holding the middle mouse button. + ''; + }; + + fakeButtons = mkOption { + default = false; + type = types.bool; + description = '' + Switch to "bare" PS/2 mouse support in case Trackpoint buttons are not recognized + properly. This can happen for example on models like the L430, T450, T450s, on + which the Trackpoint buttons are actually a part of the Synaptics touchpad. + ''; + }; + + device = mkOption { + default = "TPPS/2 IBM TrackPoint"; + type = types.str; + description = '' + The device name of the trackpoint. You can check with xinput. + Some newer devices (example x1c6) use "TPPS/2 Elan TrackPoint". + ''; + }; + + }; + + }; + + + ###### implementation + + config = + let cfg = config.hardware.trackpoint; in + mkMerge [ + (mkIf cfg.enable { + services.udev.extraRules = + '' + ACTION=="add|change", SUBSYSTEM=="input", ATTR{name}=="${cfg.device}", ATTR{device/speed}="${toString cfg.speed}", ATTR{device/sensitivity}="${toString cfg.sensitivity}" + ''; + + system.activationScripts.trackpoint = + '' + ${config.systemd.package}/bin/udevadm trigger --attr-match=name="${cfg.device}" + ''; + }) + + (mkIf (cfg.emulateWheel) { + services.xserver.inputClassSections = [ + '' + Identifier "Trackpoint Wheel Emulation" + MatchProduct "${if cfg.fakeButtons then "PS/2 Generic Mouse" else "ETPS/2 Elantech TrackPoint|Elantech PS/2 TrackPoint|TPPS/2 IBM TrackPoint|DualPoint Stick|Synaptics Inc. Composite TouchPad / TrackPoint|ThinkPad USB Keyboard with TrackPoint|USB Trackpoint pointing device|Composite TouchPad / TrackPoint|${cfg.device}"}" + MatchDevicePath "/dev/input/event*" + Option "EmulateWheel" "true" + Option "EmulateWheelButton" "2" + Option "Emulate3Buttons" "false" + Option "XAxisMapping" "6 7" + Option "YAxisMapping" "4 5" + '' + ]; + }) + + (mkIf cfg.fakeButtons { + boot.extraModprobeConfig = "options psmouse proto=bare"; + }) + ]; +} diff --git a/nixos/modules/tasks/tty-backgrounds-combine.sh b/nixos/modules/tasks/tty-backgrounds-combine.sh new file mode 100644 index 00000000000..55c3a1ebfa8 --- /dev/null +++ b/nixos/modules/tasks/tty-backgrounds-combine.sh @@ -0,0 +1,32 @@ +source $stdenv/setup + +ttys=($ttys) +themes=($themes) + +mkdir -p $out + +defaultName=$(cd $default && ls | grep -v default) +echo $defaultName +ln -s $default/$defaultName $out/$defaultName +ln -s $defaultName $out/default + +for ((n = 0; n < ${#ttys[*]}; n++)); do + tty=${ttys[$n]} + theme=${themes[$n]} + + echo "TTY $tty -> $theme" + + if [ "$theme" != default ]; then + themeName=$(cd $theme && ls | grep -v default) + ln -sfn $theme/$themeName $out/$themeName + else + themeName=default + fi + + if test -e $out/$tty; then + echo "Multiple themes defined for the same TTY!" + exit 1 + fi + + ln -sfn $themeName $out/$tty +done |