From 01325c306867c786f2828594ec0eef1fe98c64ab Mon Sep 17 00:00:00 2001 From: Leyla Becker Date: Mon, 15 Sep 2025 15:25:05 -0500 Subject: [PATCH] feat: drafted out database configuration and sgblur config --- modules/nixos-modules/server/panoramax.nix | 229 ++++++++++++++++----- 1 file changed, 181 insertions(+), 48 deletions(-) diff --git a/modules/nixos-modules/server/panoramax.nix b/modules/nixos-modules/server/panoramax.nix index ad21738..dd026cd 100644 --- a/modules/nixos-modules/server/panoramax.nix +++ b/modules/nixos-modules/server/panoramax.nix @@ -6,16 +6,14 @@ ... }: with lib; let - cfg = config.services.panoramax; - # Database configuration assertions - dbUrlConfigured = cfg.database.url != null; + dbUrlConfigured = config.services.panoramax.database.url != null; individualDbConfigured = all (x: x != null) [ - cfg.database.host - cfg.database.port - cfg.database.username - cfg.database.password - cfg.database.name + 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 = '' @@ -23,23 +21,24 @@ with lib; let FLASK_APP=geovisio ${ if dbUrlConfigured - then "DB_URL=${cfg.database.url}" + then "DB_URL=${config.services.panoramax.database.url}" else '' - DB_HOST=${cfg.database.host} - DB_PORT=${toString cfg.database.port} - DB_USERNAME=${cfg.database.username} - DB_PASSWORD=${cfg.database.password} - DB_NAME=${cfg.database.name} + 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 (cfg.storage.fsUrl != null) "FS_URL=${cfg.storage.fsUrl}"} - ${optionalString (cfg.infrastructure.nbProxies != null) "INFRA_NB_PROXIES=${toString cfg.infrastructure.nbProxies}"} - ${optionalString (cfg.flask.secretKey != null) "FLASK_SECRET_KEY=${cfg.flask.secretKey}"} - ${optionalString (cfg.flask.sessionCookieDomain != null) "FLASK_SESSION_COOKIE_DOMAIN=${cfg.flask.sessionCookieDomain}"} - ${optionalString (cfg.api.pictures.licenseSpdxId != null) "API_PICTURES_LICENSE_SPDX_ID=${cfg.api.pictures.licenseSpdxId}"} - ${optionalString (cfg.api.pictures.licenseUrl != null) "API_PICTURES_LICENSE_URL=${cfg.api.pictures.licenseUrl}"} - ${optionalString (cfg.port != null) "PORT=${toString cfg.port}"} - ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name}=${value}") cfg.extraEnvironment)} + ${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; @@ -59,26 +58,13 @@ in { default = "panoramax"; }; - # TODO: sgblur config - 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"; - }; - 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; @@ -113,12 +99,62 @@ in { }; name = mkOption { - type = types.nullOr types.str; + 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; @@ -179,10 +215,14 @@ in { config = lib.mkIf config.services.panoramax.enable ( lib.mkMerge [ { - environment.systemPackages = with pkgs; [ - config.services.panoramax.package - python3Packages.waitress - ]; + 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"; @@ -233,10 +273,103 @@ in { } ''; } - ]; + { + 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 "/" - # TODO: auto config db + 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 = {