1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.mysql;
8
9 mysql = cfg.package;
10
11 isMariaDB =
12 let
13 pName = _p: (builtins.parseDrvName (_p.name)).name;
14 in pName mysql == pName pkgs.mariadb;
15
16 pidFile = "${cfg.pidDir}/mysqld.pid";
17
18 mysqldOptions =
19 "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${mysql} " +
20 "--pid-file=${pidFile}";
21
22 myCnf = pkgs.writeText "my.cnf"
23 ''
24 [mysqld]
25 port = ${toString cfg.port}
26 ${optionalString (cfg.bind != null) "bind-address = ${cfg.bind}" }
27 ${optionalString (cfg.replication.role == "master" || cfg.replication.role == "slave") "log-bin=mysql-bin"}
28 ${optionalString (cfg.replication.role == "master" || cfg.replication.role == "slave") "server-id = ${toString cfg.replication.serverId}"}
29 ${optionalString (cfg.ensureUsers != [])
30 ''
31 plugin-load-add = auth_socket.so
32 ''}
33 ${cfg.extraOptions}
34 '';
35
36in
37
38{
39
40 ###### interface
41
42 options = {
43
44 services.mysql = {
45
46 enable = mkOption {
47 type = types.bool;
48 default = false;
49 description = "
50 Whether to enable the MySQL server.
51 ";
52 };
53
54 package = mkOption {
55 type = types.package;
56 example = literalExample "pkgs.mysql";
57 description = "
58 Which MySQL derivation to use. MariaDB packages are supported too.
59 ";
60 };
61
62 bind = mkOption {
63 type = types.nullOr types.str;
64 default = null;
65 example = literalExample "0.0.0.0";
66 description = "Address to bind to. The default is to bind to all addresses";
67 };
68
69 port = mkOption {
70 type = types.int;
71 default = 3306;
72 description = "Port of MySQL";
73 };
74
75 user = mkOption {
76 type = types.str;
77 default = "mysql";
78 description = "User account under which MySQL runs";
79 };
80
81 dataDir = mkOption {
82 type = types.path;
83 example = "/var/lib/mysql";
84 description = "Location where MySQL stores its table files";
85 };
86
87 pidDir = mkOption {
88 default = "/run/mysqld";
89 description = "Location of the file which stores the PID of the MySQL server";
90 };
91
92 extraOptions = mkOption {
93 type = types.lines;
94 default = "";
95 example = ''
96 key_buffer_size = 6G
97 table_cache = 1600
98 log-error = /var/log/mysql_err.log
99 '';
100 description = ''
101 Provide extra options to the MySQL configuration file.
102
103 Please note, that these options are added to the
104 <literal>[mysqld]</literal> section so you don't need to explicitly
105 state it again.
106 '';
107 };
108
109 initialDatabases = mkOption {
110 default = [];
111 description = ''
112 List of database names and their initial schemas that should be used to create databases on the first startup
113 of MySQL. The schema attribute is optional: If not specified, an empty database is created.
114 '';
115 example = [
116 { name = "foodatabase"; schema = literalExample "./foodatabase.sql"; }
117 { name = "bardatabase"; }
118 ];
119 };
120
121 initialScript = mkOption {
122 default = null;
123 description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database";
124 };
125
126 ensureDatabases = mkOption {
127 default = [];
128 description = ''
129 Ensures that the specified databases exist.
130 This option will never delete existing databases, especially not when the value of this
131 option is changed. This means that databases created once through this option or
132 otherwise have to be removed manually.
133 '';
134 example = [
135 "nextcloud"
136 "matomo"
137 ];
138 };
139
140 ensureUsers = mkOption {
141 default = [];
142 description = ''
143 Ensures that the specified users exist and have at least the ensured permissions.
144 The MySQL users will be identified using Unix socket authentication. This authenticates the Unix user with the
145 same name only, and that without the need for a password.
146 This option will never delete existing users or remove permissions, especially not when the value of this
147 option is changed. This means that users created and permissions assigned once through this option or
148 otherwise have to be removed manually.
149 '';
150 example = [
151 {
152 name = "nextcloud";
153 ensurePermissions = {
154 "nextcloud.*" = "ALL PRIVILEGES";
155 };
156 }
157 {
158 name = "backup";
159 ensurePermissions = {
160 "*.*" = "SELECT, LOCK TABLES";
161 };
162 }
163 ];
164 };
165
166 # FIXME: remove this option; it's a really bad idea.
167 rootPassword = mkOption {
168 default = null;
169 description = "Path to a file containing the root password, modified on the first startup. Not specifying a root password will leave the root password empty.";
170 };
171
172 replication = {
173 role = mkOption {
174 type = types.enum [ "master" "slave" "none" ];
175 default = "none";
176 description = "Role of the MySQL server instance.";
177 };
178
179 serverId = mkOption {
180 type = types.int;
181 default = 1;
182 description = "Id of the MySQL server instance. This number must be unique for each instance";
183 };
184
185 masterHost = mkOption {
186 type = types.str;
187 description = "Hostname of the MySQL master server";
188 };
189
190 slaveHost = mkOption {
191 type = types.str;
192 description = "Hostname of the MySQL slave server";
193 };
194
195 masterUser = mkOption {
196 type = types.str;
197 description = "Username of the MySQL replication user";
198 };
199
200 masterPassword = mkOption {
201 type = types.str;
202 description = "Password of the MySQL replication user";
203 };
204
205 masterPort = mkOption {
206 type = types.int;
207 default = 3306;
208 description = "Port number on which the MySQL master server runs";
209 };
210 };
211 };
212
213 };
214
215
216 ###### implementation
217
218 config = mkIf config.services.mysql.enable {
219
220 services.mysql.dataDir =
221 mkDefault (if versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql"
222 else "/var/mysql");
223
224 users.users.mysql = {
225 description = "MySQL server user";
226 group = "mysql";
227 uid = config.ids.uids.mysql;
228 };
229
230 users.groups.mysql.gid = config.ids.gids.mysql;
231
232 environment.systemPackages = [mysql];
233
234 systemd.services.mysql = let
235 hasNotify = (cfg.package == pkgs.mariadb);
236 in {
237 description = "MySQL Server";
238
239 after = [ "network.target" ];
240 wantedBy = [ "multi-user.target" ];
241
242 unitConfig.RequiresMountsFor = "${cfg.dataDir}";
243
244 path = [
245 # Needed for the mysql_install_db command in the preStart script
246 # which calls the hostname command.
247 pkgs.nettools
248 ];
249
250 preStart =
251 ''
252 if ! test -e ${cfg.dataDir}/mysql; then
253 mkdir -m 0700 -p ${cfg.dataDir}
254 chown -R ${cfg.user} ${cfg.dataDir}
255 ${mysql}/bin/mysql_install_db ${mysqldOptions}
256 touch /tmp/mysql_init
257 fi
258
259 mkdir -m 0755 -p ${cfg.pidDir}
260 chown -R ${cfg.user} ${cfg.pidDir}
261 '';
262
263 serviceConfig = {
264 Type = if hasNotify then "notify" else "simple";
265 RuntimeDirectory = "mysqld";
266 ExecStart = "${mysql}/bin/mysqld --defaults-extra-file=${myCnf} ${mysqldOptions}";
267 };
268
269 postStart = ''
270 ${lib.optionalString (!hasNotify) ''
271 # Wait until the MySQL server is available for use
272 count=0
273 while [ ! -e /run/mysqld/mysqld.sock ]
274 do
275 if [ $count -eq 30 ]
276 then
277 echo "Tried 30 times, giving up..."
278 exit 1
279 fi
280
281 echo "MySQL daemon not yet started. Waiting for 1 second..."
282 count=$((count++))
283 sleep 1
284 done
285 ''}
286
287 if [ -f /tmp/mysql_init ]
288 then
289 ${concatMapStrings (database:
290 ''
291 # Create initial databases
292 if ! test -e "${cfg.dataDir}/${database.name}"; then
293 echo "Creating initial database: ${database.name}"
294 ( echo 'create database `${database.name}`;'
295
296 ${optionalString (database ? "schema") ''
297 echo 'use `${database.name}`;'
298
299 if [ -f "${database.schema}" ]
300 then
301 cat ${database.schema}
302 elif [ -d "${database.schema}" ]
303 then
304 cat ${database.schema}/mysql-databases/*.sql
305 fi
306 ''}
307 ) | ${mysql}/bin/mysql -u root -N
308 fi
309 '') cfg.initialDatabases}
310
311 ${optionalString (cfg.replication.role == "master")
312 ''
313 # Set up the replication master
314
315 ( echo "use mysql;"
316 echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
317 echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
318 echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
319 ) | ${mysql}/bin/mysql -u root -N
320 ''}
321
322 ${optionalString (cfg.replication.role == "slave")
323 ''
324 # Set up the replication slave
325
326 ( echo "stop slave;"
327 echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
328 echo "start slave;"
329 ) | ${mysql}/bin/mysql -u root -N
330 ''}
331
332 ${optionalString (cfg.initialScript != null)
333 ''
334 # Execute initial script
335 cat ${cfg.initialScript} | ${mysql}/bin/mysql -u root -N
336 ''}
337
338 ${optionalString (cfg.rootPassword != null)
339 ''
340 # Change root password
341
342 ( echo "use mysql;"
343 echo "update user set Password=password('$(cat ${cfg.rootPassword})') where User='root';"
344 echo "flush privileges;"
345 ) | ${mysql}/bin/mysql -u root -N
346 ''}
347
348 rm /tmp/mysql_init
349 fi
350
351 ${optionalString (cfg.ensureDatabases != []) ''
352 (
353 ${concatMapStrings (database: ''
354 echo "CREATE DATABASE IF NOT EXISTS ${database};"
355 '') cfg.ensureDatabases}
356 ) | ${mysql}/bin/mysql -u root -N
357 ''}
358
359 ${concatMapStrings (user:
360 ''
361 ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};"
362 ${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
363 echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
364 '') user.ensurePermissions)}
365 ) | ${mysql}/bin/mysql -u root -N
366 '') cfg.ensureUsers}
367
368 ''; # */
369 };
370
371 };
372
373}