1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 cfg = config.services.keepalived; 8 9 keepalivedConf = pkgs.writeText "keepalived.conf" '' 10 global_defs { 11 ${optionalString cfg.enableScriptSecurity "enable_script_security"} 12 ${snmpGlobalDefs} 13 ${cfg.extraGlobalDefs} 14 } 15 16 ${vrrpScriptStr} 17 ${vrrpInstancesStr} 18 ${cfg.extraConfig} 19 ''; 20 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" 29 ); 30 31 vrrpScriptStr = concatStringsSep "\n" (map (s: 32 '' 33 vrrp_script ${s.name} { 34 script "${s.script}" 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} 41 42 ${s.extraConfig} 43 } 44 '' 45 ) vrrpScripts); 46 47 vrrpInstancesStr = concatStringsSep "\n" (map (i: 48 '' 49 vrrp_instance ${i.name} { 50 interface ${i.interface} 51 state ${i.state} 52 virtual_router_id ${toString i.virtualRouterId} 53 priority ${toString i.priority} 54 ${optionalString i.noPreempt "nopreempt"} 55 56 ${optionalString i.useVmac ( 57 "use_vmac" + optionalString (i.vmacInterface != null) " ${i.vmacInterface}" 58 )} 59 ${optionalString i.vmacXmitBase "vmac_xmit_base"} 60 61 ${optionalString (i.unicastSrcIp != null) "unicast_src_ip ${i.unicastSrcIp}"} 62 unicast_peer { 63 ${concatStringsSep "\n" i.unicastPeers} 64 } 65 66 virtual_ipaddress { 67 ${concatMapStringsSep "\n" virtualIpLine i.virtualIps} 68 } 69 70 ${optionalString (builtins.length i.trackScripts > 0) '' 71 track_script { 72 ${concatStringsSep "\n" i.trackScripts} 73 } 74 ''} 75 76 ${optionalString (builtins.length i.trackInterfaces > 0) '' 77 track_interface { 78 ${concatStringsSep "\n" i.trackInterfaces} 79 } 80 ''} 81 82 ${i.extraConfig} 83 } 84 '' 85 ) vrrpInstances); 86 87 virtualIpLine = ip: ip.addr 88 + optionalString (notNullOrEmpty ip.brd) " brd ${ip.brd}" 89 + optionalString (notNullOrEmpty ip.dev) " dev ${ip.dev}" 90 + optionalString (notNullOrEmpty ip.scope) " scope ${ip.scope}" 91 + optionalString (notNullOrEmpty ip.label) " label ${ip.label}"; 92 93 notNullOrEmpty = s: !(s == null || s == ""); 94 95 vrrpScripts = mapAttrsToList (name: config: 96 { 97 inherit name; 98 } // config 99 ) cfg.vrrpScripts; 100 101 vrrpInstances = mapAttrsToList (iName: iConfig: 102 { 103 name = iName; 104 } // iConfig 105 ) cfg.vrrpInstances; 106 107 vrrpInstanceAssertions = i: [ 108 { assertion = i.interface != ""; 109 message = "services.keepalived.vrrpInstances.${i.name}.interface option cannot be empty."; 110 } 111 { assertion = i.virtualRouterId >= 0 && i.virtualRouterId <= 255; 112 message = "services.keepalived.vrrpInstances.${i.name}.virtualRouterId must be an integer between 0..255."; 113 } 114 { assertion = i.priority >= 0 && i.priority <= 255; 115 message = "services.keepalived.vrrpInstances.${i.name}.priority must be an integer between 0..255."; 116 } 117 { assertion = i.vmacInterface == null || i.useVmac; 118 message = "services.keepalived.vrrpInstances.${i.name}.vmacInterface has no effect when services.keepalived.vrrpInstances.${i.name}.useVmac is not set."; 119 } 120 { assertion = !i.vmacXmitBase || i.useVmac; 121 message = "services.keepalived.vrrpInstances.${i.name}.vmacXmitBase has no effect when services.keepalived.vrrpInstances.${i.name}.useVmac is not set."; 122 } 123 ] ++ flatten (map (virtualIpAssertions i.name) i.virtualIps) 124 ++ flatten (map (vrrpScriptAssertion i.name) i.trackScripts); 125 126 virtualIpAssertions = vrrpName: ip: [ 127 { assertion = ip.addr != ""; 128 message = "The 'addr' option for an services.keepalived.vrrpInstances.${vrrpName}.virtualIps entry cannot be empty."; 129 } 130 ]; 131 132 vrrpScriptAssertion = vrrpName: scriptName: { 133 assertion = builtins.hasAttr scriptName cfg.vrrpScripts; 134 message = "services.keepalived.vrrpInstances.${vrrpName} trackscript ${scriptName} is not defined in services.keepalived.vrrpScripts."; 135 }; 136 137 pidFile = "/run/keepalived.pid"; 138 139in 140{ 141 142 options = { 143 services.keepalived = { 144 145 enable = mkOption { 146 type = types.bool; 147 default = false; 148 description = lib.mdDoc '' 149 Whether to enable Keepalived. 150 ''; 151 }; 152 153 enableScriptSecurity = mkOption { 154 type = types.bool; 155 default = false; 156 description = lib.mdDoc '' 157 Don't run scripts configured to be run as root if any part of the path is writable by a non-root user. 158 ''; 159 }; 160 161 snmp = { 162 163 enable = mkOption { 164 type = types.bool; 165 default = false; 166 description = lib.mdDoc '' 167 Whether to enable the builtin AgentX subagent. 168 ''; 169 }; 170 171 socket = mkOption { 172 type = types.nullOr types.str; 173 default = null; 174 description = lib.mdDoc '' 175 Socket to use for connecting to SNMP master agent. If this value is 176 set to null, keepalived's default will be used, which is 177 unix:/var/agentx/master, unless using a network namespace, when the 178 default is udp:localhost:705. 179 ''; 180 }; 181 182 enableKeepalived = mkOption { 183 type = types.bool; 184 default = false; 185 description = lib.mdDoc '' 186 Enable SNMP handling of vrrp element of KEEPALIVED MIB. 187 ''; 188 }; 189 190 enableChecker = mkOption { 191 type = types.bool; 192 default = false; 193 description = lib.mdDoc '' 194 Enable SNMP handling of checker element of KEEPALIVED MIB. 195 ''; 196 }; 197 198 enableRfc = mkOption { 199 type = types.bool; 200 default = false; 201 description = lib.mdDoc '' 202 Enable SNMP handling of RFC2787 and RFC6527 VRRP MIBs. 203 ''; 204 }; 205 206 enableRfcV2 = mkOption { 207 type = types.bool; 208 default = false; 209 description = lib.mdDoc '' 210 Enable SNMP handling of RFC2787 VRRP MIB. 211 ''; 212 }; 213 214 enableRfcV3 = mkOption { 215 type = types.bool; 216 default = false; 217 description = lib.mdDoc '' 218 Enable SNMP handling of RFC6527 VRRP MIB. 219 ''; 220 }; 221 222 enableTraps = mkOption { 223 type = types.bool; 224 default = false; 225 description = lib.mdDoc '' 226 Enable SNMP traps. 227 ''; 228 }; 229 230 }; 231 232 vrrpScripts = mkOption { 233 type = types.attrsOf (types.submodule (import ./vrrp-script-options.nix { 234 inherit lib; 235 })); 236 default = {}; 237 description = lib.mdDoc "Declarative vrrp script config"; 238 }; 239 240 vrrpInstances = mkOption { 241 type = types.attrsOf (types.submodule (import ./vrrp-instance-options.nix { 242 inherit lib; 243 })); 244 default = {}; 245 description = lib.mdDoc "Declarative vhost config"; 246 }; 247 248 extraGlobalDefs = mkOption { 249 type = types.lines; 250 default = ""; 251 description = lib.mdDoc '' 252 Extra lines to be added verbatim to the 'global_defs' block of the 253 configuration file 254 ''; 255 }; 256 257 extraConfig = mkOption { 258 type = types.lines; 259 default = ""; 260 description = lib.mdDoc '' 261 Extra lines to be added verbatim to the configuration file. 262 ''; 263 }; 264 265 secretFile = mkOption { 266 type = types.nullOr types.path; 267 default = null; 268 example = "/run/keys/keepalived.env"; 269 description = lib.mdDoc '' 270 Environment variables from this file will be interpolated into the 271 final config file using envsubst with this syntax: `$ENVIRONMENT` 272 or `''${VARIABLE}`. 273 The file should contain lines formatted as `SECRET_VAR=SECRET_VALUE`. 274 This is useful to avoid putting secrets into the nix store. 275 ''; 276 }; 277 278 }; 279 }; 280 281 config = mkIf cfg.enable { 282 283 assertions = flatten (map vrrpInstanceAssertions vrrpInstances); 284 285 systemd.timers.keepalived-boot-delay = { 286 description = "Keepalive Daemon delay to avoid instant transition to MASTER state"; 287 after = [ "network.target" "network-online.target" "syslog.target" ]; 288 requires = [ "network-online.target" ]; 289 wantedBy = [ "multi-user.target" ]; 290 timerConfig = { 291 OnActiveSec = "5s"; 292 Unit = "keepalived.service"; 293 }; 294 }; 295 296 systemd.services.keepalived = let 297 finalConfigFile = if cfg.secretFile == null then keepalivedConf else "/run/keepalived/keepalived.conf"; 298 in { 299 description = "Keepalive Daemon (LVS and VRRP)"; 300 after = [ "network.target" "network-online.target" "syslog.target" ]; 301 wants = [ "network-online.target" ]; 302 serviceConfig = { 303 Type = "forking"; 304 PIDFile = pidFile; 305 KillMode = "process"; 306 RuntimeDirectory = "keepalived"; 307 EnvironmentFile = lib.optional (cfg.secretFile != null) cfg.secretFile; 308 ExecStartPre = lib.optional (cfg.secretFile != null) 309 (pkgs.writeShellScript "keepalived-pre-start" '' 310 umask 077 311 ${pkgs.envsubst}/bin/envsubst -i "${keepalivedConf}" > ${finalConfigFile} 312 ''); 313 ExecStart = "${pkgs.keepalived}/sbin/keepalived" 314 + " -f ${finalConfigFile}" 315 + " -p ${pidFile}" 316 + optionalString cfg.snmp.enable " --snmp"; 317 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 318 Restart = "always"; 319 RestartSec = "1s"; 320 }; 321 }; 322 }; 323}