1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.mysqlBackup;
9 defaultUser = "mysqlbackup";
10
11 # Newer mariadb versions warn of the usage of 'mysqldump' and recommend 'mariadb-dump' (https://mariadb.com/kb/en/mysqldump/)
12 dumpBinary =
13 if
14 (
15 lib.getName config.services.mysql.package == lib.getName pkgs.mariadb
16 && lib.versionAtLeast config.services.mysql.package.version "11.0.0"
17 )
18 then
19 "${config.services.mysql.package}/bin/mariadb-dump"
20 else
21 "${config.services.mysql.package}/bin/mysqldump";
22
23 compressionAlgs = {
24 gzip = rec {
25 pkg = pkgs.gzip;
26 ext = ".gz";
27 minLevel = 1;
28 maxLevel = 9;
29 cmd = compressionLevelFlag: "${pkg}/bin/gzip -c ${cfg.gzipOptions} ${compressionLevelFlag}";
30 };
31 xz = rec {
32 pkg = pkgs.xz;
33 ext = ".xz";
34 minLevel = 0;
35 maxLevel = 9;
36 cmd = compressionLevelFlag: "${pkg}/bin/xz -z -c ${compressionLevelFlag} -";
37 };
38 zstd = rec {
39 pkg = pkgs.zstd;
40 ext = ".zst";
41 minLevel = 1;
42 maxLevel = 19;
43 cmd = compressionLevelFlag: "${pkg}/bin/zstd ${compressionLevelFlag} -";
44 };
45 };
46
47 compressionLevelFlag = lib.optionalString (cfg.compressionLevel != null) (
48 "-" + toString cfg.compressionLevel
49 );
50
51 selectedAlg = compressionAlgs.${cfg.compressionAlg};
52 compressionCmd = selectedAlg.cmd compressionLevelFlag;
53
54 shouldUseSingleTransaction =
55 db:
56 if lib.isBool cfg.singleTransaction then
57 cfg.singleTransaction
58 else
59 lib.elem db cfg.singleTransaction;
60
61 backupScript = ''
62 set -o pipefail
63 failed=""
64 ${lib.concatMapStringsSep "\n" backupDatabaseScript cfg.databases}
65 if [ -n "$failed" ]; then
66 echo "Backup of database(s) failed:$failed"
67 exit 1
68 fi
69 '';
70
71 backupDatabaseScript = db: ''
72 dest="${cfg.location}/${db}${selectedAlg.ext}"
73 if ${dumpBinary} ${lib.optionalString (shouldUseSingleTransaction db) "--single-transaction"} ${db} | ${compressionCmd} > $dest.tmp; then
74 mv $dest.tmp $dest
75 echo "Backed up to $dest"
76 else
77 echo "Failed to back up to $dest"
78 rm -f $dest.tmp
79 failed="$failed ${db}"
80 fi
81 '';
82
83in
84{
85 options = {
86 services.mysqlBackup = {
87 enable = lib.mkEnableOption "MySQL backups";
88
89 calendar = lib.mkOption {
90 type = lib.types.str;
91 default = "01:15:00";
92 description = ''
93 Configured when to run the backup service systemd unit (DayOfWeek Year-Month-Day Hour:Minute:Second).
94 '';
95 };
96
97 compressionAlg = lib.mkOption {
98 type = lib.types.enum (lib.attrNames compressionAlgs);
99 default = "gzip";
100 description = ''
101 Compression algorithm to use for database dumps.
102 '';
103 };
104
105 compressionLevel = lib.mkOption {
106 type = lib.types.nullOr lib.types.int;
107 default = null;
108 description = ''
109 Compression level to use for ${lib.concatStringsSep ", " (lib.init (lib.attrNames compressionAlgs))} or ${lib.last (lib.attrNames compressionAlgs)}.
110 ${lib.concatStringsSep "\n" (
111 lib.mapAttrsToList (
112 name: algo: "- For ${name}: ${toString algo.minLevel}-${toString algo.maxLevel}"
113 ) compressionAlgs
114 )}
115
116 :::{.note}
117 If compression level is also specified in gzipOptions, the gzipOptions value will be overwritten
118 :::
119 '';
120 };
121
122 user = lib.mkOption {
123 type = lib.types.str;
124 default = defaultUser;
125 description = ''
126 User to be used to perform backup.
127 '';
128 };
129
130 databases = lib.mkOption {
131 default = [ ];
132 type = lib.types.listOf lib.types.str;
133 description = ''
134 List of database names to dump.
135 '';
136 };
137
138 location = lib.mkOption {
139 type = lib.types.path;
140 default = "/var/backup/mysql";
141 description = ''
142 Location to put the compressed MySQL database dumps.
143 '';
144 };
145
146 singleTransaction = lib.mkOption {
147 default = false;
148 type = lib.types.oneOf [
149 lib.types.bool
150 (lib.types.listOf lib.types.str)
151 ];
152 description = ''
153 Whether to create database dump in a single transaction.
154 Can be either a boolean for all databases or a list of database names.
155 '';
156 };
157
158 gzipOptions = lib.mkOption {
159 default = "--no-name --rsyncable";
160 type = lib.types.str;
161 description = ''
162 Command line options to use when invoking `gzip`.
163 Only used when compression is set to "gzip".
164 If compression level is specified both here and in compressionLevel, the compressionLevel value will take precedence.
165 '';
166 };
167 };
168 };
169
170 config = lib.mkIf cfg.enable {
171 # assert config to be correct
172 assertions = [
173 {
174 assertion =
175 cfg.compressionLevel == null
176 || selectedAlg.minLevel <= cfg.compressionLevel && cfg.compressionLevel <= selectedAlg.maxLevel;
177 message = "${cfg.compressionAlg} compression level must be between ${toString selectedAlg.minLevel} and ${toString selectedAlg.maxLevel}";
178 }
179 {
180 assertion =
181 !(lib.isList cfg.singleTransaction)
182 || lib.all (db: lib.elem db cfg.databases) cfg.singleTransaction;
183 message = "All databases in singleTransaction must be included in the databases option";
184 }
185 ];
186
187 # ensure unix user to be used to perform backup exist.
188 users.users = lib.optionalAttrs (cfg.user == defaultUser) {
189 ${defaultUser} = {
190 isSystemUser = true;
191 createHome = false;
192 home = cfg.location;
193 group = "nogroup";
194 };
195 };
196
197 # add the compression tool to the system environment.
198 environment.systemPackages = [ selectedAlg.pkg ];
199
200 # ensure database user to be used to perform backup exist.
201 services.mysql.ensureUsers = [
202 {
203 name = cfg.user;
204 ensurePermissions =
205 let
206 privs = "SELECT, SHOW VIEW, TRIGGER, LOCK TABLES";
207 grant = db: lib.nameValuePair "\\`${db}\\`.*" privs;
208 in
209 lib.listToAttrs (map grant cfg.databases);
210 }
211 ];
212
213 systemd = {
214 timers.mysql-backup = {
215 description = "Mysql backup timer";
216 wantedBy = [ "timers.target" ];
217 timerConfig = {
218 OnCalendar = cfg.calendar;
219 AccuracySec = "5m";
220 Unit = "mysql-backup.service";
221 };
222 };
223 services.mysql-backup = {
224 description = "MySQL backup service";
225 enable = true;
226 serviceConfig = {
227 Type = "oneshot";
228 User = cfg.user;
229 };
230 script = backupScript;
231 };
232 tmpfiles.rules = [
233 "d ${cfg.location} 0700 ${cfg.user} - - -"
234 ];
235 };
236 };
237
238 meta.maintainers = [ lib.maintainers._6543 ];
239}