Initial Bitpoll Nix package and service
This commit is contained in:
commit
0b3e086c03
5 changed files with 898 additions and 0 deletions
349
module.nix
Normal file
349
module.nix
Normal file
|
@ -0,0 +1,349 @@
|
|||
{ 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 ];
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue