diff --git a/flake.nix b/flake.nix index 86a25f8..daf887d 100644 --- a/flake.nix +++ b/flake.nix @@ -40,13 +40,14 @@ modules/blocks/vpn.nix modules/services/arr.nix + modules/services/audiobookshelf.nix modules/services/deluge.nix + modules/services/grocy.nix modules/services/hledger.nix modules/services/home-assistant.nix modules/services/jellyfin.nix modules/services/nextcloud-server.nix modules/services/vaultwarden.nix - modules/services/audiobookshelf.nix ]; in { @@ -102,12 +103,13 @@ } // (vm_test "audiobookshelf" ./test/vm/audiobookshelf.nix) // (vm_test "authelia" ./test/vm/authelia.nix) + // (vm_test "grocy" ./test/vm/grocy.nix) // (vm_test "jellyfin" ./test/vm/jellyfin.nix) // (vm_test "ldap" ./test/vm/ldap.nix) // (vm_test "lib" ./test/vm/lib.nix) - // (vm_test "postgresql" ./test/vm/postgresql.nix) // (vm_test "monitoring" ./test/vm/monitoring.nix) // (vm_test "nextcloud" ./test/vm/nextcloud.nix) + // (vm_test "postgresql" ./test/vm/postgresql.nix) // (vm_test "ssl" ./test/vm/ssl.nix) ); } diff --git a/modules/services/grocy.nix b/modules/services/grocy.nix new file mode 100644 index 0000000..fb57a89 --- /dev/null +++ b/modules/services/grocy.nix @@ -0,0 +1,106 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.shb.grocy; + + contracts = pkgs.callPackage ../contracts {}; + + fqdn = "${cfg.subdomain}.${cfg.domain}"; +in +{ + options.shb.grocy = { + enable = lib.mkEnableOption "selfhostblocks.grocy"; + + subdomain = lib.mkOption { + type = lib.types.str; + description = "Subdomain under which grocy will be served."; + example = "grocy"; + }; + + domain = lib.mkOption { + type = lib.types.str; + description = "domain under which grocy will be served."; + example = "mydomain.com"; + }; + + dataDir = lib.mkOption { + description = "Folder where Grocy will store all its data."; + type = lib.types.str; + default = "/var/lib/grocy"; + }; + + currency = lib.mkOption { + type = lib.types.str; + description = "ISO 4217 code for the currency to display."; + default = "USD"; + example = "NOK"; + }; + + culture = lib.mkOption { + type = lib.types.enum [ "de" "en" "da" "en_GB" "es" "fr" "hu" "it" "nl" "no" "pl" "pt_BR" "ru" "sk_SK" "sv_SE" "tr" ]; + default = "en"; + description = lib.mdDoc '' + Display language of the frontend. + ''; + }; + + ssl = lib.mkOption { + description = "Path to SSL files"; + type = lib.types.nullOr contracts.ssl.certs; + default = null; + }; + + extraServiceConfig = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + description = "Extra configuration given to the systemd service file."; + default = {}; + example = lib.literalExpression '' + { + MemoryHigh = "512M"; + MemoryMax = "900M"; + } + ''; + }; + + logLevel = lib.mkOption { + type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]); + description = "Enable logging."; + default = false; + example = true; + }; + }; + + config = lib.mkIf cfg.enable (lib.mkMerge [{ + + services.grocy = { + enable = true; + hostName = fqdn; + nginx.enableSSL = !(isNull cfg.ssl); + dataDir = cfg.dataDir; + settings.currency = cfg.currency; + settings.culture = cfg.culture; + }; + + services.phpfpm.pools.grocy.group = lib.mkForce "grocy"; + + users.groups.grocy = {}; + users.users.grocy.group = lib.mkForce "grocy"; + + services.nginx.virtualHosts."${fqdn}" = { + enableACME = lib.mkForce false; + sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert; + sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key; + }; + + # We backup the whole grocy directory and set permissions for the backup user accordingly. + users.groups.grocy.members = [ "backup" ]; + users.groups.media.members = [ "backup" ]; + shb.backup.instances.grocy = { + sourceDirectories = [ + config.services.grocy.dataDir + ]; + }; + } { + systemd.services.grocyd.serviceConfig = cfg.extraServiceConfig; + }]); +} diff --git a/test/vm/grocy.nix b/test/vm/grocy.nix new file mode 100644 index 0000000..08fe0ae --- /dev/null +++ b/test/vm/grocy.nix @@ -0,0 +1,128 @@ +{ pkgs, lib, ... }: +{ + basic = pkgs.nixosTest { + name = "grocy-basic"; + + nodes.server = { config, pkgs, ... }: { + imports = [ + { + options = { + shb.backup = lib.mkOption { type = lib.types.anything; }; + }; + } + ../../modules/services/grocy.nix + ]; + + shb.grocy = { + enable = true; + domain = "example.com"; + subdomain = "g"; + }; + # Nginx port. + networking.firewall.allowedTCPPorts = [ 80 ]; + }; + + nodes.client = {}; + + # TODO: Test login + testScript = { nodes, ... }: '' + import json + + def curl(target, format, endpoint): + return json.loads(target.succeed( + "curl --fail-with-body --silent --show-error --output /dev/null --location" + + " --connect-to g.example.com:443:server:443" + + " --connect-to g.example.com:80:server:80" + + f" --write-out '{format}'" + + " " + endpoint + )) + + start_all() + server.wait_for_unit("phpfpm-grocy.service") + server.wait_for_unit("nginx.service") + server.wait_for_open_unix_socket("${nodes.server.services.phpfpm.pools.grocy.socket}") + + response = curl(client, """{"code":%{response_code}}""", "http://g.example.com") + + if response['code'] != 200: + raise Exception(f"Code is {response['code']}") + ''; + }; + + cert = pkgs.nixosTest { + name = "grocy-cert"; + + nodes.server = { config, pkgs, ... }: { + imports = [ + { + options = { + shb.backup = lib.mkOption { type = lib.types.anything; }; + shb.authelia = lib.mkOption { type = lib.types.anything; }; + }; + } + ../../modules/blocks/nginx.nix + ../../modules/blocks/ssl.nix + ../../modules/services/grocy.nix + ]; + + shb.certs = { + cas.selfsigned.myca = { + name = "My CA"; + }; + certs.selfsigned = { + n = { + ca = config.shb.certs.cas.selfsigned.myca; + domain = "*.example.com"; + group = "nginx"; + }; + }; + }; + + systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ]; + systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ]; + + shb.grocy = { + enable = true; + domain = "example.com"; + subdomain = "g"; + ssl = config.shb.certs.certs.selfsigned.n; + }; + # Nginx port. + networking.firewall.allowedTCPPorts = [ 80 443 ]; + + shb.nginx.accessLog = true; + }; + + nodes.client = {}; + + # TODO: Test login + testScript = { nodes, ... }: '' + import json + import os + import pathlib + + def curl(target, format, endpoint): + return json.loads(target.succeed( + "curl --fail-with-body --silent --show-error --output /dev/null --location" + + " --connect-to g.example.com:443:server:443" + + " --connect-to g.example.com:80:server:80" + + f" --write-out '{format}'" + + " " + endpoint + )) + + start_all() + server.wait_for_unit("phpfpm-grocy.service") + server.wait_for_unit("nginx.service") + server.wait_for_open_unix_socket("${nodes.server.services.phpfpm.pools.grocy.socket}") + + server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt") + client.succeed("rm -r /etc/ssl/certs") + client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt") + + response = curl(client, """{"code":%{response_code}}""", "https://g.example.com") + + if response['code'] != 200: + raise Exception(f"Code is {response['code']}") + ''; + }; +}