at 18.03-beta 12 kB view raw
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}