diff --git a/modules/indico.nix b/modules/indico.nix new file mode 100644 index 0000000..2431535 --- /dev/null +++ b/modules/indico.nix @@ -0,0 +1,423 @@ +# Inspired by https://git.hubrecht.ovh/hubrecht/nix-infra +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.indico; + pythonFmt = pkgs.formats.pythonVars { }; + yamlFmt = pkgs.formats.yaml {}; + indico = pkgs.callPackage ../pkgs/indico { }; + pythonEnv = pkgs.python3.withPackages (ps: [ + (ps.toPythonModule indico) + ps.gunicorn + ]); + redisSocket = config.services.redis.servers.${cfg.redis.name}.unixSocket; + indicoSocket = "/run/indico/indico.sock"; + baseDir = "${pythonEnv}/${pythonEnv.sitePackages}/indico"; + loggingFile = yamlFmt.generate "logging.yaml" { + version = 1; + root = { + level = "INFO"; + handlers = [ "stderr" ]; + }; + loggers = { + indico.handlers = [ "stderr" ]; + celery.handlers = [ "stderr" ]; + }; + handlers.stderr = { + class = "logging.StreamHandler"; + formatter = "default"; + }; + formatters.default = { + format = "%(levelname)s %(request_id)s %(user_id)s %(name)s %(message)s"; + }; + }; + configFile = pythonFmt.generate "indico.conf" { + SQLALCHEMY_DATABASE_URI = cfg.database; + CACHE_DIR = "${cfg.stateDir}/cache"; + TEMP_DIR = "${cfg.stateDir}/tmp"; + LOG_DIR = "/dev/null"; + STATIC_FILE_METHOD = [ + "xaccelredirect" + { ${cfg.stateDir} = "/.xsf/indico"; } + ]; + STORAGE_BACKENDS = { + default = "fs:${cfg.storageDir}"; + }; + DEFAULT_LOCALE = cfg.defaultLocale; + DEFAULT_TIMEZONE = cfg.defaultTimezone; + REDIS_CACHE_URL = cfg.cacheRedis; + CELERY_BROKER = cfg.celeryBrokerRedis; + USE_PROXY = true; + BASE_URL = cfg.baseUrl; + SECRET_KEY = cfg.secretKey; + LOGGING_CONFIG_FILE = loggingFile; + NO_REPLY_EMAIL = cfg.email.noReply; + SUPPORT_EMAIL = cfg.email.support; + SMTP_SENDER_FALLBACK = cfg.email.senderFallback; + PUBLIC_SUPPORT_EMAIL = cfg.email.publicSupport; + SMTP_SERVER = [ cfg.email.smtp.host cfg.email.smtp.port ]; + SMTP_LOGIN = cfg.email.smtp.login; + SMTP_PASSWORD = cfg.email.smtp.password; + SMTP_USE_TLS = cfg.email.smtp.useTLS; + }; +in +{ + # TODO cProfile; indico standalone command is *very* slow + # (~30s just to print the help) + + options.services.indico = { + enable = lib.mkEnableOption "indico"; + + # TODO use + package = lib.mkPackageOption pkgs "indico" { }; + + user = lib.mkOption { + type = lib.types.str; + default = "indico"; + description = "User under which indico should run."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "indico"; + description = "Group under which indico should run."; + }; + + email = { + noReply = lib.mkOption { + type = lib.types.str; + description = "No-reply email address."; + }; + + senderFallback = lib.mkOption { + type = lib.types.str; + default = cfg.email.noReply; + description = "Sender fallback email address."; + }; + + publicSupport = lib.mkOption { + type = lib.types.str; + description = "Public support email address."; + }; + + support = lib.mkOption { + type = lib.types.str; + description = "Support email address."; + }; + + smtp = { + host = lib.mkOption { + type = lib.types.str; + description = "SMTP host."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 587; + description = "SMTP port."; + }; + + useTLS = lib.mkOption { + type = lib.types.bool; + default = true; + description = "SMTP use TLS."; + }; + + login = lib.mkOption { + type = lib.types.str; + description = "SMTP login."; + }; + + password = lib.mkOption { + type = lib.types.str; + description = "SMTP password."; + }; + }; + }; + + secretKey = lib.mkOption { + type = lib.types.str; + description = "Secret key."; + }; + + baseUrl = lib.mkOption { + type = lib.types.str; + default = lib.optionalString + cfg.nginx.enable + "https://${cfg.nginx.domain}"; + description = "Base URL."; + }; + + defaultLocale = lib.mkOption { + type = lib.types.str; + default = "fr_FR"; + description = "Default locale."; + }; + + defaultTimezone = lib.mkOption { + type = lib.types.str; + default = "Europe/Paris"; + description = "Default timezone."; + }; + + nginx = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to setup an NGinx virtual host."; + }; + + domain = lib.mkOption { + type = lib.types.str; + description = "NGinx virtual host domain."; + }; + }; + + redis = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to setup a Redis server."; + }; + + name = lib.mkOption { + type = lib.types.str; + default = "indico"; + description = "Redis server name."; + }; + }; + + cacheRedis = lib.mkOption { + type = lib.types.str; + default = lib.optionalString cfg.redis.enable + "unix://${redisSocket}?db=0"; + description = "Redis cache URL."; + }; + + celeryBrokerRedis = lib.mkOption { + type = lib.types.str; + default = lib.optionalString cfg.redis.enable + "redis+socket://${redisSocket}?virtual_host=1"; + description = "Redis Celery broker URL."; + }; + + stateDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/indico"; + description = '' + State directory (used for cache and temporary files). + ''; + }; + + storageDir = lib.mkOption { + type = lib.types.path; + default = "${cfg.stateDir}/storage"; + description = "Storage directory."; + }; + + postgresql = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to setup a PostgreSQL server."; + }; + }; + + database = lib.mkOption { + type = lib.types.str; + default = lib.optionalString + cfg.postgresql.enable + "postgresql://@/${cfg.user}?host=/run/postgresql"; + description = "Database URL."; + }; + }; + + config = lib.mkIf cfg.enable { + users.users = { + ${cfg.user} = { + isSystemUser = true; + group = cfg.group; + }; + } // (lib.optionalAttrs cfg.nginx.enable { + ${config.services.nginx.user}.extraGroups = [ cfg.group ]; + }); + + users.groups.${cfg.group} = { }; + + services.postgresql = lib.mkIf cfg.postgresql.enable { + enable = true; + package = pkgs.postgresql_16; # TODO add setting + # TODO db name != cfg.user + ensureDatabases = [ cfg.user ]; + ensureUsers = [ + { + name = cfg.user; + ensureDBOwnership = true; + } + ]; + }; + + environment.systemPackages = + let + indico = pkgs.writeScriptBin "indico" '' + #!${pkgs.runtimeShell} + sudo=exec + if [[ "$USER" != "indico" ]]; then + sudo='exec /run/wrappers/bin/sudo -u indico -E INDICO_CONFIG' + fi + export INDICO_CONFIG=${configFile} + $sudo ${lib.getExe' pythonEnv "indico"} "$@" + ''; + in + [ indico ]; + + services.redis = lib.mkIf cfg.redis.enable { + servers.${cfg.redis.name} = { + enable = true; + group = cfg.group; + }; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/cache' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/tmp' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.storageDir}' 0750 ${cfg.user} ${cfg.group} - -" + ]; + + systemd.services = + let + psqlExtensionsCommands = '' + CREATE EXTENSION IF NOT EXISTS unaccent; + CREATE EXTENSION IF NOT EXISTS pg_trgm; + ''; + # TODO StateDirectory, CacheDirectory? + common = { + environment.INDICO_CONFIG = configFile; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Group = cfg.group; + User = cfg.user; + # Restart = "on-failure"; + RuntimeDirectory = "indico"; + }; + }; + in + { + indico-db = lib.recursiveUpdate common { + description = "Indico database preparation and upgrade"; + after = [ + "postgresql.service" + ]; + serviceConfig.Type = "oneshot"; + # Source: pretalx module ; passer par un service oneshot + script = '' + versionFile="${cfg.stateDir}/version" + if [[ ! -f "$versionFile" ]]; then + ${lib.getExe' config.services.postgresql.package "psql"} \ + -d "${cfg.database}" \ + -c "${psqlExtensionsCommands}" + ${lib.getExe' pythonEnv "indico"} db prepare + echo "${indico.version}" > "$versionFile" + fi + version="$(cat "$versionFile" 2>/dev/null || echo 0)" + # FIXME: not tested; may not work + if [[ "$version" != "${indico.version}" ]]; then + ${lib.getExe' pythonEnv "indico"} db upgrade + echo "${indico.version}" > "$versionFile" + fi + ''; + }; + + indico-worker = lib.recursiveUpdate common { + description = "Indico background worker"; + after = [ + "network.target" + "redis-indico.service" + "postgresql.service" + "indico-db.service" + ]; + serviceConfig.ExecStart = '' + ${lib.getExe' pythonEnv "indico"} celery worker -B + ''; + }; + + indico-web = lib.recursiveUpdate common { + description = "Indico web service"; + after = [ + "network.target" + "redis-indico.service" + "postgresql.service" + "indico-worker.service" + "indico-db.service" + ]; + # TODO bind on a TCP socket when cfg.nginx.enable == false? + serviceConfig.ExecStart = '' + ${lib.getExe' pythonEnv "gunicorn"} \ + --bind unix:${indicoSocket} \ + --name=indico \ + indico.web.wsgi + ''; + }; + }; + + systemd.sockets = lib.mkIf cfg.nginx.enable { + indico-web.socketConfig = { + ListenStream = indicoSocket; + SocketUser = config.services.nginx.user; + }; + }; + + services.nginx = lib.mkIf cfg.nginx.enable { + enable = true; + recommendedProxySettings = lib.mkDefault true; + recommendedOptimisation = lib.mkDefault true; + recommendedTlsSettings = lib.mkDefault true; + recommendedGzipSettings = lib.mkDefault true; + upstreams.indico.servers."unix:${indicoSocket}" = { }; + virtualHosts.${cfg.nginx.domain} = { + enableACME = true; + forceSSL = true; + locations = { + "/.xsf/indico/" = { + alias = "${cfg.stateDir}/"; + extraConfig = '' + internal; + ''; + }; + "~ ^/(images|fonts)(.*)/(.+?)(__v[0-9a-f]+)?\\.([^.]+)$" = { + alias = "${baseDir}/web/static/$1$2/$3.$5"; + priority = 900; + extraConfig = '' + access_log off; + ''; + }; + "~ ^/(css|dist|images|fonts)/(.*)$" = { + alias = "${baseDir}/web/static/$1/$2"; + extraConfig = '' + access_log off; + ''; + }; + "= /robots.txt" = { + alias = "${baseDir}/web/static/robots.txt"; + extraConfig = '' + access_log off; + ''; + }; + "/" = { + proxyPass = "http://indico"; + # FIXME is it useful? + extraConfig = '' + client_max_body_size 1G; + ''; + }; + }; + }; + }; + }; +} diff --git a/profiles/indico.nix b/profiles/indico.nix index 8f6350e..dda3b6d 100644 --- a/profiles/indico.nix +++ b/profiles/indico.nix @@ -1,116 +1,29 @@ -{ config, lib, pkgs, python3, ... }: -let - # cfg = config.services.indico; - # pythonFmt = pkgs.formats.pythonVars { }; - indico = pkgs.callPackage ../pkgs/indico { }; - pythonEnv = indico.python.withPackages (ps: [ - indico - # (ps.toPythonModule indico) - ps.gunicorn - ]); -in { - # TODO cProfile; indico is *very* slow (~30s just to print the help) - - # + sudo indico - environment.systemPackages = [ indico ]; - - services.redis.servers.indico.enable = true; - - systemd.services.indico-web = { - description = "Indico web service"; - after = [ - "network.target" - "redis-indico.service" - "postgresql.service" - ]; - wantedBy = [ "multi-user.target" ]; - # TODO migrations - serviceConfig = { - User = "indico"; - Group = "indico"; - ExecStart = "${lib.getExe' pythonEnv "gunicorn"} --bind unix:/run/indico/indico.sock --name=indico indico.wsgi"; - Restart = "on-failure"; - }; - enable = true; - }; - - systemd.sockets.gunicorn-web = { - socketConfig = { - ListenStream = "/run/indico/indico.sock"; - SocketUser = "nginx"; - }; - enable = true; - }; - # preStart = '' - # echo "create extension if not exists pg_trgm" | runuser -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra - # '' - - services.postgresql = { - enable = true; - package = pkgs.postgresql_16; - ensureDatabases = [ "indico" ]; - ensureUsers = [ - { - name = "indico"; - ensureDBOwnership = true; - } - ]; - }; + config, + lib, + pkgs, + ... +}: +{ + imports = [ + ../modules/indico.nix + ]; networking.firewall.allowedTCPPorts = [ 80 443 ]; - services.nginx = let - indicoBaseDir = "/tmp"; - in { + services.indico = { enable = true; - recommendedTlsSettings = true; - recommendedOptimisation = true; - recommendedGzipSettings = true; - recommendedProxySettings = true; - upstreams.indico.servers."unix:/run/indico/indico.sock" = { }; - virtualHosts."events.federez.net" = { - enableACME = true; - forceSSL = true; - locations = { - "/.xsf/indico/" = { - alias = "${indicoBaseDir}/"; - extraConfig = '' - internal; - ''; - }; - # Order? + too lax? - "~ ^/(images|fonts)(.*)/(.+?)(__v[0-9a-f]+)?\\.([^.]+)$" = { - alias = "${indicoBaseDir}/web/static/$1$2/$3.$5"; - extraConfig = '' - access_log off; - ''; - }; - "~ ^/(css|dist|images|fonts)/(.*)$" = { - alias = "${indicoBaseDir}/web/static/$1/$2"; - extraConfig = '' - access_log off; - ''; - }; - "= /robots.txt" = { - alias = "${indicoBaseDir}/web/static/robots.txt"; - extraConfig = '' - access_log off; - ''; - "/" = { - proxyPass = "http://indico"; - extraConfig = '' - client_max_body_size 1G; - ''; - }; + nginx.domain = "events.federez.net"; + email = { + noReply = "indico@federez.net"; + support = "admin@federez.net"; + publicSupport = "bureau@federez.net"; + smtp = { + host = "dodecagon.federez.net"; + login = "indico"; + password = "xxx"; }; }; + secretKey = "lQsViT9292sIkObP9ptQADGJ16bk58n7"; # FIXME: dev only }; - - users.users.indico = { - isSystemUser = true; - group = "indico"; - }; - - users.groups.indico = {}; }