forked from jan-leila/nix-config
		
	
		
			
				
	
	
		
			408 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			408 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
| {
 | |
|   config,
 | |
|   lib,
 | |
|   pkgs,
 | |
|   osConfig,
 | |
|   ...
 | |
| }:
 | |
| with lib; let
 | |
|   # Database configuration assertions
 | |
|   dbUrlConfigured = config.services.panoramax.database.url != null;
 | |
|   individualDbConfigured = all (x: x != null) [
 | |
|     config.services.panoramax.database.host
 | |
|     config.services.panoramax.database.port
 | |
|     config.services.panoramax.database.username
 | |
|     config.services.panoramax.database.password
 | |
|     config.services.panoramax.database.name
 | |
|   ];
 | |
| 
 | |
|   envContent = ''
 | |
|     # Panoramax Configuration
 | |
|     FLASK_APP=geovisio
 | |
|     ${
 | |
|       if dbUrlConfigured
 | |
|       then "DB_URL=${config.services.panoramax.database.url}"
 | |
|       else ''
 | |
|         DB_HOST=${config.services.panoramax.database.host}
 | |
|         DB_PORT=${toString config.services.panoramax.database.port}
 | |
|         DB_USERNAME=${config.services.panoramax.database.username}
 | |
|         DB_PASSWORD=${config.services.panoramax.database.password}
 | |
|         DB_NAME=${config.services.panoramax.database.name}
 | |
|       ''
 | |
|     }
 | |
|     ${optionalString (config.services.panoramax.storage.fsUrl != null) "FS_URL=${config.services.panoramax.storage.fsUrl}"}
 | |
|     ${optionalString (config.services.panoramax.infrastructure.nbProxies != null) "INFRA_NB_PROXIES=${toString config.services.panoramax.infrastructure.nbProxies}"}
 | |
|     ${optionalString (config.services.panoramax.flask.secretKey != null) "FLASK_SECRET_KEY=${config.services.panoramax.flask.secretKey}"}
 | |
|     ${optionalString (config.services.panoramax.flask.sessionCookieDomain != null) "FLASK_SESSION_COOKIE_DOMAIN=${config.services.panoramax.flask.sessionCookieDomain}"}
 | |
|     ${optionalString (config.services.panoramax.api.pictures.licenseSpdxId != null) "API_PICTURES_LICENSE_SPDX_ID=${config.services.panoramax.api.pictures.licenseSpdxId}"}
 | |
|     ${optionalString (config.services.panoramax.api.pictures.licenseUrl != null) "API_PICTURES_LICENSE_URL=${config.services.panoramax.api.pictures.licenseUrl}"}
 | |
|     ${optionalString (config.services.panoramax.port != null) "PORT=${toString config.services.panoramax.port}"}
 | |
|     ${optionalString (config.services.panoramax.sgblur.enable) "SGBLUR_API_URL=${config.services.panoramax.sgblur.url}"}
 | |
|     ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name}=${value}") config.services.panoramax.extraEnvironment)}
 | |
|   '';
 | |
| 
 | |
|   envFile = pkgs.writeText "panoramax.env" envContent;
 | |
