diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index b8f766696de4..1bb98efafd2d 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -634,6 +634,7 @@
./services/network-filesystems/glusterfs.nix
./services/network-filesystems/kbfs.nix
./services/network-filesystems/ipfs.nix
+ ./services/network-filesystems/litestream/default.nix
./services/network-filesystems/netatalk.nix
./services/network-filesystems/nfsd.nix
./services/network-filesystems/openafs/client.nix
diff --git a/nixos/modules/services/network-filesystems/litestream/default.nix b/nixos/modules/services/network-filesystems/litestream/default.nix
new file mode 100644
index 000000000000..f1806c5af0a9
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/litestream/default.nix
@@ -0,0 +1,100 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.litestream;
+ settingsFormat = pkgs.formats.yaml {};
+in
+{
+ options.services.litestream = {
+ enable = mkEnableOption "litestream";
+
+ package = mkOption {
+ description = "Package to use.";
+ default = pkgs.litestream;
+ defaultText = "pkgs.litestream";
+ type = types.package;
+ };
+
+ settings = mkOption {
+ description = ''
+ See the documentation.
+ '';
+ type = settingsFormat.type;
+ example = {
+ dbs = [
+ {
+ path = "/var/lib/db1";
+ replicas = [
+ {
+ url = "s3://mybkt.litestream.io/db1";
+ }
+ ];
+ }
+ ];
+ };
+ };
+
+ environmentFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ example = "/run/secrets/litestream";
+ description = ''
+ Environment file as defined in
+ systemd.exec5
+ .
+
+ Secrets may be passed to the service without adding them to the
+ world-readable Nix store, by specifying placeholder variables as
+ the option value in Nix and setting these variables accordingly in the
+ environment file.
+
+ By default, Litestream will perform environment variable expansion
+ within the config file before reading it. Any references to ''$VAR or
+ ''${VAR} formatted variables will be replaced with their environment
+ variable values. If no value is set then it will be replaced with an
+ empty string.
+
+
+ # Content of the environment file
+ LITESTREAM_ACCESS_KEY_ID=AKIAxxxxxxxxxxxxxxxx
+ LITESTREAM_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxxx
+
+
+ Note that this file needs to be available on the host on which
+ this exporter is running.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [ cfg.package ];
+ environment.etc = {
+ "litestream.yml" = {
+ source = settingsFormat.generate "litestream-config.yaml" cfg.settings;
+ };
+ };
+
+ systemd.services.litestream = {
+ description = "Litestream";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "networking.target" ];
+ serviceConfig = {
+ EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+ ExecStart = "${cfg.package}/bin/litestream replicate";
+ Restart = "always";
+ User = "litestream";
+ Group = "litestream";
+ };
+ };
+
+ users.users.litestream = {
+ description = "Litestream user";
+ group = "litestream";
+ isSystemUser = true;
+ };
+ users.groups.litestream = {};
+ };
+ meta.doc = ./litestream.xml;
+}
diff --git a/nixos/modules/services/network-filesystems/litestream/litestream.xml b/nixos/modules/services/network-filesystems/litestream/litestream.xml
new file mode 100644
index 000000000000..598f9be8cf63
--- /dev/null
+++ b/nixos/modules/services/network-filesystems/litestream/litestream.xml
@@ -0,0 +1,65 @@
+
+ Litestream
+
+ Litestream is a standalone streaming
+ replication tool for SQLite.
+
+
+
+ Configuration
+
+
+ Litestream service is managed by a dedicated user named litestream
+ which needs permission to the database file. Here's an example config which gives
+ required permissions to access
+ grafana database:
+
+{ pkgs, ... }:
+{
+ users.users.litestream.extraGroups = [ "grafana" ];
+
+ systemd.services.grafana.serviceConfig.ExecStartPost = "+" + pkgs.writeShellScript "grant-grafana-permissions" ''
+ timeout=10
+
+ while [ ! -f /var/lib/grafana/data/grafana.db ];
+ do
+ if [ "$timeout" == 0 ]; then
+ echo "ERROR: Timeout while waiting for /var/lib/grafana/data/grafana.db."
+ exit 1
+ fi
+
+ sleep 1
+
+ ((timeout--))
+ done
+
+ find /var/lib/grafana -type d -exec chmod -v 775 {} \;
+ find /var/lib/grafana -type f -exec chmod -v 660 {} \;
+ '';
+
+ services.litestream = {
+ enable = true;
+
+ environmentFile = "/run/secrets/litestream";
+
+ settings = {
+ dbs = [
+ {
+ path = "/var/lib/grafana/data/grafana.db";
+ replicas = [{
+ url = "s3://mybkt.litestream.io/grafana";
+ }];
+ }
+ ];
+ };
+ };
+}
+
+
+
+
+
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index cf44c03e59bc..58a47e03472d 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -227,6 +227,7 @@ in
libreswan = handleTest ./libreswan.nix {};
lightdm = handleTest ./lightdm.nix {};
limesurvey = handleTest ./limesurvey.nix {};
+ litestream = handleTest ./litestream.nix {};
locate = handleTest ./locate.nix {};
login = handleTest ./login.nix {};
loki = handleTest ./loki.nix {};
diff --git a/nixos/tests/litestream.nix b/nixos/tests/litestream.nix
new file mode 100644
index 000000000000..886fbfef9cf5
--- /dev/null
+++ b/nixos/tests/litestream.nix
@@ -0,0 +1,93 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+ name = "litestream";
+ meta = with pkgs.lib.maintainers; {
+ maintainers = [ jwygoda ];
+ };
+
+ machine =
+ { pkgs, ... }:
+ { services.litestream = {
+ enable = true;
+ settings = {
+ dbs = [
+ {
+ path = "/var/lib/grafana/data/grafana.db";
+ replicas = [{
+ url = "sftp://foo:bar@127.0.0.1:22/home/foo/grafana";
+ }];
+ }
+ ];
+ };
+ };
+ systemd.services.grafana.serviceConfig.ExecStartPost = "+" + pkgs.writeShellScript "grant-grafana-permissions" ''
+ timeout=10
+
+ while [ ! -f /var/lib/grafana/data/grafana.db ];
+ do
+ if [ "$timeout" == 0 ]; then
+ echo "ERROR: Timeout while waiting for /var/lib/grafana/data/grafana.db."
+ exit 1
+ fi
+
+ sleep 1
+
+ ((timeout--))
+ done
+
+ find /var/lib/grafana -type d -exec chmod -v 775 {} \;
+ find /var/lib/grafana -type f -exec chmod -v 660 {} \;
+ '';
+ services.openssh = {
+ enable = true;
+ allowSFTP = true;
+ listenAddresses = [ { addr = "127.0.0.1"; port = 22; } ];
+ };
+ services.grafana = {
+ enable = true;
+ security = {
+ adminUser = "admin";
+ adminPassword = "admin";
+ };
+ addr = "localhost";
+ port = 3000;
+ extraOptions = {
+ DATABASE_URL = "sqlite3:///var/lib/grafana/data/grafana.db?cache=private&mode=rwc&_journal_mode=WAL";
+ };
+ };
+ users.users.foo = {
+ isNormalUser = true;
+ password = "bar";
+ };
+ users.users.litestream.extraGroups = [ "grafana" ];
+ };
+
+ testScript = ''
+ start_all()
+ machine.wait_until_succeeds("test -d /home/foo/grafana")
+ machine.wait_for_open_port(3000)
+ machine.succeed("""
+ curl -sSfN -X PUT -H "Content-Type: application/json" -d '{
+ "oldPassword": "admin",
+ "newPassword": "newpass",
+ "confirmNew": "newpass"
+ }' http://admin:admin@127.0.0.1:3000/api/user/password
+ """)
+ # https://litestream.io/guides/systemd/#simulating-a-disaster
+ machine.systemctl("stop litestream.service")
+ machine.succeed(
+ "rm -f /var/lib/grafana/data/grafana.db "
+ "/var/lib/grafana/data/grafana.db-shm "
+ "/var/lib/grafana/data/grafana.db-wal"
+ )
+ machine.succeed(
+ "litestream restore /var/lib/grafana/data/grafana.db "
+ "&& chown grafana:grafana /var/lib/grafana/data/grafana.db "
+ "&& chmod 660 /var/lib/grafana/data/grafana.db"
+ )
+ machine.systemctl("restart grafana.service")
+ machine.wait_for_open_port(3000)
+ machine.succeed(
+ "curl -sSfN -u admin:newpass http://127.0.0.1:3000/api/org/users | grep admin\@localhost"
+ )
+ '';
+})