349 lines
9.3 KiB
Nix
349 lines
9.3 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.services.bitpoll;
|
|
|
|
# Persistent data directory
|
|
dataDir = "/var/lib/bitpoll";
|
|
|
|
# Generate Django settings file
|
|
settingsFile = pkgs.writeText "bitpoll-settings.py" ''
|
|
# Bitpoll NixOS Configuration
|
|
import os
|
|
from bitpoll.settings.production import *
|
|
|
|
# Security settings
|
|
SECRET_KEY = '${cfg.secretKey}'
|
|
FIELD_ENCRYPTION_KEY = '${cfg.encryptionKey}'
|
|
DEBUG = ${boolToString cfg.debug}
|
|
ALLOWED_HOSTS = ${builtins.toJSON cfg.allowedHosts}
|
|
|
|
# Localization
|
|
LANGUAGE_CODE = '${cfg.language}'
|
|
TIME_ZONE = '${cfg.timezone}'
|
|
|
|
# Database configuration
|
|
DATABASES = {
|
|
'default': {
|
|
'ENGINE': 'django.db.backends.postgresql',
|
|
'NAME': '${cfg.database.name}',
|
|
'USER': '${cfg.database.user}',
|
|
'PASSWORD': '${cfg.database.password}',
|
|
'HOST': '${cfg.database.host}',
|
|
'PORT': '${toString cfg.database.port}',
|
|
}
|
|
}
|
|
|
|
# File storage paths
|
|
MEDIA_ROOT = '${dataDir}/media'
|
|
STATIC_ROOT = '${dataDir}/static'
|
|
|
|
# Additional settings
|
|
${concatStringsSep "\n" (mapAttrsToList (k: v: "${k} = ${builtins.toJSON v}") cfg.extraSettings)}
|
|
'';
|
|
|
|
# Generate uWSGI configuration
|
|
uwsgiConfig = pkgs.writeText "uwsgi.ini" ''
|
|
[uwsgi]
|
|
procname-master = uwsgi bitpoll
|
|
master = true
|
|
socket = ${cfg.listenAddress}:${toString cfg.port}
|
|
${optionalString (cfg.httpPort != null) "http-socket = ${cfg.listenAddress}:${toString cfg.httpPort}"}
|
|
|
|
plugins = python3
|
|
|
|
chdir = ${dataDir}
|
|
virtualenv = ${cfg.package}
|
|
pythonpath = ${cfg.package}/lib/python*/site-packages
|
|
|
|
module = bitpoll.wsgi:application
|
|
env = DJANGO_SETTINGS_MODULE=bitpoll.settings
|
|
env = BITPOLL_SETTINGS_FILE=${settingsFile}
|
|
env = LANG=C.UTF-8
|
|
env = LC_ALL=C.UTF-8
|
|
|
|
# Process management
|
|
uid = ${cfg.user}
|
|
gid = ${cfg.group}
|
|
umask = 027
|
|
|
|
processes = ${toString cfg.processes}
|
|
threads = ${toString cfg.threads}
|
|
cheaper = ${toString cfg.cheaperProcesses}
|
|
|
|
# Logging
|
|
disable-logging = ${boolToString cfg.disableLogging}
|
|
|
|
# Static files
|
|
static-map = /static=${dataDir}/static
|
|
|
|
# Additional uWSGI options
|
|
${cfg.extraUwsgiConfig}
|
|
'';
|
|
|
|
in {
|
|
options.services.bitpoll = {
|
|
enable = mkEnableOption "Bitpoll polling application";
|
|
|
|
package = mkOption {
|
|
type = types.package;
|
|
default = pkgs.bitpoll or (pkgs.callPackage ./package.nix { });
|
|
description = "The Bitpoll package to use";
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "bitpoll";
|
|
description = "User account under which Bitpoll runs";
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = "bitpoll";
|
|
description = "Group under which Bitpoll runs";
|
|
};
|
|
|
|
listenAddress = mkOption {
|
|
type = types.str;
|
|
default = "127.0.0.1";
|
|
description = "Address to listen on";
|
|
};
|
|
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 3008;
|
|
description = "Port for uWSGI socket";
|
|
};
|
|
|
|
httpPort = mkOption {
|
|
type = types.nullOr types.port;
|
|
default = 3009;
|
|
description = "Port for HTTP socket (null to disable)";
|
|
};
|
|
|
|
# Django settings
|
|
secretKey = mkOption {
|
|
type = types.str;
|
|
description = "Django secret key";
|
|
example = "your-secret-key-here";
|
|
};
|
|
|
|
encryptionKey = mkOption {
|
|
type = types.str;
|
|
description = "Field encryption key";
|
|
example = "BnEAJ5eEXb4HfYbaCPuW5RKQSoO02Uhz1RH93eQz0GM=";
|
|
};
|
|
|
|
debug = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Enable Django debug mode";
|
|
};
|
|
|
|
allowedHosts = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ "*" ];
|
|
description = "List of allowed hosts";
|
|
};
|
|
|
|
language = mkOption {
|
|
type = types.str;
|
|
default = "en-us";
|
|
description = "Language code";
|
|
};
|
|
|
|
timezone = mkOption {
|
|
type = types.str;
|
|
default = "Europe/Berlin";
|
|
description = "Time zone";
|
|
};
|
|
|
|
# Database settings
|
|
database = {
|
|
name = mkOption {
|
|
type = types.str;
|
|
default = "bitpoll";
|
|
description = "Database name";
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "bitpoll";
|
|
description = "Database user";
|
|
};
|
|
|
|
password = mkOption {
|
|
type = types.str;
|
|
default = "";
|
|
description = "Database password";
|
|
};
|
|
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = "Database host";
|
|
};
|
|
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 5432;
|
|
description = "Database port";
|
|
};
|
|
};
|
|
|
|
# uWSGI settings
|
|
processes = mkOption {
|
|
type = types.int;
|
|
default = 8;
|
|
description = "Number of uWSGI processes";
|
|
};
|
|
|
|
threads = mkOption {
|
|
type = types.int;
|
|
default = 4;
|
|
description = "Number of threads per process";
|
|
};
|
|
|
|
cheaperProcesses = mkOption {
|
|
type = types.int;
|
|
default = 2;
|
|
description = "Minimum number of processes";
|
|
};
|
|
|
|
disableLogging = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Disable uWSGI request logging";
|
|
};
|
|
|
|
extraUwsgiConfig = mkOption {
|
|
type = types.lines;
|
|
default = "";
|
|
description = "Additional uWSGI configuration";
|
|
};
|
|
|
|
extraSettings = mkOption {
|
|
type = types.attrsOf types.anything;
|
|
default = { };
|
|
description = "Additional Django settings";
|
|
};
|
|
|
|
# PostgreSQL integration
|
|
enablePostgreSQL = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Enable and configure PostgreSQL";
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
# PostgreSQL setup
|
|
services.postgresql = mkIf cfg.enablePostgreSQL {
|
|
enable = true;
|
|
ensureDatabases = [ cfg.database.name ];
|
|
ensureUsers = [{
|
|
name = cfg.database.user;
|
|
ensureDBOwnership = true;
|
|
}];
|
|
};
|
|
|
|
# Create user and group
|
|
users.users.${cfg.user} = {
|
|
isSystemUser = true;
|
|
group = cfg.group;
|
|
home = dataDir;
|
|
createHome = true;
|
|
};
|
|
|
|
users.groups.${cfg.group} = { };
|
|
|
|
# Create data directories
|
|
systemd.tmpfiles.rules = [
|
|
"d ${dataDir} 0750 ${cfg.user} ${cfg.group} -"
|
|
"d ${dataDir}/media 0750 ${cfg.user} ${cfg.group} -"
|
|
"d ${dataDir}/static 0750 ${cfg.user} ${cfg.group} -"
|
|
];
|
|
|
|
# Bitpoll service
|
|
systemd.services.bitpoll = {
|
|
description = "Bitpoll polling application";
|
|
after = [ "network.target" ] ++ optional cfg.enablePostgreSQL "postgresql.service";
|
|
wants = optional cfg.enablePostgreSQL "postgresql.service";
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
environment = {
|
|
DJANGO_SETTINGS_MODULE = "bitpoll.settings";
|
|
BITPOLL_SETTINGS_FILE = "${settingsFile}";
|
|
PYTHONPATH = "${cfg.package}/lib/python*/site-packages";
|
|
};
|
|
|
|
serviceConfig = {
|
|
Type = "notify";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
WorkingDirectory = dataDir;
|
|
ExecStart = "${pkgs.uwsgi}/bin/uwsgi --ini ${uwsgiConfig}";
|
|
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
|
KillMode = "mixed";
|
|
KillSignal = "SIGINT";
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
ReadWritePaths = [ dataDir ];
|
|
NoNewPrivileges = true;
|
|
|
|
# Security hardening
|
|
CapabilityBoundingSet = "";
|
|
DeviceAllow = "";
|
|
LockPersonality = true;
|
|
MemoryDenyWriteExecute = true;
|
|
PrivateDevices = true;
|
|
ProtectClock = true;
|
|
ProtectControlGroups = true;
|
|
ProtectHostname = true;
|
|
ProtectKernelLogs = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectProc = "invisible";
|
|
ProcSubset = "pid";
|
|
RemoveIPC = true;
|
|
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
SystemCallArchitectures = "native";
|
|
SystemCallFilter = [ "@system-service" "~@privileged @resources" ];
|
|
UMask = "0027";
|
|
};
|
|
|
|
# Pre-start script for migrations and static files
|
|
preStart = ''
|
|
# Wait for database to be ready
|
|
${optionalString cfg.enablePostgreSQL ''
|
|
while ! ${pkgs.postgresql}/bin/pg_isready -h ${cfg.database.host} -p ${toString cfg.database.port} -U ${cfg.database.user} -d ${cfg.database.name}; do
|
|
echo "Waiting for PostgreSQL..."
|
|
sleep 2
|
|
done
|
|
''}
|
|
|
|
# Run migrations
|
|
cd ${dataDir}
|
|
export DJANGO_SETTINGS_MODULE=bitpoll.settings
|
|
export BITPOLL_SETTINGS_FILE=${settingsFile}
|
|
export PYTHONPATH=${cfg.package}/lib/python*/site-packages
|
|
|
|
${cfg.package}/bin/python ${cfg.package}/manage.py migrate --noinput
|
|
${cfg.package}/bin/python ${cfg.package}/manage.py collectstatic --noinput --clear
|
|
|
|
# Set proper permissions
|
|
chown -R ${cfg.user}:${cfg.group} ${dataDir}
|
|
chmod -R u=rwX,g=rX,o= ${dataDir}
|
|
'';
|
|
};
|
|
|
|
# Open firewall ports if needed
|
|
networking.firewall.allowedTCPPorts = mkIf (cfg.httpPort != null) [ cfg.httpPort ];
|
|
};
|
|
}
|