munnik
February 28, 2025, 11:03am
1
In my configuration, I want to change the fixed publishTo
values to a generated value. So that:
services.gosk = {
enable = true;
processors = rec {
connectAis = {
command = "connect";
publishTo = 6001;
configFile = config.sops.templates.connectAis.path;
};
mapAis = {
command = "map";
publishTo = 6002;
subscribeTo = [ connectAis.publishTo ];
configFile = config.sops.templates.connectAis.path;
};
};
};
Becomes something like this:
services.gosk = {
enable = true;
processors = rec {
connectAis = {
command = "connect";
publishTo = portNumberFunction;
configFile = config.sops.templates.connectAis.path;
};
mapAis = {
command = "map";
publishTo = portNumberFunction;
subscribeTo = [ connectAis.publishTo ];
configFile = config.sops.templates.connectAis.path;
};
};
};
Where portNumberFunction
returns a port number that changes each call. This not a pure function, so it should take an argument, I guess. Can I somehow use the position in the attribute set to achieve this?
1 Like
max
February 28, 2025, 3:39pm
2
I have a thing for this that hashes a name of your choosing and truncates the numerical value of the hash so it fits into a port range (e.g. 40000-50000). It’s a random-looking but deterministic way of generating port numbers. I use this extensively to hook up backend services to my load balancers.
munnik
February 28, 2025, 7:16pm
3
Thanks! That looks like what I need. I will try to figure out how to incorporate that in my code. I’m pretty new to Nix and still struggling with the syntax.
munnik
February 28, 2025, 10:09pm
4
I think I’m getting there, but still I’m missing some things. My complete config for the service is:
{ config, lib, pkgs, utils, ... }:
with lib;
let
cfg = config.services.gosk;
# Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
escapeUnitName = name:
lib.concatMapStrings (s: if lib.isList s then "-" else s)
(builtins.split "[^a-zA-Z0-9_.\\-]+" name);
portHash = flip pipe [
(builtins.hashString "md5")
(builtins.substring 0 7)
(hash: (fromTOML "v=0x${hash}").v)
(flip mod cfg.reservedPorts.amount)
(add cfg.reservedPorts.start)
];
in {
options.services.gosk = {
enable = mkEnableOption "Go Signal K service";
package = mkPackageOption pkgs "gosk" { };
reservedPorts = {
amount = mkOption {
type = with types; int;
default = 10000;
description = "Amount of ports to reserve at most.";
};
start = mkOption {
type = with types; port;
default = 30000;
description = "Starting point for reserved ports.";
};
};
processors = mkOption {
type = types.attrsOf (types.submodule {
options = {
args = mkOption {
type = types.separatedString " ";
default = "";
example = "write database mapped";
description = "Arguments to use for this gosk service.";
};
subscribeTo = mkOption {
type = with types; listOf port;
default = [ ];
example = [ 6000 6001 ];
description = "Ports to subscribe to.";
};
publish = mkOption {
type = with types; bool;
default = true;
example = false;
description = "Determines if the services publishes its output.";
};
publishTo = mkOption {
type = with types; port;
default = portHash "__I want to use the name and args values here__";
example = 6000;
description =
"Port to publish to, if not defined a 'random' port will be chosen.";
};
configFile = mkOption {
type = with types; nullOr path;
default = null;
example = ./gosk.yaml;
description =
"Configuration file in YAML format for this gosk service.";
};
after = mkOption {
type = types.listOf utils.systemdUtils.lib.unitNameType;
default = [ ];
example = [ "postgresql" ];
description = "Systemd services to wait for";
};
extraArgs = mkOption {
type = with types; listOf str;
default = [ ];
example = ''
[ "--pmport" "127.0.0.1:9203" ]
'';
description = "Extra arguments for the gosk processor.";
};
};
});
default = { };
example = literalExpression ''
{
mapAis = {
args = "map";
subscribeTo = [ connectAis.publishTo ];
configFile = ./gosk.yaml;
};
}
'';
description = "gosk service to run.";
};
};
config = mkIf cfg.enable {
systemd.services = mapAttrs' (name: c:
nameValuePair "gosk-${escapeUnitName name}" (mkMerge [{
inherit (c) after;
description = "gosk ${name}";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = toString ([ "${pkgs.gosk}/bin/gosk" ]
++ lib.optional (c.args != "") c.args ++ [
(concatMapStringsSep " "
(port: "-s tcp://127.0.0.1:${toString port}") c.subscribeTo)
] ++ lib.optional (isInt c.publishTo)
"-p tcp://127.0.0.1:${toString c.publishTo}"
++ lib.optional (c.configFile != null)
"--config \${CREDENTIALS_DIRECTORY}/gosk.yaml" ++ c.extraArgs);
Restart = "always";
LoadCredential =
if c.configFile != null then "gosk.yaml:${c.configFile}" else "";
DynamicUser = "yes";
};
}])) cfg.processors;
# sops.templates = mapAttrs' (name: c:
# ) cfg.processors;
};
meta.maintainers = with maintainers; [ munnik ];
}
And to enable the service I have:
{ config, ... }:
let signalKSelf = "vessels.urn:mrn:imo:mmsi:244770688";
in {
sops.secrets = {
mosquittoPassword = { };
postgresqlGoskPassword = { };
};
services.gosk = {
enable = true;
processors = rec {
connectAis = {
args = "connect";
publishTo = 7000;
configFile = config.sops.templates.connectAis.path;
};
connectAmpero = {
args = "connect";
configFile = config.sops.templates.connectAmpero.path;
};
mapAis = {
args = "map";
subscribeTo = [ connectAis.publishTo ];
configFile = config.sops.templates.connectAis.path;
};
};
};
sops.templates = {
connectAis = {
content = ''
---
name: "AIS"
protocol: "nmea0183"
url: "file:///dev/ttyUSBCom0"
baudRate: 38400
dataBits: 8
stopBits: 1
parity: "N"
'';
};
connectAmpero = {
content = ''
---
name: "Ampero modules"
protocol: "modbus"
url: "rtu:///dev/ttyUSBCom2"
baudRate: 19200
dataBits: 8
stopBits: 1
parity: "N"
registerGroups:
- slave: 1 # read counter 1 and 2
functionCode: 4
address: 52
numberOfCoilsOrRegisters: 4
pollingInterval: 5s
'';
};
mapAis = {
content = ''
---
context: "${signalKSelf}" # if the data itself doesn't provide a context then this context is used
protocol: "nmea0183"
'';
};
};
}
Two questions remaining:
If I don’t specify publishTo
in the connectAis
processor, I can’t use it in the subscribeTo
of mapAis
. The error I get is:
error: attribute 'publishTo' missing
at /nix/store/3s2s159zm791l7msk967wy1sqakaw8f1-source/hosts/node0001/services/gosk/default.nix:23:25:
22| args = "map";
23| subscribeTo = [ connectAis.publishTo ];
| ^
24| configFile = config.sops.templates.connectAis.path;
It seems you can’t access default values? For the connectAmpero
processor, the port number is successfully generated. Output of systemctl status gosk-connectAmpero.service
:
Process: 12822 ExecStart=/nix/store/fq0fgkp5f7nqh55gvkwh0xq1l2ilnsfd-gosk-0.1.103/bin/gosk connect -p tcp://127.0.0.1:34636 --config ${CREDENTIALS_DIRECTORY}/gosk.yaml (code=exited, status=1/FAILURE)
How can I use the args
and name
of the processor as arguments in the portHash
function, see the text __I want to use the name and args values here__
in the config above. I think I need to do this in the section config = mkIf cfg.enable { }
but I’m not sure how.
Any other tips and ideas regarding this config are more than welcome. I still have to learn a lot.
munnik
March 6, 2025, 7:05pm
5
I resolved all the issues with the following config, this also include numbering of the network ports.
{ config, lib, pkgs, utils, ... }:
let
# Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
escapeUnitName = name:
"gosk-" + lib.concatMapStrings (s: if lib.isList s then "-" else s)
(builtins.split "[^a-zA-Z0-9_.\\-]+" name);
processorPort = processor:
toString (config.services.gosk.reservedPorts.start
+ (lib.lists.findFirstIndex (p: p == processor) 0
(lib.mapAttrsToList (_: value: value)
config.services.gosk.processors)));
processor = {
options = {
args = lib.mkOption {
type = with lib.types; separatedString " ";
default = "";
example = "write database mapped";
description = "Arguments to use for this gosk service.";
};
extraArgs = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = ''
[ "--pmport" "127.0.0.1:9203" ]
'';
description = "Extra arguments for the gosk processor.";
};
config = lib.mkOption {
type = with lib.types; attrs;
default = { };
example = {
username = lib.literalExpression "mosquittoUsername;";
password =
lib.literalExpression "config.sops.placeholder.mosquittoPassword;";
url = "mqtt://10.24.0.1:1883";
interval = "10s";
buffer_size = 1000;
example_required_file = ./extraFiles/test.bin;
};
description = "Configuration for this gosk service.";
};
subscribeTo = lib.mkOption {
type = with lib.types; listOf (submodule processor);
default = [ ];
example = lib.literalExpression "[ connectAis ]";
description = "Processors to subscribe to.";
};
after = lib.mkOption {
type = with lib.types; listOf utils.systemdUtils.lib.unitNameType;
default = [ ];
example = [ "postgresql" ];
description = "Systemd services to wait for";
};
};
};
in {
options.services.gosk = {
enable = lib.mkEnableOption "Go Signal K service";
package = lib.mkPackageOption pkgs "gosk" { };
reservedPorts = {
amount = lib.mkOption {
type = with lib.types; int;
default = 10000;
description = "Amount of ports to reserve at most.";
};
start = lib.mkOption {
type = with lib.types; port;
default = 30000;
description = "Starting point for reserved ports.";
};
};
processors = lib.mkOption {
type = with lib.types; attrsOf (submodule processor);
default = { };
example = lib.literalExpression ''
{
mapAis = {
args = "map";
subscribeTo = [ connectAis ];
configFile = ./gosk.yaml;
};
}
'';
description = "gosk service to run.";
};
};
config = lib.mkIf config.services.gosk.enable {
systemd.services = lib.mapAttrs' (name: p:
let
hasSubscribers = lib.elem p (lib.lists.flatten
(lib.mapAttrsToList (_: p: p.subscribeTo)
config.services.gosk.processors));
in lib.nameValuePair (escapeUnitName name) {
inherit (p) after;
description = "gosk ${name}";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = toString ([ "${pkgs.gosk}/bin/gosk" ]
++ lib.optional (p.args != "") p.args ++ p.extraArgs ++ [
(lib.concatMapStringsSep " "
(processor: "-s tcp://127.0.0.1:${processorPort processor}")
p.subscribeTo)
] ++ lib.optional hasSubscribers
((processor: "-p tcp://127.0.0.1:${processorPort processor}") p)
++ lib.optional (p.config != { })
"--config \${CREDENTIALS_DIRECTORY}/config.json");
Restart = "always";
LoadCredential = if p.config != { } then
"config.json:${config.sops.templates."${escapeUnitName name}".path}"
else
"";
DynamicUser = "yes";
};
}) config.services.gosk.processors;
sops.templates = lib.mapAttrs' (name: _:
lib.nameValuePair (escapeUnitName name) {
content =
builtins.toJSON config.services.gosk.processors."${name}".config;
}) config.services.gosk.processors;
};
meta.maintainers = with lib.maintainers; [ munnik ];
}