concatMapAttrs not working as I thought it would work

I’m trying to write a module which I can use in my nixos flake for all systems.

Running:
nix eval ".#nixosConfigurations.mysystem.config.systemd.network.networks"
gives only the enp2, and not both networks. When I comment out enp2, I do see enp1.
I think I’m improperly using concatMapAttrs , but I don’t know what do change.

I have something like this in the flake for a particular system:

        ncfg.networking.wired-interfaces = {
          "enp1" = {
            assignment = "dynamic";
            metric = 30;
           };
          "enp2" = {
            assignment = "static";
            metric = 100;
            requiredForOnline = false;
            description = "Air-gapped  network: 192.168.1.0/24";
            ipv4.addr = "192.168.1.10/24";
            ipv4.gateway = "192.168.1.1";
            ipv6.addr = "fd00:192:168:1::10/64";
           };
         };

The networking module

{
  pkgs,
  config,
  lib,
  ...
}:
let
  cfg = config.ncfg.networking;
in
{
  options.ncfg.networking = {
    wired-interfaces = lib.mkOption {
      description = ''
        wired interfaces
      '';
      default = { };
      type = lib.types.attrsOf (
        lib.types.submodule (
          { name, ... }:
          {
            options = {
              assignment = lib.mkOption {
                type = lib.types.nullOr (
                  lib.types.enum [
                    "dynamic"
                    "static"
                  ]
                );
                default = "dynamic";
                description = ''
                  Dynamic or Static assignment of the interface: ${name}
                '';
              };
              metric = lib.mkOption {
                type = lib.types.ints.between 0 4294967295;
                default = 1000;
                description = "The unit name";
              };
              requiredForOnline = lib.mkOption {
                type = lib.types.bool;
                default = true;
                description = "interface not required for online";
              };
              description = lib.mkOption {
                type = lib.types.nullOr lib.types.str;
                description = "The description";
              };
              ipv4 = {
                addr = lib.mkOption {
                  type = lib.types.nullOr lib.types.str;
                  default = null;
                  description = "ipv4 address including subnet mask";
                  example = "192.0.2.100/24";
                };
                gateway = lib.mkOption {
                  type = lib.types.nullOr lib.types.str;
                  default = null;
                  description = "ipv4 gateway";
                };
              };
              ipv6 = {
                addr = lib.mkOption {
                  type = lib.types.nullOr lib.types.str;
                  default = null;
                  description = "ipv6 address including subnet mask";
                  example = "2001:DB8::2/64";
                };
                gateway = lib.mkOption {
                  type = lib.types.nullOr lib.types.str;
                  default = null;
                  description = "ipv6 gateway";
                };
              };
            };
          }
        )
      );
    };
  };

  config = lib.mkIf (cfg.wired-interfaces != { }) (
    lib.mkMerge [
      {
        systemd.network.enable = true;

        # # Don't block boot/nixos-rebuild on all interfaces, wired usually unplugged
        systemd.network.wait-online.enable = false;
        systemd.network.wait-online.anyInterface = true;
      }

      {
        systemd.network = lib.concatMapAttrs (interface: attrs: {
          networks."${toString attrs.metric}-${interface}" = {
            matchConfig = {
              Name = "${interface}"; # Match the interface by name
            };

            networkConfig = lib.mkIf (attrs.assignment == "dynamic") {
              # start a DHCP Client for IPv4 Addressing/Routing
              DHCP = "ipv4";
              # accept Router Advertisements for Stateless IPv6 Autoconfiguraton (SLAAC)
              IPv6AcceptRA = true;
            };

            dhcpV4Config.RouteMetric = lib.mkIf (attrs.assignment == "dynamic") attrs.metric;
            dhcpV6Config.RouteMetric = lib.mkIf (attrs.assignment == "dynamic") attrs.metric;
            ### dhcpV4Config.Metric = attrs.metric;
            ### dhcpV6Config.Metric = attrs.metric;

            address =
              # configure addresses including subnet mask
              lib.optionals (attrs.assignment == "static" && attrs.ipv6.addr != null) [
                attrs.ipv6.addr
              ]
              ++ lib.optionals (attrs.assignment == "static" && attrs.ipv4.addr != null) [
                attrs.ipv4.addr
              ];
            routes =
              # create default routes for both IPv6 and IPv4
              lib.optionals (attrs.assignment == "static" && attrs.ipv6.gateway != null) [
                { Gateway = attrs.ipv6.gateway; }
              ]
              ++ lib.optionals (attrs.assignment == "static" && attrs.ipv4.gateway != null) [
                { Gateway = attrs.ipv4.gateway; }
              ];

            # Make the routes on this interface a dependency for network-online.target
            linkConfig.RequiredForOnline = lib.mkIf attrs.requiredForOnline "routable";
          };
        }) cfg.wired-interfaces;
      }
    ]
  );
}

concatMapAttrs does a shallow merge, not a deep one. You’re merging multiple attrsets that all have a single networks attribute, and that means that the last one wins, for exactly the same reason as this:

nix-repl> :p { foo.bar = 1; } // { foo.baz = 2; }
{
  foo = { baz = 2; };
}

Your case has an easy solution: replace systemd.network = ... { networks."${toString attrs.metric}-${interface}" = ... with systemd.network.networks = ... { "${toString attrs.metric}-${interface}" = ....

A more complicated case could require a nested lib.mkMerge.

2 Likes