diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..a75461d29 --- /dev/null +++ b/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1625281901, + "narHash": "sha256-DkZDtTIPzhXATqIps2ifNFpnI+PTcfMYdcrx/oFm00Q=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "09c38c29f2c719cd76ca17a596c2fdac9e186ceb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "utils": "utils" + } + }, + "utils": { + "locked": { + "lastModified": 1623875721, + "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..07788df23 --- /dev/null +++ b/flake.nix @@ -0,0 +1,32 @@ +{ + description = "Bookwyrm decentralized reading and reviewing server"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + utils = { + url = "github:numtide/flake-utils"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + }; + + outputs = { self, nixpkgs, utils }: + utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + in { + packages.bookwyrm = pkgs.callPackage ./nix/default.nix { }; + + defaultPackage = self.packages.${system}.bookwyrm; + } + ) // + { + nixosModule = {pkgs, ...}@args: + import ./nix/moduleInner.nix (args // + { bookwyrm = self.packages.${pkgs.system}.bookwyrm; }); + }; +} diff --git a/nix/module.nix b/nix/module.nix index 7ebd56b0b..36092c455 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -1,467 +1,14 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - bookwyrm = (pkgs.callPackage ./default.nix { }); - cfg = config.services.bookwyrm; - celeryRedisPath = if cfg.celeryRedis.unixSocket != null then - "redis+socket://${cfg.celeryRedis.unixSocket}?virtual_host=0" - else - "redis://${cfg.celeryRedis.host}:${toString cfg.celeryRedis.port}/0"; - env = { - DEBUG = if cfg.debug then "true" else "false"; - DOMAIN = cfg.domain; - ALLOWED_HOSTS = lib.concatStringsSep "," cfg.allowedHosts; - BOOKWYRM_DATABASE_BACKEND = "postgres"; - MEDIA_ROOT = (builtins.toPath cfg.stateDir) + "/images"; - STATIC_ROOT = (builtins.toPath cfg.stateDir) + "/static"; - POSTGRES_HOST = cfg.database.host; - POSTGRES_USER = cfg.database.user; - POSTGRES_PORT = (toString cfg.database.port); - POSTGRES_DB = cfg.database.database; - REDIS_ACTIVITY_HOST = cfg.activityRedis.host; - REDIS_ACTIVITY_PORT = (toString cfg.activityRedis.port); - REDIS_ACTIVITY_SOCKET = (if cfg.activityRedis.unixSocket != null then cfg.activityRedis.unixSocket else ""); - CELERY_BROKER = celeryRedisPath; - CELERY_RESULT_BACKEND = celeryRedisPath; - EMAIL_HOST = cfg.email.host; - EMAIL_PORT = (toString cfg.email.port); - EMAIL_HOST_USER = cfg.email.user; - EMAIL_USE_TLS = if cfg.email.useTLS then "true" else "false"; - OL_URL = "https://openlibrary.org"; - }; - # mapping of env variable → its secret file - envSecrets = (filterAttrs (_: v: v != null) { - SECRET_KEY = cfg.secretKeyFile; - POSTGRES_PASSWORD = cfg.database.passwordFile; - EMAIL_HOST_PASSWORD = cfg.email.passwordFile; - }); - - redisCreateLocally = cfg.celeryRedis.createLocally || cfg.activityRedis.createLocally; - - loadEnv = (pkgs.writeScript "load-bookwyrm-env" '' - #!/usr/bin/env bash - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: "export ${n}=${lib.escapeShellArg v}") env)} - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''export ${n}="$(cat ${lib.escapeShellArg v})"'') envSecrets)} - ''); - bookwyrmManageScript = (pkgs.writeScriptBin "bookwyrm-manage" '' - #!/usr/bin/env bash - source ${loadEnv} - exec ${bookwyrm}/bin/python ${bookwyrm}/manage.py "$@" - ''); -in -{ - options.services.bookwyrm = { - enable = mkEnableOption "bookwyrm"; - - user = mkOption { - type = types.str; - default = "bookwyrm"; - description = "User to run bookwyrm as."; - }; - - group = mkOption { - type = types.str; - default = "bookwyrm"; - description = "Group to run bookwyrm as."; - }; - - stateDir = mkOption { - type = types.str; - default = "/var/lib/bookwyrm"; - description = "Data directory for Bookwyrm."; - }; - - bindTo = mkOption { - type = types.listOf types.str; - default = [ "unix:/run/bookwyrm/bookwyrm.sock" ]; - example = [ "unix:/run/bookwyrm-gunicorn.sock" "127.0.0.1:12345" ]; - description = "List of sockets for Gunicorn to bind to"; - }; - - flowerArgs = mkOption { - type = types.listOf types.str; - default = [ "--unix_socket=/run/bookwyrm/bookwyrm-flower.sock" ]; - description = "Arguments to pass to Flower (Celery management frontend)."; - }; - - secretKey = mkOption { - type = types.str; - default = ""; - description = '' - Secret key, for Django to use for cryptographic signing. This is stored - in plain text in the Nix store. Consider using - instead. - ''; - }; - - secretKeyFile = mkOption { - type = types.nullOr types.path; - default = null; - example = "/run/secrets/bookwyrm-key"; - description = '' - A file containing the secret key, corrsponding to - . - ''; - }; - - domain = mkOption { - type = types.str; - example = "bookwyrm.example.com"; - description = '' - Domain under which Bookwyrm is running; - ''; - }; - - allowedHosts = mkOption { - type = types.listOf types.str; - default = [ "*" ]; - example = [ "bookwyrm.example.com" ]; - description = '' - List of hosts to allow, checked against the Host: - header. Leave default to accept all hosts. - ''; - }; - - debug = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enable debug mode. Debug mode should not be used in - production, but provides more verbose logging. - ''; - }; - - database = { - createLocally = mkOption { - type = types.bool; - default = true; - description = "Create the database and user locally."; - }; - - host = mkOption { - type = types.str; - default = ""; - description = '' - Postgresql host address. Set to empty string "" to - use unix sockets. - ''; - }; - - port = mkOption { - type = types.int; - default = config.services.postgresql.port; - description = "Port of the Postgresql server to connect to."; - }; - - database = mkOption { - type = types.str; - default = "bookwyrm"; - description = "Name of the database to use."; - }; - - user = mkOption { - type = types.str; - default = "bookwyrm"; - description = "Database role to use for bookwyrm"; - }; - - password = mkOption { - type = types.str; - default = ""; - description = '' - Password to use for database authentication. This will be stored in the - Nix store; consider using instead. - ''; - }; - - passwordFile = mkOption { - type = types.nullOr types.path; - default = null; - example = "/run/secrets/bookwyrm-db-password"; - description = '' - A file containing the database password corresponding to - . - ''; - }; - }; - - activityRedis = { - createLocally = mkOption { - type = types.bool; - default = true; - description = "Ensure Redis is running locally and use it."; - }; - - # note that there are assertions to prevent three of these being null - host = mkOption { - type = types.nullOr types.str; - default = "127.0.0.1"; - description = "Activity Redis host address."; - }; - - port = mkOption { - type = types.nullOr types.int; - default = config.services.redis.port; - description = "Activity Redis port."; - }; - - unixSocket = mkOption { - type = types.nullOr types.str; - default = null; - example = "/run/redis/redis.sock"; - description = '' - Unix socket to connect to for activity Redis. When set, the host and - port option will be ignored. - ''; - }; - }; - - celeryRedis = { - createLocally = mkOption { - type = types.bool; - default = true; - description = "Ensure Redis is running locally and use it."; - }; - - # note that there are assertions to prevent all three of these being null - host = mkOption { - type = types.nullOr types.str; - default = "127.0.0.1"; - description = "Activity Redis host address."; - }; - - port = mkOption { - type = types.nullOr types.int; - default = config.services.redis.port; - description = "Activity Redis port."; - }; - - unixSocket = mkOption { - type = types.nullOr types.str; - default = null; - example = "/run/redis/redis.sock"; - description = '' - Unix socket to connect to for celery Redis. When set, the host and - port option will be ignored. - ''; - }; - }; - - email = { - host = mkOption { - type = types.str; - example = "smtp.example.com"; - description = "SMTP server host address."; - }; - - port = mkOption { - type = types.int; - example = "465"; - description = "SMTP server port."; - }; - - user = mkOption { - type = types.str; - default = ""; - description = "Username to use for SMTP authentication."; - }; - - password = mkOption { - type = types.str; - default = ""; - description = '' - Password to use for SMTP authentication. This will be stored in the - Nix store; consider using instead. - ''; - }; - - passwordFile = mkOption { - type = types.nullOr types.path; - default = null; - example = "/run/secrets/bookwyrm-smtp-password"; - description = '' - A file containing the SMTP password corresponding to - . - ''; - }; - - useTLS = mkOption { - type = types.bool; - default = true; - description = "Whether to use TLS for communication with the SMTP server."; - }; - }; - }; - - config = mkIf cfg.enable { - warnings = - (optional (cfg.secretKey != "") "config.services.bookwyrm.secretKey will be stored in plain text in the Nix store, where it will be world readable. To avoid this, consider using config.services.bookwyrm.secretKeyFile instead.") - ++ (optional (cfg.database.password != "") "config.services.bookwyrm.database.password will be stored in plain text in the Nix store, where it will be world readable. To avoid this, consider using config.services.bookwyrm.database.passwordFile instead.") - ++ (optional (cfg.email.password != "") "config.services.bookwyrm.email.password will be stored in plain text in the Nix store, where it will be world readable. To avoid this, consider using config.services.bookwyrm.email.passwordFile instead."); - - assertions = [ - { assertion = cfg.activityRedis.unixSocket != null || (cfg.activityRedis.host != null && cfg.activityRedis.port != null); - message = "config.services.bookwyrm.activityRedis needs to have either a unixSocket defined, or both a host and a port defined."; - } - { assertion = cfg.celeryRedis.unixSocket != null || (cfg.celeryRedis.host != null && cfg.celeryRedis.port != null); - message = "config.services.bookwyrm.celeryRedis needs to have either a unixSocket defined, or both a host and a port defined."; - } - ]; - - services.bookwyrm.secretKeyFile = - (mkDefault (toString (pkgs.writeTextFile { - name = "bookwyrm-secretkeyfile"; - text = cfg.secretKey; - }))); - - services.bookwyrm.database.passwordFile = - (mkDefault (toString (pkgs.writeTextFile { - name = "bookwyrm-secretkeyfile"; - text = cfg.database.password; - }))); - - services.bookwyrm.email.passwordFile = - (mkDefault (toString (pkgs.writeTextFile { - name = "bookwyrm-email-passwordfile"; - text = cfg.email.password; - }))); - - users.users = mkIf (cfg.user == "bookwyrm") { - bookwyrm = { - home = cfg.stateDir; - group = "bookwyrm"; - useDefaultShell = true; - isSystemUser = true; - }; - }; - - users.groups = mkIf (cfg.group == "bookwyrm") { - bookwyrm = { }; - }; - - services.postgresql = optionalAttrs (cfg.database.createLocally) { - enable = true; - - ensureDatabases = [ cfg.database.database ]; - ensureUsers = [ - { name = cfg.database.user; - ensurePermissions = { "DATABASE ${cfg.database.database}" = "ALL PRIVILEGES"; }; - } - ]; - }; - - services.redis = optionalAttrs (cfg.activityRedis.createLocally || cfg.celeryRedis.createLocally) { - enable = true; - }; - - systemd.targets.bookwyrm = { - description = "Target for all bookwyrm services"; - wantedBy = [ "multi-user.target" ]; - }; - - systemd.tmpfiles.rules = [ - "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" - "d /run/bookwyrm 0770 ${cfg.user} ${cfg.group} - -" - "d '${cfg.stateDir}/images' 0750 ${cfg.user} ${cfg.group} - -" - "d '${cfg.stateDir}/static' 0750 ${cfg.user} ${cfg.group} - -" - ]; - - systemd.services.bookwyrm = { - description = "Bookwyrm reading and reviewing social network server"; - after = [ - "network.target" - "redis.service" - "postgresql.service" - "bookwyrm-celery.service" - ] ++ optional redisCreateLocally "redis.service" - ++ optional cfg.database.createLocally "postgresql.service"; - bindsTo = [ - "bookwyrm-celery.service" - ] ++ optional redisCreateLocally "redis.service" - ++ optional cfg.database.createLocally "postgresql.service"; - wantedBy = [ "bookwyrm.target" ]; - partOf = [ "bookwyrm.target" ]; - environment = env; - - serviceConfig = { - Type = "simple"; - User = cfg.user; - Group = cfg.group; - WorkingDirectory = cfg.stateDir; - }; - - preStart = '' - ${concatStringsSep "\n" (mapAttrsToList (n: v: ''export ${n}="$(cat ${escapeShellArg v})"'') envSecrets)} - ${bookwyrm}/bin/python ${bookwyrm}/manage.py migrate --noinput - ${bookwyrm}/bin/python ${bookwyrm}/manage.py collectstatic --noinput - ''; - - script = '' - ${concatStringsSep "\n" (mapAttrsToList (n: v: ''export ${n}="$(cat ${escapeShellArg v})"'') envSecrets)} - exec ${bookwyrm}/bin/gunicorn bookwyrm.wsgi:application \ - ${concatStringsSep " " (map (elem: "--bind ${elem}") cfg.bindTo)} \ - --umask 0007 - ''; - }; - - systemd.services.bookwyrm-celery = { - description = "Celery service for bookwyrm."; - after = [ - "network.target" - ] ++ optional redisCreateLocally "redis.service"; - bindsTo = optionals redisCreateLocally [ - "redis.service" - ]; - wantedBy = [ "bookwyrm.target" ]; - partOf = [ "bookwyrm.target" ]; - environment = env; - - serviceConfig = { - Type = "simple"; - User = cfg.user; - Group = cfg.group; - WorkingDirectory = cfg.stateDir; - }; - - script = '' - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''export ${n}="$(cat ${escapeShellArg v})"'') envSecrets)} - exec ${bookwyrm}/bin/celery worker -A=celerywyrm --loglevel=INFO - ''; - - }; - - systemd.services.bookwyrm-flower = { - description = "Flower monitoring tool for bookwyrm-celery"; - after = [ - "network.target" - "redis.service" - "bookwyrm-celery.service" - ] ++ optional redisCreateLocally "redis.service"; - bindsTo = optionals redisCreateLocally [ - "redis.service" - ]; - wantedBy = [ "bookwyrm.target" ]; - partOf = [ "bookwyrm.target" ]; - environment = { - CELERY_BROKER_URL = env.CELERY_BROKER; - }; - - serviceConfig = { - Type = "simple"; - User = cfg.user; - Group = cfg.group; - WorkingDirectory = cfg.stateDir; - }; - - script = '' - ${bookwyrm}/bin/flower \ - ${lib.concatStringsSep " " cfg.flowerArgs} - ''; - - }; - - environment.systemPackages = [ bookwyrmManageScript ]; - }; -} +{pkgs, ...}@args: + import ./moduleInner.nix (args // ( + let + pinnedPkgs = import (pkgs.fetchFromGitHub { + owner = "NixOS"; + repo = "nixpkgs"; + rev = "09c38c29f2c719cd76ca17a596c2fdac9e186ceb"; + sha256 = "0i6kcs0zxwfaflcg6wfkwcinfnilkxlb6ad29v01bkhg6asl6ihf"; + }) { + # we need to inherit config to propagate allowUnfree settings (which + # are need for bookwyrm) + inherit (pkgs) system config; + }; + in { bookwyrm = pinnedPkgs.callPackage ./default.nix { };})) \ No newline at end of file diff --git a/nix/moduleInner.nix b/nix/moduleInner.nix new file mode 100644 index 000000000..697a514c0 --- /dev/null +++ b/nix/moduleInner.nix @@ -0,0 +1,466 @@ +{ config, lib, pkgs, bookwyrm, ... }: + +with lib; + +let + cfg = config.services.bookwyrm; + celeryRedisPath = if cfg.celeryRedis.unixSocket != null then + "redis+socket://${cfg.celeryRedis.unixSocket}?virtual_host=0" + else + "redis://${cfg.celeryRedis.host}:${toString cfg.celeryRedis.port}/0"; + env = { + DEBUG = if cfg.debug then "true" else "false"; + DOMAIN = cfg.domain; + ALLOWED_HOSTS = lib.concatStringsSep "," cfg.allowedHosts; + BOOKWYRM_DATABASE_BACKEND = "postgres"; + MEDIA_ROOT = (builtins.toPath cfg.stateDir) + "/images"; + STATIC_ROOT = (builtins.toPath cfg.stateDir) + "/static"; + POSTGRES_HOST = cfg.database.host; + POSTGRES_USER = cfg.database.user; + POSTGRES_PORT = (toString cfg.database.port); + POSTGRES_DB = cfg.database.database; + REDIS_ACTIVITY_HOST = cfg.activityRedis.host; + REDIS_ACTIVITY_PORT = (toString cfg.activityRedis.port); + REDIS_ACTIVITY_SOCKET = (if cfg.activityRedis.unixSocket != null then cfg.activityRedis.unixSocket else ""); + CELERY_BROKER = celeryRedisPath; + CELERY_RESULT_BACKEND = celeryRedisPath; + EMAIL_HOST = cfg.email.host; + EMAIL_PORT = (toString cfg.email.port); + EMAIL_HOST_USER = cfg.email.user; + EMAIL_USE_TLS = if cfg.email.useTLS then "true" else "false"; + OL_URL = "https://openlibrary.org"; + }; + # mapping of env variable → its secret file + envSecrets = (filterAttrs (_: v: v != null) { + SECRET_KEY = cfg.secretKeyFile; + POSTGRES_PASSWORD = cfg.database.passwordFile; + EMAIL_HOST_PASSWORD = cfg.email.passwordFile; + }); + + redisCreateLocally = cfg.celeryRedis.createLocally || cfg.activityRedis.createLocally; + + loadEnv = (pkgs.writeScript "load-bookwyrm-env" '' + #!/usr/bin/env bash + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: "export ${n}=${lib.escapeShellArg v}") env)} + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''export ${n}="$(cat ${lib.escapeShellArg v})"'') envSecrets)} + ''); + bookwyrmManageScript = (pkgs.writeScriptBin "bookwyrm-manage" '' + #!/usr/bin/env bash + source ${loadEnv} + exec ${bookwyrm}/bin/python ${bookwyrm}/manage.py "$@" + ''); +in +{ + options.services.bookwyrm = { + enable = mkEnableOption "bookwyrm"; + + user = mkOption { + type = types.str; + default = "bookwyrm"; + description = "User to run bookwyrm as."; + }; + + group = mkOption { + type = types.str; + default = "bookwyrm"; + description = "Group to run bookwyrm as."; + }; + + stateDir = mkOption { + type = types.str; + default = "/var/lib/bookwyrm"; + description = "Data directory for Bookwyrm."; + }; + + bindTo = mkOption { + type = types.listOf types.str; + default = [ "unix:/run/bookwyrm/bookwyrm.sock" ]; + example = [ "unix:/run/bookwyrm-gunicorn.sock" "127.0.0.1:12345" ]; + description = "List of sockets for Gunicorn to bind to"; + }; + + flowerArgs = mkOption { + type = types.listOf types.str; + default = [ "--unix_socket=/run/bookwyrm/bookwyrm-flower.sock" ]; + description = "Arguments to pass to Flower (Celery management frontend)."; + }; + + secretKey = mkOption { + type = types.str; + default = ""; + description = '' + Secret key, for Django to use for cryptographic signing. This is stored + in plain text in the Nix store. Consider using + instead. + ''; + }; + + secretKeyFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/secrets/bookwyrm-key"; + description = '' + A file containing the secret key, corrsponding to + . + ''; + }; + + domain = mkOption { + type = types.str; + example = "bookwyrm.example.com"; + description = '' + Domain under which Bookwyrm is running; + ''; + }; + + allowedHosts = mkOption { + type = types.listOf types.str; + default = [ "*" ]; + example = [ "bookwyrm.example.com" ]; + description = '' + List of hosts to allow, checked against the Host: + header. Leave default to accept all hosts. + ''; + }; + + debug = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable debug mode. Debug mode should not be used in + production, but provides more verbose logging. + ''; + }; + + database = { + createLocally = mkOption { + type = types.bool; + default = true; + description = "Create the database and user locally."; + }; + + host = mkOption { + type = types.str; + default = ""; + description = '' + Postgresql host address. Set to empty string "" to + use unix sockets. + ''; + }; + + port = mkOption { + type = types.int; + default = config.services.postgresql.port; + description = "Port of the Postgresql server to connect to."; + }; + + database = mkOption { + type = types.str; + default = "bookwyrm"; + description = "Name of the database to use."; + }; + + user = mkOption { + type = types.str; + default = "bookwyrm"; + description = "Database role to use for bookwyrm"; + }; + + password = mkOption { + type = types.str; + default = ""; + description = '' + Password to use for database authentication. This will be stored in the + Nix store; consider using instead. + ''; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/secrets/bookwyrm-db-password"; + description = '' + A file containing the database password corresponding to + . + ''; + }; + }; + + activityRedis = { + createLocally = mkOption { + type = types.bool; + default = true; + description = "Ensure Redis is running locally and use it."; + }; + + # note that there are assertions to prevent three of these being null + host = mkOption { + type = types.nullOr types.str; + default = "127.0.0.1"; + description = "Activity Redis host address."; + }; + + port = mkOption { + type = types.nullOr types.int; + default = config.services.redis.port; + description = "Activity Redis port."; + }; + + unixSocket = mkOption { + type = types.nullOr types.str; + default = null; + example = "/run/redis/redis.sock"; + description = '' + Unix socket to connect to for activity Redis. When set, the host and + port option will be ignored. + ''; + }; + }; + + celeryRedis = { + createLocally = mkOption { + type = types.bool; + default = true; + description = "Ensure Redis is running locally and use it."; + }; + + # note that there are assertions to prevent all three of these being null + host = mkOption { + type = types.nullOr types.str; + default = "127.0.0.1"; + description = "Activity Redis host address."; + }; + + port = mkOption { + type = types.nullOr types.int; + default = config.services.redis.port; + description = "Activity Redis port."; + }; + + unixSocket = mkOption { + type = types.nullOr types.str; + default = null; + example = "/run/redis/redis.sock"; + description = '' + Unix socket to connect to for celery Redis. When set, the host and + port option will be ignored. + ''; + }; + }; + + email = { + host = mkOption { + type = types.str; + example = "smtp.example.com"; + description = "SMTP server host address."; + }; + + port = mkOption { + type = types.int; + example = "465"; + description = "SMTP server port."; + }; + + user = mkOption { + type = types.str; + default = ""; + description = "Username to use for SMTP authentication."; + }; + + password = mkOption { + type = types.str; + default = ""; + description = '' + Password to use for SMTP authentication. This will be stored in the + Nix store; consider using instead. + ''; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/secrets/bookwyrm-smtp-password"; + description = '' + A file containing the SMTP password corresponding to + . + ''; + }; + + useTLS = mkOption { + type = types.bool; + default = true; + description = "Whether to use TLS for communication with the SMTP server."; + }; + }; + }; + + config = mkIf cfg.enable { + warnings = + (optional (cfg.secretKey != "") "config.services.bookwyrm.secretKey will be stored in plain text in the Nix store, where it will be world readable. To avoid this, consider using config.services.bookwyrm.secretKeyFile instead.") + ++ (optional (cfg.database.password != "") "config.services.bookwyrm.database.password will be stored in plain text in the Nix store, where it will be world readable. To avoid this, consider using config.services.bookwyrm.database.passwordFile instead.") + ++ (optional (cfg.email.password != "") "config.services.bookwyrm.email.password will be stored in plain text in the Nix store, where it will be world readable. To avoid this, consider using config.services.bookwyrm.email.passwordFile instead."); + + assertions = [ + { assertion = cfg.activityRedis.unixSocket != null || (cfg.activityRedis.host != null && cfg.activityRedis.port != null); + message = "config.services.bookwyrm.activityRedis needs to have either a unixSocket defined, or both a host and a port defined."; + } + { assertion = cfg.celeryRedis.unixSocket != null || (cfg.celeryRedis.host != null && cfg.celeryRedis.port != null); + message = "config.services.bookwyrm.celeryRedis needs to have either a unixSocket defined, or both a host and a port defined."; + } + ]; + + services.bookwyrm.secretKeyFile = + (mkDefault (toString (pkgs.writeTextFile { + name = "bookwyrm-secretkeyfile"; + text = cfg.secretKey; + }))); + + services.bookwyrm.database.passwordFile = + (mkDefault (toString (pkgs.writeTextFile { + name = "bookwyrm-secretkeyfile"; + text = cfg.database.password; + }))); + + services.bookwyrm.email.passwordFile = + (mkDefault (toString (pkgs.writeTextFile { + name = "bookwyrm-email-passwordfile"; + text = cfg.email.password; + }))); + + users.users = mkIf (cfg.user == "bookwyrm") { + bookwyrm = { + home = cfg.stateDir; + group = "bookwyrm"; + useDefaultShell = true; + isSystemUser = true; + }; + }; + + users.groups = mkIf (cfg.group == "bookwyrm") { + bookwyrm = { }; + }; + + services.postgresql = optionalAttrs (cfg.database.createLocally) { + enable = true; + + ensureDatabases = [ cfg.database.database ]; + ensureUsers = [ + { name = cfg.database.user; + ensurePermissions = { "DATABASE ${cfg.database.database}" = "ALL PRIVILEGES"; }; + } + ]; + }; + + services.redis = optionalAttrs (cfg.activityRedis.createLocally || cfg.celeryRedis.createLocally) { + enable = true; + }; + + systemd.targets.bookwyrm = { + description = "Target for all bookwyrm services"; + wantedBy = [ "multi-user.target" ]; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d /run/bookwyrm 0770 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/images' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/static' 0750 ${cfg.user} ${cfg.group} - -" + ]; + + systemd.services.bookwyrm = { + description = "Bookwyrm reading and reviewing social network server"; + after = [ + "network.target" + "redis.service" + "postgresql.service" + "bookwyrm-celery.service" + ] ++ optional redisCreateLocally "redis.service" + ++ optional cfg.database.createLocally "postgresql.service"; + bindsTo = [ + "bookwyrm-celery.service" + ] ++ optional redisCreateLocally "redis.service" + ++ optional cfg.database.createLocally "postgresql.service"; + wantedBy = [ "bookwyrm.target" ]; + partOf = [ "bookwyrm.target" ]; + environment = env; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.stateDir; + }; + + preStart = '' + ${concatStringsSep "\n" (mapAttrsToList (n: v: ''export ${n}="$(cat ${escapeShellArg v})"'') envSecrets)} + ${bookwyrm}/bin/python ${bookwyrm}/manage.py migrate --noinput + ${bookwyrm}/bin/python ${bookwyrm}/manage.py collectstatic --noinput + ''; + + script = '' + ${concatStringsSep "\n" (mapAttrsToList (n: v: ''export ${n}="$(cat ${escapeShellArg v})"'') envSecrets)} + exec ${bookwyrm}/bin/gunicorn bookwyrm.wsgi:application \ + ${concatStringsSep " " (map (elem: "--bind ${elem}") cfg.bindTo)} \ + --umask 0007 + ''; + }; + + systemd.services.bookwyrm-celery = { + description = "Celery service for bookwyrm."; + after = [ + "network.target" + ] ++ optional redisCreateLocally "redis.service"; + bindsTo = optionals redisCreateLocally [ + "redis.service" + ]; + wantedBy = [ "bookwyrm.target" ]; + partOf = [ "bookwyrm.target" ]; + environment = env; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.stateDir; + }; + + script = '' + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''export ${n}="$(cat ${escapeShellArg v})"'') envSecrets)} + exec ${bookwyrm}/bin/celery worker -A=celerywyrm --loglevel=INFO + ''; + + }; + + systemd.services.bookwyrm-flower = { + description = "Flower monitoring tool for bookwyrm-celery"; + after = [ + "network.target" + "redis.service" + "bookwyrm-celery.service" + ] ++ optional redisCreateLocally "redis.service"; + bindsTo = optionals redisCreateLocally [ + "redis.service" + ]; + wantedBy = [ "bookwyrm.target" ]; + partOf = [ "bookwyrm.target" ]; + environment = { + CELERY_BROKER_URL = env.CELERY_BROKER; + }; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.stateDir; + }; + + script = '' + ${bookwyrm}/bin/flower \ + ${lib.concatStringsSep " " cfg.flowerArgs} + ''; + + }; + + environment.systemPackages = [ bookwyrmManageScript ]; + }; +} diff --git a/pyproject.toml b/pyproject.toml index 43c29ae95..1840c416c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ packages = [ { include = "bookwyrm"}, { include = "celerywyrm" } ] python = "^3.9" celery = "4.4.2" colorthief = "0.2.1" -Django = "3.2.4" +Django = "^3.2" django-model-utils = "4.0.0" environs = "7.2.0" flower = "0.9.7"