Make a function available to other parts of the config

Dear all,

I’m learning Nix and my config gets better every day. I use it to manage remote machines with GitHub - serokell/deploy-rs: A simple multi-profile Nix-flake deploy tool.. The machines share some config in JSON files, which is more or less the same. So I made some functions that can take one or more parameters and generate the config files. In a machine config I now have:

{ config, ... }:
let
  signalKSelf = "vessels.urn:mrn:imo:mmsi:244770688";
  tools = import
    ../../../../modules/tools; # TODO: must be a better way to import the functions
in {
  imports = [ ./postgresql.nix ];

  sops.secrets = {
    mosquittoPassword = { };
    postgresqlGoskPassword = { };
  };

  services.gosk = {
    enable = true;
    processors = rec {
      # connectors
      connectAis = {
        args = "connect";
        config = {
          name = "AIS";
          protocol = "nmea0183";
          url = "tcp://127.0.0.1:10110";
        };
        after = [ "ais-catcher.service" ];
      };
      connectAmpero = {
        args = "connect";
        config = {
          name = "Ampero modules";
          protocol = "modbus";
          url = "rtu:///dev/ttyUSBCom2";
          baudRate = 19200;
          dataBits = 8;
          stopBits = 1;
          parity = "N";
          registerGroups = tools.gosk.registerGroupsForAmpero8AI 1 "5s";
        };
      };
      connectAutoPilot = {
        args = "connect";
        config = {
          name = "Auto Pilot";
          protocol = "nmea0183";
          url = "file:///dev/ttyUSBCom3";
          baudRate = 4800;
          dataBits = 8;
          stopBits = 1;
          parity = "N";
        };
      };
      connectShaftPowerMeter = {
        args = "connect";
        config = {
          name = "Shaft Power Meter";
          protocol = "canbus";
          url = "sock://slcan0";
        };
      };
      connectEmuFwd = {
        args = "connect";
        config = {
          name = "EMU modules";
          protocol = "modbus";
          url = "tcp://192.168.5.1:502";
          registerGroups = tools.gosk.registerGroupsForEMU 1 "5s";
        };
      };
      connectEmuAft = {
        args = "connect";
        config = {
          name = "EMU modules";
          protocol = "modbus";
          url = "tcp://192.168.4.1:502";
          registerGroups = tools.gosk.registerGroupsForEMU 1 "5s";
        };
      };
      connectSygo = {
        args = "connect";
        config = {
          name = "Sygo";
          protocol = "csv";
          url = "file:///dev/ttyUSBCom1";
          baudRate = 19200;
          dataBits = 8;
          stopBits = 1;
          parity = "N";
        };
      };

      # mappers
      mapAis = {
        args = "map";
        subscribeTo = [ connectAis ];
        config = {
          context = "${signalKSelf}";
          protocol = "nmea0183";
        };
      };
      mapAmpero = {
        args = "map";
        subscribeTo = [ connectAmpero ];
        config = {
          context = "${signalKSelf}";
          protocol = "modbus";
          mappings = [
            {
              slave = 1;
              functionCode = 4;
              address = 52;
              numberOfCoilsOrRegisters = 1;
              expression = "deltas[52] / 500000.0 / (timedeltas[52] / 1000.0)";
              path = "propulsion.mainEngine.fuel.rate.supply";
            }
            {
              slave = 1;
              functionCode = 4;
              address = 54;
              numberOfCoilsOrRegisters = 1;
              expression = "deltas[54] / 500000.0 / (timedeltas[54] / 1000.0)";
              path = "propulsion.mainEngine.fuel.rate.return";
            }
          ];
        };
      };
      mapAutoPilot = {
        args = "map";
        subscribeTo = [ connectAutoPilot ];
        config = {
          context = "${signalKSelf}";
          protocol = "nmea0183";
        };
      };
      mapShaftPowerMeter = {
        args = "map";
        subscribeTo = [ connectShaftPowerMeter ];
        config = {
          context = "${signalKSelf}";
          protocol = "canbus";
          dbcFile = ./TelMA_1_shaft.dbc;
          mappings = [
            {
              name = "Temperature";
              origin = "TelMA_Data";
              expression = "value + 273.15";
              path = "environment.inside.engineRoom.temperature";
            }
            {
              name = "RPM";
              origin = "TelMA_Data";
              expression = "value / 60";
              path = "propulsion.mainEngine.drive.revolutions";
            }
            {
              name = "Torque_Sum_Raw_Data";
              origin = "Raw_Data";
              expression = "value";
              path = "propulsion.mainEngine.drive.torque";
            }
          ];
        };
      };
      mapEmuAft = {
        args = "map";
        subscribeTo = [ connectEmuAft ];
        config = {
          context = "${signalKSelf}";
          protocol = "modbus";
          protocolOptions = { skipFaultDetection = "true"; };
          mappings = tools.gosk.mappingsForEMU 1 "generatorAft";
        };
      };
      mapEmuFwd = {
        args = "map";
        subscribeTo = [ connectEmuFwd ];
        config = {
          context = "${signalKSelf}";
          protocol = "modbus";
          protocolOptions = { skipFaultDetection = "true"; };
          mappings = tools.gosk.mappingsForEMU 1 "generatorFwd";
        };
      };
      mapSygo = {
        args = "map";
        subscribeTo = [ connectSygo ];
        config = {
          context = "${signalKSelf}";
          protocol = "csv";
          separator = ",";
          mappings = tools.gosk.mappingsForSYGO ++ [
            {
              beginsWith = "B118,";
              path = "tanks.fuel.portAft.currentVolume";
              expression =
                "heightToVolume(floatValues[0] / 100.0, sensorheight, heights, volumes)";
            }
            {
              beginsWith = "B119,";
              path = "tanks.fuel.starboardAft.currentVolume";
              expression =
                "heightToVolume(floatValues[0] / 100.0, sensorheight, heights, volumes)";
            }
          ];
        };
      };

      # proxies
      proxyRaw = {
        args = "proxy";
        subscribeTo = [
          connectAis
          connectAmpero
          connectAutoPilot
          connectEmuAft
          connectEmuFwd
          connectShaftPowerMeter
          connectSygo
        ];
      };
      proxyMapped = {
        args = "proxy";
        subscribeTo = [
          mapAis
          mapAmpero
          mapAutoPilot
          mapEmuAft
          mapEmuFwd
          mapShaftPowerMeter
          mapSygo
        ];
      };

      # database writers
      writeDatabaseRaw = {
        args = "write database mapped";
        subscribeTo = [ proxyRaw ];
        config = {
          url =
            "postgres://gosk:${config.sops.placeholder.postgresqlGoskPassword}@127.0.0.1:5432/gosk?sslmode=disable&application_name=gosk_writer_postgresql";
          batch_flush_length = 100;
          batch_flush_interval = "1m";
          buffer_size = 100;
          number_of_workers = 1;
          timeout = "10s";
        };
        # after = [ "container@postgresql-15-test.service" ];
      };
      writeDatabaseMapped = {
        args = "write database mapped";
        subscribeTo = [ proxyMapped ];
        config = {
          url =
            "postgres://gosk:${config.sops.placeholder.postgresqlGoskPassword}@127.0.0.1:5432/gosk?sslmode=disable&application_name=gosk_writer_postgresql";
          batch_flush_length = 100;
          batch_flush_interval = "1m";
          buffer_size = 100;
          number_of_workers = 1;
          timeout = "10s";
        };
        # after = [ "container@postgresql-15-test.service" ];
      };
    };
  };
}

