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.replication.role == "master" || cfg.replication.role == "slave") "log-bin=mysql-bin"}
24 ${optionalString (cfg.replication.role == "master" || cfg.replication.role == "slave") "server-id = ${toString cfg.replication.serverId}"}
25 ${optionalString (cfg.replication.role == "slave" && !atLeast55)
26 ''
27 master-host = ${cfg.replication.masterHost}
28 master-user = ${cfg.replication.masterUser}
29 master-password = ${cfg.replication.masterPassword}
30 master-port = ${toString cfg.replication.masterPort}
31 ''}
32 ${cfg.extraOptions}
33 '';
34
35in
36
37{
38
39 ###### interface
40
41 options = {
42
43 services.mysql = {
44
45 enable = mkOption {
46 default = false;
47 description = "
48 Whether to enable the MySQL server.
49 ";
50 };
51
52 package = mkOption {
53 type = types.package;
54 example = literalExample "pkgs.mysql";
55 description = "
56 Which MySQL derivation to use.
57 ";
58 };
59
60 port = mkOption {
61 default = "3306";
62 description = "Port of MySQL";
63 };
64
65 user = mkOption {
66 default = "mysql";
67 description = "User account under which MySQL runs";
68 };
69
70 dataDir = mkOption {
71 default = "/var/mysql"; # !!! should be /var/db/mysql
72 description = "Location where MySQL stores its table files";
73 };
74
75 pidDir = mkOption {
76 default = "/run/mysqld";
77 description = "Location of the file which stores the PID of the MySQL server";
78 };
79
80 extraOptions = mkOption {
81 default = "";
82 example = ''
83 key_buffer_size = 6G
84 table_cache = 1600
85 log-error = /var/log/mysql_err.log
86 '';
87 description = ''
88 Provide extra options to the MySQL configuration file.
89
90 Please note, that these options are added to the
91 <literal>[mysqld]</literal> section so you don't need to explicitly
92 state it again.
93 '';
94 };
95
96 initialDatabases = mkOption {
97 default = [];
98 description = "List of database names and their initial schemas that should be used to create databases on the first startup of MySQL";
99 example = [
100 { name = "foodatabase"; schema = literalExample "./foodatabase.sql"; }
101 { name = "bardatabase"; schema = literalExample "./bardatabase.sql"; }
102 ];
103 };
104
105 initialScript = mkOption {
106 default = null;
107 description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database";
108 };
109
110 # FIXME: remove this option; it's a really bad idea.
111 rootPassword = mkOption {
112 default = null;
113 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.";
114 };
115
116 replication = {
117 role = mkOption {
118 default = "none";
119 description = "Role of the MySQL server instance. Can be either: master, slave or none";
120 };
121
122 serverId = mkOption {
123 default = 1;
124 description = "Id of the MySQL server instance. This number must be unique for each instance";
125 };
126
127 masterHost = mkOption {
128 description = "Hostname of the MySQL master server";
129 };
130
131 slaveHost = mkOption {
132 description = "Hostname of the MySQL slave server";
133 };
134
135 masterUser = mkOption {
136 description = "Username of the MySQL replication user";
137 };
138
139 masterPassword = mkOption {
140 description = "Password of the MySQL replication user";
141 };
142
143 masterPort = mkOption {
144 default = 3306;
145 description = "Port number on which the MySQL master server runs";
146 };
147 };
148 };
149
150 };
151
152
153 ###### implementation
154
155 config = mkIf config.services.mysql.enable {
156
157 users.extraUsers.mysql = {
158 description = "MySQL server user";
159 group = "mysql";
160 uid = config.ids.uids.mysql;
161 };
162
163 users.extraGroups.mysql.gid = config.ids.gids.mysql;
164
165 environment.systemPackages = [mysql];
166
167 systemd.services.mysql =
168 { description = "MySQL Server";
169
170 wantedBy = [ "multi-user.target" ];
171
172 unitConfig.RequiresMountsFor = "${cfg.dataDir}";
173
174 path = [
175 # Needed for the mysql_install_db command in the preStart script
176 # which calls the hostname command.
177 pkgs.nettools
178 ];
179
180 preStart =
181 ''
182 if ! test -e ${cfg.dataDir}/mysql; then
183 mkdir -m 0700 -p ${cfg.dataDir}
184 chown -R ${cfg.user} ${cfg.dataDir}
185 ${mysql}/bin/mysql_install_db ${mysqldOptions}
186 touch /tmp/mysql_init
187 fi
188
189 mkdir -m 0755 -p ${cfg.pidDir}
190 chown -R ${cfg.user} ${cfg.pidDir}
191
192 # Make the socket directory
193 mkdir -p /run/mysqld
194 chmod 0755 /run/mysqld
195 chown -R ${cfg.user} /run/mysqld
196 '';
197
198 serviceConfig.ExecStart = "${mysql}/bin/mysqld --defaults-extra-file=${myCnf} ${mysqldOptions}";
199
200 postStart =
201 ''
202 # Wait until the MySQL server is available for use
203 count=0
204 while [ ! -e /run/mysqld/mysqld.sock ]
205 do
206 if [ $count -eq 30 ]
207 then
208 echo "Tried 30 times, giving up..."
209 exit 1
210 fi
211
212 echo "MySQL daemon not yet started. Waiting for 1 second..."
213 count=$((count++))
214 sleep 1
215 done
216
217 if [ -f /tmp/mysql_init ]
218 then
219 ${concatMapStrings (database:
220 ''
221 # Create initial databases
222 if ! test -e "${cfg.dataDir}/${database.name}"; then
223 echo "Creating initial database: ${database.name}"
224 ( echo "create database ${database.name};"
225 echo "use ${database.name};"
226
227 if [ -f "${database.schema}" ]
228 then
229 cat ${database.schema}
230 elif [ -d "${database.schema}" ]
231 then
232 cat ${database.schema}/mysql-databases/*.sql
233 fi
234 ) | ${mysql}/bin/mysql -u root -N
235 fi
236 '') cfg.initialDatabases}
237
238 ${optionalString (cfg.replication.role == "master" && atLeast55)
239 ''
240 # Set up the replication master
241
242 ( echo "use mysql;"
243 echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
244 echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
245 echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
246 ) | ${mysql}/bin/mysql -u root -N
247 ''}
248
249 ${optionalString (cfg.replication.role == "slave" && atLeast55)
250 ''
251 # Set up the replication slave
252
253 ( echo "stop slave;"
254 echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
255 echo "start slave;"
256 ) | ${mysql}/bin/mysql -u root -N
257 ''}
258
259 ${optionalString (cfg.initialScript != null)
260 ''
261 # Execute initial script
262 cat ${cfg.initialScript} | ${mysql}/bin/mysql -u root -N
263 ''}
264
265 ${optionalString (cfg.rootPassword != null)
266 ''
267 # Change root password
268
269 ( echo "use mysql;"
270 echo "update user set Password=password('$(cat ${cfg.rootPassword})') where User='root';"
271 echo "flush privileges;"
272 ) | ${mysql}/bin/mysql -u root -N
273 ''}
274
275 rm /tmp/mysql_init
276 fi
277 ''; # */
278 };
279
280 };
281
282}