diff --git a/users/sterni/machines/ingeborg/minecraft.nix b/users/sterni/machines/ingeborg/minecraft.nix index ae367c598..df6c531de 100644 --- a/users/sterni/machines/ingeborg/minecraft.nix +++ b/users/sterni/machines/ingeborg/minecraft.nix @@ -38,11 +38,10 @@ let }; in -# TODO(sterni): regular backups of carpet world - { imports = [ ../../modules/minecraft-fabric.nix + ../../modules/backup-minecraft-fabric.nix ]; config = { @@ -65,6 +64,11 @@ in minecraft-rcon.file = depot.users.sterni.secrets."minecraft-rcon.age"; }; + services.backup-minecraft-fabric-servers = { + enable = true; + repository = "/srv/backup/from-local/minecraft"; + }; + services.minecraft-fabric-server = { creative = { enable = false; # not actively used diff --git a/users/sterni/modules/backup-minecraft-fabric.nix b/users/sterni/modules/backup-minecraft-fabric.nix new file mode 100644 index 000000000..a80a7f51a --- /dev/null +++ b/users/sterni/modules/backup-minecraft-fabric.nix @@ -0,0 +1,125 @@ +# Companion module to minecraft-fabric.nix which automatically and regularly +# creates backups of all minecraft servers' worlds to a shared borg(1) +# repository. +# +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 sterni +{ pkgs, depot, config, lib, ... }: + +let + inherit (depot.nix) getBins; + + bins = getBins pkgs.borgbackup [ "borg" ] + // getBins pkgs.mcrcon [ "mcrcon" ]; + + unvaried = ls: builtins.all (l: l == builtins.head ls) ls; + + cfg = config.services.backup-minecraft-fabric-servers; + + instances = lib.filterAttrs (_: i: i.enable) config.services.minecraft-fabric-server; + users = lib.mapAttrsToList (_: i: i.user) instances; + groups = lib.mapAttrsToList (_: i: i.group) instances; + + mkBackupScript = instanceName: instanceCfg: + let + archivePrefix = "minecraft-fabric-${instanceName}-world-${builtins.baseNameOf instanceCfg.world}-"; + in + + pkgs.writeShellScript "backup-minecraft-fabric-${instanceName}" '' + export MCRCON_HOST="localhost" + export MCRCON_PORT="${toString instanceCfg.serverProperties."rcon.port"}" + # Unfortunately, mcrcon can't read the password from a file + export MCRCON_PASS="$(cat "''${CREDENTIALS_DIRECTORY}/${instanceName}-rcon-password")" + + ${bins.mcrcon} save-all + unset MCRCON_PASS + + # Give the server plenty of time to save + sleep 60 + + ${bins.borg} ${lib.escapeShellArgs [ + "create" + "--verbose" "--filter" "AMEU" "--list" + "--stats" "--show-rc" + "--compression" "zlib" + "${cfg.repository}::${archivePrefix}{now}" + instanceCfg.world + ]} + + ${bins.borg} ${lib.escapeShellArgs [ + "prune" + "--list" + "--show-rc" + "--glob-archives" "${archivePrefix}*" + "--keep-hourly" "168" + "--keep-daily" "31" + "--keep-monthly" "6" + "--keep-yearly" "2" + cfg.repository + ]} + + ${bins.borg} compact ${lib.escapeShellArg cfg.repository} + ''; +in + +{ + imports = [ + ./minecraft-fabric.nix + ]; + + options = { + services.backup-minecraft-fabric-servers = { + enable = lib.mkEnableOption "backups of all Minecraft fabric servers"; + + repository = lib.mkOption { + type = lib.types.path; + description = "Path to the borg(1) repository to use for all backups."; + default = "/var/lib/backup/minecraft-fabric"; + }; + }; + }; + + config = lib.mkIf (cfg.enable && builtins.length (builtins.attrNames instances) > 0) { + assertions = [ + { + assertion = unvaried users && unvaried groups; + message = "all instances under services.minecraft-fabric-server must use the same user and group"; + } + ]; + + environment.systemPackages = [ + pkgs.borgbackup + ]; + + systemd = { + services.backup-minecraft-fabric-servers = { + description = "Backup world of all fabric based Minecraft servers"; + wantedBy = [ ]; + after = builtins.map + (name: "minecraft-fabric-${name}.service") + (builtins.attrNames instances); + + script = lib.concatStrings (lib.mapAttrsToList mkBackupScript instances); + + serviceConfig = { + Type = "oneshot"; + User = builtins.head users; + Group = builtins.head groups; + LoadCredential = lib.mapAttrsToList + (instanceName: instanceCfg: "${instanceName}-rcon-password:${instanceCfg.rconPasswordFile}") + instances; + }; + }; + + timers.backup-minecraft-fabric-servers = { + description = "Regularly backup Minecraft fabric servers"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-*-* 00/3:00:00"; + Persistent = true; + RandomizedDelaySec = "1h"; + }; + }; + }; + }; +}