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.extraUsers.mysql = {
225 description = "MySQL server user";
226 group = "mysql";
227 uid = config.ids.uids.mysql;
228 };
229
230 users.extraGroups.mysql.gid = config.ids.gids.mysql;
231
232 environment.systemPackages = [mysql];
233
234 systemd.services.mysql =
235 { description = "MySQL Server";
236
237 after = [ "network.target" ];
238 wantedBy = [ "multi-user.target" ];
239
240 unitConfig.RequiresMountsFor = "${cfg.dataDir}";
241
242 path = [
243 # Needed for the mysql_install_db command in the preStart script
244 # which calls the hostname command.
245 pkgs.nettools
246 ];
247
248 preStart =
249 ''
250 if ! test -e ${cfg.dataDir}/mysql; then
251 mkdir -m 0700 -p ${cfg.dataDir}
252 chown -R ${cfg.user} ${cfg.dataDir}
253 ${mysql}/bin/mysql_install_db ${mysqldOptions}
254 touch /tmp/mysql_init
255 fi
256
257 mkdir -m 0755 -p ${cfg.pidDir}
258 chown -R ${cfg.user} ${cfg.pidDir}
259
260 # Make the socket directory
261 mkdir -p /run/mysqld
262 chmod 0755 /run/mysqld
263 chown -R ${cfg.user} /run/mysqld
264 '';
265
266 serviceConfig.ExecStart = "${mysql}/bin/mysqld --defaults-extra-file=${myCnf} ${mysqldOptions}";
267
268 postStart =
269 ''
270 # Wait until the MySQL server is available for use
271 count=0
272 while [ ! -e /run/mysqld/mysqld.sock ]
273 do
274 if [ $count -eq 30 ]
275 then
276 echo "Tried 30 times, giving up..."
277 exit 1
278 fi
279
280 echo "MySQL daemon not yet started. Waiting for 1 second..."
281 count=$((count++))
282 sleep 1
283 done
284
285 if [ -f /tmp/mysql_init ]
286 then
287 ${concatMapStrings (database:
288 ''
289 # Create initial databases
290 if ! test -e "${cfg.dataDir}/${database.name}"; then
291 echo "Creating initial database: ${database.name}"
292 ( echo 'create database `${database.name}`;'
293
294 ${optionalString (database ? "schema") ''
295 echo 'use `${database.name}`;'
296
297 if [ -f "${database.schema}" ]
298 then
299 cat ${database.schema}
300 elif [ -d "${database.schema}" ]
301 then
302 cat ${database.schema}/mysql-databases/*.sql
303 fi
304 ''}
305 ) | ${mysql}/bin/mysql -u root -N
306 fi
307 '') cfg.initialDatabases}
308
309 ${optionalString (cfg.replication.role == "master")
310 ''
311 # Set up the replication master
312
313 ( echo "use mysql;"
314 echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
315 echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
316 echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
317 ) | ${mysql}/bin/mysql -u root -N
318 ''}
319
320 ${optionalString (cfg.replication.role == "slave")
321 ''
322 # Set up the replication slave
323
324 ( echo "stop slave;"
325 echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
326 echo "start slave;"
327 ) | ${mysql}/bin/mysql -u root -N
328 ''}
329
330 ${optionalString (cfg.initialScript != null)
331 ''
332 # Execute initial script
333 cat ${cfg.initialScript} | ${mysql}/bin/mysql -u root -N
334 ''}
335
336 ${optionalString (cfg.rootPassword != null)
337 ''
338 # Change root password
339
340 ( echo "use mysql;"
341 echo "update user set Password=password('$(cat ${cfg.rootPassword})') where User='root';"
342 echo "flush privileges;"
343 ) | ${mysql}/bin/mysql -u root -N
344 ''}
345
346 rm /tmp/mysql_init
347 fi
348
349 ${optionalString (cfg.ensureDatabases != []) ''
350 (
351 ${concatMapStrings (database: ''
352 echo "CREATE DATABASE IF NOT EXISTS ${database};"
353 '') cfg.ensureDatabases}
354 ) | ${mysql}/bin/mysql -u root -N
355 ''}
356
357 ${concatMapStrings (user:
358 ''
359 ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};"
360 ${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
361 echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
362 '') user.ensurePermissions)}
363 ) | ${mysql}/bin/mysql -u root -N
364 '') cfg.ensureUsers}
365
366 ''; # */
367 };
368
369 };
370
371}