at master 13 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 utils, 6 ... 7}: 8let 9 inherit (lib) 10 getExe 11 mapAttrs 12 mkEnableOption 13 mkIf 14 mkPackageOption 15 mkOption 16 types 17 optional 18 optionalString 19 ; 20 21 cfg = config.services.lasuite-meet; 22 23 pythonEnvironment = mapAttrs ( 24 _: value: 25 if value == null then 26 "None" 27 else if value == true then 28 "True" 29 else if value == false then 30 "False" 31 else 32 toString value 33 ) cfg.settings; 34 35 commonServiceConfig = { 36 RuntimeDirectory = "lasuite-meet"; 37 StateDirectory = "lasuite-meet"; 38 WorkingDirectory = "/var/lib/lasuite-meet"; 39 40 User = "lasuite-meet"; 41 DynamicUser = true; 42 SupplementaryGroups = mkIf cfg.redis.createLocally [ 43 config.services.redis.servers.lasuite-meet.group 44 ]; 45 # hardening 46 AmbientCapabilities = ""; 47 CapabilityBoundingSet = [ "" ]; 48 DevicePolicy = "closed"; 49 LockPersonality = true; 50 NoNewPrivileges = true; 51 PrivateDevices = true; 52 PrivateTmp = true; 53 PrivateUsers = true; 54 ProcSubset = "pid"; 55 ProtectClock = true; 56 ProtectControlGroups = true; 57 ProtectHome = true; 58 ProtectHostname = true; 59 ProtectKernelLogs = true; 60 ProtectKernelModules = true; 61 ProtectKernelTunables = true; 62 ProtectProc = "invisible"; 63 ProtectSystem = "strict"; 64 RemoveIPC = true; 65 RestrictAddressFamilies = [ 66 "AF_INET" 67 "AF_INET6" 68 "AF_UNIX" 69 ]; 70 RestrictNamespaces = true; 71 RestrictRealtime = true; 72 RestrictSUIDSGID = true; 73 SystemCallArchitectures = "native"; 74 MemoryDenyWriteExecute = true; 75 EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile; 76 UMask = "0077"; 77 }; 78in 79{ 80 options.services.lasuite-meet = { 81 enable = mkEnableOption "SuiteNumérique Meet"; 82 83 backendPackage = mkPackageOption pkgs "lasuite-meet" { }; 84 85 frontendPackage = mkPackageOption pkgs "lasuite-meet-frontend" { }; 86 87 bind = mkOption { 88 type = types.str; 89 default = "unix:/run/lasuite-meet/gunicorn.sock"; 90 example = "127.0.0.1:8000"; 91 description = '' 92 The path, host/port or file descriptior to bind the gunicorn socket to. 93 94 See <https://docs.gunicorn.org/en/stable/settings.html#bind> for possible options. 95 ''; 96 }; 97 98 enableNginx = mkEnableOption "enable and configure Nginx for reverse proxying" // { 99 default = true; 100 }; 101 102 secretKeyPath = mkOption { 103 type = types.nullOr types.path; 104 default = null; 105 description = '' 106 Path to the Django secret key. 107 108 The key can be generated using: 109 ``` 110 python3 -c 'import secrets; print(secrets.token_hex())' 111 ``` 112 113 If not set, the secret key will be automatically generated. 114 ''; 115 }; 116 117 postgresql = { 118 createLocally = mkEnableOption "Configure local PostgreSQL database server for meet"; 119 }; 120 121 redis = { 122 createLocally = mkEnableOption "Configure local Redis cache server for meet"; 123 }; 124 125 livekit = { 126 enable = mkEnableOption "Configure local livekit server" // { 127 default = true; 128 }; 129 130 openFirewall = mkEnableOption "Open firewall ports for livekit"; 131 132 keyFile = mkOption { 133 type = lib.types.path; 134 description = '' 135 LiveKit key file holding one or multiple application secrets. 136 Use `livekit-server generate-keys` to generate a random key name and secret. 137 138 The file should have the YAML format `<keyname>: <secret>`. 139 Example: 140 `lasuite-meet: f6lQGaHtM5HfgZjIcec3cOCRfiDqIine4CpZZnqdT5cE` 141 142 Individual key/secret pairs need to be passed to clients to connect to this instance. 143 ''; 144 145 }; 146 147 settings = mkOption { 148 type = types.attrs; 149 default = { }; 150 description = '' 151 Settings to pass to the livekit server. 152 153 See `services.livekit.settings` for more details. 154 ''; 155 }; 156 }; 157 158 gunicorn = { 159 extraArgs = mkOption { 160 type = types.listOf types.str; 161 default = [ 162 "--name=meet" 163 "--workers=3" 164 ]; 165 description = '' 166 Extra arguments to pass to the gunicorn process. 167 ''; 168 }; 169 }; 170 171 celery = { 172 extraArgs = mkOption { 173 type = types.listOf types.str; 174 default = [ ]; 175 description = '' 176 Extra arguments to pass to the celery process. 177 ''; 178 }; 179 }; 180 181 domain = mkOption { 182 type = types.str; 183 description = '' 184 Domain name of the meet instance. 185 ''; 186 }; 187 188 settings = mkOption { 189 type = types.submodule { 190 freeformType = types.attrsOf ( 191 types.nullOr ( 192 types.oneOf [ 193 types.str 194 types.bool 195 types.path 196 types.int 197 ] 198 ) 199 ); 200 201 options = { 202 DJANGO_CONFIGURATION = mkOption { 203 type = types.str; 204 internal = true; 205 default = "Production"; 206 description = "The configuration that Django will use"; 207 }; 208 209 DJANGO_SETTINGS_MODULE = mkOption { 210 type = types.str; 211 internal = true; 212 default = "meet.settings"; 213 description = "The configuration module that Django will use"; 214 }; 215 216 DJANGO_SECRET_KEY_FILE = mkOption { 217 type = types.path; 218 default = 219 if cfg.secretKeyPath == null then "/var/lib/lasuite-meet/django_secret_key" else cfg.secretKeyPath; 220 description = "The path to the file containing Django's secret key"; 221 }; 222 223 DJANGO_DATA_DIR = mkOption { 224 type = types.path; 225 default = "/var/lib/lasuite-meet"; 226 description = "Path to the data directory"; 227 }; 228 229 DJANGO_ALLOWED_HOSTS = mkOption { 230 type = types.str; 231 default = if cfg.enableNginx then "localhost,127.0.0.1,${cfg.domain}" else ""; 232 defaultText = lib.literalExpression '' 233 if cfg.enableNginx then "localhost,127.0.0.1,''${cfg.domain}" else "" 234 ''; 235 description = "Comma-separated list of hosts that are able to connect to the server"; 236 }; 237 238 DB_NAME = mkOption { 239 type = types.str; 240 default = "lasuite-meet"; 241 description = "Name of the database"; 242 }; 243 244 DB_USER = mkOption { 245 type = types.str; 246 default = "lasuite-meet"; 247 description = "User of the database"; 248 }; 249 250 DB_HOST = mkOption { 251 type = types.nullOr types.str; 252 default = if cfg.postgresql.createLocally then "/run/postgresql" else null; 253 description = "Host of the database"; 254 }; 255 256 REDIS_URL = mkOption { 257 type = types.nullOr types.str; 258 default = 259 if cfg.redis.createLocally then 260 "unix://${config.services.redis.servers.lasuite-meet.unixSocket}?db=0" 261 else 262 null; 263 description = "URL of the redis backend"; 264 }; 265 266 CELERY_BROKER_URL = mkOption { 267 type = types.nullOr types.str; 268 default = 269 if cfg.redis.createLocally then 270 "redis+socket://${config.services.redis.servers.lasuite-meet.unixSocket}?db=1" 271 else 272 null; 273 description = "URL of the redis backend for celery"; 274 }; 275 276 LIVEKIT_API_URL = mkOption { 277 type = types.nullOr types.str; 278 default = if cfg.enableNginx && cfg.livekit.enable then "http://${cfg.domain}/livekit" else null; 279 defaultText = lib.literalExpression '' 280 if cfg.enableNginx && cfg.livekit.enable then 281 "http://$${cfg.domain}/livekit" 282 else 283 null 284 ''; 285 description = "URL to the livekit server"; 286 }; 287 }; 288 }; 289 290 default = { }; 291 example = '' 292 { 293 DJANGO_ALLOWED_HOSTS = "*"; 294 } 295 ''; 296 description = '' 297 Configuration options of meet. 298 See https://github.com/suitenumerique/meet/blob/v${cfg.backendPackage.version}/docs/env.md 299 `REDIS_URL` and `CELERY_BROKER_URL` are set if `services.lasuite-meet.redis.createLocally` is true. 300 `DB_NAME` `DB_USER` and `DB_HOST` are set if `services.lasuite-meet.postgresql.createLocally` is true. 301 ''; 302 }; 303 304 environmentFile = mkOption { 305 type = types.nullOr types.path; 306 default = null; 307 description = '' 308 Path to environment file. 309 310 This can be useful to pass secrets to meet via tools like `agenix` or `sops`. 311 ''; 312 }; 313 }; 314 315 config = mkIf cfg.enable { 316 systemd.services.lasuite-meet = { 317 description = "Meet from SuiteNumérique"; 318 after = [ 319 "network.target" 320 ] 321 ++ (optional cfg.postgresql.createLocally "postgresql.service") 322 ++ (optional cfg.redis.createLocally "redis-lasuite-meet.service"); 323 324 wants = 325 (optional cfg.postgresql.createLocally "postgresql.service") 326 ++ (optional cfg.redis.createLocally "redis-lasuite-meet.service"); 327 328 wantedBy = [ "multi-user.target" ]; 329 330 preStart = '' 331 if [ ! -f .version ]; then 332 touch .version 333 fi 334 335 ${optionalString (cfg.secretKeyPath == null) '' 336 if [[ ! -f /var/lib/lasuite-meet/django_secret_key ]]; then 337 ( 338 umask 0377 339 tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge /var/lib/lasuite-meet/django_secret_key 340 ) 341 fi 342 ''} 343 if [ "${cfg.backendPackage.version}" != "$(cat .version)" ]; then 344 ${getExe cfg.backendPackage} migrate 345 echo -n "${cfg.backendPackage.version}" > .version 346 fi 347 ''; 348 349 environment = pythonEnvironment; 350 351 serviceConfig = { 352 BindReadOnlyPaths = "${cfg.backendPackage}/share/static:/var/lib/lasuite-meet/static"; 353 354 ExecStart = utils.escapeSystemdExecArgs ( 355 [ 356 (lib.getExe' cfg.backendPackage "gunicorn") 357 "--bind=${cfg.bind}" 358 ] 359 ++ cfg.gunicorn.extraArgs 360 ++ [ "meet.wsgi:application" ] 361 ); 362 } 363 // commonServiceConfig; 364 }; 365 366 systemd.services.lasuite-meet-celery = { 367 description = "Meet Celery broker from SuiteNumérique"; 368 after = [ 369 "network.target" 370 ] 371 ++ (optional cfg.postgresql.createLocally "postgresql.service") 372 ++ (optional cfg.redis.createLocally "redis-lasuite-meet.service"); 373 374 wants = 375 (optional cfg.postgresql.createLocally "postgresql.service") 376 ++ (optional cfg.redis.createLocally "redis-lasuite-meet.service"); 377 378 wantedBy = [ "multi-user.target" ]; 379 380 environment = pythonEnvironment; 381 382 serviceConfig = { 383 ExecStart = utils.escapeSystemdExecArgs ( 384 [ (lib.getExe' cfg.backendPackage "celery") ] 385 ++ cfg.celery.extraArgs 386 ++ [ 387 "--app=meet.celery_app" 388 "worker" 389 ] 390 ); 391 } 392 // commonServiceConfig; 393 }; 394 395 services.postgresql = mkIf cfg.postgresql.createLocally { 396 enable = true; 397 ensureDatabases = [ "lasuite-meet" ]; 398 ensureUsers = [ 399 { 400 name = "lasuite-meet"; 401 ensureDBOwnership = true; 402 } 403 ]; 404 }; 405 406 services.redis.servers.lasuite-meet = mkIf cfg.redis.createLocally { enable = true; }; 407 408 services.livekit = mkIf cfg.livekit.enable { 409 inherit (cfg.livekit) 410 enable 411 settings 412 keyFile 413 openFirewall 414 ; 415 }; 416 417 services.nginx = mkIf cfg.enableNginx { 418 enable = true; 419 420 virtualHosts.${cfg.domain} = { 421 root = cfg.frontendPackage; 422 423 extraConfig = '' 424 error_page 404 = /index.html; 425 ''; 426 427 locations."/api" = { 428 proxyPass = "http://${cfg.bind}"; 429 recommendedProxySettings = true; 430 }; 431 432 locations."/admin" = { 433 proxyPass = "http://${cfg.bind}"; 434 recommendedProxySettings = true; 435 }; 436 437 locations."/static" = { 438 root = "${cfg.backendPackage}/share"; 439 }; 440 441 locations."/livekit" = mkIf cfg.livekit.enable { 442 proxyPass = "http://localhost:${toString config.services.livekit.settings.port}"; 443 recommendedProxySettings = true; 444 proxyWebsockets = true; 445 extraConfig = '' 446 rewrite ^/livekit/(.*)$ /$1 break; 447 ''; 448 }; 449 }; 450 }; 451 }; 452 453 meta = { 454 buildDocsInSandbox = false; 455 maintainers = [ lib.maintainers.soyouzpanda ]; 456 }; 457}