diff --git a/lib/attrsets.nix b/lib/attrsets.nix index 8bb4ef972fd8c..c82fb1f5c243f 100644 --- a/lib/attrsets.nix +++ b/lib/attrsets.nix @@ -872,6 +872,63 @@ rec { else []; + /** + Recursively collect sets that verify a given predicate named `pred` + from the set `attrs`. The recursion is stopped when the predicate is + verified. This version of `collect` also collects the attribute paths + of the items. + + + # Inputs + + `pred` + + : Given an attribute's value, determine if recursion should stop. + + `attrs` + + : The attribute set to recursively collect. + + # Type + + ``` + collect' :: (AttrSet -> Bool) -> AttrSet -> [{ path :: [ String ], value :: x }] + ``` + + # Examples + :::{.example} + ## `lib.attrsets.collect'` usage example + + ```nix + collect' isList { a = { b = ["b"]; }; c = [1]; } + => [ + { path = [ "a" "b" ]; value = [ "b" ]; + { path = [ "c" ]; value = [ 1 ]; } + ] + + collect (x: x ? outPath) + { a = { outPath = "a/"; }; b = { outPath = "b/"; }; } + => [ + { path = [ "a" ]; value = { outPath = "a/"; }; } + { path = [ "b" ]; value = { outPath = "b/"; }; } + ] + ``` + + ::: + */ + collect' = let + collect'' = + path: + pred: + value: + if pred value then + [ { inherit path value; } ] + else if isAttrs value then + concatMap ({ name, value }: collect'' (path ++ [ name ]) pred value) (attrsToList value) + else + []; + in collect'' []; + /** Return the cartesian product of attribute set value combinations. diff --git a/lib/default.nix b/lib/default.nix index 4d0035945aaa9..71b159a0da434 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -83,7 +83,7 @@ let toExtension; inherit (self.attrsets) attrByPath hasAttrByPath setAttrByPath getAttrFromPath attrVals attrNames attrValues getAttrs catAttrs filterAttrs - filterAttrsRecursive foldlAttrs foldAttrs collect nameValuePair mapAttrs + filterAttrsRecursive foldlAttrs foldAttrs collect collect' nameValuePair mapAttrs mapAttrs' mapAttrsToList attrsToList concatMapAttrs mapAttrsRecursive mapAttrsRecursiveCond genAttrs isDerivation toDerivation optionalAttrs zipAttrsWithNames zipAttrsWith zipAttrs recursiveUpdateUntil diff --git a/lib/modules.nix b/lib/modules.nix index 381480a1a0735..dcf73f05c657e 100644 --- a/lib/modules.nix +++ b/lib/modules.nix @@ -1106,14 +1106,13 @@ let visible = false; apply = x: throw "The option `${showOption optionName}' can no longer be used since it's been removed. ${replacementInstructions}"; }); - config.assertions = - let opt = getAttrFromPath optionName options; in [{ - assertion = !opt.isDefined; - message = '' - The option definition `${showOption optionName}' in ${showFiles opt.files} no longer has any effect; please remove it. - ${replacementInstructions} - ''; - }]; + config.assertions = setAttrByPath (optionName ++ [ "removed" ]) (let opt = getAttrFromPath optionName options; in { + assertion = !opt.isDefined; + message = '' + The option definition `${showOption optionName}' in ${showFiles opt.files} no longer has any effect; please remove it. + ${replacementInstructions} + ''; + }); }; /* Return a module that causes a warning to be shown if the @@ -1194,14 +1193,15 @@ let })) from); config = { - warnings = filter (x: x != "") (map (f: - let val = getAttrFromPath f config; - opt = getAttrFromPath f options; - in - optionalString - (val != "_mkMergedOptionModule") - "The option `${showOption f}' defined in ${showFiles opt.files} has been changed to `${showOption to}' that has a different type. Please read `${showOption to}' documentation and update your configuration accordingly." - ) from); + warnings = foldl' recursiveUpdate {} (map (path: setAttrByPath (path ++ ["mergedOption"]) { + condition = (getAttrFromPath path config) != "_mkMergedOptionModule"; + message = let + opt = getAttrFromPath path options; + in '' + The option `${showOption path}' defined in ${showFiles opt.files} has been changed to `${showOption to}' that has a different type. + Please read `${showOption to}' documentation and update your configuration accordingly. + ''; + }) from); } // setAttrByPath to (mkMerge (optional (any (f: (getAttrFromPath f config) != "_mkMergedOptionModule") from) @@ -1357,8 +1357,10 @@ let }); config = mkIf condition (mkMerge [ (optionalAttrs (options ? warnings) { - warnings = optional (warn && fromOpt.isDefined) - "The option `${showOption from}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption to}'."; + warnings = setAttrByPath (from ++ [ "aliased" ]) { + condition = warn && fromOpt.isDefined; + message = "The option `${showOption from}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption to}'."; + }; }) (if withPriority then mkAliasAndWrapDefsWithPriority (setAttrByPath to) fromOpt diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix index 116d86cdfb3fb..bf55753810d17 100644 --- a/lib/tests/misc.nix +++ b/lib/tests/misc.nix @@ -35,6 +35,8 @@ let callPackageWith cartesianProduct cli + collect + collect' composeExtensions composeManyExtensions concatLines @@ -235,6 +237,46 @@ runTests { ]; }; + testCollect = { + expr = [ + (collect (x: x ? special) { a.b.c.special = true; x.y.z.special = false; }) + (collect (x: x == 1) { a = 1; b = 2; c = 3; d.inner = 1; }) + ]; + expected = [ + [ { special = true; } { special = false; } ] + [ 1 1 ] + ]; + }; + + testCollect' = { + expr = [ + (collect' (x: x ? special) { a.b.c.special = true; x.y.z.special = false; }) + (collect' (x: x == 1) { a = 1; b = 2; c = 3; d.inner = 1; }) + ]; + expected = [ + [ + { + path = [ "a" "b" "c" ]; + value = { special = true; }; + } + { + path = [ "x" "y" "z" ]; + value = { special = false; }; + } + ] + [ + { + path = [ "a" ]; + value = 1; + } + { + path = [ "d" "inner" ]; + value = 1; + } + ] + ]; + }; + testComposeExtensions = { expr = let obj = makeExtensible (self: { foo = self.bar; }); f = self: super: { bar = false; baz = true; }; diff --git a/lib/tests/modules/doRename-warnings.nix b/lib/tests/modules/doRename-warnings.nix index 6f0f1e87e3aa5..f0191aa89f61f 100644 --- a/lib/tests/modules/doRename-warnings.nix +++ b/lib/tests/modules/doRename-warnings.nix @@ -3,12 +3,25 @@ (lib.doRename { from = ["a" "b"]; to = ["c" "d" "e"]; warn = true; use = x: x; visible = true; }) ]; options = { - warnings = lib.mkOption { type = lib.types.listOf lib.types.str; }; + warnings = lib.mkOption { + type = let + checkedWarningItemType = let + check = x: x ? condition && x ? message; + in lib.types.addCheck (lib.types.attrsOf lib.types.anything) check; + + nestedWarningAttrsType = let + nestedWarningItemType = lib.types.either checkedWarningItemType (lib.types.attrsOf nestedWarningItemType); + in nestedWarningItemType; + in lib.types.submodule { freeformType = nestedWarningAttrsType; }; + }; + c.d.e = lib.mkOption {}; result = lib.mkOption {}; }; config = { a.b = 1234; - result = lib.concatStringsSep "%" config.warnings; + result = let + warning = config.warnings.a.b.aliased; + in lib.optionalString warning.condition warning.message; }; } diff --git a/nixos/doc/manual/development/assertions.section.md b/nixos/doc/manual/development/assertions.section.md index eb5158c90f98c..46ba1458a069c 100644 --- a/nixos/doc/manual/development/assertions.section.md +++ b/nixos/doc/manual/development/assertions.section.md @@ -12,12 +12,13 @@ This is an example of using `warnings`. { config, lib, ... }: { config = lib.mkIf config.services.foo.enable { - warnings = - if config.services.foo.bar - then [ ''You have enabled the bar feature of the foo service. - This is known to cause some specific problems in certain situations. - '' ] - else []; + warnings.services.foo.beCarefulWithBar = { + condition = config.services.foo.bar; + message = '' + You have enabled the bar feature of the foo service. + This is known to cause some specific problems in certain situations. + ''; + }; }; } ``` @@ -30,11 +31,10 @@ This example, extracted from the [`syslogd` module](https://github.com/NixOS/nix { config, lib, ... }: { config = lib.mkIf config.services.syslogd.enable { - assertions = - [ { assertion = !config.services.rsyslogd.enable; - message = "rsyslogd conflicts with syslogd"; - } - ]; + assertions.services.syslogd.rsyslogdConflict = { + assertion = !config.services.rsyslogd.enable; + message = "rsyslogd conflicts with syslogd"; + }; }; } ``` diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 69646e550f1f3..3d5d459b533dc 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -13,12 +13,14 @@ let flatten flip foldr + genAttrs getAttr hasAttr id length listToAttrs literalExpression + mapAttrs mapAttrs' mapAttrsToList match @@ -509,8 +511,6 @@ let gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid"; sdInitrdUidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) config.boot.initrd.systemd.users) "uid"; sdInitrdGidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) config.boot.initrd.systemd.groups) "gid"; - groupNames = lib.mapAttrsToList (n: g: g.name) cfg.groups; - usersWithoutExistingGroup = lib.filterAttrs (n: u: u.group != "" && !lib.elem u.group groupNames) cfg.users; spec = pkgs.writeText "users-groups.json" (builtins.toJSON { inherit (cfg) mutableUsers; @@ -845,26 +845,18 @@ in { }; }; - assertions = [ - { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique); + assertions.users = { + uidsGidsUnique = { + assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique); message = "UIDs and GIDs must be unique!"; - } - { assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique); + }; + systemdInitrdUidsGidsUnique = { + assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique); message = "systemd initrd UIDs and GIDs must be unique!"; - } - { assertion = usersWithoutExistingGroup == {}; - message = - let - errUsers = lib.attrNames usersWithoutExistingGroup; - missingGroups = lib.unique (lib.mapAttrsToList (n: u: u.group) usersWithoutExistingGroup); - mkConfigHint = group: "users.groups.${group} = {};"; - in '' - The following users have a primary group that is undefined: ${lib.concatStringsSep " " errUsers} - Hint: Add this to your NixOS configuration: - ${lib.concatStringsSep "\n " (map mkConfigHint missingGroups)} - ''; - } - { # If mutableUsers is false, to prevent users creating a + }; + + noLockout = { + # If mutableUsers is false, to prevent users creating a # configuration that locks them out of the system, ensure that # there is at least one "privileged" account that has a # password or an SSH authorized key. Privileged accounts are @@ -892,58 +884,72 @@ in { However you are most probably better off by setting users.mutableUsers = true; and manually running passwd root to set the root password. ''; - } - ] ++ flatten (flip mapAttrsToList cfg.users (name: user: - [ - { - assertion = (user.hashedPassword != null) - -> (match ".*:.*" user.hashedPassword == null); - message = '' + }; + + users = flip mapAttrs cfg.users (name: user: { + noColonInHashedPassword = { + assertion = (user.hashedPassword != null) -> (match ".*:.*" user.hashedPassword == null); + message = '' The password hash of user "${user.name}" contains a ":" character. This is invalid and would break the login system because the fields of /etc/shadow (file where hashes are stored) are colon-separated. - Please check the value of option `users.users."${user.name}".hashedPassword`.''; - } - { - assertion = let - isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 1000); - in xor isEffectivelySystemUser user.isNormalUser; - message = '' - Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set. - ''; - } - { - assertion = user.group != ""; - message = '' - users.users.${user.name}.group is unset. This used to default to - nogroup, but this is unsafe. For example you can create a group - for this user with: - users.users.${user.name}.group = "${user.name}"; - users.groups.${user.name} = {}; - ''; - } - ] ++ (map (shell: { - assertion = !user.ignoreShellProgramCheck -> (user.shell == pkgs.${shell}) -> (config.programs.${shell}.enable == true); - message = '' - users.users.${user.name}.shell is set to ${shell}, but - programs.${shell}.enable is not true. This will cause the ${shell} - shell to lack the basic nix directories in its PATH and might make - logging in as that user impossible. You can fix it with: - programs.${shell}.enable = true; - - If you know what you're doing and you are fine with the behavior, - set users.users.${user.name}.ignoreShellProgramCheck = true; - instead. - ''; - }) [ + Please check the value of option `users.users."${user.name}".hashedPassword`. + ''; + }; + + userKindDefined = { + assertion = let + isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 1000); + in xor isEffectivelySystemUser user.isNormalUser; + message = '' + Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set. + ''; + }; + + userHasPrimaryGroup = { + assertion = user.group != ""; + message = '' + users.users.${user.name}.group is unset. This used to default to + nogroup, but this is unsafe. For example you can create a group + for this user with: + users.users.${user.name}.group = "${user.name}"; + users.groups.${user.name} = {}; + ''; + }; + + userPrimaryGroupExists = { + assertion = cfg.groups ? ${user.group}; + message = '' + users.users.${user.name}.group is set to "${user.group}", but this group is not defined. + + Hint: Add this to your NixOS configuration: + "users.groups.${user.group} = {};"; + ''; + }; + + shells = genAttrs [ "fish" "xonsh" "zsh" - ]) - )); + ] (shell: { + assertion = !user.ignoreShellProgramCheck -> (user.shell == pkgs.${shell}) -> (config.programs.${shell}.enable == true); + message = '' + users.users.${user.name}.shell is set to ${shell}, but + programs.${shell}.enable is not true. This will cause the ${shell} + shell to lack the basic nix directories in its PATH and might make + logging in as that user impossible. You can fix it with: + programs.${shell}.enable = true; + + If you know what you're doing and you are fine with the behavior, + set users.users.${user.name}.ignoreShellProgramCheck = true; + instead. + ''; + }); + }); + }; - warnings = - flip concatMap (attrValues cfg.users) (user: let + warnings.users.users = flip mapAttrs cfg.users (name: user: { + ambiguousPasswordConfiguration = let unambiguousPasswordConfiguration = 1 >= length (filter (x: x != null) ([ user.hashedPassword user.hashedPasswordFile @@ -954,16 +960,19 @@ in { user.initialHashedPassword user.initialPassword ])); - in optional (!unambiguousPasswordConfiguration) '' - The user '${user.name}' has multiple of the options - `hashedPassword`, `password`, `hashedPasswordFile`, `initialPassword` - & `initialHashedPassword` set to a non-null value. - The options silently discard others by the order of precedence - given above which can lead to surprising results. To resolve this warning, - set at most one of the options above to a non-`null` value. - '') - ++ filter (x: x != null) ( - flip mapAttrsToList cfg.users (_: user: + in { + condition = !unambiguousPasswordConfiguration; + message = '' + The user '${user.name}' has multiple of the options + `hashedPassword`, `password`, `hashedPasswordFile`, `initialPassword` + & `initialHashedPassword` set to a non-null value. + The options silently discard others by the order of precedence + given above which can lead to surprising results. To resolve this warning, + set at most one of the options above to a non-`null` value. + ''; + }; + + invalidPasswordHash = let # This regex matches a subset of the Modular Crypto Format (MCF)[1] # informal standard. Since this depends largely on the OS or the # specific implementation of crypt(3) we only support the (sane) @@ -972,31 +981,33 @@ in { # common mistakes like typing the plaintext password. # # [1]: https://en.wikipedia.org/wiki/Crypt_(C) - let - sep = "\\$"; - base64 = "[a-zA-Z0-9./]+"; - id = cryptSchemeIdPatternGroup; - name = "[a-z0-9-]+"; - value = "[a-zA-Z0-9/+.-]+"; - options = "${name}(=${value})?(,${name}=${value})*"; - scheme = "${id}(${sep}${options})?"; - content = "${base64}${sep}${base64}(${sep}${base64})?"; - mcf = "^${sep}${scheme}${sep}${content}$"; - in - if (allowsLogin user.hashedPassword + sep = "\\$"; + base64 = "[a-zA-Z0-9./]+"; + id = cryptSchemeIdPatternGroup; + name = "[a-z0-9-]+"; + value = "[a-zA-Z0-9/+.-]+"; + options = "${name}(=${value})?(,${name}=${value})*"; + scheme = "${id}(${sep}${options})?"; + content = "${base64}${sep}${base64}(${sep}${base64})?"; + mcf = "^${sep}${scheme}${sep}${content}$"; + in { + condition = allowsLogin user.hashedPassword && user.hashedPassword != "" # login without password - && match mcf user.hashedPassword == null) - then '' + && match mcf user.hashedPassword == null; + message = '' The password hash of user "${user.name}" may be invalid. You must set a valid hash or the user will be locked out of their account. Please - check the value of option `users.users."${user.name}".hashedPassword`.'' - else null) - ++ flip mapAttrsToList cfg.users (name: user: - if user.passwordFile != null then - ''The option `users.users."${name}".passwordFile' has been renamed '' + - ''to `users.users."${name}".hashedPasswordFile'.'' - else null) - ); - }; + check the value of option `users.users."${user.name}".hashedPassword`. + ''; + }; + passwordFileDeprecation = { + condition = user.passwordFile != null; + message = '' + The option `users.users."${user.name}".passwordFile' has been renamed + to `users.users."${user.name}".hashedPasswordFile'. + ''; + }; + }); + }; } diff --git a/nixos/modules/misc/assertions.nix b/nixos/modules/misc/assertions.nix index 550b3ac97f6a8..9f91ef38529e9 100644 --- a/nixos/modules/misc/assertions.nix +++ b/nixos/modules/misc/assertions.nix @@ -1,16 +1,90 @@ { lib, ... }: +{ + options = { + assertions = lib.mkOption { + type = let + assertionItemType = lib.types.submodule ({ config, ... }: { + options = { + enable = lib.mkOption { + description = '' + Whether to enable this assertion. -with lib; + This option is mostly useful for users, in order to forcefully disable assertions they believe to be + erroneous while waiting for someone to fix the assertion upstream. + ''; + type = lib.types.bool; + default = true; + example = false; + }; -{ + assertion = lib.mkOption { + description = "Condition to be asserted. If this is `false`, the evaluation will throw the error"; + type = lib.types.bool; + example = false; + }; - options = { + message = lib.mkOption { + description = "The contents of the error message that should be shown upon triggering a false assertion"; + type = if config.lazy then lib.types.unspecified else lib.types.nonEmptyStr; + example = "This is an example error message"; + }; + + lazy = lib.mkOption { + description = '' + Whether to avoid evaluating the message contents until the assertion condition occurs. + + This will also disable typechecking. + + ::: {.note} + We do not recommend you enable this. It is mostly intended for backwards compatibility. + If you do need to enable it, make sure to double check that your `message` always will + evaluate successfully whenever the assertion would trigger. + ::: + ''; + type = lib.types.bool; + default = false; + example = true; + }; + }; + }); + + # This might be replaced when tagged submodules or similar are available, + # see https://github.com/NixOS/nixpkgs/pull/254790 + checkedAssertionItemType = let + check = x: x ? assertion && x ? message; + in lib.types.addCheck assertionItemType check; + + nestedAssertionAttrsType = with lib.types; let + nestedAssertionItemType = oneOf [ + checkedAssertionItemType + (attrsOf nestedAssertionItemType) + ]; + in nestedAssertionItemType // { + description = "nested attrs of (${assertionItemType.description})"; + }; - assertions = mkOption { - type = types.listOf types.unspecified; + # Backwards compatibility for assertions that are still written as attrs inside a list. + # The attribute name will be set to the sha256 sum of the assertion message, e.g. `assertions.legacy."" = { ... }` + coercedAssertionAttrs = let + coerce = xs: { + legacy = lib.listToAttrs (lib.imap0 (i: assertion: lib.nameValuePair "anon-${toString i}" (assertion // { lazy = true; })) xs); + }; + in with lib.types; coercedTo (listOf (attrsOf unspecified)) coerce (submodule { freeformType = nestedAssertionAttrsType; }); + in coercedAssertionAttrs; internal = true; - default = []; - example = [ { assertion = false; message = "you can't enable this for that reason"; } ]; + default = { }; + example = lib.literalExpression '' + { + programs.foo.dontUseSomeOption = { + assertion = !config.programs.foo.settings.someOption; + message = "You can't enable foo's someOption for some reason"; + }; + services.bar.mutuallyExclusiveWithFoo = { + assertion = config.services.bar.enable -> !config.programs.foo.enable; + message = "You can't use the 'foo' program if you're using the 'bar' service"; + }; + } + ''; description = '' This option allows modules to express conditions that must hold for the evaluation of the system configuration to @@ -18,17 +92,81 @@ with lib; ''; }; - warnings = mkOption { + warnings = lib.mkOption { + type = let + warningItemType = lib.types.submodule { + options = { + enable = lib.mkOption { + description = '' + Whether to enable this warning. + + This option is mostly useful for users, in order to forcefully disable warnings they believe to be + erroneous while waiting for someone to fix the condition upstream. + ''; + type = lib.types.bool; + default = true; + example = false; + }; + + condition = lib.mkOption { + description = "Condition that triggers the warning message. If this is `true`, the warning will be shown"; + type = lib.types.bool; + example = true; + }; + + message = lib.mkOption { + description = "The contents of the warning message that should be shown upon triggering the condition"; + type = lib.types.nonEmptyStr; + example = "This is an example warning message"; + }; + }; + }; + + # This might be replaced when tagged submodules or similar are available, + # see https://github.com/NixOS/nixpkgs/pull/254790 + checkedWarningItemType = let + check = x: x ? condition && x ? message; + in lib.types.addCheck warningItemType check; + + nestedWarningAttrsType = with lib.types; let + nestedWarningItemType = oneOf [ + checkedWarningItemType + (attrsOf nestedWarningItemType) + ]; + in nestedWarningItemType // { + description = "nested attrs of (${warningItemType.description})"; + }; + + # Backwards compatibility for warnings that are still written as strings inside a list. + # The attribute name will be set to the sha256 sum of the warning message, e.g. `warnings."" = { ... }` + coercedWarningAttrs = let + coerce = xs: { + legacy = lib.listToAttrs (map (message: lib.nameValuePair (builtins.hashString "sha256" message) { + inherit message; + condition = true; + }) xs); + }; + in with lib.types; coercedTo (listOf str) coerce (submodule { freeformType = nestedWarningAttrsType; }); + in coercedWarningAttrs; internal = true; - default = []; - type = types.listOf types.str; - example = [ "The `foo' service is deprecated and will go away soon!" ]; + default = { }; + example = lib.literalExpression '' + { + services.foo.deprecationNotice = { + condition = config.services.foo.enable; + message = "The `foo' service is deprecated and will go away soon!"; + }; + services.bar.otherWarning = { + condition = !config.services.bar.settings.importantOption; + message = "You might want to enable services.bar.settings.importantOption, or everything is going to break"; + }; + } + ''; description = '' This option allows modules to show warnings to users during the evaluation of the system configuration. ''; }; - }; # impl of assertions is in } diff --git a/nixos/modules/security/wrappers/default.nix b/nixos/modules/security/wrappers/default.nix index b5dae96d79c6b..e05658b098499 100644 --- a/nixos/modules/security/wrappers/default.nix +++ b/nixos/modules/security/wrappers/default.nix @@ -228,16 +228,15 @@ in ###### implementation config = { - - assertions = lib.mapAttrsToList - (name: opts: - { assertion = opts.setuid || opts.setgid -> opts.capabilities == ""; - message = '' - The security.wrappers.${name} wrapper is not valid: - setuid/setgid and capabilities are mutually exclusive. - ''; - } - ) wrappers; + assertions.security.wrappers = lib.flip lib.mapAttrs wrappers (name: opts: { + setuidSetgidCapabilitiesAreMutuallyExclusive = { + assertion = opts.setuid || opts.setgid -> opts.capabilities == ""; + message = '' + The security.wrappers.${name} wrapper is not valid: + setuid/setgid and capabilities are mutually exclusive. + ''; + }; + }); security.wrappers = let diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix index c698c9005aaf8..5350f471a690c 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters.nix @@ -229,8 +229,8 @@ let nftables = config.networking.nftables.enable; in mkIf conf.enable { - warnings = conf.warnings or []; - assertions = conf.assertions or []; + warnings = conf.warnings or {}; + assertions = conf.assertions or {}; users.users."${name}-exporter" = (mkIf (conf.user == "${name}-exporter" && !enableDynamicUser) { description = "Prometheus ${name} exporter service user"; isSystemUser = true; @@ -306,122 +306,150 @@ in }; config = mkMerge ([{ - assertions = [ { - assertion = cfg.ipmi.enable -> (cfg.ipmi.configFile != null) -> ( - !(lib.hasPrefix "/tmp/" cfg.ipmi.configFile) - ); - message = '' - Config file specified in `services.prometheus.exporters.ipmi.configFile' must - not reside within /tmp - it won't be visible to the systemd service. - ''; - } { - assertion = cfg.ipmi.enable -> (cfg.ipmi.webConfigFile != null) -> ( - !(lib.hasPrefix "/tmp/" cfg.ipmi.webConfigFile) - ); - message = '' - Config file specified in `services.prometheus.exporters.ipmi.webConfigFile' must - not reside within /tmp - it won't be visible to the systemd service. - ''; - } { - assertion = cfg.snmp.enable -> ( - (cfg.snmp.configurationPath == null) != (cfg.snmp.configuration == null) - ); - message = '' - Please ensure you have either `services.prometheus.exporters.snmp.configuration' - or `services.prometheus.exporters.snmp.configurationPath' set! - ''; - } { - assertion = cfg.mikrotik.enable -> ( - (cfg.mikrotik.configFile == null) != (cfg.mikrotik.configuration == null) - ); - message = '' - Please specify either `services.prometheus.exporters.mikrotik.configuration' - or `services.prometheus.exporters.mikrotik.configFile'. - ''; - } { - assertion = cfg.mail.enable -> ( - (cfg.mail.configFile == null) != (cfg.mail.configuration == null) - ); - message = '' - Please specify either 'services.prometheus.exporters.mail.configuration' - or 'services.prometheus.exporters.mail.configFile'. - ''; - } { - assertion = cfg.mysqld.runAsLocalSuperUser -> config.services.mysql.enable; - message = '' - The exporter is configured to run as 'services.mysql.user', but - 'services.mysql.enable' is set to false. - ''; - } { - assertion = cfg.nextcloud.enable -> ( - (cfg.nextcloud.passwordFile == null) != (cfg.nextcloud.tokenFile == null) - ); - message = '' - Please specify either 'services.prometheus.exporters.nextcloud.passwordFile' or - 'services.prometheus.exporters.nextcloud.tokenFile' - ''; - } { - assertion = cfg.sql.enable -> ( - (cfg.sql.configFile == null) != (cfg.sql.configuration == null) - ); - message = '' - Please specify either 'services.prometheus.exporters.sql.configuration' or - 'services.prometheus.exporters.sql.configFile' - ''; - } { - assertion = cfg.scaphandre.enable -> (pkgs.stdenv.targetPlatform.isx86_64 == true); - message = '' - Scaphandre only support x86_64 architectures. - ''; - } { - assertion = cfg.scaphandre.enable -> ((lib.kernel.whenHelpers pkgs.linux.version).whenOlder "5.11" true).condition == false; - message = '' - Scaphandre requires a kernel version newer than '5.11', '${pkgs.linux.version}' given. - ''; - } { - assertion = cfg.scaphandre.enable -> (builtins.elem "intel_rapl_common" config.boot.kernelModules); - message = '' - Scaphandre needs 'intel_rapl_common' kernel module to be enabled. Please add it in 'boot.kernelModules'. - ''; - } { - assertion = cfg.idrac.enable -> ( - (cfg.idrac.configurationPath == null) != (cfg.idrac.configuration == null) - ); - message = '' - Please ensure you have either `services.prometheus.exporters.idrac.configuration' - or `services.prometheus.exporters.idrac.configurationPath' set! - ''; - } { - assertion = cfg.deluge.enable -> ( - (cfg.deluge.delugePassword == null) != (cfg.deluge.delugePasswordFile == null) - ); - message = '' - Please ensure you have either `services.prometheus.exporters.deluge.delugePassword' - or `services.prometheus.exporters.deluge.delugePasswordFile' set! - ''; - } { - assertion = cfg.pgbouncer.enable -> ( - xor (cfg.pgbouncer.connectionEnvFile == null) (cfg.pgbouncer.connectionString == null) - ); - message = '' - Options `services.prometheus.exporters.pgbouncer.connectionEnvFile` and - `services.prometheus.exporters.pgbouncer.connectionString` are mutually exclusive! - ''; - }] ++ (flip map (attrNames exporterOpts) (exporter: { - assertion = cfg.${exporter}.firewallFilter != null -> cfg.${exporter}.openFirewall; - message = '' - The `firewallFilter'-option of exporter ${exporter} doesn't have any effect unless - `openFirewall' is set to `true'! - ''; - })) ++ config.services.prometheus.exporters.assertions; - warnings = [ - (mkIf (config.services.prometheus.exporters.idrac.enable && config.services.prometheus.exporters.idrac.configurationPath != null) '' + assertions.services.prometheus.exporters = { + ipmi.nonEmptyConfigFile = { + assertion = cfg.ipmi.enable -> (cfg.ipmi.configFile != null) -> ( + !(lib.hasPrefix "/tmp/" cfg.ipmi.configFile) + ); + message = '' + Config file specified in `services.prometheus.exporters.ipmi.configFile' must + not reside within /tmp - it won't be visible to the systemd service. + ''; + }; + ipmi.nonEmptyWebConfigFile = { + assertion = cfg.ipmi.enable -> (cfg.ipmi.webConfigFile != null) -> ( + !(lib.hasPrefix "/tmp/" cfg.ipmi.webConfigFile) + ); + message = '' + Config file specified in `services.prometheus.exporters.ipmi.webConfigFile' must + not reside within /tmp - it won't be visible to the systemd service. + ''; + }; + snmp.nonEmptyConfiguration = { + assertion = cfg.snmp.enable -> ( + (cfg.snmp.configurationPath == null) != (cfg.snmp.configuration == null) + ); + message = '' + Please ensure you have either `services.prometheus.exporters.snmp.configuration' + or `services.prometheus.exporters.snmp.configurationPath' set! + ''; + }; + mikrotik.nonEmptyConfiguration = { + assertion = cfg.mikrotik.enable -> ( + (cfg.mikrotik.configFile == null) != (cfg.mikrotik.configuration == null) + ); + message = '' + Please specify either `services.prometheus.exporters.mikrotik.configuration' + or `services.prometheus.exporters.mikrotik.configFile'. + ''; + }; + mail.nonEmptyConfiguration = { + assertion = cfg.mail.enable -> ( + (cfg.mail.configFile == null) != (cfg.mail.configuration == null) + ); + message = '' + Please specify either 'services.prometheus.exporters.mail.configuration' + or 'services.prometheus.exporters.mail.configFile'. + ''; + }; + mysqld.serverEnabledIfRunAsLocalSuperUser = { + assertion = cfg.mysqld.runAsLocalSuperUser -> config.services.mysql.enable; + message = '' + The exporter is configured to run as 'services.mysql.user', but + 'services.mysql.enable' is set to false. + ''; + }; + nextcloud.nonEmptyPassword = { + assertion = cfg.nextcloud.enable -> ( + (cfg.nextcloud.passwordFile == null) != (cfg.nextcloud.tokenFile == null) + ); + message = '' + Please specify either 'services.prometheus.exporters.nextcloud.passwordFile' or + 'services.prometheus.exporters.nextcloud.tokenFile' + ''; + }; + sql.nonEmptyConfiguration = { + assertion = cfg.sql.enable -> ( + (cfg.sql.configFile == null) != (cfg.sql.configuration == null) + ); + message = '' + Please specify either 'services.prometheus.exporters.sql.configuration' or + 'services.prometheus.exporters.sql.configFile' + ''; + }; + scaphandre.platformIsx86_64 = { + assertion = cfg.scaphandre.enable -> (pkgs.stdenv.targetPlatform.isx86_64 == true); + message = '' + Scaphandre only support x86_64 architectures. + ''; + }; + scaphandre.kernelIsNewerThan511 = { + assertion = cfg.scaphandre.enable -> ((lib.kernel.whenHelpers pkgs.linux.version).whenOlder "5.11" true).condition == false; + message = '' + Scaphandre requires a kernel version newer than '5.11', '${pkgs.linux.version}' given. + ''; + }; + scaphandre.kernelModuleInterlRaplCommonIsEnabled = { + assertion = cfg.scaphandre.enable -> (builtins.elem "intel_rapl_common" config.boot.kernelModules); + message = '' + Scaphandre needs 'intel_rapl_common' kernel module to be enabled. Please add it in 'boot.kernelModules'. + ''; + }; + idrac.nonEmptyConfiguration = { + assertion = cfg.idrac.enable -> ( + (cfg.idrac.configurationPath == null) != (cfg.idrac.configuration == null) + ); + message = '' + Please ensure you have either `services.prometheus.exporters.idrac.configuration' + or `services.prometheus.exporters.idrac.configurationPath' set! + ''; + }; + deluge.nonEmptyPassword = { + assertion = cfg.deluge.enable -> ( + (cfg.deluge.delugePassword == null) != (cfg.deluge.delugePasswordFile == null) + ); + message = '' + Please ensure you have either `services.prometheus.exporters.deluge.delugePassword' + or `services.prometheus.exporters.deluge.delugePasswordFile' set! + ''; + }; + pgbouncer.nonEmptyConnection = { + assertion = cfg.pgbouncer.enable -> ( + xor (cfg.pgbouncer.connectionEnvFile == null) (cfg.pgbouncer.connectionString == null) + ); + message = '' + Options `services.prometheus.exporters.pgbouncer.connectionEnvFile` and + `services.prometheus.exporters.pgbouncer.connectionString` are mutually exclusive! + ''; + }; + } + // + (lib.listToAttrs + (map + (exporter: lib.nameValuePair exporter { + assertion = cfg.${exporter}.firewallFilter != null -> cfg.${exporter}.openFirewall; + message = '' + The `firewallFilter'-option of exporter ${exporter} doesn't have any effect unless + `openFirewall' is set to `true'! + ''; + }) + (attrNames exporterOpts) + )) + // + config.services.prometheus.exporters.assertions; + + warnings = { + services.prometheus.exporters.idrac.configurationPathOverrides = { + condition = config.services.prometheus.exporters.idrac.enable && config.services.prometheus.exporters.idrac.configurationPath != null; + message = '' Configuration file in `services.prometheus.exporters.idrac.configurationPath` may override `services.prometheus.exporters.idrac.listenAddress` and/or `services.prometheus.exporters.idrac.port`. Consider using `services.prometheus.exporters.idrac.configuration` instead. - '' - ) - ] ++ config.services.prometheus.exporters.warnings; + ''; + }; + } + // + config.services.prometheus.exporters.warnings; }] ++ [(mkIf config.services.prometheus.exporters.rtl_433.enable { hardware.rtl-sdr.enable = mkDefault true; })] ++ [(mkIf config.services.postfix.enable { diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix index b678656f2e046..5360b7d5340b7 100644 --- a/nixos/modules/services/networking/hostapd.nix +++ b/nixos/modules/services/networking/hostapd.nix @@ -1127,77 +1127,69 @@ in { ]; config = mkIf cfg.enable { - assertions = - [ - { - assertion = cfg.radios != {}; - message = "At least one radio must be configured with hostapd!"; - } - ] - # Radio warnings - ++ (concatLists (mapAttrsToList ( - radio: radioCfg: - [ - { - assertion = radioCfg.networks != {}; - message = "hostapd radio ${radio}: At least one network must be configured!"; - } - # XXX: There could be many more useful assertions about (band == xy) -> ensure other required settings. - # see https://github.com/openwrt/openwrt/blob/539cb5389d9514c99ec1f87bd4465f77c7ed9b93/package/kernel/mac80211/files/lib/netifd/wireless/mac80211.sh#L158 - { - assertion = length (filter (bss: bss == radio) (attrNames radioCfg.networks)) == 1; - message = ''hostapd radio ${radio}: Exactly one network must be named like the radio, for reasons internal to hostapd.''; - } - { - assertion = (radioCfg.wifi4.enable && builtins.elem "HT40-" radioCfg.wifi4.capabilities) -> radioCfg.channel != 0; - message = ''hostapd radio ${radio}: using ACS (channel = 0) together with HT40- (wifi4.capabilities) is unsupported by hostapd''; - } - ] - # BSS warnings - ++ (concatLists (mapAttrsToList (bss: bssCfg: let - auth = bssCfg.authentication; - countWpaPasswordDefinitions = count (x: x != null) [ - auth.wpaPassword - auth.wpaPasswordFile - auth.wpaPskFile - ]; - in [ - { - assertion = hasPrefix radio bss; - message = "hostapd radio ${radio} bss ${bss}: The bss (network) name ${bss} is invalid. It must be prefixed by the radio name for reasons internal to hostapd. A valid name would be e.g. ${radio}, ${radio}-1, ..."; - } - { - assertion = (length (attrNames radioCfg.networks) > 1) -> (bssCfg.bssid != null); - message = ''hostapd radio ${radio} bss ${bss}: bssid must be specified manually (for now) since this radio uses multiple BSS.''; - } - { - assertion = countWpaPasswordDefinitions <= 1; - message = ''hostapd radio ${radio} bss ${bss}: must use at most one WPA password option (wpaPassword, wpaPasswordFile, wpaPskFile)''; - } - { - assertion = auth.wpaPassword != null -> (stringLength auth.wpaPassword >= 8 && stringLength auth.wpaPassword <= 63); - message = ''hostapd radio ${radio} bss ${bss}: uses a wpaPassword of invalid length (must be in [8,63]).''; - } - { - assertion = auth.saePasswords == [] || auth.saePasswordsFile == null; - message = ''hostapd radio ${radio} bss ${bss}: must use only one SAE password option (saePasswords or saePasswordsFile)''; - } - { - assertion = auth.mode == "wpa3-sae" -> (auth.saePasswords != [] || auth.saePasswordsFile != null); - message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires defining a sae password option''; - } - { - assertion = auth.mode == "wpa3-sae-transition" -> (auth.saePasswords != [] || auth.saePasswordsFile != null) && countWpaPasswordDefinitions == 1; - message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option''; - } - { - assertion = (auth.mode == "wpa2-sha1" || auth.mode == "wpa2-sha256") -> countWpaPasswordDefinitions == 1; - message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-PSK which requires defining a wpa password option''; - } - ]) - radioCfg.networks)) - ) - cfg.radios)); + assertions.services.hostapd.atLeastOneRadio = { + assertion = cfg.radios != {}; + message = "At least one radio must be configured with hostapd!"; + }; + + assertions.services.hostapd.radios = lib.flip lib.mapAttrs cfg.radios (radio: radioCfg: { + atLeastOneNetwork = { + assertion = radioCfg.networks != {}; + message = "hostapd radio ${radio}: At least one network must be configured!"; + }; + # XXX: There could be many more useful assertions about (band == xy) -> ensure other required settings. + # see https://github.com/openwrt/openwrt/blob/539cb5389d9514c99ec1f87bd4465f77c7ed9b93/package/kernel/mac80211/files/lib/netifd/wireless/mac80211.sh#L158 + exactlyOneRadioNamedNetwork = { + assertion = length (filter (bss: bss == radio) (attrNames radioCfg.networks)) == 1; + message = ''hostapd radio ${radio}: Exactly one network must be named like the radio, for reasons internal to hostapd.''; + }; + noACSWithHT40 = { + assertion = (radioCfg.wifi4.enable && builtins.elem "HT40-" radioCfg.wifi4.capabilities) -> radioCfg.channel != 0; + message = ''hostapd radio ${radio}: using ACS (channel = 0) together with HT40- (wifi4.capabilities) is unsupported by hostapd''; + }; + + bssWarnings = lib.flip lib.mapAttrs radioCfg.networks (bss: bssCfg: let + auth = bssCfg.authentication; + countWpaPasswordDefinitions = count (x: x != null) [ + auth.wpaPassword + auth.wpaPasswordFile + auth.wpaPskFile + ]; + in { + radioHasBssPrefix = { + assertion = hasPrefix radio bss; + message = "hostapd radio ${radio} bss ${bss}: The bss (network) name ${bss} is invalid. It must be prefixed by the radio name for reasons internal to hostapd. A valid name would be e.g. ${radio}, ${radio}-1, ..."; + }; + multipleBssManualBssid = { + assertion = (length (attrNames radioCfg.networks) > 1) -> (bssCfg.bssid != null); + message = ''hostapd radio ${radio} bss ${bss}: bssid must be specified manually (for now) since this radio uses multiple BSS.''; + }; + singleWpaCredentialSource = { + assertion = countWpaPasswordDefinitions <= 1; + message = ''hostapd radio ${radio} bss ${bss}: must use at most one WPA password option (wpaPassword, wpaPasswordFile, wpaPskFile)''; + }; + validWpaPasswordLength = { + assertion = auth.wpaPassword != null -> (stringLength auth.wpaPassword >= 8 && stringLength auth.wpaPassword <= 63); + message = ''hostapd radio ${radio} bss ${bss}: uses a wpaPassword of invalid length (must be in [8,63]).''; + }; + singleSaePasswordSource = { + assertion = auth.saePasswords == [] || auth.saePasswordsFile == null; + message = ''hostapd radio ${radio} bss ${bss}: must use only one SAE password option (saePasswords or saePasswordsFile)''; + }; + wpa3SaeRequiresPassword = { + assertion = auth.mode == "wpa3-sae" -> (auth.saePasswords != [] || auth.saePasswordsFile != null); + message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE which requires defining a sae password option''; + }; + wpa3SaeTransitionRequiresWpaSaePasswords = { + assertion = auth.mode == "wpa3-sae-transition" -> (auth.saePasswords != [] || auth.saePasswordsFile != null) && countWpaPasswordDefinitions == 1; + message = ''hostapd radio ${radio} bss ${bss}: uses WPA3-SAE in transition mode requires defining both a wpa password option and a sae password option''; + }; + wpa2PskRequiresWpaPassword = { + assertion = (auth.mode == "wpa2-sha1" || auth.mode == "wpa2-sha256") -> countWpaPasswordDefinitions == 1; + message = ''hostapd radio ${radio} bss ${bss}: uses WPA2-PSK which requires defining a wpa password option''; + }; + }); + }); environment.systemPackages = [cfg.package]; diff --git a/nixos/modules/services/web-servers/fcgiwrap.nix b/nixos/modules/services/web-servers/fcgiwrap.nix index 36a327b9ab9f9..7eb190073458e 100644 --- a/nixos/modules/services/web-servers/fcgiwrap.nix +++ b/nixos/modules/services/web-servers/fcgiwrap.nix @@ -94,28 +94,28 @@ in { }; config = { - assertions = concatLists (mapAttrsToList (name: cfg: [ - { + assertions.services.fcgiwrap.instances = mapAttrs (name: cfg: { + socketOwnerRequired = { assertion = cfg.socket.type == "unix" -> cfg.socket.user != null; message = "Socket owner is required for the UNIX socket type."; - } - { + }; + socketGroupRequired = { assertion = cfg.socket.type == "unix" -> cfg.socket.group != null; - message = "Socket owner is required for the UNIX socket type."; - } - { + message = "Socket group is required for the UNIX socket type."; + }; + socketOwnerOnlyForUnix = { assertion = cfg.socket.user != null -> cfg.socket.type == "unix"; message = "Socket owner can only be set for the UNIX socket type."; - } - { + }; + socketGroupOnlyForUnix = { assertion = cfg.socket.group != null -> cfg.socket.type == "unix"; - message = "Socket owner can only be set for the UNIX socket type."; - } - { + message = "Socket group can only be set for the UNIX socket type."; + }; + socketModeOnlyForUnix = { assertion = cfg.socket.mode != null -> cfg.socket.type == "unix"; message = "Socket mode can only be set for the UNIX socket type."; - } - ]) config.services.fcgiwrap.instances); + }; + }) config.services.fcgiwrap.instances; systemd.services = forEachInstance (cfg: { after = [ "nss-user-lookup.target" ]; diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix index ed0ece19f2fa2..8cf904d541943 100644 --- a/nixos/modules/system/activation/top-level.nix +++ b/nixos/modules/system/activation/top-level.nix @@ -61,11 +61,40 @@ let # Handle assertions and warnings - failedAssertions = map (x: x.message) (filter (x: !x.assertion) config.assertions); + collectUntil = let + collectUntil' = + path: + pred: + dontProceedPred: + value: + if pred value then + [ { inherit path value; } ] + else if dontProceedPred value then + [ ] + else if isAttrs value then + concatMap ({ name, value }: collectUntil' (path ++ [ name ]) pred dontProceedPred value) (attrsToList value) + else + []; + in collectUntil' []; + + formatAttrPath = let + escape = s: "\"" + (builtins.replaceStrings ["\""] ["\\\""] s) + "\""; + in lib.concatMapStringsSep "." (p: if builtins.match "[a-zA-Z_-][a-zA-Z0-9_-]*" p == null then escape p else p); + + failedAssertions = let + assertions = collectUntil + (x: builtins.isAttrs x && !(x.assertion or true) && (x.enable or false) && x ? message) + (x: builtins.isAttrs x && (x.lazy or false)) + config.assertions; + in map (x: "assertions." + (formatAttrPath x.path) + ":\n" + x.value.message) assertions; + + failedWarnings = let + warnings = lib.collect' (x: builtins.isAttrs x && (x.condition or false) && (x.enable or false) && x ? message) config.warnings; + in map (x: "warnings." (formatAttrPath x.path) + ":\n" + x.value.message) warnings; baseSystemAssertWarn = if failedAssertions != [] then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}" - else showWarnings config.warnings baseSystem; + else showWarnings failedWarnings baseSystem; # Replace runtime dependencies system = foldr ({ oldDependency, newDependency }: drv: diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix index 5c95cd3d451ef..5f823d2379969 100644 --- a/nixos/modules/tasks/filesystems.nix +++ b/nixos/modules/tasks/filesystems.nix @@ -321,7 +321,7 @@ in config = { - assertions = let + assertions.fileSystems = let ls = sep: concatMapStringsSep sep (x: x.mountPoint); resizableFSes = [ "ext3" @@ -329,31 +329,31 @@ in "btrfs" "xfs" ]; - notAutoResizable = fs: fs.autoResize && !(builtins.elem fs.fsType resizableFSes); - 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 '' + in { + topologicallySorted = { + assertion = ! (fileSystems' ? cycle); + message = '' + The ‘fileSystems’ option can't be topologically sorted: + mountpoint dependency path ${ls " -> " fileSystems'.cycle or []} loops to ${ls ", " fileSystems'.loops or []} + ''; + }; + resizable = mapAttrs (name: fs: { + assertion = fs.autoResize -> builtins.elem fs.fsType resizableFSes; + message = '' Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = "${fs.fsType}"' ${optionalString (fs.fsType == "auto") "fsType has to be explicitly set and"} only the following support it: ${lib.concatStringsSep ", " resizableFSes}. ''; - } - { - assertion = ! (any (fs: fs.formatOptions != null) fileSystems); - message = let - fs = head (filter (fs: fs.formatOptions != null) fileSystems); - in '' + }) config.fileSystems; + formatOptionsDeprecated = mapAttrs (name: fs: { + assertion = fs.formatOptions == null; + message = '' 'fileSystems..formatOptions' has been removed, since systemd-makefs does not support any way to provide formatting options. ''; - } - ]; + }) config.fileSystems; + }; # Export for use in other modules system.build.fileSystems = fileSystems; diff --git a/nixos/modules/tasks/network-interfaces-scripted.nix b/nixos/modules/tasks/network-interfaces-scripted.nix index bbf2d337aac64..92bdc6c485c73 100644 --- a/nixos/modules/tasks/network-interfaces-scripted.nix +++ b/nixos/modules/tasks/network-interfaces-scripted.nix @@ -44,14 +44,16 @@ let 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); - }; + bondWarnings = { + warnings.networking.bonds.deprecatedOptions = lib.mapAttrs (bondName: bond: + lib.mergeAttrsList (map (optName: { + ${optName} = { + condition = (cfg.bonds.${bondName}.${optName} or null) != null; + message = "${bondName}.${optName} is deprecated, use ${bondName}.driverOptions"; + }; + }) bondDeprecation.deprecated) + ) cfg.bonds; + }; normalConfig = { systemd.network.links = let diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix index 7e3e24c727b20..d8bf1501c5152 100644 --- a/nixos/modules/tasks/network-interfaces.nix +++ b/nixos/modules/tasks/network-interfaces.nix @@ -19,8 +19,6 @@ let ++ 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 != { }; @@ -1338,42 +1336,54 @@ in ###### implementation config = { + warnings.networking.interfaces = lib.mapAttrs (_: i: i.warnings) cfg.interfaces; + warnings.networking.systemdNetworkdDhcpdClash = { + condition = config.systemd.network.enable && cfg.useDHCP && !cfg.useNetworkd; + message = '' + The combination of `systemd.network.enable = true`, `networking.useDHCP = true` and `networking.useNetworkd = false` + can cause both networkd and dhcpcd to manage the same interfaces. This can lead to loss of networking. + It is recommended you choose only one of networkd (by also enabling `networking.useNetworkd`) or scripting + (by disabling `systemd.network.enable`) + ''; + }; - warnings = (concatMap (i: i.warnings) interfaces) ++ (lib.optional - (config.systemd.network.enable && cfg.useDHCP && !cfg.useNetworkd) '' - The combination of `systemd.network.enable = true`, `networking.useDHCP = true` and `networking.useNetworkd = false` can cause both networkd and dhcpcd to manage the same interfaces. This can lead to loss of networking. It is recommended you choose only one of networkd (by also enabling `networking.useNetworkd`) or scripting (by disabling `systemd.network.enable`) - ''); - - assertions = - (forEach interfaces (i: { + assertions.networking.interfaces = lib.flip lib.mapAttrs cfg.interfaces (iName: iCfg: { + nameLessThan16Chars = { # 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; + assertion = stringLength iName < 16; message = '' - The name of networking.interfaces."${i.name}" is too long, it needs to be less than 16 characters. + The name of networking.interfaces."${iName}" is too long, it needs to be less than 16 characters. ''; - })) ++ (forEach slaveIfs (i: { - assertion = i.ipv4.addresses == [ ] && i.ipv6.addresses == [ ]; + }; + + slaveInterfacesMustNotHaveIpAddresses = { + assertion = elem iName slaves -> (iCfg.ipv4.addresses == [ ] && iCfg.ipv6.addresses == [ ]); message = '' - The networking.interfaces."${i.name}" must not have any defined ips when it is a slave. + The networking.interfaces."${iName}" must not have any defined ips when it is a slave. ''; - })) ++ (forEach interfaces (i: { - assertion = i.tempAddress != "disabled" -> cfg.enableIPv6; + }; + + tempAddressOnlyForIpv6 = { + assertion = iCfg.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; + }; + + noMacAddressForTunDevices = { + assertion = (iCfg.virtual && iCfg.virtualType == "tun") -> iCfg.macAddress == null; message = '' - Setting a MAC Address for tun device ${i.name} isn't supported. + Setting a MAC Address for tun device ${iName} isn't supported. ''; - })) ++ [ - { - assertion = cfg.hostId == null || (stringLength cfg.hostId == 8 && isHexString cfg.hostId); - message = "Invalid value given to the networking.hostId option."; - } - ]; + }; + }); + + assertions.networking.validHostId = { + 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"