nix-config/modules/nixos-modules/server/panoramax/panoramax.nix

353 lines
12 KiB
Nix

{
config,
lib,
pkgs,
...
}: {
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 [
{
# Create panoramax user and group
users.users.${config.services.panoramax.user} = {
isSystemUser = true;
group = config.services.panoramax.group;
home = "/var/lib/panoramax";
createHome = true;
};
users.groups.${config.services.panoramax.group} = {};
# Ensure storage directory exists with correct permissions
systemd.tmpfiles.rules = [
"d '${config.services.panoramax.settings.storage.fsUrl}' 0755 ${config.services.panoramax.user} ${config.services.panoramax.group} - -"
];
systemd.services.panoramax-api = {
description = "Panoramax API server (self hosted map street view)";
after = ["network.target" "postgresql.service"];
wantedBy = ["multi-user.target"];
environment =
{
# Core Flask configuration
FLASK_APP = "geovisio";
# Database configuration
DB_HOST = config.services.panoramax.database.host;
DB_PORT = toString config.services.panoramax.database.port;
DB_USERNAME = config.services.panoramax.database.user;
DB_NAME = config.services.panoramax.database.name;
# Storage configuration
FS_URL = config.services.panoramax.settings.storage.fsUrl;
# Infrastructure configuration
INFRA_NB_PROXIES = toString config.services.panoramax.settings.infrastructure.nbProxies;
# Application configuration
PORT = toString config.services.panoramax.port;
# Python path to include the panoramax package
PYTHONPATH = "${config.services.panoramax.package}/${pkgs.python3.sitePackages}";
}
// (lib.optionalAttrs (config.services.panoramax.settings.flask.secretKey != null) {
FLASK_SECRET_KEY = config.services.panoramax.settings.flask.secretKey;
})
// (lib.optionalAttrs (config.services.panoramax.settings.flask.sessionCookieDomain != null) {
FLASK_SESSION_COOKIE_DOMAIN = config.services.panoramax.settings.flask.sessionCookieDomain;
})
// (lib.optionalAttrs (config.services.panoramax.settings.api.pictures.licenseSpdxId != null) {
API_PICTURES_LICENSE_SPDX_ID = config.services.panoramax.settings.api.pictures.licenseSpdxId;
})
// (lib.optionalAttrs (config.services.panoramax.settings.api.pictures.licenseUrl != null) {
API_PICTURES_LICENSE_URL = config.services.panoramax.settings.api.pictures.licenseUrl;
})
// (lib.optionalAttrs config.services.sgblur.enable {
SGBLUR_API_URL = config.services.sgblur.url;
})
// config.services.panoramax.settings.extraEnvironment;
path = with pkgs; [
(python3.withPackages (ps: with ps; [config.services.panoramax.package waitress]))
];
serviceConfig = {
ExecStart = "${pkgs.python3.withPackages (ps: with ps; [config.services.panoramax.package waitress])}/bin/waitress-serve --port ${toString config.services.panoramax.port} --call geovisio:create_app";
User = config.services.panoramax.user;
Group = config.services.panoramax.group;
WorkingDirectory = "/var/lib/panoramax";
Restart = "always";
RestartSec = 5;
# Security hardening
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [
"/var/lib/panoramax"
config.services.panoramax.settings.storage.fsUrl
];
NoNewPrivileges = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictSUIDSGID = true;
RestrictRealtime = true;
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
SystemCallArchitectures = "native";
};
};
# Open firewall if requested
networking.firewall.allowedTCPPorts = lib.mkIf config.services.panoramax.openFirewall [
config.services.panoramax.port
];
}
(lib.mkIf config.services.sgblur.enable {
# SGBlur service configuration
systemd.services.sgblur = {
description = "SGBlur face and license plate blurring service";
after = ["network.target"];
wantedBy = ["multi-user.target"];
path = with pkgs; [
config.services.sgblur.package
python3
python3Packages.waitress
];
serviceConfig = {
ExecStart = "${pkgs.python3Packages.waitress}/bin/waitress-serve --host ${config.services.sgblur.host} --port ${toString config.services.sgblur.port} src.detect.detect_api:app";
WorkingDirectory = "${config.services.sgblur.package}";
Restart = "always";
RestartSec = 5;
# Basic security hardening
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
NoNewPrivileges = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictSUIDSGID = true;
RestrictRealtime = true;
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
SystemCallArchitectures = "native";
};
};
networking.firewall.allowedTCPPorts = lib.mkIf config.services.panoramax.openFirewall [
config.services.sgblur.port
];
})
(lib.mkIf config.services.panoramax.database.createDB {
services.postgresql = {
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;
-- TODO: how can we ensure that this runs after the databases have been created
-- 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}"
''
];
})
]);
}