diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 988632fc4434..3f4549911fe4 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -118,6 +118,8 @@ Use `services.pipewire.extraConfig` or `services.pipewire.configPackages` for Pi Matter Controller Server exposing websocket connections for use with other services, notably Home Assistant. Available as [services.matter-server](#opt-services.matter-server.enable) +- [db-rest](https://github.com/derhuerst/db-rest), a wrapper around Deutsche Bahn's internal API for public transport data. Available as [services.db-rest](#opt-services.db-rest.enable). + - [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable). The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been marked deprecated and will be dropped after 24.05 due to lack of maintenance of the anki-sync-server softwares. diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index a45507d5ee3c..dda647ac1994 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -690,6 +690,7 @@ ./services/misc/clipmenu.nix ./services/misc/confd.nix ./services/misc/cpuminer-cryptonight.nix + ./services/misc/db-rest.nix ./services/misc/devmon.nix ./services/misc/dictd.nix ./services/misc/disnix.nix diff --git a/nixos/modules/services/misc/db-rest.nix b/nixos/modules/services/misc/db-rest.nix new file mode 100644 index 000000000000..fbf8b327af04 --- /dev/null +++ b/nixos/modules/services/misc/db-rest.nix @@ -0,0 +1,182 @@ +{ config, pkgs, lib, ... }: +let + inherit (lib) mkOption types mkIf mkMerge mkDefault mkEnableOption mkPackageOption maintainers; + cfg = config.services.db-rest; +in +{ + options = { + services.db-rest = { + enable = mkEnableOption "db-rest service"; + + user = mkOption { + type = types.str; + default = "db-rest"; + description = "User account under which db-rest runs."; + }; + + group = mkOption { + type = types.str; + default = "db-rest"; + description = "Group under which db-rest runs."; + }; + + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "The host address the db-rest server should listen on."; + }; + + port = mkOption { + type = types.port; + default = 3000; + description = "The port the db-rest server should listen on."; + }; + + redis = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable caching with redis for db-rest."; + }; + + createLocally = mkOption { + type = types.bool; + default = true; + description = "Configure a local redis server for db-rest."; + }; + + host = mkOption { + type = with types; nullOr str; + default = null; + description = "Redis host."; + }; + + port = mkOption { + type = with types; nullOr port; + default = null; + description = "Redis port."; + }; + + user = mkOption { + type = with types; nullOr str; + default = null; + description = "Optional username used for authentication with redis."; + }; + + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/db-rest/pasword-redis-db"; + description = "Path to a file containing the redis password."; + }; + + useSSL = mkOption { + type = types.bool; + default = true; + description = "Use SSL if using a redis network connection."; + }; + }; + + package = mkPackageOption pkgs "db-rest" { }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = (cfg.redis.enable && !cfg.redis.createLocally) -> (cfg.redis.host != null && cfg.redis.port != null); + message = '' + {option}`services.db-rest.redis.createLocally` and redis network connection ({option}`services.db-rest.redis.host` or {option}`services.db-rest.redis.port`) enabled. Disable either of them. + ''; + } + { + assertion = (cfg.redis.enable && !cfg.redis.createLocally) -> (cfg.redis.passwordFile != null); + message = '' + {option}`services.db-rest.redis.createLocally` is disabled, but {option}`services.db-rest.redis.passwordFile` is not set. + ''; + } + ]; + + systemd.services.db-rest = mkMerge [ + { + description = "db-rest service"; + after = [ "network.target" ] + ++ lib.optional cfg.redis.createLocally "redis-db-rest.service"; + requires = lib.optional cfg.redis.createLocally "redis-db-rest.service"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + Restart = "always"; + RestartSec = 5; + WorkingDirectory = cfg.package; + User = cfg.user; + Group = cfg.group; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + MemoryDenyWriteExecute = false; + LoadCredential = lib.optional (cfg.redis.enable && cfg.redis.passwordFile != null) "REDIS_PASSWORD:${cfg.redis.passwordFile}"; + ExecStart = mkDefault "${cfg.package}/bin/db-rest"; + + RemoveIPC = true; + NoNewPrivileges = true; + PrivateDevices = true; + ProtectClock = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + PrivateMounts = true; + SystemCallArchitectures = "native"; + ProtectHostname = true; + LockPersonality = true; + ProtectKernelTunables = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RestrictNamespaces = true; + ProtectSystem = "strict"; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectHome = true; + PrivateUsers = true; + PrivateTmp = true; + CapabilityBoundingSet = ""; + }; + environment = { + NODE_ENV = "production"; + NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt"; + HOSTNAME = cfg.host; + PORT = toString cfg.port; + }; + } + (mkIf cfg.redis.enable (if cfg.redis.createLocally then + { environment.REDIS_URL = config.services.redis.servers.db-rest.unixSocket; } + else + { + script = + let + username = lib.optionalString (cfg.redis.user != null) (cfg.redis.user); + host = cfg.redis.host; + port = toString cfg.redis.port; + protocol = if cfg.redis.useSSL then "rediss" else "redis"; + in + '' + export REDIS_URL="${protocol}://${username}:$(${config.systemd.package}/bin/systemd-creds cat REDIS_PASSWORD)@${host}:${port}" + exec ${cfg.package}/bin/db-rest + ''; + })) + ]; + + users.users = lib.mkMerge [ + (lib.mkIf (cfg.user == "db-rest") { + db-rest = { + isSystemUser = true; + group = cfg.group; + }; + }) + (lib.mkIf cfg.redis.createLocally { ${cfg.user}.extraGroups = [ "redis-db-rest" ]; }) + ]; + + users.groups = lib.mkIf (cfg.group == "db-rest") { db-rest = { }; }; + + services.redis.servers.db-rest.enable = cfg.redis.enable && cfg.redis.createLocally; + }; + meta.maintainers = with maintainers; [ marie ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index fe02a97d6ff1..a99fedaddd76 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -236,6 +236,7 @@ in { darling = handleTest ./darling.nix {}; dae = handleTest ./dae.nix {}; davis = handleTest ./davis.nix {}; + db-rest = handleTest ./db-rest.nix {}; dconf = handleTest ./dconf.nix {}; deconz = handleTest ./deconz.nix {}; deepin = handleTest ./deepin.nix {}; diff --git a/nixos/tests/db-rest.nix b/nixos/tests/db-rest.nix new file mode 100644 index 000000000000..9249da904acb --- /dev/null +++ b/nixos/tests/db-rest.nix @@ -0,0 +1,107 @@ +import ./make-test-python.nix ({ pkgs, ... }: +{ + name = "db-rest"; + meta.maintainers = with pkgs.lib.maintainers; [ marie ]; + + nodes = { + database = { + networking = { + interfaces.eth1 = { + ipv4.addresses = [ + { address = "192.168.2.10"; prefixLength = 24; } + ]; + }; + firewall.allowedTCPPorts = [ 31638 ]; + }; + + services.redis.servers.db-rest = { + enable = true; + bind = "0.0.0.0"; + requirePass = "choochoo"; + port = 31638; + }; + }; + + serverWithTcp = { pkgs, ... }: { + environment = { + etc = { + "db-rest/password-redis-db".text = '' + choochoo + ''; + }; + }; + + networking = { + interfaces.eth1 = { + ipv4.addresses = [ + { address = "192.168.2.11"; prefixLength = 24; } + ]; + }; + firewall.allowedTCPPorts = [ 3000 ]; + }; + + services.db-rest = { + enable = true; + host = "0.0.0.0"; + redis = { + enable = true; + createLocally = false; + host = "192.168.2.10"; + port = 31638; + passwordFile = "/etc/db-rest/password-redis-db"; + useSSL = false; + }; + }; + }; + + serverWithUnixSocket = { pkgs, ... }: { + networking = { + interfaces.eth1 = { + ipv4.addresses = [ + { address = "192.168.2.12"; prefixLength = 24; } + ]; + }; + firewall.allowedTCPPorts = [ 3000 ]; + }; + + services.db-rest = { + enable = true; + host = "0.0.0.0"; + redis = { + enable = true; + createLocally = true; + }; + }; + }; + + client = { + environment.systemPackages = [ pkgs.jq ]; + networking = { + interfaces.eth1 = { + ipv4.addresses = [ + { address = "192.168.2.13"; prefixLength = 24; } + ]; + }; + }; + }; + }; + + testScript = '' + start_all() + + with subtest("db-rest redis with TCP socket"): + database.wait_for_unit("redis-db-rest.service") + database.wait_for_open_port(31638) + + serverWithTcp.wait_for_unit("db-rest.service") + serverWithTcp.wait_for_open_port(3000) + + client.succeed("curl --fail --get http://192.168.2.11:3000/stations --data-urlencode 'query=Köln Hbf' | jq -r '.\"8000207\".name' | grep 'Köln Hbf'") + + with subtest("db-rest redis with Unix socket"): + serverWithUnixSocket.wait_for_unit("db-rest.service") + serverWithUnixSocket.wait_for_open_port(3000) + + client.succeed("curl --fail --get http://192.168.2.12:3000/stations --data-urlencode 'query=Köln Hbf' | jq -r '.\"8000207\".name' | grep 'Köln Hbf'") + ''; +}) diff --git a/pkgs/servers/db-rest/default.nix b/pkgs/servers/db-rest/default.nix index 8594dccbbd00..e8fb0ae506a2 100644 --- a/pkgs/servers/db-rest/default.nix +++ b/pkgs/servers/db-rest/default.nix @@ -4,6 +4,7 @@ , nodejs_18 , nix-update-script , fetchpatch +, nixosTests }: buildNpmPackage rec { pname = "db-rest"; @@ -25,6 +26,9 @@ buildNpmPackage rec { ''; passthru.updateScript = nix-update-script { }; + passthru.tests = { + inherit (nixosTests) db-rest; + }; meta = { description = "A clean REST API wrapping around the Deutsche Bahn API";