diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 52de4b23370b..530ae1d1cf03 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -420,6 +420,7 @@ ./services/networking/i2p.nix ./services/networking/iodine.nix ./services/networking/ircd-hybrid/default.nix + ./services/networking/keepalived/default.nix ./services/networking/kippo.nix ./services/networking/kresd.nix ./services/networking/lambdabot.nix diff --git a/nixos/modules/services/networking/keepalived/default.nix b/nixos/modules/services/networking/keepalived/default.nix new file mode 100644 index 000000000000..378cd9365848 --- /dev/null +++ b/nixos/modules/services/networking/keepalived/default.nix @@ -0,0 +1,245 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.keepalived; + + keepalivedConf = pkgs.writeText "keepalived.conf" '' + global_defs { + ${snmpGlobalDefs} + ${cfg.extraGlobalDefs} + } + + ${vrrpInstancesStr} + ${cfg.extraConfig} + ''; + + snmpGlobalDefs = with cfg.snmp; optionalString enable ( + optionalString (socket != null) "snmp_socket ${socket}\n" + + optionalString enableKeepalived "enable_snmp_keepalived\n" + + optionalString enableChecker "enable_snmp_checker\n" + + optionalString enableRfc "enable_snmp_rfc\n" + + optionalString enableRfcV2 "enable_snmp_rfcv2\n" + + optionalString enableRfcV3 "enable_snmp_rfcv3\n" + + optionalString enableTraps "enable_traps" + ); + + vrrpInstancesStr = concatStringsSep "\n" (map (i: + '' + vrrp_instance ${i.name} { + interface ${i.interface} + state ${i.state} + virtual_router_id ${toString i.virtualRouterId} + priority ${toString i.priority} + ${optionalString i.noPreempt "nopreempt"} + + ${optionalString i.useVmac ( + "use_vmac" + optionalString (i.vmacInterface != null) " ${i.vmacInterface}" + )} + ${optionalString i.vmacXmitBase "vmac_xmit_base"} + + ${optionalString (i.unicastSrcIp != null) "unicast_src_ip ${i.unicastSrcIp}"} + unicast_peer { + ${concatStringsSep "\n" i.unicastPeers} + } + + virtual_ipaddress { + ${concatMapStringsSep "\n" virtualIpLine i.virtualIps} + } + + ${i.extraConfig} + } + '' + ) vrrpInstances); + + virtualIpLine = (ip: + ip.addr + + optionalString (notNullOrEmpty ip.brd) " brd ${ip.brd}" + + optionalString (notNullOrEmpty ip.dev) " dev ${ip.dev}" + + optionalString (notNullOrEmpty ip.scope) " scope ${ip.scope}" + + optionalString (notNullOrEmpty ip.label) " label ${ip.label}" + ); + + notNullOrEmpty = s: !(s == null || s == ""); + + vrrpInstances = mapAttrsToList (iName: iConfig: + { + name = iName; + } // iConfig + ) cfg.vrrpInstances; + + vrrpInstanceAssertions = i: [ + { assertion = i.interface != ""; + message = "services.keepalived.vrrpInstances.${i.name}.interface option cannot be empty."; + } + { assertion = i.virtualRouterId >= 0 && i.virtualRouterId <= 255; + message = "services.keepalived.vrrpInstances.${i.name}.virtualRouterId must be an integer between 0..255."; + } + { assertion = i.priority >= 0 && i.priority <= 255; + message = "services.keepalived.vrrpInstances.${i.name}.priority must be an integer between 0..255."; + } + { assertion = i.vmacInterface == null || i.useVmac; + message = "services.keepalived.vrrpInstances.${i.name}.vmacInterface has no effect when services.keepalived.vrrpInstances.${i.name}.useVmac is not set."; + } + { assertion = !i.vmacXmitBase || i.useVmac; + message = "services.keepalived.vrrpInstances.${i.name}.vmacXmitBase has no effect when services.keepalived.vrrpInstances.${i.name}.useVmac is not set."; + } + ] ++ flatten (map (virtualIpAssertions i.name) i.virtualIps); + + virtualIpAssertions = vrrpName: ip: [ + { assertion = ip.addr != ""; + message = "The 'addr' option for an services.keepalived.vrrpInstances.${vrrpName}.virtualIps entry cannot be empty."; + } + ]; + + pidFile = "/run/keepalived.pid"; + +in +{ + + options = { + services.keepalived = { + + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable Keepalived. + ''; + }; + + snmp = { + + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable the builtin AgentX subagent. + ''; + }; + + socket = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Socket to use for connecting to SNMP master agent. If this value is + set to null, keepalived's default will be used, which is + unix:/var/agentx/master, unless using a network namespace, when the + default is udp:localhost:705. + ''; + }; + + enableKeepalived = mkOption { + type = types.bool; + default = false; + description = '' + Enable SNMP handling of vrrp element of KEEPALIVED MIB. + ''; + }; + + enableChecker = mkOption { + type = types.bool; + default = false; + description = '' + Enable SNMP handling of checker element of KEEPALIVED MIB. + ''; + }; + + enableRfc = mkOption { + type = types.bool; + default = false; + description = '' + Enable SNMP handling of RFC2787 and RFC6527 VRRP MIBs. + ''; + }; + + enableRfcV2 = mkOption { + type = types.bool; + default = false; + description = '' + Enable SNMP handling of RFC2787 VRRP MIB. + ''; + }; + + enableRfcV3 = mkOption { + type = types.bool; + default = false; + description = '' + Enable SNMP handling of RFC6527 VRRP MIB. + ''; + }; + + enableTraps = mkOption { + type = types.bool; + default = false; + description = '' + Enable SNMP traps. + ''; + }; + + }; + + vrrpInstances = mkOption { + type = types.attrsOf (types.submodule (import ./vrrp-options.nix { + inherit lib; + })); + default = {}; + description = "Declarative vhost config"; + }; + + extraGlobalDefs = mkOption { + type = types.lines; + default = ""; + description = '' + Extra lines to be added verbatim to the 'global_defs' block of the + configuration file + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra lines to be added verbatim to the configuration file. + ''; + }; + + }; + }; + + config = mkIf cfg.enable { + + assertions = flatten (map vrrpInstanceAssertions vrrpInstances); + + systemd.timers.keepalived-boot-delay = { + description = "Keepalive Daemon delay to avoid instant transition to MASTER state"; + after = [ "network.target" "network-online.target" "syslog.target" ]; + requires = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + timerConfig = { + OnActiveSec = "5s"; + Unit = "keepalived.service"; + }; + }; + + systemd.services.keepalived = { + description = "Keepalive Daemon (LVS and VRRP)"; + after = [ "network.target" "network-online.target" "syslog.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "forking"; + PIDFile = pidFile; + KillMode = "process"; + ExecStart = "${pkgs.keepalived}/sbin/keepalived" + + " -f ${keepalivedConf}" + + " -p ${pidFile}" + + optionalString cfg.snmp.enable " --snmp"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + Restart = "always"; + RestartSec = "1s"; + }; + }; + }; +} diff --git a/nixos/modules/services/networking/keepalived/virtual-ip-options.nix b/nixos/modules/services/networking/keepalived/virtual-ip-options.nix new file mode 100644 index 000000000000..1b8889b1b472 --- /dev/null +++ b/nixos/modules/services/networking/keepalived/virtual-ip-options.nix @@ -0,0 +1,50 @@ +{ lib } : + +with lib; +{ + options = { + + addr = mkOption { + type = types.str; + description = '' + IP address, optionally with a netmask: IPADDR[/MASK] + ''; + }; + + brd = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The broadcast address on the interface. + ''; + }; + + dev = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The name of the device to add the address to. + ''; + }; + + scope = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The scope of the area where this address is valid. + ''; + }; + + label = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Each address may be tagged with a label string. In order to preserve + compatibility with Linux-2.0 net aliases, this string must coincide with + the name of the device or must be prefixed with the device name followed + by colon. + ''; + }; + + }; +} diff --git a/nixos/modules/services/networking/keepalived/vrrp-options.nix b/nixos/modules/services/networking/keepalived/vrrp-options.nix new file mode 100644 index 000000000000..79eff3ae5419 --- /dev/null +++ b/nixos/modules/services/networking/keepalived/vrrp-options.nix @@ -0,0 +1,121 @@ +{ lib } : + +with lib; +{ + options = { + + interface = mkOption { + type = types.str; + description = '' + Interface for inside_network, bound by vrrp. + ''; + }; + + state = mkOption { + type = types.enum [ "MASTER" "BACKUP" ]; + default = "BACKUP"; + description = '' + Initial state. As soon as the other machine(s) come up, an election will + be held and the machine with the highest "priority" will become MASTER. + So the entry here doesn't matter a whole lot. + ''; + }; + + virtualRouterId = mkOption { + type = types.int; + description = '' + Arbitrary unique number 0..255. Used to differentiate multiple instances + of vrrpd running on the same NIC (and hence same socket). + ''; + }; + + priority = mkOption { + type = types.int; + default = 100; + description = '' + For electing MASTER, highest priority wins. To be MASTER, make 50 more + than other machines. + ''; + }; + + noPreempt = mkOption { + type = types.bool; + default = false; + description = '' + VRRP will normally preempt a lower priority machine when a higher + priority machine comes online. "nopreempt" allows the lower priority + machine to maintain the master role, even when a higher priority machine + comes back online. NOTE: For this to work, the initial state of this + entry must be BACKUP. + ''; + }; + + useVmac = mkOption { + type = types.bool; + default = false; + description = '' + Use VRRP Virtual MAC. + ''; + }; + + vmacInterface = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Name of the vmac interface to use. keepalived will come up with a name + if you don't specify one. + ''; + }; + + vmacXmitBase = mkOption { + type = types.bool; + default = false; + description = '' + Send/Recv VRRP messages from base interface instead of VMAC interface. + ''; + }; + + unicastSrcIp = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Default IP for binding vrrpd is the primary IP on interface. If you + want to hide location of vrrpd, use this IP as src_addr for unicast + vrrp packets. + ''; + }; + + unicastPeers = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Do not send VRRP adverts over VRRP multicast group. Instead it sends + adverts to the following list of ip addresses using unicast design + fashion. It can be cool to use VRRP FSM and features in a networking + environment where multicast is not supported! IP Addresses specified can + IPv4 as well as IPv6. + ''; + }; + + virtualIps = mkOption { + type = types.listOf (types.submodule (import ./virtual-ip-options.nix { + inherit lib; + })); + default = []; + example = literalExample '' + TODO: Example + ''; + description = "Declarative vhost config"; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra lines to be added verbatim to the vrrp_instance section. + ''; + }; + + }; + +}