1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.libvirtd.autoSnapshot;
10
11 # Function to get VM config with defaults
12 getVMConfig =
13 vm:
14 if lib.isString vm then
15 {
16 name = vm;
17 inherit (cfg) snapshotType keep;
18 }
19 else
20 {
21 inherit (vm) name;
22 snapshotType = if vm.snapshotType != null then vm.snapshotType else cfg.snapshotType;
23 keep = if vm.keep != null then vm.keep else cfg.keep;
24 };
25
26 # Main backup script combining all VM scripts
27 backupScript = ''
28 set -eo pipefail
29
30 # Initialize failure tracking
31 failed=""
32
33 # Define the VM snapshot function
34 function snap_vm() {
35 local vmName="$1"
36 local snapshotType="$2"
37 local keep="$3"
38
39 # Add validation for VM name
40 if ! echo "$vmName" | ${pkgs.gnugrep}/bin/grep -qE '^[a-zA-Z0-9_.-]+$'; then
41 echo "Invalid VM name: '$vmName'"
42 failed="$failed $vmName"
43 return
44 fi
45
46 echo "Processing VM: $vmName"
47
48 # Check if VM exists
49 if ! ${pkgs.libvirt}/bin/virsh dominfo "$vmName" >/dev/null 2>&1; then
50 echo "VM '$vmName' does not exist, skipping"
51 return
52 fi
53
54 # Create new snapshot
55 local snapshot_name
56 snapshot_name="${cfg.prefix}_$(date +%Y-%m-%d_%H%M%S)"
57 local snapshot_opts=""
58 [[ "$snapshotType" == "external" ]] && snapshot_opts="--disk-only"
59 if ! ${pkgs.libvirt}/bin/virsh snapshot-create-as \
60 "$vmName" \
61 "$snapshot_name" \
62 "Automatic backup snapshot" \
63 $snapshot_opts \
64 --atomic; then
65 echo "Failed to create snapshot for $vmName"
66 failed="$failed $vmName"
67 return
68 fi
69
70 # List all automatic snapshots for this VM
71 readarray -t SNAPSHOTS < <(${pkgs.libvirt}/bin/virsh snapshot-list "$vmName" --name | ${pkgs.gnugrep}/bin/grep "^${cfg.prefix}_")
72
73 # Count snapshots
74 local snapshot_count=''${#SNAPSHOTS[@]}
75
76 # Delete old snapshots if we have more than the keep limit
77 if [[ $snapshot_count -gt $keep ]]; then
78 # Sort snapshots by date (they're named with date prefix)
79 readarray -t TO_DELETE < <(printf '%s\n' "''${SNAPSHOTS[@]}" | ${pkgs.coreutils}/bin/sort | ${pkgs.coreutils}/bin/head -n -$keep)
80 for snap in "''${TO_DELETE[@]}"; do
81 echo "Removing old snapshot $snap from $vmName"
82
83 # Check if snapshot is internal or external
84 local snapshot_location
85 snapshot_location=$(${pkgs.libvirt}/bin/virsh snapshot-info "$vmName" --snapshotname "$snap" | ${pkgs.gnugrep}/bin/grep "Location:" | ${pkgs.gawk}/bin/awk '{print $2}')
86
87 local delete_opts=""
88 [[ "$snapshot_location" == "internal" ]] && delete_opts="--metadata"
89
90 if ! ${pkgs.libvirt}/bin/virsh snapshot-delete "$vmName" "$snap" $delete_opts; then
91 echo "Failed to remove snapshot $snap from $vmName"
92 failed="$failed $vmName(cleanup)"
93 fi
94 done
95 fi
96 }
97
98 ${
99 if cfg.vms == null then
100 ''
101 # Process all VMs
102 ${pkgs.libvirt}/bin/virsh list --all --name | while read -r vm; do
103 # Skip empty lines
104 [ -z "$vm" ] && continue
105
106 # Call snap_vm function with default settings
107 snap_vm "$vm" ${cfg.snapshotType} ${toString cfg.keep}
108 done
109 ''
110 else
111 ''
112 # Process specific VMs from the list
113 ${lib.concatMapStrings (
114 vm: with getVMConfig vm; "snap_vm '${name}' ${snapshotType} ${toString keep}\n"
115 ) cfg.vms}
116 ''
117 }
118
119 # Report any failures
120 if [ -n "$failed" ]; then
121 echo "Snapshot operation failed for:$failed"
122 exit 1
123 fi
124
125 exit 0
126 '';
127in
128{
129 options = {
130 services.libvirtd.autoSnapshot = {
131 enable = lib.mkEnableOption "LibVirt VM snapshots";
132
133 calendar = lib.mkOption {
134 type = lib.types.str;
135 default = "04:15:00";
136 description = ''
137 When to create snapshots (systemd calendar format).
138 Default is 4:15 AM.
139 '';
140 };
141
142 prefix = lib.mkOption {
143 type = lib.types.str;
144 default = "autosnap";
145 description = ''
146 Prefix for automatic snapshot names.
147 This is used to identify and manage automatic snapshots
148 separately from manual ones.
149 '';
150 };
151
152 keep = lib.mkOption {
153 type = lib.types.int;
154 default = 2;
155 description = "Default number of snapshots to keep for VMs that don't specify a keep value.";
156 };
157
158 snapshotType = lib.mkOption {
159 type = lib.types.enum [
160 "internal"
161 "external"
162 ];
163 default = "internal";
164 description = "Type of snapshot to create (internal or external).";
165 };
166
167 vms = lib.mkOption {
168 type = lib.types.nullOr (
169 lib.types.listOf (
170 lib.types.oneOf [
171 lib.types.str
172 (lib.types.submodule {
173 options = {
174 name = lib.mkOption {
175 type = lib.types.str;
176 description = "Name of the VM";
177 };
178 snapshotType = lib.mkOption {
179 type = lib.types.nullOr (
180 lib.types.enum [
181 "internal"
182 "external"
183 ]
184 );
185 default = null;
186 description = ''
187 Type of snapshot to create (internal or external).
188 If not specified, uses global snapshotType (${toString cfg.snapshotType}).
189 '';
190 };
191 keep = lib.mkOption {
192 type = lib.types.nullOr lib.types.int;
193 default = null;
194 description = ''
195 Number of snapshots to keep for this VM.
196 If not specified, uses global keep (${toString cfg.keep}).
197 '';
198 };
199 };
200 })
201 ]
202 )
203 );
204 default = null;
205 description = ''
206 If specified only the list of VMs will be snapshotted else all existing one. Each entry can be either:
207 - A string (VM name, uses default settings)
208 - An attribute set with VM configuration
209 '';
210 example = lib.literalExpression ''
211 [
212 "myvm1" # Uses defaults
213 {
214 name = "myvm2";
215 keep = 30; # Override retention
216 }
217 ]
218 '';
219 };
220 };
221 };
222
223 config = lib.mkIf cfg.enable {
224 assertions = [
225 {
226 assertion = (cfg.vms == null) || (lib.isList cfg.vms && cfg.vms != [ ]);
227 message = "'services.libvirtd.autoSnapshot.vms' must either be null for all VMs or a non-empty list of VM configurations";
228 }
229 {
230 assertion = config.virtualisation.libvirtd.enable;
231 message = "virtualisation.libvirtd must be enabled to use services.libvirtd.autoSnapshot";
232 }
233 ];
234
235 systemd = {
236 timers.libvirtd-autosnapshot = {
237 description = "LibVirt VM snapshot timer";
238 wantedBy = [ "timers.target" ];
239 timerConfig = {
240 OnCalendar = cfg.calendar;
241 AccuracySec = "5m";
242 Unit = "libvirtd-autosnapshot.service";
243 };
244 };
245
246 services.libvirtd-autosnapshot = {
247 description = "LibVirt VM snapshot service";
248 after = [ "libvirtd.service" ];
249 requires = [ "libvirtd.service" ];
250 serviceConfig = {
251 Type = "oneshot";
252 User = "root";
253 };
254 script = backupScript;
255 };
256 };
257 };
258
259 meta.maintainers = [ lib.maintainers._6543 ];
260}