forked from jan-leila/nix-config
		
	
		
			
				
	
	
		
			359 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			359 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";
 | 
						|
 | 
						|
            # 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}";
 | 
						|
          }
 | 
						|
          // (
 | 
						|
            if config.services.panoramax.database.host == "/run/postgresql"
 | 
						|
            then {
 | 
						|
              DB_URL = "postgresql://${config.services.panoramax.database.user}@/${config.services.panoramax.database.name}?host=/run/postgresql";
 | 
						|
            }
 | 
						|
            else {
 | 
						|
              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;
 | 
						|
            }
 | 
						|
          )
 | 
						|
          // (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}"
 | 
						|
        ''
 | 
						|
      ];
 | 
						|
    })
 | 
						|
  ]);
 | 
						|
}
 |