I don’t like the tools = import ... line in this configuration. I need to repeat that for each host. How can I make the functions I created available like the function in e.g. lib? The flake I’m using right now is:

{
  # inspired by https://github.com/Misterio77/nix-config

  description = "Sustainable Motion servers and edge computers";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
    nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable";

    systems.url = "github:nix-systems/default-linux";

    disko.url = "github:nix-community/disko";
    disko.inputs.nixpkgs.follows = "nixpkgs";

    deploy-rs.url = "github:serokell/deploy-rs";
    deploy-rs.inputs.nixpkgs.follows = "nixpkgs";

    crowdsec.url = "git+https://codeberg.org/kampka/nix-flake-crowdsec.git";
    crowdsec.inputs.nixpkgs.follows = "nixpkgs";

    sops-nix.url = "github:Mic92/sops-nix";
    sops-nix.inputs.nixpkgs.follows = "nixpkgs";

    home-manager.url = "github:nix-community/home-manager/release-24.11";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";

    nixos-facter-modules.url = "github:numtide/nixos-facter-modules";

    programsdb.url = "github:wamserma/flake-programs-sqlite";
    programsdb.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = { self, nixpkgs, deploy-rs, home-manager, nixos-facter-modules, ...
    }@inputs:
    let
      inherit (self) outputs;
      inherit (nixpkgs) lib;

      users = [ "munnik" "smit" ];
      systems = {
        "hetzner-otap01" = {
          inherit users;
          hostname = "hetzner-otap01.sustainablemotion.io";
        };
        "hetzner-prod01" = {
          inherit users;
          hostname = "hetzner-prod01.sustainablemotion.io";
        };
        "node0001" = {
          inherit users;
          hostname = "192.168.122.53";
        };
        "node0002" = {
          inherit users;
          hostname = "192.168.122.159";
        };
      };
    in {
      inherit lib;
      nixosModules = import ./modules/nixos;
      overlays = import ./overlays { inherit inputs outputs; };

      formatter.x86_64-linux =
        nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;

      homeConfigurations = builtins.listToAttrs (map (userName:
        lib.nameValuePair "${userName}"
        (home-manager.lib.homeManagerConfiguration {
          pkgs = import nixpkgs { system = "x86_64-linux"; };
          modules = [ ./home/common ./home/${userName} ];
        })) users);

      nixosConfigurations = builtins.mapAttrs (systemName: _:
        lib.nixosSystem {
          system = "x86_64-linux";
          modules = [
            ./hosts/${systemName}
            nixos-facter-modules.nixosModules.facter
            { config.facter.reportPath = ./hosts/${systemName}/facter.json; }
          ];
          specialArgs = { inherit inputs outputs; };
        }) systems;

      deploy = {
        nodes = builtins.mapAttrs (systemName: systemConfig: {
          inherit (systemConfig) hostname;
          profiles = {
            system = {
              user = "root";
              path = deploy-rs.lib.x86_64-linux.activate.nixos
                self.nixosConfigurations."${systemName}";
            };
          } // builtins.listToAttrs (map (userName:
            lib.nameValuePair "${userName}" {
              user = "${userName}";
              path = deploy-rs.lib.x86_64-linux.activate.home-manager
                self.homeConfigurations."${userName}";
            }) systemConfig.users);
        }) systems;
      };

      # This is highly advised, and will prevent many possible mistakes
      checks =
        builtins.mapAttrs (_: deployLib: deployLib.deployChecks self.deploy)
        deploy-rs.lib;
    };
}

Pass tools = import ./modules/tools; to nixosConfigurationsspecialArgs, it will be available as a parameter ({ lib, tools, ... }:).

Alternatively, in a module define a fake NixOS option, like myfunction, and set it to your function. Then you can access it from other modules using config.myfunction.