218 lines
6.2 KiB
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}";
|
|
};
|
|
};
|
|
};
|
|
}
|