540 lines
15 KiB
Nix
540 lines
15 KiB
Nix
# 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
|
|
]
|
|
++ lib.optionals (cfg.ldap != null)
|
|
indico.optional-dependencies.ldap);
|
|
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";
|
|
};
|
|
};
|
|
ldapConfig = {
|
|
uri = cfg.ldap.uri;
|
|
bind_dn = cfg.ldap.bindDN;
|
|
bind_password = cfg.ldap.bindPassword;
|
|
timeout = 30;
|
|
verify_cert = true;
|
|
page_size = 1500;
|
|
uid = cfg.ldap.uid;
|
|
user_base = cfg.ldap.userBaseDN;
|
|
user_filter = cfg.ldap.userFilter;
|
|
gid = cfg.ldap.gid;
|
|
group_base = cfg.ldap.groupBaseDN;
|
|
group_filter = cfg.ldap.groupFilter;
|
|
member_of_attr = cfg.ldap.memberOf;
|
|
ad_group_style = false;
|
|
};
|
|
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;
|
|
} // lib.optionalAttrs (cfg.ldap != null) {
|
|
AUTH_PROVIDERS = {
|
|
ldap = {
|
|
type = "ldap";
|
|
title = "LDAP";
|
|
ldap = ldapConfig;
|
|
default = true;
|
|
};
|
|
};
|
|
IDENTITY_PROVIDERS = {
|
|
ldap = {
|
|
type = "ldap";
|
|
title = "LDAP";
|
|
ldap = ldapConfig;
|
|
mapping = cfg.ldap.mapping;
|
|
trusted_email = cfg.ldap.trustedEmail;
|
|
synced_fields = cfg.ldap.syncedFields;
|
|
};
|
|
};
|
|
});
|
|
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.";
|
|
};
|
|
|
|
ldap = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.submodule {
|
|
options = {
|
|
uri = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "LDAP server URI.";
|
|
};
|
|
|
|
bindDN = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "LDAP server bind DN.";
|
|
};
|
|
|
|
bindPassword = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "LDAP server bind password.";
|
|
};
|
|
|
|
uid = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "uid";
|
|
description = "LDAP UID attribute.";
|
|
};
|
|
|
|
userBaseDN = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "LDAP users base DN.";
|
|
};
|
|
|
|
userFilter = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "LDAP users filter.";
|
|
};
|
|
|
|
gid = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "gid";
|
|
description = "LDAP GID attribute.";
|
|
};
|
|
|
|
groupBaseDN = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "LDAP groups base DN.";
|
|
};
|
|
|
|
groupFilter = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "LDAP groups filter.";
|
|
};
|
|
|
|
memberOf = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "memberOf";
|
|
description = "LDAP memberOf attribute.";
|
|
};
|
|
|
|
mapping = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.str;
|
|
default = {
|
|
first_name = "givenName";
|
|
last_name = "sn";
|
|
email = "mail";
|
|
affiliation = "company";
|
|
phone = "telephoneNumber";
|
|
};
|
|
description = "Mapping between local and LDAP fields.";
|
|
};
|
|
|
|
trustedEmail = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Whether LDAP emails are to be trusted.";
|
|
};
|
|
|
|
syncedFields = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ "first_name" "last_name" ];
|
|
description = "Fields to sync with LDAP server.";
|
|
};
|
|
};
|
|
});
|
|
default = null;
|
|
description = "LDAP authentication and identity provider configuration.";
|
|
};
|
|
};
|
|
|
|
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 --preserve-env=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;
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|