photoncloud-monorepo/nix/modules/chainfire.nix

218 lines
6.2 KiB
Nix

{ config, lib, pkgs, ... }:
let
cfg = config.services.chainfire;
tomlFormat = pkgs.formats.toml { };
stripLeadingZeros = digits:
if digits == "" then ""
else if lib.hasPrefix "0" digits then stripLeadingZeros (lib.removePrefix "0" digits)
else digits;
numericIdString = value:
let
captures = builtins.match ".*?([0-9]+)$" value;
digits =
if captures == null
then throw "services.chainfire.nodeId must end with digits (got '${value}')"
else builtins.elemAt captures 0;
normalized = stripLeadingZeros digits;
in
if normalized == "" then "0" else normalized;
numericId = value: builtins.fromJSON (numericIdString value);
hostFromAddr = addr:
let captures = builtins.match "(.+):[0-9]+" addr;
in if captures == null then null else builtins.elemAt captures 0;
apiAddrArg =
if cfg.apiAddr != null
then cfg.apiAddr
else "0.0.0.0:${toString cfg.port}";
raftAddrArg =
if cfg.raftAddr != null
then cfg.raftAddr
else "0.0.0.0:${toString cfg.raftPort}";
gossipAddrArg =
if cfg.gossipAddr != null
then cfg.gossipAddr
else
let host = hostFromAddr apiAddrArg;
in if host != null then "${host}:${toString cfg.gossipPort}" else "0.0.0.0:${toString cfg.gossipPort}";
initialMembers = map
(peer:
let
parts = lib.splitString "=" peer;
rawId =
if builtins.length parts == 2
then builtins.elemAt parts 0
else throw "services.chainfire.initialPeers entries must be 'nodeId=host:port' (got '${peer}')";
raftAddr = builtins.elemAt parts 1;
in {
id = numericId rawId;
raft_addr = raftAddr;
})
cfg.initialPeers;
chainfireConfigFile = tomlFormat.generate "chainfire.toml" {
node = {
id = numericId cfg.nodeId;
name = cfg.nodeId;
role = cfg.role;
};
storage = {
data_dir = toString cfg.dataDir;
};
network = {
api_addr = apiAddrArg;
http_addr = "0.0.0.0:${toString cfg.httpPort}";
raft_addr = raftAddrArg;
gossip_addr = gossipAddrArg;
};
cluster = {
id = cfg.clusterId;
initial_members = initialMembers;
bootstrap = cfg.bootstrap;
};
raft = {
role = cfg.raftRole;
};
};
in
{
options.services.chainfire = {
enable = lib.mkEnableOption "chainfire cluster coordination service";
nodeId = lib.mkOption {
type = lib.types.str;
default = config.networking.hostName;
description = "Unique node identifier for the Raft cluster";
};
port = lib.mkOption {
type = lib.types.port;
default = 2379;
description = "Port for chainfire API";
};
raftPort = lib.mkOption {
type = lib.types.port;
default = 2380;
description = "Port for chainfire Raft protocol";
};
raftAddr = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Full address for Raft (host:port). If null, uses 0.0.0.0:raftPort";
};
apiAddr = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Full address for API (host:port). If null, uses 0.0.0.0:port";
};
initialPeers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Initial Raft peers for cluster bootstrap (format: nodeId=addr:port)";
example = [ "node01=10.0.0.1:2380" "node02=10.0.0.2:2380" ];
};
gossipPort = lib.mkOption {
type = lib.types.port;
default = 2381;
description = "Port for chainfire gossip protocol";
};
httpPort = lib.mkOption {
type = lib.types.port;
default = 8081;
description = "Port for chainfire HTTP/admin API (used for cluster join)";
};
gossipAddr = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Full gossip advertise/listen address (host:port). If null, derives from apiAddr host and gossipPort.";
};
clusterId = lib.mkOption {
type = lib.types.int;
default = 1;
description = "Cluster identifier written into the ChainFire config.";
};
bootstrap = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether this node boots using the configured initial_members set.";
};
role = lib.mkOption {
type = lib.types.enum [ "control_plane" "worker" ];
default = "control_plane";
description = "Logical node role advertised through ChainFire gossip metadata.";
};
raftRole = lib.mkOption {
type = lib.types.enum [ "voter" "learner" "none" ];
default = "voter";
description = "Raft participation role written into the ChainFire config.";
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/chainfire";
description = "Data directory for chainfire";
};
settings = lib.mkOption {
type = lib.types.attrs;
default = {};
description = "Additional configuration settings";
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.chainfire-server or (throw "chainfire-server package not found");
description = "Package to use for chainfire";
};
};
config = lib.mkIf cfg.enable {
# Create system user
users.users.chainfire = {
isSystemUser = true;
group = "chainfire";
description = "Chainfire service user";
home = cfg.dataDir;
};
users.groups.chainfire = {};
# Create systemd service
systemd.services.chainfire = {
description = "Chainfire Distributed Cluster Coordination Service";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "simple";
User = "chainfire";
Group = "chainfire";
Restart = "on-failure";
RestartSec = "10s";
# State directory management
StateDirectory = "chainfire";
StateDirectoryMode = "0750";
# Security hardening
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ cfg.dataDir ];
ExecStart = "${cfg.package}/bin/chainfire --config ${chainfireConfigFile}";
};
};
};
}