| in {
 | |
|   options.services.panoramax = {
 | |
|     enable = lib.mkEnableOption "panoramax";
 | |
| 
 | |
|     package = lib.mkOption {
 | |
|       type = lib.types.package;
 | |
|       default = pkgs.panoramax;
 | |
|       description = "The panoramax package to use";
 | |
|     };
 | |
| 
 | |
|     subdomain = lib.mkOption {
 | |
|       type = lib.types.str;
 | |
|       description = "subdomain of base domain that panoramax will be hosted at";
 | |
|       default = "panoramax";
 | |
|     };
 | |
| 
 | |
|     database = {
 | |
|       createDB = mkOption {
 | |
|         type = types.bool;
 | |
|         default = true;
 | |
|         description = "Whether to automatically create the database and user";
 | |
|       };
 | |
| 
 | |
|       url = mkOption {
 | |
|         type = types.nullOr types.str;
 | |
|         default = null;
 | |
|         description = ''
 | |
|           Complete database URL connection string (e.g., "postgresql://user:password@host:port/dbname").
 | |
|           If provided, individual database options (host, port, username, password, name) are ignored.
 | |
|         '';
 | |
|       };
 | |
| 
 | |
|       port = mkOption {
 | |
|         type = types.nullOr types.port;
 | |
|         default = 5432;
 | |
|         description = "Database port (ignored if database.url is set)";
 | |
|       };
 | |
| 
 | |
|       host = mkOption {
 | |
|         type = types.nullOr types.str;
 | |
|         default = "localhost";
 | |
|         description = "Database host (ignored if database.url is set)";
 | |
|       };
 | |
| 
 | |
|       username = mkOption {
 | |
|         type = types.nullOr types.str;
 | |
|         default = "panoramax";
 | |
|         description = "Database username (ignored if database.url is set)";
 | |
|       };
 | |
| 
 | |
|       password = mkOption {
 | |
|         type = types.nullOr types.str;
 | |
|         default = null;
 | |
|         description = "Database password (ignored if database.url is set)";
 | |
|       };
 | |
| 
 | |
|       name = mkOption {
 | |
|         type = types.str;
 | |
|         default = "panoramax";
 | |
|         description = "Database name (ignored if database.url is set)";
 | |
|       };
 | |
|     };
 | |
| 
 | |
|     sgblur = {
 | |
|       enable = mkOption {
 | |
|         type = types.bool;
 | |
|         default = false;
 | |
|         description = "Whether to enable sgblur integration for face and license plate blurring";
 | |
|       };
 | |
| 
 | |
|       package = mkOption {
 | |
|         type = types.package;
 | |
|         default = pkgs.sgblur;
 | |
|         description = "The sgblur package to use";
 | |
|       };
 | |
| 
 | |
|       port = mkOption {
 | |
|         type = types.port;
 | |
|         default = 8080;
 | |
|         description = "Port for the sgblur service";
 | |
|       };
 | |
| 
 | |
|       host = mkOption {
 | |
|         type = types.str;
 | |
|         default = "127.0.0.1";
 | |
|         description = "Host to bind the sgblur service to";
 | |
|       };
 | |
| 
 | |
|       url = mkOption {
 | |
|         type = types.str;
 | |
|         default = "http://127.0.0.1:8080";
 | |
|         description = "URL where sgblur service is accessible";
 | |
|       };
 | |
|     };
 | |
| 
 | |
|     port = mkOption {
 | |
|       type = types.nullOr types.port;
 | |
|       default = 5000;
 | |
|       description = "Port for the Panoramax service";
 | |
|     };
 | |
| 
 | |
|     host = mkOption {
 | |
|       type = types.str;
 | |
|       default = "127.0.0.1";
 | |
|       description = "Host to bind the Panoramax service to";
 | |
|     };
 | |
| 
 | |
|     urlScheme = mkOption {
 | |
|       type = types.enum ["http" "https"];
 | |
|       default = "https";
 | |
|       description = "URL scheme for the application";
 | |
|     };
 | |
| 
 | |
|     storage = {
 | |
|       fsUrl = mkOption {
 | |
|         type = types.nullOr types.str;
 | |
|         default = "/var/lib/panoramax/storage";
 | |
|         description = "File system URL for storage";
 | |
|       };
 | |
|     };
 | |
| 
 | |
|     infrastructure = {
 | |
|       nbProxies = mkOption {
 | |
|         type = types.nullOr types.int;
 | |
|         default = 1;
 | |
|         description = "Number of proxies in front of the application";
 | |
|       };
 | |
|     };
 | |
| 
 | |
|     flask = {
 | |
|       secretKey = mkOption {
 | |
|         type = types.nullOr types.str;
 | |
|         default = null;
 | |
|         description = "Flask secret key for session security";
 | |
|       };
 | |
| 
 | |
|       sessionCookieDomain = mkOption {
 | |
|         type = types.nullOr types.str;
 | |
|         default = null;
 | |
|         description = "Flask session cookie domain";
 | |
|       };
 | |
|     };
 | |
| 
 | |
|     api = {
 | |
|       pictures = {
 | |
|         licenseSpdxId = mkOption {
 | |
|           type = types.nullOr types.str;
 | |
|           default = null;
 | |
|           description = "SPDX license identifier for API pictures";
 | |
|         };
 | |
| 
 | |
|         licenseUrl = mkOption {
 | |
|           type = types.nullOr types.str;
 | |
|           default = null;
 | |
|           description = "License URL for API pictures";
 | |
|         };
 | |
|       };
 | |
|     };
 | |
| 
 | |
|     extraEnvironment = mkOption {
 | |
|       type = types.attrsOf types.str;
 | |
|       default = {};
 | |
|       description = "Additional environment variables";
 | |
|       example = {
 | |
|         CUSTOM_SETTING = "value";
 | |
|         DEBUG = "true";
 | |
|       };
 | |
|     };
 | |
|   };
 | |
| 
 | |
|   config = lib.mkIf config.services.panoramax.enable (
 | |
|     lib.mkMerge [
 | |
|       {
 | |
|         environment.systemPackages = with pkgs;
 | |
|           [
 | |
|             config.services.panoramax.package
 | |
|             python3Packages.waitress
 | |
|           ]
 | |
|           ++ optionals config.services.panoramax.sgblur.enable [
 | |
|             config.services.panoramax.sgblur.package
 | |
|           ];
 | |
| 
 | |
|         systemd.services.panoramax = {
 | |
|           description = "Panoramax Service";
 | |
|           after = ["network.target"];
 | |
|           wantedBy = ["multi-user.target"];
 | |
|           serviceConfig = {
 | |
|             ExecStart = "${pkgs.python3Packages.waitress}/bin/waitress-serve --env-file=${envFile} --host=${config.services.panoramax.host} --port=${toString config.services.panoramax.port} --url-scheme=${config.services.panoramax.urlScheme} --call geovisio:create_app";
 | |
|             Restart = "always";
 | |
|             User = "panoramax";
 | |
|             Group = "panoramax";
 | |
|             WorkingDirectory = "/var/lib/panoramax";
 | |
|             Environment = "PYTHONPATH=${config.services.panoramax.package}/lib/python3.11/site-packages";
 | |
|           };
 | |
|         };
 | |
| 
 | |
|         users.users.panoramax = {
 | |
|           isSystemUser = true;
 | |
|           group = "panoramax";
 | |
|           home = "/var/lib/panoramax";
 | |
|           createHome = true;
 | |
|         };
 | |
| 
 | |
|         users.groups.panoramax = {};
 | |
| 
 | |
|         systemd.tmpfiles.rules = [
 | |
|           "d /var/lib/panoramax 0755 panoramax panoramax -"
 | |
|           "d ${config.services.panoramax.storage.fsUrl} 0755 panoramax panoramax -"
 | |
|         ];
 | |
| 
 | |
|         assertions = [
 | |
|           {
 | |
|             assertion = dbUrlConfigured || individualDbConfigured;
 | |
|             message = ''
 | |
|               Panoramax database configuration requires either:
 | |
|               - A complete database URL (services.panoramax.database.url), OR
 | |
|               - All individual database options (host, port, username, password, name)
 | |
| 
 | |
|               Currently configured:
 | |
|               - database.url: ${
 | |
|                 if dbUrlConfigured
 | |
|                 then "✓ configured"
 | |
|                 else "✗ not configured"
 | |
|               }
 | |
|               - individual options: ${
 | |
|                 if individualDbConfigured
 | |
|                 then "✓ all configured"
 | |
|                 else "✗ some missing"
 | |
|               }
 | |
|             '';
 | |
|           }
 | |
|           {
 | |
|             assertion = !config.services.panoramax.database.createDB || config.services.panoramax.database.url == null || (lib.hasPrefix "/run/" config.services.panoramax.database.url || lib.hasPrefix "unix:" config.services.panoramax.database.url || lib.hasPrefix "/" config.services.panoramax.database.host);
 | |
|             message = ''
 | |
|               Panoramax createDB option can only be used with socket connections when a database URL is provided.
 | |
|               Socket connections are identified by:
 | |
|               - URLs starting with "unix:"
 | |
|               - URLs starting with "/run/"
 | |
|               - Host paths starting with "/"
 | |
| 
 | |
|               Current configuration:
 | |
|               - createDB: ${lib.boolToString config.services.panoramax.database.createDB}
 | |
|               - database.url: ${
 | |
|                 if config.services.panoramax.database.url != null
 | |
|                 then config.services.panoramax.database.url
 | |
|                 else "not set"
 | |
|               }
 | |
|               - database.host: ${config.services.panoramax.database.host}
 | |
|             '';
 | |
|           }
 | |
|         ];
 | |
|       }
 | |
|       (
 | |
|         lib.mkIf config.services.panoramax.sgblur.enable {
 | |
|           systemd.services.sgblur = {
 | |
|             description = "SGBlur AI-powered face and license plate blurring service";
 | |
|             after = ["network.target"];
 | |
|             wantedBy = ["multi-user.target"];
 | |
|             serviceConfig = {
 | |
|               ExecStart = "${config.services.panoramax.sgblur.package}/bin/uvicorn sgblur.main:app --host ${config.services.panoramax.sgblur.host} --port ${toString config.services.panoramax.sgblur.port}";
 | |
|               Restart = "always";
 | |
|               User = "sgblur";
 | |
|               Group = "sgblur";
 | |
|               WorkingDirectory = "/var/lib/sgblur";
 | |
|               Environment = "PYTHONPATH=${config.services.panoramax.sgblur.package}/lib/python3.11/site-packages";
 | |
|             };
 | |
|           };
 | |
| 
 | |
|           users.users.sgblur = {
 | |
|             isSystemUser = true;
 | |
|             group = "sgblur";
 | |
|             home = "/var/lib/sgblur";
 | |
|             createHome = true;
 | |
|           };
 | |
| 
 | |
|           users.groups.sgblur = {};
 | |
| 
 | |
|           systemd.tmpfiles.rules = [
 | |
|             "d /var/lib/sgblur 0755 sgblur sgblur -"
 | |
|           ];
 | |
| 
 | |
|           # Update panoramax service dependencies when sgblur is enabled
 | |
|           systemd.services.panoramax = {
 | |
|             after = ["sgblur.service"];
 | |
|             wants = ["sgblur.service"];
 | |
|           };
 | |
|         }
 | |
|       )
 | |
|       (
 | |
|         lib.mkIf config.services.panoramax.database.createDB {
 | |
|           services.postgresql = {
 | |
|             enable = true;
 | |
|             ensureDatabases = [config.services.panoramax.database.name];
 | |
|             ensureUsers = [
 | |
|               {
 | |
|                 name = config.services.panoramax.database.username;
 | |
|                 ensureDBOwnership = true;
 | |
|                 ensureClauses.login = true;
 | |
|               }
 | |
|             ];
 | |
|             extensions = ps: with ps; [postgis];
 | |
|             settings = {
 | |
|               shared_preload_libraries = ["postgis"];
 | |
|             };
 | |
|           };
 | |
| 
 | |
|           systemd.services.postgresql.serviceConfig.ExecStartPost = let
 | |
|             sqlFile = pkgs.writeText "panoramax-postgis-setup.sql" ''
 | |
|               CREATE EXTENSION IF NOT EXISTS postgis;
 | |
|               CREATE EXTENSION IF NOT EXISTS postgis_topology;
 | |
|               CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;
 | |
|               CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder;
 | |
| 
 | |
|               ALTER SCHEMA public OWNER TO ${config.services.panoramax.database.username};
 | |
|               GRANT ALL ON SCHEMA public TO ${config.services.panoramax.database.username};
 | |
|             '';
 | |
|           in [
 | |
|             ''
 | |
|               ${lib.getExe' config.services.postgresql.package "psql"} -d "${config.services.panoramax.database.name}" -f "${sqlFile}"
 | |
|             ''
 | |
|           ];
 | |
| 
 | |
|           systemd.services.panoramax = {
 | |
|             after = ["postgresql.service"];
 | |
|             requires = ["postgresql.service"];
 | |
|           };
 | |
|         }
 | |
|       )
 | |
|       (
 | |
|         lib.mkIf config.host.reverse_proxy.enable {
 | |
|           host = {
 | |
|             reverse_proxy.subdomains.${config.services.panoramax.subdomain} = {
 | |
|               target = "http://localhost:${toString config.services.panoramax.port}";
 | |
| 
 | |
|               websockets.enable = true;
 | |
|               forwardHeaders.enable = true;
 | |
| 
 | |
|               extraConfig = ''
 | |
|                 # allow large file uploads for panoramic images
 | |
|                 client_max_body_size 100M;
 | |
| 
 | |
|                 # set timeout for image processing
 | |
|                 proxy_read_timeout 300s;
 | |
|                 proxy_send_timeout 300s;
 | |
|                 send_timeout       300s;
 | |
|                 proxy_redirect     off;
 | |
|               '';
 | |
|             };
 | |
|           };
 | |
|         }
 | |
|       )
 | |
|       (
 | |
|         lib.mkIf config.services.fail2ban {
 | |
|           # TODO: configure options for fail2ban
 | |
|         }
 | |
|       )
 | |
|       (
 | |
|         lib.mkIf osConfig.host.impermanence.enable {
 | |
|           # TODO: configure impermanence for panoramax data
 | |
|         }
 | |
|       )
 | |
|     ]
 | |
|   );
 | |
| }
 |