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}