at master 11 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.services.nipap; 10 iniFmt = pkgs.formats.ini { }; 11 12 configFile = iniFmt.generate "nipap.conf" cfg.settings; 13 14 defaultUser = "nipap"; 15 defaultAuthBackend = "local"; 16 dataDir = "/var/lib/nipap"; 17 18 defaultServiceConfig = { 19 WorkingDirectory = dataDir; 20 User = cfg.user; 21 Group = config.users.users."${cfg.user}".group; 22 Restart = "on-failure"; 23 RestartSec = 30; 24 }; 25 26 escapedHost = host: if lib.hasInfix ":" host then "[${host}]" else host; 27in 28{ 29 options.services.nipap = { 30 enable = lib.mkEnableOption "global Neat IP Address Planner (NIPAP) configuration"; 31 32 user = lib.mkOption { 33 type = lib.types.str; 34 description = "User to use for running NIPAP services."; 35 default = defaultUser; 36 }; 37 38 settings = lib.mkOption { 39 description = '' 40 Configuration options to set in /etc/nipap/nipap.conf. 41 ''; 42 43 default = { }; 44 45 type = lib.types.submodule { 46 freeformType = iniFmt.type; 47 48 options = { 49 nipapd = { 50 listen = lib.mkOption { 51 type = lib.types.str; 52 default = "::1"; 53 description = "IP address to bind nipapd to."; 54 }; 55 port = lib.mkOption { 56 type = lib.types.port; 57 default = 1337; 58 description = "Port to bind nipapd to."; 59 }; 60 61 foreground = lib.mkOption { 62 type = lib.types.bool; 63 default = true; 64 description = "Remain in foreground rather than forking to background."; 65 }; 66 debug = lib.mkOption { 67 type = lib.types.bool; 68 default = false; 69 description = "Enable debug logging."; 70 }; 71 72 db_host = lib.mkOption { 73 type = lib.types.str; 74 default = ""; 75 description = "PostgreSQL host to connect to. Empty means use UNIX socket."; 76 }; 77 db_name = lib.mkOption { 78 type = lib.types.str; 79 default = cfg.user; 80 defaultText = defaultUser; 81 description = "Name of database to use on PostgreSQL server."; 82 }; 83 }; 84 85 auth = { 86 default_backend = lib.mkOption { 87 type = lib.types.str; 88 default = defaultAuthBackend; 89 description = "Name of auth backend to use by default."; 90 }; 91 auth_cache_timeout = lib.mkOption { 92 type = lib.types.int; 93 default = 3600; 94 description = "Seconds to store cached auth entries for."; 95 }; 96 }; 97 }; 98 }; 99 }; 100 101 authBackendSettings = lib.mkOption { 102 description = '' 103 auth.backends options to set in /etc/nipap/nipap.conf. 104 ''; 105 106 default = { 107 "${defaultAuthBackend}" = { 108 type = "SqliteAuth"; 109 db_path = "${dataDir}/local_auth.db"; 110 }; 111 }; 112 113 type = lib.types.submodule { 114 freeformType = iniFmt.type; 115 }; 116 }; 117 118 nipapd = { 119 enable = lib.mkEnableOption "nipapd server"; 120 package = lib.mkPackageOption pkgs "nipap" { }; 121 122 database.createLocally = lib.mkOption { 123 type = lib.types.bool; 124 default = true; 125 description = "Create a nipap database automatically."; 126 }; 127 }; 128 129 nipap-www = { 130 enable = lib.mkEnableOption "nipap-www server"; 131 package = lib.mkPackageOption pkgs "nipap-www" { }; 132 133 xmlrpcURIFile = lib.mkOption { 134 type = lib.types.nullOr lib.types.path; 135 default = null; 136 description = "Path to file containing XMLRPC URI for use by web UI - this is a secret, since it contains auth credentials. If null, it will be initialized assuming that the auth database is local."; 137 }; 138 139 workers = lib.mkOption { 140 type = lib.types.int; 141 default = 4; 142 description = "Number of worker processes for Gunicorn to fork."; 143 }; 144 umask = lib.mkOption { 145 type = lib.types.str; 146 default = "0"; 147 description = "umask for files written by Gunicorn, including UNIX socket."; 148 }; 149 150 unixSocket = lib.mkOption { 151 type = lib.types.nullOr lib.types.str; 152 default = null; 153 description = "Path to UNIX socket to bind to."; 154 example = "/run/nipap/nipap-www.sock"; 155 }; 156 host = lib.mkOption { 157 type = lib.types.nullOr lib.types.str; 158 default = "::"; 159 description = "Host to bind to."; 160 }; 161 port = lib.mkOption { 162 type = lib.types.nullOr lib.types.port; 163 default = 21337; 164 description = "Port to bind to."; 165 }; 166 }; 167 }; 168 169 config = lib.mkIf cfg.enable ( 170 lib.mkMerge [ 171 { 172 systemd.tmpfiles.rules = [ 173 "d '${dataDir}' - ${cfg.user} ${config.users.users."${cfg.user}".group} - -" 174 ]; 175 176 environment.etc."nipap/nipap.conf" = { 177 source = configFile; 178 }; 179 180 services.nipap.settings = lib.attrsets.mapAttrs' (name: value: { 181 name = "auth.backends.${name}"; 182 inherit value; 183 }) cfg.authBackendSettings; 184 185 services.nipap.nipapd.enable = lib.mkDefault true; 186 services.nipap.nipap-www.enable = lib.mkDefault true; 187 188 environment.systemPackages = [ 189 cfg.nipapd.package 190 ]; 191 } 192 (lib.mkIf (cfg.user == defaultUser) { 193 users.users."${defaultUser}" = { 194 isSystemUser = true; 195 group = defaultUser; 196 home = dataDir; 197 }; 198 users.groups."${defaultUser}" = { }; 199 }) 200 (lib.mkIf (cfg.nipapd.enable && cfg.nipapd.database.createLocally) { 201 services.postgresql = { 202 enable = true; 203 extensions = ps: with ps; [ ip4r ]; 204 ensureUsers = [ 205 { 206 name = cfg.user; 207 } 208 ]; 209 ensureDatabases = [ cfg.settings.nipapd.db_name ]; 210 }; 211 212 systemd.services.postgresql.serviceConfig.ExecStartPost = 213 let 214 sqlFile = pkgs.writeText "nipapd-setup.sql" '' 215 CREATE EXTENSION IF NOT EXISTS ip4r; 216 217 ALTER SCHEMA public OWNER TO "${cfg.user}"; 218 ALTER DATABASE "${cfg.settings.nipapd.db_name}" OWNER TO "${cfg.user}"; 219 ''; 220 in 221 [ 222 '' 223 ${lib.getExe' config.services.postgresql.finalPackage "psql"} -d "${cfg.settings.nipapd.db_name}" -f "${sqlFile}" 224 '' 225 ]; 226 }) 227 (lib.mkIf cfg.nipapd.enable { 228 systemd.services.nipapd = 229 let 230 pkg = cfg.nipapd.package; 231 in 232 { 233 description = "Neat IP Address Planner"; 234 after = [ 235 "network.target" 236 "systemd-tmpfiles-setup.service" 237 ] 238 ++ lib.optional (cfg.settings.nipapd.db_host == "") "postgresql.target"; 239 requires = lib.optional (cfg.settings.nipapd.db_host == "") "postgresql.target"; 240 wantedBy = [ "multi-user.target" ]; 241 preStart = lib.optionalString (cfg.settings.auth.default_backend == defaultAuthBackend) '' 242 # Create/upgrade local auth database 243 umask 077 244 ${pkg}/bin/nipap-passwd create-database >/dev/null 2>&1 245 ${pkg}/bin/nipap-passwd upgrade-database >/dev/null 2>&1 246 ''; 247 serviceConfig = defaultServiceConfig // { 248 KillSignal = "SIGINT"; 249 ExecStart = '' 250 ${pkg}/bin/nipapd \ 251 --auto-install-db \ 252 --auto-upgrade-db \ 253 --foreground \ 254 --no-pid-file 255 ''; 256 }; 257 }; 258 }) 259 (lib.mkIf cfg.nipap-www.enable { 260 assertions = [ 261 { 262 assertion = 263 cfg.nipap-www.xmlrpcURIFile == null -> cfg.settings.auth.default_backend == defaultAuthBackend; 264 message = "If no XMLRPC URI secret file is specified, then the default auth backend must be in use to automatically generate credentials."; 265 } 266 ]; 267 268 # Ensure that _something_ exists in the [www] group. 269 services.nipap.settings.www = lib.mkDefault { }; 270 271 systemd.services.nipap-www = 272 let 273 pkg = cfg.nipap-www.package; 274 in 275 { 276 description = "Neat IP Address Planner web server"; 277 after = [ 278 "network.target" 279 "systemd-tmpfiles-setup.service" 280 ] 281 ++ lib.optional cfg.nipapd.enable "nipapd.service"; 282 wantedBy = [ "multi-user.target" ]; 283 environment = { 284 PYTHONPATH = pkg.pythonPath; 285 }; 286 serviceConfig = defaultServiceConfig; 287 script = 288 let 289 bind = 290 if cfg.nipap-www.unixSocket != null then 291 "unix:${cfg.nipap-www.unixSocket}" 292 else 293 "${escapedHost cfg.nipap-www.host}:${toString cfg.nipap-www.port}"; 294 generateXMLRPC = cfg.nipap-www.xmlrpcURIFile == null; 295 xmlrpcURIFile = if generateXMLRPC then "${dataDir}/www_xmlrpc_uri" else cfg.nipap-www.xmlrpcURIFile; 296 in 297 '' 298 test -f "${dataDir}/www_secret" || { 299 umask 0077 300 ${pkg.python}/bin/python -c "import secrets; print(secrets.token_hex())" > "${dataDir}/www_secret" 301 } 302 export FLASK_SECRET_KEY="$(cat "${dataDir}/www_secret")" 303 304 # Ensure that we have an XMLRPC URI. 305 ${ 306 if generateXMLRPC then 307 '' 308 test -f "${dataDir}/www_xmlrpc_uri" || { 309 umask 0077 310 www_password="$(${pkg.python}/bin/python -c "import secrets; print(secrets.token_hex())")" 311 ${cfg.nipapd.package}/bin/nipap-passwd add --username nipap-www --password "''${www_password}" --name "User account for the web UI" --trusted 312 313 echo "http://nipap-www@${defaultAuthBackend}:''${www_password}@${escapedHost cfg.settings.nipapd.listen}:${toString cfg.settings.nipapd.port}" > "${xmlrpcURIFile}" 314 } 315 '' 316 else 317 "" 318 } 319 export FLASK_XMLRPC_URI="$(cat "${xmlrpcURIFile}")" 320 321 exec "${pkg.gunicorn}/bin/gunicorn" \ 322 --preload --workers ${toString cfg.nipap-www.workers} \ 323 --pythonpath "${pkg}/${pkg.python.sitePackages}" \ 324 --bind ${bind} --umask ${cfg.nipap-www.umask} \ 325 "nipapwww:create_app()" 326 ''; 327 }; 328 }) 329 ] 330 ); 331 332 meta.maintainers = with lib.maintainers; [ lukegb ]; 333}