1 { config, lib, pkgs, ... }:
7 cfg = config.services.keepalived;
9 keepalivedConf = pkgs.writeText "keepalived.conf" ''
11 ${optionalString cfg.enableScriptSecurity "enable_script_security"}
13 ${cfg.extraGlobalDefs}
21 snmpGlobalDefs = with cfg.snmp; optionalString enable (
22 optionalString (socket != null) "snmp_socket ${socket}\n"
23 + optionalString enableKeepalived "enable_snmp_keepalived\n"
24 + optionalString enableChecker "enable_snmp_checker\n"
25 + optionalString enableRfc "enable_snmp_rfc\n"
26 + optionalString enableRfcV2 "enable_snmp_rfcv2\n"
27 + optionalString enableRfcV3 "enable_snmp_rfcv3\n"
28 + optionalString enableTraps "enable_traps"
31 vrrpScriptStr = concatStringsSep "\n" (map (s:
33 vrrp_script ${s.name} {
35 interval ${toString s.interval}
36 fall ${toString s.fall}
37 rise ${toString s.rise}
38 timeout ${toString s.timeout}
39 weight ${toString s.weight}
40 user ${s.user} ${optionalString (s.group != null) s.group}
47 vrrpInstancesStr = concatStringsSep "\n" (map (i:
49 vrrp_instance ${i.name} {
50 interface ${i.interface}
52 virtual_router_id ${toString i.virtualRouterId}
53 priority ${toString i.priority}
54 ${optionalString i.noPreempt "nopreempt"}
56 ${optionalString i.useVmac (
57 "use_vmac" + optionalString (i.vmacInterface != null) " ${i.vmacInterface}"
59 ${optionalString i.vmacXmitBase "vmac_xmit_base"}
61 ${optionalString (i.unicastSrcIp != null) "unicast_src_ip ${i.unicastSrcIp}"}
63 ${concatStringsSep "\n" i.unicastPeers}
67 ${concatMapStringsSep "\n" virtualIpLine i.virtualIps}
70 ${optionalString (builtins.length i.trackScripts > 0) ''
72 ${concatStringsSep "\n" i.trackScripts}
76 ${optionalString (builtins.length i.trackInterfaces > 0) ''
78 ${concatStringsSep "\n" i.trackInterfaces}
89 + optionalString (notNullOrEmpty ip.brd) " brd ${ip.brd}"
90 + optionalString (notNullOrEmpty ip.dev) " dev ${ip.dev}"
91 + optionalString (notNullOrEmpty ip.scope) " scope ${ip.scope}"
92 + optionalString (notNullOrEmpty ip.label) " label ${ip.label}"
95 notNullOrEmpty = s: !(s == null || s == "");
97 vrrpScripts = mapAttrsToList (name: config:
103 vrrpInstances = mapAttrsToList (iName: iConfig:
109 vrrpInstanceAssertions = i: [
110 { assertion = i.interface != "";
111 message = "services.keepalived.vrrpInstances.${i.name}.interface option cannot be empty.";
113 { assertion = i.virtualRouterId >= 0 && i.virtualRouterId <= 255;
114 message = "services.keepalived.vrrpInstances.${i.name}.virtualRouterId must be an integer between 0..255.";
116 { assertion = i.priority >= 0 && i.priority <= 255;
117 message = "services.keepalived.vrrpInstances.${i.name}.priority must be an integer between 0..255.";
119 { assertion = i.vmacInterface == null || i.useVmac;
120 message = "services.keepalived.vrrpInstances.${i.name}.vmacInterface has no effect when services.keepalived.vrrpInstances.${i.name}.useVmac is not set.";
122 { assertion = !i.vmacXmitBase || i.useVmac;
123 message = "services.keepalived.vrrpInstances.${i.name}.vmacXmitBase has no effect when services.keepalived.vrrpInstances.${i.name}.useVmac is not set.";
125 ] ++ flatten (map (virtualIpAssertions i.name) i.virtualIps)
126 ++ flatten (map (vrrpScriptAssertion i.name) i.trackScripts);
128 virtualIpAssertions = vrrpName: ip: [
129 { assertion = ip.addr != "";
130 message = "The 'addr' option for an services.keepalived.vrrpInstances.${vrrpName}.virtualIps entry cannot be empty.";
134 vrrpScriptAssertion = vrrpName: scriptName: {
135 assertion = builtins.hasAttr scriptName cfg.vrrpScripts;
136 message = "services.keepalived.vrrpInstances.${vrrpName} trackscript ${scriptName} is not defined in services.keepalived.vrrpScripts.";
139 pidFile = "/run/keepalived.pid";
145 services.keepalived = {
150 description = lib.mdDoc ''
151 Whether to enable Keepalived.
155 enableScriptSecurity = mkOption {
158 description = lib.mdDoc ''
159 Don't run scripts configured to be run as root if any part of the path is writable by a non-root user.
168 description = lib.mdDoc ''
169 Whether to enable the builtin AgentX subagent.
174 type = types.nullOr types.str;
176 description = lib.mdDoc ''
177 Socket to use for connecting to SNMP master agent. If this value is
178 set to null, keepalived's default will be used, which is
179 unix:/var/agentx/master, unless using a network namespace, when the
180 default is udp:localhost:705.
184 enableKeepalived = mkOption {
187 description = lib.mdDoc ''
188 Enable SNMP handling of vrrp element of KEEPALIVED MIB.
192 enableChecker = mkOption {
195 description = lib.mdDoc ''
196 Enable SNMP handling of checker element of KEEPALIVED MIB.
200 enableRfc = mkOption {
203 description = lib.mdDoc ''
204 Enable SNMP handling of RFC2787 and RFC6527 VRRP MIBs.
208 enableRfcV2 = mkOption {
211 description = lib.mdDoc ''
212 Enable SNMP handling of RFC2787 VRRP MIB.
216 enableRfcV3 = mkOption {
219 description = lib.mdDoc ''
220 Enable SNMP handling of RFC6527 VRRP MIB.
224 enableTraps = mkOption {
227 description = lib.mdDoc ''
234 vrrpScripts = mkOption {
235 type = types.attrsOf (types.submodule (import ./vrrp-script-options.nix {
239 description = lib.mdDoc "Declarative vrrp script config";
242 vrrpInstances = mkOption {
243 type = types.attrsOf (types.submodule (import ./vrrp-instance-options.nix {
247 description = lib.mdDoc "Declarative vhost config";
250 extraGlobalDefs = mkOption {
253 description = lib.mdDoc ''
254 Extra lines to be added verbatim to the 'global_defs' block of the
259 extraConfig = mkOption {
262 description = lib.mdDoc ''
263 Extra lines to be added verbatim to the configuration file.
267 secretFile = mkOption {
268 type = types.nullOr types.path;
270 example = "/run/keys/keepalived.env";
271 description = lib.mdDoc ''
272 Environment variables from this file will be interpolated into the
273 final config file using envsubst with this syntax: `$ENVIRONMENT`
275 The file should contain lines formatted as `SECRET_VAR=SECRET_VALUE`.
276 This is useful to avoid putting secrets into the nix store.
283 config = mkIf cfg.enable {
285 assertions = flatten (map vrrpInstanceAssertions vrrpInstances);
287 systemd.timers.keepalived-boot-delay = {
288 description = "Keepalive Daemon delay to avoid instant transition to MASTER state";
289 after = [ "network.target" "network-online.target" "syslog.target" ];
290 requires = [ "network-online.target" ];
291 wantedBy = [ "multi-user.target" ];
294 Unit = "keepalived.service";
298 systemd.services.keepalived = let
299 finalConfigFile = if cfg.secretFile == null then keepalivedConf else "/run/keepalived/keepalived.conf";
301 description = "Keepalive Daemon (LVS and VRRP)";
302 after = [ "network.target" "network-online.target" "syslog.target" ];
303 wants = [ "network-online.target" ];
307 KillMode = "process";
308 RuntimeDirectory = "keepalived";
309 EnvironmentFile = lib.optional (cfg.secretFile != null) cfg.secretFile;
310 ExecStartPre = lib.optional (cfg.secretFile != null)
311 (pkgs.writeShellScript "keepalived-pre-start" ''
313 ${pkgs.envsubst}/bin/envsubst -i "${keepalivedConf}" > ${finalConfigFile}
315 ExecStart = "${pkgs.keepalived}/sbin/keepalived"
316 + " -f ${finalConfigFile}"
318 + optionalString cfg.snmp.enable " --snmp";
319 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";