1{ config, lib, pkgs, utils, ... }:
2
3with lib;
4let
5
6 dataDir = "/var/lib/consul";
7 cfg = config.services.consul;
8
9 configOptions = {
10 data_dir = dataDir;
11 ui_config = {
12 enabled = cfg.webUi;
13 };
14 } // cfg.extraConfig;
15
16 configFiles = [ "/etc/consul.json" "/etc/consul-addrs.json" ]
17 ++ cfg.extraConfigFiles;
18
19 devices = attrValues (filterAttrs (_: i: i != null) cfg.interface);
20 systemdDevices = forEach devices
21 (i: "sys-subsystem-net-devices-${utils.escapeSystemdPath i}.device");
22in
23{
24 options = {
25
26 services.consul = {
27
28 enable = mkOption {
29 type = types.bool;
30 default = false;
31 description = ''
32 Enables the consul daemon.
33 '';
34 };
35
36 package = mkPackageOption pkgs "consul" { };
37
38 webUi = mkOption {
39 type = types.bool;
40 default = false;
41 description = ''
42 Enables the web interface on the consul http port.
43 '';
44 };
45
46 leaveOnStop = mkOption {
47 type = types.bool;
48 default = false;
49 description = ''
50 If enabled, causes a leave action to be sent when closing consul.
51 This allows a clean termination of the node, but permanently removes
52 it from the cluster. You probably don't want this option unless you
53 are running a node which going offline in a permanent / semi-permanent
54 fashion.
55 '';
56 };
57
58 interface = {
59
60 advertise = mkOption {
61 type = types.nullOr types.str;
62 default = null;
63 description = ''
64 The name of the interface to pull the advertise_addr from.
65 '';
66 };
67
68 bind = mkOption {
69 type = types.nullOr types.str;
70 default = null;
71 description = ''
72 The name of the interface to pull the bind_addr from.
73 '';
74 };
75 };
76
77 forceAddrFamily = mkOption {
78 type = types.enum [ "any" "ipv4" "ipv6" ];
79 default = "any";
80 description = ''
81 Whether to bind ipv4/ipv6 or both kind of addresses.
82 '';
83 };
84
85 forceIpv4 = mkOption {
86 type = types.nullOr types.bool;
87 default = null;
88 description = ''
89 Deprecated: Use consul.forceAddrFamily instead.
90 Whether we should force the interfaces to only pull ipv4 addresses.
91 '';
92 };
93
94 dropPrivileges = mkOption {
95 type = types.bool;
96 default = true;
97 description = ''
98 Whether the consul agent should be run as a non-root consul user.
99 '';
100 };
101
102 extraConfig = mkOption {
103 default = { };
104 type = types.attrsOf types.anything;
105 description = ''
106 Extra configuration options which are serialized to json and added
107 to the config.json file.
108 '';
109 };
110
111 extraConfigFiles = mkOption {
112 default = [ ];
113 type = types.listOf types.str;
114 description = ''
115 Additional configuration files to pass to consul
116 NOTE: These will not trigger the service to be restarted when altered.
117 '';
118 };
119
120 alerts = {
121 enable = mkEnableOption "consul-alerts";
122
123 package = mkPackageOption pkgs "consul-alerts" { };
124
125 listenAddr = mkOption {
126 description = "Api listening address.";
127 default = "localhost:9000";
128 type = types.str;
129 };
130
131 consulAddr = mkOption {
132 description = "Consul api listening address";
133 default = "localhost:8500";
134 type = types.str;
135 };
136
137 watchChecks = mkOption {
138 description = "Whether to enable check watcher.";
139 default = true;
140 type = types.bool;
141 };
142
143 watchEvents = mkOption {
144 description = "Whether to enable event watcher.";
145 default = true;
146 type = types.bool;
147 };
148 };
149
150 };
151
152 };
153
154 config = mkIf cfg.enable (
155 mkMerge [{
156
157 users.users.consul = {
158 description = "Consul agent daemon user";
159 isSystemUser = true;
160 group = "consul";
161 # The shell is needed for health checks
162 shell = "/run/current-system/sw/bin/bash";
163 };
164 users.groups.consul = {};
165
166 environment = {
167 etc."consul.json".text = builtins.toJSON configOptions;
168 # We need consul.d to exist for consul to start
169 etc."consul.d/dummy.json".text = "{ }";
170 systemPackages = [ cfg.package ];
171 };
172
173 warnings = lib.flatten [
174 (lib.optional (cfg.forceIpv4 != null) ''
175 The option consul.forceIpv4 is deprecated, please use
176 consul.forceAddrFamily instead.
177 '')
178 ];
179
180 systemd.services.consul = {
181 wantedBy = [ "multi-user.target" ];
182 after = [ "network.target" ] ++ systemdDevices;
183 bindsTo = systemdDevices;
184 restartTriggers = [ config.environment.etc."consul.json".source ]
185 ++ mapAttrsToList (_: d: d.source)
186 (filterAttrs (n: _: hasPrefix "consul.d/" n) config.environment.etc);
187
188 serviceConfig = {
189 ExecStart = "@${lib.getExe cfg.package} consul agent -config-dir /etc/consul.d"
190 + concatMapStrings (n: " -config-file ${n}") configFiles;
191 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
192 PermissionsStartOnly = true;
193 User = if cfg.dropPrivileges then "consul" else null;
194 Restart = "on-failure";
195 TimeoutStartSec = "infinity";
196 } // (optionalAttrs (cfg.leaveOnStop) {
197 ExecStop = "${lib.getExe cfg.package} leave";
198 });
199
200 path = with pkgs; [ iproute2 gawk cfg.package ];
201 preStart = let
202 family = if cfg.forceAddrFamily == "ipv6" then
203 "-6"
204 else if cfg.forceAddrFamily == "ipv4" then
205 "-4"
206 else
207 "";
208 in ''
209 mkdir -m 0700 -p ${dataDir}
210 chown -R consul ${dataDir}
211
212 # Determine interface addresses
213 getAddrOnce () {
214 ip ${family} addr show dev "$1" scope global \
215 | awk -F '[ /\t]*' '/inet/ {print $3}' | head -n 1
216 }
217 getAddr () {
218 ADDR="$(getAddrOnce $1)"
219 LEFT=60 # Die after 1 minute
220 while [ -z "$ADDR" ]; do
221 sleep 1
222 LEFT=$(expr $LEFT - 1)
223 if [ "$LEFT" -eq "0" ]; then
224 echo "Address lookup timed out"
225 exit 1
226 fi
227 ADDR="$(getAddrOnce $1)"
228 done
229 echo "$ADDR"
230 }
231 echo "{" > /etc/consul-addrs.json
232 delim=" "
233 ''
234 + concatStrings (flip mapAttrsToList cfg.interface (name: i:
235 optionalString (i != null) ''
236 echo "$delim \"${name}_addr\": \"$(getAddr "${i}")\"" >> /etc/consul-addrs.json
237 delim=","
238 ''))
239 + ''
240 echo "}" >> /etc/consul-addrs.json
241 '';
242 };
243 }
244
245 # deprecated
246 (mkIf (cfg.forceIpv4 != null && cfg.forceIpv4) {
247 services.consul.forceAddrFamily = "ipv4";
248 })
249
250 (mkIf (cfg.alerts.enable) {
251 systemd.services.consul-alerts = {
252 wantedBy = [ "multi-user.target" ];
253 after = [ "consul.service" ];
254
255 path = [ cfg.package ];
256
257 serviceConfig = {
258 ExecStart = ''
259 ${lib.getExe cfg.alerts.package} start \
260 --alert-addr=${cfg.alerts.listenAddr} \
261 --consul-addr=${cfg.alerts.consulAddr} \
262 ${optionalString cfg.alerts.watchChecks "--watch-checks"} \
263 ${optionalString cfg.alerts.watchEvents "--watch-events"}
264 '';
265 User = if cfg.dropPrivileges then "consul" else null;
266 Restart = "on-failure";
267 };
268 };
269 })
270
271 ]);
272}