|
|
|
@ -62,7 +62,7 @@ in |
|
|
|
|
Group account under which MySQL runs. |
|
|
|
|
|
|
|
|
|
<note><para> |
|
|
|
|
If left as the default value this user will automatically be created |
|
|
|
|
If left as the default value this group will automatically be created |
|
|
|
|
on system activation, otherwise you are responsible for |
|
|
|
|
ensuring the user exists before the MySQL service starts. |
|
|
|
|
</para></note> |
|
|
|
@ -283,7 +283,7 @@ in |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
masterPort = mkOption { |
|
|
|
|
type = types.int; |
|
|
|
|
type = types.port; |
|
|
|
|
default = 3306; |
|
|
|
|
description = "Port number on which the MySQL master server runs."; |
|
|
|
|
}; |
|
|
|
@ -295,7 +295,7 @@ in |
|
|
|
|
|
|
|
|
|
###### implementation |
|
|
|
|
|
|
|
|
|
config = mkIf config.services.mysql.enable { |
|
|
|
|
config = mkIf cfg.enable { |
|
|
|
|
|
|
|
|
|
services.mysql.dataDir = |
|
|
|
|
mkDefault (if versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql" |
|
|
|
@ -334,192 +334,188 @@ in |
|
|
|
|
|
|
|
|
|
environment.etc."my.cnf".source = cfg.configFile; |
|
|
|
|
|
|
|
|
|
systemd.services.mysql = let |
|
|
|
|
hasNotify = isMariaDB; |
|
|
|
|
in { |
|
|
|
|
description = "MySQL Server"; |
|
|
|
|
|
|
|
|
|
after = [ "network.target" ]; |
|
|
|
|
wantedBy = [ "multi-user.target" ]; |
|
|
|
|
restartTriggers = [ cfg.configFile ]; |
|
|
|
|
|
|
|
|
|
unitConfig.RequiresMountsFor = "${cfg.dataDir}"; |
|
|
|
|
|
|
|
|
|
path = [ |
|
|
|
|
# Needed for the mysql_install_db command in the preStart script |
|
|
|
|
# which calls the hostname command. |
|
|
|
|
pkgs.nettools |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
preStart = if isMariaDB then '' |
|
|
|
|
if ! test -e ${cfg.dataDir}/mysql; then |
|
|
|
|
${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions} |
|
|
|
|
touch ${cfg.dataDir}/mysql_init |
|
|
|
|
fi |
|
|
|
|
'' else '' |
|
|
|
|
if ! test -e ${cfg.dataDir}/mysql; then |
|
|
|
|
${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure |
|
|
|
|
touch ${cfg.dataDir}/mysql_init |
|
|
|
|
fi |
|
|
|
|
''; |
|
|
|
|
|
|
|
|
|
script = '' |
|
|
|
|
# https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery |
|
|
|
|
if test -n "''${_WSREP_START_POSITION}"; then |
|
|
|
|
if test -e "${cfg.package}/bin/galera_recovery"; then |
|
|
|
|
VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1 |
|
|
|
|
fi |
|
|
|
|
systemd.services.mysql = { |
|
|
|
|
description = "MySQL Server"; |
|
|
|
|
|
|
|
|
|
after = [ "network.target" ]; |
|
|
|
|
wantedBy = [ "multi-user.target" ]; |
|
|
|
|
restartTriggers = [ cfg.configFile ]; |
|
|
|
|
|
|
|
|
|
unitConfig.RequiresMountsFor = cfg.dataDir; |
|
|
|
|
|
|
|
|
|
path = [ |
|
|
|
|
# Needed for the mysql_install_db command in the preStart script |
|
|
|
|
# which calls the hostname command. |
|
|
|
|
pkgs.nettools |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
preStart = if isMariaDB then '' |
|
|
|
|
if ! test -e ${cfg.dataDir}/mysql; then |
|
|
|
|
${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions} |
|
|
|
|
touch ${cfg.dataDir}/mysql_init |
|
|
|
|
fi |
|
|
|
|
'' else '' |
|
|
|
|
if ! test -e ${cfg.dataDir}/mysql; then |
|
|
|
|
${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure |
|
|
|
|
touch ${cfg.dataDir}/mysql_init |
|
|
|
|
fi |
|
|
|
|
''; |
|
|
|
|
|
|
|
|
|
script = '' |
|
|
|
|
# https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery |
|
|
|
|
if test -n "''${_WSREP_START_POSITION}"; then |
|
|
|
|
if test -e "${cfg.package}/bin/galera_recovery"; then |
|
|
|
|
VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1 |
|
|
|
|
fi |
|
|
|
|
fi |
|
|
|
|
|
|
|
|
|
# The last two environment variables are used for starting Galera clusters |
|
|
|
|
exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION |
|
|
|
|
''; |
|
|
|
|
|
|
|
|
|
postStart = let |
|
|
|
|
# The super user account to use on *first* run of MySQL server |
|
|
|
|
superUser = if isMariaDB then cfg.user else "root"; |
|
|
|
|
in '' |
|
|
|
|
${optionalString (!isMariaDB) '' |
|
|
|
|
# Wait until the MySQL server is available for use |
|
|
|
|
count=0 |
|
|
|
|
while [ ! -e /run/mysqld/mysqld.sock ] |
|
|
|
|
do |
|
|
|
|
if [ $count -eq 30 ] |
|
|
|
|
then |
|
|
|
|
echo "Tried 30 times, giving up..." |
|
|
|
|
exit 1 |
|
|
|
|
fi |
|
|
|
|
|
|
|
|
|
echo "MySQL daemon not yet started. Waiting for 1 second..." |
|
|
|
|
count=$((count++)) |
|
|
|
|
sleep 1 |
|
|
|
|
done |
|
|
|
|
''} |
|
|
|
|
|
|
|
|
|
if [ -f ${cfg.dataDir}/mysql_init ] |
|
|
|
|
then |
|
|
|
|
# While MariaDB comes with a 'mysql' super user account since 10.4.x, MySQL does not |
|
|
|
|
# Since we don't want to run this service as 'root' we need to ensure the account exists on first run |
|
|
|
|
( echo "CREATE USER IF NOT EXISTS '${cfg.user}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};" |
|
|
|
|
echo "GRANT ALL PRIVILEGES ON *.* TO '${cfg.user}'@'localhost' WITH GRANT OPTION;" |
|
|
|
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N |
|
|
|
|
|
|
|
|
|
# The last two environment variables are used for starting Galera clusters |
|
|
|
|
exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION |
|
|
|
|
''; |
|
|
|
|
|
|
|
|
|
postStart = let |
|
|
|
|
# The super user account to use on *first* run of MySQL server |
|
|
|
|
superUser = if isMariaDB then cfg.user else "root"; |
|
|
|
|
in '' |
|
|
|
|
${optionalString (!hasNotify) '' |
|
|
|
|
# Wait until the MySQL server is available for use |
|
|
|
|
count=0 |
|
|
|
|
while [ ! -e /run/mysqld/mysqld.sock ] |
|
|
|
|
do |
|
|
|
|
if [ $count -eq 30 ] |
|
|
|
|
then |
|
|
|
|
echo "Tried 30 times, giving up..." |
|
|
|
|
exit 1 |
|
|
|
|
fi |
|
|
|
|
|
|
|
|
|
echo "MySQL daemon not yet started. Waiting for 1 second..." |
|
|
|
|
count=$((count++)) |
|
|
|
|
sleep 1 |
|
|
|
|
done |
|
|
|
|
''} |
|
|
|
|
|
|
|
|
|
if [ -f ${cfg.dataDir}/mysql_init ] |
|
|
|
|
then |
|
|
|
|
# While MariaDB comes with a 'mysql' super user account since 10.4.x, MySQL does not |
|
|
|
|
# Since we don't want to run this service as 'root' we need to ensure the account exists on first run |
|
|
|
|
( echo "CREATE USER IF NOT EXISTS '${cfg.user}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};" |
|
|
|
|
echo "GRANT ALL PRIVILEGES ON *.* TO '${cfg.user}'@'localhost' WITH GRANT OPTION;" |
|
|
|
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N |
|
|
|
|
|
|
|
|
|
${concatMapStrings (database: '' |
|
|
|
|
# Create initial databases |
|
|
|
|
if ! test -e "${cfg.dataDir}/${database.name}"; then |
|
|
|
|
echo "Creating initial database: ${database.name}" |
|
|
|
|
( echo 'create database `${database.name}`;' |
|
|
|
|
|
|
|
|
|
${optionalString (database.schema != null) '' |
|
|
|
|
echo 'use `${database.name}`;' |
|
|
|
|
|
|
|
|
|
# TODO: this silently falls through if database.schema does not exist, |
|
|
|
|
# we should catch this somehow and exit, but can't do it here because we're in a subshell. |
|
|
|
|
if [ -f "${database.schema}" ] |
|
|
|
|
then |
|
|
|
|
cat ${database.schema} |
|
|
|
|
elif [ -d "${database.schema}" ] |
|
|
|
|
then |
|
|
|
|
cat ${database.schema}/mysql-databases/*.sql |
|
|
|
|
fi |
|
|
|
|
''} |
|
|
|
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N |
|
|
|
|
fi |
|
|
|
|
'') cfg.initialDatabases} |
|
|
|
|
|
|
|
|
|
${optionalString (cfg.replication.role == "master") |
|
|
|
|
'' |
|
|
|
|
# Set up the replication master |
|
|
|
|
|
|
|
|
|
( echo "use mysql;" |
|
|
|
|
echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;" |
|
|
|
|
echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');" |
|
|
|
|
echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';" |
|
|
|
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N |
|
|
|
|
''} |
|
|
|
|
|
|
|
|
|
${optionalString (cfg.replication.role == "slave") |
|
|
|
|
'' |
|
|
|
|
# Set up the replication slave |
|
|
|
|
|
|
|
|
|
( echo "stop slave;" |
|
|
|
|
echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';" |
|
|
|
|
echo "start slave;" |
|
|
|
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N |
|
|
|
|
''} |
|
|
|
|
|
|
|
|
|
${optionalString (cfg.initialScript != null) |
|
|
|
|
'' |
|
|
|
|
# Execute initial script |
|
|
|
|
# using toString to avoid copying the file to nix store if given as path instead of string, |
|
|
|
|
# as it might contain credentials |
|
|
|
|
cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N |
|
|
|
|
''} |
|
|
|
|
|
|
|
|
|
rm ${cfg.dataDir}/mysql_init |
|
|
|
|
fi |
|
|
|
|
|
|
|
|
|
${optionalString (cfg.ensureDatabases != []) '' |
|
|
|
|
( |
|
|
|
|
${concatMapStrings (database: '' |
|
|
|
|
echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;" |
|
|
|
|
'') cfg.ensureDatabases} |
|
|
|
|
# Create initial databases |
|
|
|
|
if ! test -e "${cfg.dataDir}/${database.name}"; then |
|
|
|
|
echo "Creating initial database: ${database.name}" |
|
|
|
|
( echo 'create database `${database.name}`;' |
|
|
|
|
|
|
|
|
|
${optionalString (database.schema != null) '' |
|
|
|
|
echo 'use `${database.name}`;' |
|
|
|
|
|
|
|
|
|
# TODO: this silently falls through if database.schema does not exist, |
|
|
|
|
# we should catch this somehow and exit, but can't do it here because we're in a subshell. |
|
|
|
|
if [ -f "${database.schema}" ] |
|
|
|
|
then |
|
|
|
|
cat ${database.schema} |
|
|
|
|
elif [ -d "${database.schema}" ] |
|
|
|
|
then |
|
|
|
|
cat ${database.schema}/mysql-databases/*.sql |
|
|
|
|
fi |
|
|
|
|
''} |
|
|
|
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N |
|
|
|
|
fi |
|
|
|
|
'') cfg.initialDatabases} |
|
|
|
|
|
|
|
|
|
${optionalString (cfg.replication.role == "master") |
|
|
|
|
'' |
|
|
|
|
# Set up the replication master |
|
|
|
|
|
|
|
|
|
( echo "use mysql;" |
|
|
|
|
echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;" |
|
|
|
|
echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');" |
|
|
|
|
echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';" |
|
|
|
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N |
|
|
|
|
''} |
|
|
|
|
|
|
|
|
|
${optionalString (cfg.replication.role == "slave") |
|
|
|
|
'' |
|
|
|
|
# Set up the replication slave |
|
|
|
|
|
|
|
|
|
( echo "stop slave;" |
|
|
|
|
echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';" |
|
|
|
|
echo "start slave;" |
|
|
|
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N |
|
|
|
|
''} |
|
|
|
|
|
|
|
|
|
${optionalString (cfg.initialScript != null) |
|
|
|
|
'' |
|
|
|
|
# Execute initial script |
|
|
|
|
# using toString to avoid copying the file to nix store if given as path instead of string, |
|
|
|
|
# as it might contain credentials |
|
|
|
|
cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N |
|
|
|
|
''} |
|
|
|
|
|
|
|
|
|
rm ${cfg.dataDir}/mysql_init |
|
|
|
|
fi |
|
|
|
|
|
|
|
|
|
${optionalString (cfg.ensureDatabases != []) '' |
|
|
|
|
( |
|
|
|
|
${concatMapStrings (database: '' |
|
|
|
|
echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;" |
|
|
|
|
'') cfg.ensureDatabases} |
|
|
|
|
) | ${cfg.package}/bin/mysql -N |
|
|
|
|
''} |
|
|
|
|
|
|
|
|
|
${concatMapStrings (user: |
|
|
|
|
'' |
|
|
|
|
( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};" |
|
|
|
|
${concatStringsSep "\n" (mapAttrsToList (database: permission: '' |
|
|
|
|
echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';" |
|
|
|
|
'') user.ensurePermissions)} |
|
|
|
|
) | ${cfg.package}/bin/mysql -N |
|
|
|
|
''} |
|
|
|
|
|
|
|
|
|
${concatMapStrings (user: |
|
|
|
|
'' |
|
|
|
|
( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};" |
|
|
|
|
${concatStringsSep "\n" (mapAttrsToList (database: permission: '' |
|
|
|
|
echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';" |
|
|
|
|
'') user.ensurePermissions)} |
|
|
|
|
) | ${cfg.package}/bin/mysql -N |
|
|
|
|
'') cfg.ensureUsers} |
|
|
|
|
''; |
|
|
|
|
|
|
|
|
|
serviceConfig = mkMerge [ |
|
|
|
|
{ |
|
|
|
|
Type = if hasNotify then "notify" else "simple"; |
|
|
|
|
Restart = "on-abort"; |
|
|
|
|
RestartSec = "5s"; |
|
|
|
|
|
|
|
|
|
# User and group |
|
|
|
|
User = cfg.user; |
|
|
|
|
Group = cfg.group; |
|
|
|
|
# Runtime directory and mode |
|
|
|
|
RuntimeDirectory = "mysqld"; |
|
|
|
|
RuntimeDirectoryMode = "0755"; |
|
|
|
|
# Access write directories |
|
|
|
|
ReadWritePaths = [ cfg.dataDir ]; |
|
|
|
|
# Capabilities |
|
|
|
|
CapabilityBoundingSet = ""; |
|
|
|
|
# Security |
|
|
|
|
NoNewPrivileges = true; |
|
|
|
|
# Sandboxing |
|
|
|
|
ProtectSystem = "strict"; |
|
|
|
|
ProtectHome = true; |
|
|
|
|
PrivateTmp = true; |
|
|
|
|
PrivateDevices = true; |
|
|
|
|
ProtectHostname = true; |
|
|
|
|
ProtectKernelTunables = true; |
|
|
|
|
ProtectKernelModules = true; |
|
|
|
|
ProtectControlGroups = true; |
|
|
|
|
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; |
|
|
|
|
LockPersonality = true; |
|
|
|
|
MemoryDenyWriteExecute = true; |
|
|
|
|
RestrictRealtime = true; |
|
|
|
|
RestrictSUIDSGID = true; |
|
|
|
|
PrivateMounts = true; |
|
|
|
|
# System Call Filtering |
|
|
|
|
SystemCallArchitectures = "native"; |
|
|
|
|
} |
|
|
|
|
(mkIf (cfg.dataDir == "/var/lib/mysql") { |
|
|
|
|
StateDirectory = "mysql"; |
|
|
|
|
StateDirectoryMode = "0700"; |
|
|
|
|
}) |
|
|
|
|
]; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
'') cfg.ensureUsers} |
|
|
|
|
''; |
|
|
|
|
|
|
|
|
|
serviceConfig = mkMerge [ |
|
|
|
|
{ |
|
|
|
|
Type = if isMariaDB then "notify" else "simple"; |
|
|
|
|
Restart = "on-abort"; |
|
|
|
|
RestartSec = "5s"; |
|
|
|
|
|
|
|
|
|
# User and group |
|
|
|
|
User = cfg.user; |
|
|
|
|
Group = cfg.group; |
|
|
|
|
# Runtime directory and mode |
|
|
|
|
RuntimeDirectory = "mysqld"; |
|
|
|
|
RuntimeDirectoryMode = "0755"; |
|
|
|
|
# Access write directories |
|
|
|
|
ReadWritePaths = [ cfg.dataDir ]; |
|
|
|
|
# Capabilities |
|
|
|
|
CapabilityBoundingSet = ""; |
|
|
|
|
# Security |
|
|
|
|
NoNewPrivileges = true; |
|
|
|
|
# Sandboxing |
|
|
|
|
ProtectSystem = "strict"; |
|
|
|
|
ProtectHome = true; |
|
|
|
|
PrivateTmp = true; |
|
|
|
|
PrivateDevices = true; |
|
|
|
|
ProtectHostname = true; |
|
|
|
|
ProtectKernelTunables = true; |
|
|
|
|
ProtectKernelModules = true; |
|
|
|
|
ProtectControlGroups = true; |
|
|
|
|
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; |
|
|
|
|
LockPersonality = true; |
|
|
|
|
MemoryDenyWriteExecute = true; |
|
|
|
|
RestrictRealtime = true; |
|
|
|
|
RestrictSUIDSGID = true; |
|
|
|
|
PrivateMounts = true; |
|
|
|
|
# System Call Filtering |
|
|
|
|
SystemCallArchitectures = "native"; |
|
|
|
|
} |
|
|
|
|
(mkIf (cfg.dataDir == "/var/lib/mysql") { |
|
|
|
|
StateDirectory = "mysql"; |
|
|
|
|
StateDirectoryMode = "0700"; |
|
|
|
|
}) |
|
|
|
|
]; |
|
|
|
|
}; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|