1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.mysql;
8
9 mysql = cfg.package;
10
11 atLeast55 = versionAtLeast mysql.mysqlVersion "5.5";
12
13 pidFile = "${cfg.pidDir}/mysqld.pid";
14
15 mysqldOptions =
16 "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${mysql} " +
17 "--pid-file=${pidFile}";
18
19 myCnf = pkgs.writeText "my.cnf"
20 ''
21 [mysqld]
22 port = ${toString cfg.port}
23 ${optionalString (cfg.bind != null) "bind-address = ${cfg.bind}" }
24 ${optionalString (cfg.replication.role == "master" || cfg.replication.role == "slave") "log-bin=mysql-bin"}
25 ${optionalString (cfg.replication.role == "master" || cfg.replication.role == "slave") "server-id = ${toString cfg.replication.serverId}"}
26 ${optionalString (cfg.replication.role == "slave" && !atLeast55)
27 ''
28 master-host = ${cfg.replication.masterHost}
29 master-user = ${cfg.replication.masterUser}
30 master-password = ${cfg.replication.masterPassword}
31 master-port = ${toString cfg.replication.masterPort}
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.
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 it 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 = "List of database names and their initial schemas that should be used to create databases on the first startup of MySQL";
112 example = [
113 { name = "foodatabase"; schema = literalExample "./foodatabase.sql"; }
114 { name = "bardatabase"; schema = literalExample "./bardatabase.sql"; }
115 ];
116 };
117
118 initialScript = mkOption {
119 default = null;
120 description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database";
121 };
122
123 # FIXME: remove this option; it's a really bad idea.
124 rootPassword = mkOption {
125 default = null;
126 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.";
127 };
128
129 replication = {
130 role = mkOption {
131 type = types.enum [ "master" "slave" "none" ];
132 default = "none";
133 description = "Role of the MySQL server instance.";
134 };
135
136 serverId = mkOption {
137 type = types.int;
138 default = 1;
139 description = "Id of the MySQL server instance. This number must be unique for each instance";
140 };
141
142 masterHost = mkOption {
143 type = types.str;
144 description = "Hostname of the MySQL master server";
145 };
146
147 slaveHost = mkOption {
148 type = types.str;
149 description = "Hostname of the MySQL slave server";
150 };
151
152 masterUser = mkOption {
153 type = types.str;
154 description = "Username of the MySQL replication user";
155 };
156
157 masterPassword = mkOption {
158 type = types.str;
159 description = "Password of the MySQL replication user";
160 };
161
162 masterPort = mkOption {
163 type = types.int;
164 default = 3306;
165 description = "Port number on which the MySQL master server runs";
166 };
167 };
168 };
169
170 };
171
172
173 ###### implementation
174
175 config = mkIf config.services.mysql.enable {
176
177 services.mysql.dataDir =
178 mkDefault (if versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql"
179 else "/var/mysql");
180
181 users.extraUsers.mysql = {
182 description = "MySQL server user";
183 group = "mysql";
184 uid = config.ids.uids.mysql;
185 };
186
187 users.extraGroups.mysql.gid = config.ids.gids.mysql;
188
189 environment.systemPackages = [mysql];
190
191 systemd.services.mysql =
192 { description = "MySQL Server";
193
194 after = [ "network.target" ];
195 wantedBy = [ "multi-user.target" ];
196
197 unitConfig.RequiresMountsFor = "${cfg.dataDir}";
198
199 path = [
200 # Needed for the mysql_install_db command in the preStart script
201 # which calls the hostname command.
202 pkgs.nettools
203 ];
204
205 preStart =
206 ''
207 if ! test -e ${cfg.dataDir}/mysql; then
208 mkdir -m 0700 -p ${cfg.dataDir}
209 chown -R ${cfg.user} ${cfg.dataDir}
210 ${mysql}/bin/mysql_install_db ${mysqldOptions}
211 touch /tmp/mysql_init
212 fi
213
214 mkdir -m 0755 -p ${cfg.pidDir}
215 chown -R ${cfg.user} ${cfg.pidDir}
216
217 # Make the socket directory
218 mkdir -p /run/mysqld
219 chmod 0755 /run/mysqld
220 chown -R ${cfg.user} /run/mysqld
221 '';
222
223 serviceConfig.ExecStart = "${mysql}/bin/mysqld --defaults-extra-file=${myCnf} ${mysqldOptions}";
224
225 postStart =
226 ''
227 # Wait until the MySQL server is available for use
228 count=0
229 while [ ! -e /run/mysqld/mysqld.sock ]
230 do
231 if [ $count -eq 30 ]
232 then
233 echo "Tried 30 times, giving up..."
234 exit 1
235 fi
236
237 echo "MySQL daemon not yet started. Waiting for 1 second..."
238 count=$((count++))
239 sleep 1
240 done
241
242 if [ -f /tmp/mysql_init ]
243 then
244 ${concatMapStrings (database:
245 ''
246 # Create initial databases
247 if ! test -e "${cfg.dataDir}/${database.name}"; then
248 echo "Creating initial database: ${database.name}"
249 ( echo "create database ${database.name};"
250 echo "use ${database.name};"
251
252 if [ -f "${database.schema}" ]
253 then
254 cat ${database.schema}
255 elif [ -d "${database.schema}" ]
256 then
257 cat ${database.schema}/mysql-databases/*.sql
258 fi
259 ) | ${mysql}/bin/mysql -u root -N
260 fi
261 '') cfg.initialDatabases}
262
263 ${optionalString (cfg.replication.role == "master" && atLeast55)
264 ''
265 # Set up the replication master
266
267 ( echo "use mysql;"
268 echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
269 echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
270 echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
271 ) | ${mysql}/bin/mysql -u root -N
272 ''}
273
274 ${optionalString (cfg.replication.role == "slave" && atLeast55)
275 ''
276 # Set up the replication slave
277
278 ( echo "stop slave;"
279 echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
280 echo "start slave;"
281 ) | ${mysql}/bin/mysql -u root -N
282 ''}
283
284 ${optionalString (cfg.initialScript != null)
285 ''
286 # Execute initial script
287 cat ${cfg.initialScript} | ${mysql}/bin/mysql -u root -N
288 ''}
289
290 ${optionalString (cfg.rootPassword != null)
291 ''
292 # Change root password
293
294 ( echo "use mysql;"
295 echo "update user set Password=password('$(cat ${cfg.rootPassword})') where User='root';"
296 echo "flush privileges;"
297 ) | ${mysql}/bin/mysql -u root -N
298 ''}
299
300 rm /tmp/mysql_init
301 fi
302 ''; # */
303 };
304
305 };
306
307}