diff --git a/modules/nixos-modules/server/panoramax/default.nix b/modules/nixos-modules/server/panoramax/default.nix index 779f284..f029ee3 100644 --- a/modules/nixos-modules/server/panoramax/default.nix +++ b/modules/nixos-modules/server/panoramax/default.nix @@ -1,334 +1,8 @@ -{ - config, - lib, - pkgs, - ... -}: -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 { +{...}: { imports = [ ./proxy.nix ./fail2ban.nix ./impermanence.nix + ./panoramax.nix ]; - - options.services.panoramax = { - enable = lib.mkEnableOption "panoramax"; - - package = lib.mkOption { - type = lib.types.package; - default = pkgs.panoramax; - description = "The panoramax package to use"; - }; - - 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.database.createDB { - systemd.services.panoramax = { - after = ["postgresql.service"]; - requires = ["postgresql.service"]; - }; - - 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}" - '' - ]; - }) - ]); } diff --git a/modules/nixos-modules/server/panoramax/panoramax.nix b/modules/nixos-modules/server/panoramax/panoramax.nix new file mode 100644 index 0000000..cdbc632 --- /dev/null +++ b/modules/nixos-modules/server/panoramax/panoramax.nix @@ -0,0 +1,253 @@ +{ + config, + lib, + pkgs, + ... +}: let + dbUrlConfigured = config.services.panoramax.database.url != null; + individualDbConfigured = lib.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} + '' + } + ${lib.optionalString (config.services.panoramax.storage.fsUrl != null) "FS_URL=${config.services.panoramax.storage.fsUrl}"} + ${lib.optionalString (config.services.panoramax.infrastructure.nbProxies != null) "INFRA_NB_PROXIES=${toString config.services.panoramax.infrastructure.nbProxies}"} + ${lib.optionalString (config.services.panoramax.flask.secretKey != null) "FLASK_SECRET_KEY=${config.services.panoramax.flask.secretKey}"} + ${lib.optionalString (config.services.panoramax.flask.sessionCookieDomain != null) "FLASK_SESSION_COOKIE_DOMAIN=${config.services.panoramax.flask.sessionCookieDomain}"} + ${lib.optionalString (config.services.panoramax.api.pictures.licenseSpdxId != null) "API_PICTURES_LICENSE_SPDX_ID=${config.services.panoramax.api.pictures.licenseSpdxId}"} + ${lib.optionalString (config.services.panoramax.api.pictures.licenseUrl != null) "API_PICTURES_LICENSE_URL=${config.services.panoramax.api.pictures.licenseUrl}"} + ${lib.optionalString (config.services.panoramax.port != null) "PORT=${toString config.services.panoramax.port}"} + ${lib.optionalString (config.services.panoramax.sgblur.enable) "SGBLUR_API_URL=${config.services.panoramax.sgblur.url}"} + ${lib.concatStringsSep "\n" (lib.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"; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "panoramax"; + description = "The user panoramax should run as."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "panoramax"; + description = "The group panoramax should run as."; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Host to bind the panoramax service to"; + }; + + port = lib.mkOption { + type = lib.types.nullOr lib.types.port; + default = 5000; + description = "Port for the panoramax service"; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to open the panoramax port in the firewall"; + }; + + settings = { + urlScheme = lib.mkOption { + type = lib.types.enum ["http" "https"]; + default = "https"; + description = "URL scheme for the application"; + }; + + storage = { + fsUrl = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = "/var/lib/panoramax/storage"; + description = "File system URL for storage"; + }; + }; + + infrastructure = { + nbProxies = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = 1; + description = "Number of proxies in front of the application"; + }; + }; + + flask = { + secretKey = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Flask secret key for session security"; + }; + + sessionCookieDomain = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Flask session cookie domain"; + }; + }; + + api = { + pictures = { + licenseSpdxId = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "SPDX license identifier for API pictures"; + }; + + licenseUrl = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "License URL for API pictures"; + }; + }; + }; + + extraEnvironment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = {}; + description = "Additional environment variables"; + example = { + CUSTOM_SETTING = "value"; + DEBUG = "true"; + }; + }; + }; + + database = { + createDB = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to automatically create the database and user"; + }; + + name = lib.mkOption { + type = lib.types.str; + default = "panoramax"; + description = "The name of the panoramax database"; + }; + + host = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = "/run/postgresql"; + description = "Hostname or address of the postgresql server. If an absolute path is given here, it will be interpreted as a unix socket path."; + }; + + port = lib.mkOption { + type = lib.types.nullOr lib.types.port; + default = 5432; + description = "Port of the postgresql server."; + }; + + user = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = "panoramax"; + description = "The database user for panoramax."; + }; + + # TODO: password file for external database + }; + + sgblur = { + # TODO: configs to bind to sgblur + }; + }; + sgblur = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to enable sgblur integration for face and license plate blurring"; + }; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.sgblur; + description = "The sgblur package to use"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = "Port for the sgblur service"; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Host to bind the sgblur service to"; + }; + + url = lib.mkOption { + type = lib.types.str; + default = "http://127.0.0.1:8080"; + description = "URL where sgblur service is accessible"; + }; + }; + }; + + config = lib.mkIf config.services.panoramax.enable (lib.mkMerge [ + { + # TODO: start panoramax service + } + (lib.mkIf config.services.sgblur.enable { + # TODO: start sg blur config + }) + (lib.mkIf config.services.panoramax.database.createDB { + services.postgresql = lib.mkIf config.services.panoramax.database.enable { + enable = true; + ensureDatabases = lib.mkIf config.services.panoramax.database.createDB [config.services.panoramax.database.name]; + ensureUsers = lib.mkIf config.services.panoramax.database.createDB [ + { + name = config.services.panoramax.database.user; + ensureDBOwnership = true; + ensureClauses.login = true; + } + ]; + extensions = ps: with ps; [postgis]; + }; + systemd.services.postgresql.serviceConfig.ExecStartPost = let + sqlFile = pkgs.writeText "panoramax-postgis-setup.sql" '' + CREATE EXTENSION IF NOT EXISTS postgis; + + ALTER DATABASE ${config.services.panoramax.database.name} SET TIMEZONE TO 'UTC'; + + GRANT SET ON PARAMETER session_replication_role TO ${config.services.panoramax.database.user}; + ''; + in [ + '' + ${lib.getExe' config.services.postgresql.package "psql"} -d "${config.services.panoramax.database.user}" -f "${sqlFile}" + '' + ]; + }) + ]); +}