1{ config, lib, pkgs, utils, ... }:
2with lib;
3let
4 cfg = config.services.unifi;
5 stateDir = "/var/lib/unifi";
6 cmd = ''
7 @${cfg.jrePackage}/bin/java java \
8 ${optionalString (cfg.initialJavaHeapSize != null) "-Xms${(toString cfg.initialJavaHeapSize)}m"} \
9 ${optionalString (cfg.maximumJavaHeapSize != null) "-Xmx${(toString cfg.maximumJavaHeapSize)}m"} \
10 -jar ${stateDir}/lib/ace.jar
11 '';
12 mountPoints = [
13 {
14 what = "${cfg.unifiPackage}/dl";
15 where = "${stateDir}/dl";
16 }
17 {
18 what = "${cfg.unifiPackage}/lib";
19 where = "${stateDir}/lib";
20 }
21 {
22 what = "${cfg.mongodbPackage}/bin";
23 where = "${stateDir}/bin";
24 }
25 {
26 what = "${cfg.dataDir}";
27 where = "${stateDir}/data";
28 }
29 ];
30 systemdMountPoints = map (m: "${utils.escapeSystemdPath m.where}.mount") mountPoints;
31in
32{
33
34 options = {
35
36 services.unifi.enable = mkOption {
37 type = types.bool;
38 default = false;
39 description = ''
40 Whether or not to enable the unifi controller service.
41 '';
42 };
43
44 services.unifi.jrePackage = mkOption {
45 type = types.package;
46 default = pkgs.jre8;
47 defaultText = "pkgs.jre8";
48 description = ''
49 The JRE package to use. Check the release notes to ensure it is supported.
50 '';
51 };
52
53 services.unifi.unifiPackage = mkOption {
54 type = types.package;
55 default = pkgs.unifiLTS;
56 defaultText = "pkgs.unifiLTS";
57 description = ''
58 The unifi package to use.
59 '';
60 };
61
62 services.unifi.mongodbPackage = mkOption {
63 type = types.package;
64 default = pkgs.mongodb;
65 defaultText = "pkgs.mongodb";
66 description = ''
67 The mongodb package to use.
68 '';
69 };
70
71 services.unifi.dataDir = mkOption {
72 type = types.str;
73 default = "${stateDir}/data";
74 description = ''
75 Where to store the database and other data.
76
77 This directory will be bind-mounted to ${stateDir}/data as part of the service startup.
78 '';
79 };
80
81 services.unifi.openPorts = mkOption {
82 type = types.bool;
83 default = true;
84 description = ''
85 Whether or not to open the minimum required ports on the firewall.
86
87 This is necessary to allow firmware upgrades and device discovery to
88 work. For remote login, you should additionally open (or forward) port
89 8443.
90 '';
91 };
92
93 services.unifi.initialJavaHeapSize = mkOption {
94 type = types.nullOr types.int;
95 default = null;
96 example = 1024;
97 description = ''
98 Set the initial heap size for the JVM in MB. If this option isn't set, the
99 JVM will decide this value at runtime.
100 '';
101 };
102
103 services.unifi.maximumJavaHeapSize = mkOption {
104 type = types.nullOr types.int;
105 default = null;
106 example = 4096;
107 description = ''
108 Set the maximimum heap size for the JVM in MB. If this option isn't set, the
109 JVM will decide this value at runtime.
110 '';
111 };
112
113 };
114
115 config = mkIf cfg.enable {
116
117 users.users.unifi = {
118 uid = config.ids.uids.unifi;
119 description = "UniFi controller daemon user";
120 home = "${stateDir}";
121 };
122
123 networking.firewall = mkIf cfg.openPorts {
124 # https://help.ubnt.com/hc/en-us/articles/218506997
125 allowedTCPPorts = [
126 8080 # Port for UAP to inform controller.
127 8880 # Port for HTTP portal redirect, if guest portal is enabled.
128 8843 # Port for HTTPS portal redirect, ditto.
129 6789 # Port for UniFi mobile speed test.
130 ];
131 allowedUDPPorts = [
132 3478 # UDP port used for STUN.
133 10001 # UDP port used for device discovery.
134 ];
135 };
136
137 # We must create the binary directories as bind mounts instead of symlinks
138 # This is because the controller resolves all symlinks to absolute paths
139 # to be used as the working directory.
140 systemd.mounts = map ({ what, where }: {
141 bindsTo = [ "unifi.service" ];
142 partOf = [ "unifi.service" ];
143 unitConfig.RequiresMountsFor = stateDir;
144 options = "bind";
145 what = what;
146 where = where;
147 }) mountPoints;
148
149 systemd.tmpfiles.rules = [
150 "d '${stateDir}' 0700 unifi - - -"
151 "d '${stateDir}/data' 0700 unifi - - -"
152 "d '${stateDir}/webapps' 0700 unifi - - -"
153 "L+ '${stateDir}/webapps/ROOT' - - - - ${cfg.unifiPackage}/webapps/ROOT"
154 ];
155
156 systemd.services.unifi = {
157 description = "UniFi controller daemon";
158 wantedBy = [ "multi-user.target" ];
159 after = [ "network.target" ] ++ systemdMountPoints;
160 partOf = systemdMountPoints;
161 bindsTo = systemdMountPoints;
162 unitConfig.RequiresMountsFor = stateDir;
163 # This a HACK to fix missing dependencies of dynamic libs extracted from jars
164 environment.LD_LIBRARY_PATH = with pkgs.stdenv; "${cc.cc.lib}/lib";
165 # Make sure package upgrades trigger a service restart
166 restartTriggers = [ cfg.unifiPackage cfg.mongodbPackage ];
167
168 serviceConfig = {
169 Type = "simple";
170 ExecStart = "${(removeSuffix "\n" cmd)} start";
171 ExecStop = "${(removeSuffix "\n" cmd)} stop";
172 Restart = "on-failure";
173 User = "unifi";
174 UMask = "0077";
175 WorkingDirectory = "${stateDir}";
176 };
177 };
178
179 };
180
181 meta.maintainers = with lib.maintainers; [ erictapen ];
182}