at 18.09-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.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}