nixpkgs/nixos/modules/services/backup/libvirtd-autosnapshot.nix
2025-09-07 12:17:56 +02:00

261 lines
7.7 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.libvirtd.autoSnapshot;
# Function to get VM config with defaults
getVMConfig =
vm:
if lib.isString vm then
{
name = vm;
inherit (cfg) snapshotType keep;
}
else
{
inherit (vm) name;
snapshotType = if vm.snapshotType != null then vm.snapshotType else cfg.snapshotType;
keep = if vm.keep != null then vm.keep else cfg.keep;
};
# Main backup script combining all VM scripts
backupScript = ''
set -eo pipefail
# Initialize failure tracking
failed=""
# Define the VM snapshot function
function snap_vm() {
local vmName="$1"
local snapshotType="$2"
local keep="$3"
# Add validation for VM name
if ! echo "$vmName" | ${pkgs.gnugrep}/bin/grep -qE '^[a-zA-Z0-9_.-]+$'; then
echo "Invalid VM name: '$vmName'"
failed="$failed $vmName"
return
fi
echo "Processing VM: $vmName"
# Check if VM exists
if ! ${pkgs.libvirt}/bin/virsh dominfo "$vmName" >/dev/null 2>&1; then
echo "VM '$vmName' does not exist, skipping"
return
fi
# Create new snapshot
local snapshot_name
snapshot_name="${cfg.prefix}_$(date +%Y-%m-%d_%H%M%S)"
local snapshot_opts=""
[[ "$snapshotType" == "external" ]] && snapshot_opts="--disk-only"
if ! ${pkgs.libvirt}/bin/virsh snapshot-create-as \
"$vmName" \
"$snapshot_name" \
"Automatic backup snapshot" \
$snapshot_opts \
--atomic; then
echo "Failed to create snapshot for $vmName"
failed="$failed $vmName"
return
fi
# List all automatic snapshots for this VM
readarray -t SNAPSHOTS < <(${pkgs.libvirt}/bin/virsh snapshot-list "$vmName" --name | ${pkgs.gnugrep}/bin/grep "^${cfg.prefix}_")
# Count snapshots
local snapshot_count=''${#SNAPSHOTS[@]}
# Delete old snapshots if we have more than the keep limit
if [[ $snapshot_count -gt $keep ]]; then
# Sort snapshots by date (they're named with date prefix)
readarray -t TO_DELETE < <(printf '%s\n' "''${SNAPSHOTS[@]}" | ${pkgs.coreutils}/bin/sort | ${pkgs.coreutils}/bin/head -n -$keep)
for snap in "''${TO_DELETE[@]}"; do
echo "Removing old snapshot $snap from $vmName"
# Check if snapshot is internal or external
local snapshot_location
snapshot_location=$(${pkgs.libvirt}/bin/virsh snapshot-info "$vmName" --snapshotname "$snap" | ${pkgs.gnugrep}/bin/grep "Location:" | ${pkgs.gawk}/bin/awk '{print $2}')
local delete_opts=""
[[ "$snapshot_location" == "internal" ]] && delete_opts="--metadata"
if ! ${pkgs.libvirt}/bin/virsh snapshot-delete "$vmName" "$snap" $delete_opts; then
echo "Failed to remove snapshot $snap from $vmName"
failed="$failed $vmName(cleanup)"
fi
done
fi
}
${
if cfg.vms == null then
''
# Process all VMs
${pkgs.libvirt}/bin/virsh list --all --name | while read -r vm; do
# Skip empty lines
[ -z "$vm" ] && continue
# Call snap_vm function with default settings
snap_vm "$vm" ${cfg.snapshotType} ${toString cfg.keep}
done
''
else
''
# Process specific VMs from the list
${lib.concatMapStrings (
vm: with getVMConfig vm; "snap_vm '${name}' ${snapshotType} ${toString keep}\n"
) cfg.vms}
''
}
# Report any failures
if [ -n "$failed" ]; then
echo "Snapshot operation failed for:$failed"
exit 1
fi
exit 0
'';
in
{
options = {
services.libvirtd.autoSnapshot = {
enable = lib.mkEnableOption "LibVirt VM snapshots";
calendar = lib.mkOption {
type = lib.types.str;
default = "04:15:00";
description = ''
When to create snapshots (systemd calendar format).
Default is 4:15 AM.
'';
};
prefix = lib.mkOption {
type = lib.types.str;
default = "autosnap";
description = ''
Prefix for automatic snapshot names.
This is used to identify and manage automatic snapshots
separately from manual ones.
'';
};
keep = lib.mkOption {
type = lib.types.int;
default = 2;
description = "Default number of snapshots to keep for VMs that don't specify a keep value.";
};
snapshotType = lib.mkOption {
type = lib.types.enum [
"internal"
"external"
];
default = "internal";
description = "Type of snapshot to create (internal or external).";
};
vms = lib.mkOption {
type = lib.types.nullOr (
lib.types.listOf (
lib.types.oneOf [
lib.types.str
(lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "Name of the VM";
};
snapshotType = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"internal"
"external"
]
);
default = null;
description = ''
Type of snapshot to create (internal or external).
If not specified, uses global snapshotType (${toString cfg.snapshotType}).
'';
};
keep = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = ''
Number of snapshots to keep for this VM.
If not specified, uses global keep (${toString cfg.keep}).
'';
};
};
})
]
)
);
default = null;
description = ''
If specified only the list of VMs will be snapshotted else all existing one. Each entry can be either:
- A string (VM name, uses default settings)
- An attribute set with VM configuration
'';
example = lib.literalExpression ''
[
"myvm1" # Uses defaults
{
name = "myvm2";
keep = 30; # Override retention
}
]
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = (cfg.vms == null) || (lib.isList cfg.vms && cfg.vms != [ ]);
message = "'services.libvirtd.autoSnapshot.vms' must either be null for all VMs or a non-empty list of VM configurations";
}
{
assertion = config.virtualisation.libvirtd.enable;
message = "virtualisation.libvirtd must be enabled to use services.libvirtd.autoSnapshot";
}
];
systemd = {
timers.libvirtd-autosnapshot = {
description = "LibVirt VM snapshot timer";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.calendar;
AccuracySec = "5m";
Unit = "libvirtd-autosnapshot.service";
};
};
services.libvirtd-autosnapshot = {
description = "LibVirt VM snapshot service";
after = [ "libvirtd.service" ];
requires = [ "libvirtd.service" ];
serviceConfig = {
Type = "oneshot";
User = "root";
};
script = backupScript;
};
};
};
meta.maintainers = [ lib.maintainers._6543 ];
}