diff --git a/ci/OWNERS b/ci/OWNERS index 8b58e7e812ce..c64ef4208e30 100644 --- a/ci/OWNERS +++ b/ci/OWNERS @@ -253,6 +253,7 @@ pkgs/development/python-modules/buildcatrust/ @ajs124 @lukegb @mweinelt /nixos/tests/postgresql @NixOS/postgres # MySQL/MariaDB and related stuff +/nixos/modules/services/databases/mysql.nix @6543 /nixos/modules/services/backup/mysql-backup.nix @6543 # Hardened profile & related modules diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix index 412fe4836cd3..68ef50651f43 100644 --- a/nixos/modules/services/databases/mysql.nix +++ b/nixos/modules/services/databases/mysql.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.services.mysql; @@ -8,8 +13,7 @@ let # Oracle MySQL has supported "notify" service type since 8.0 hasNotify = isMariaDB || (isOracle && lib.versionAtLeast cfg.package.version "8.0"); - mysqldOptions = - "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}"; + mysqldOptions = "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}"; format = pkgs.formats.ini { listsAsDuplicateKeys = true; }; configFile = format.generate "my.cnf" cfg.settings; @@ -18,11 +22,31 @@ in { imports = [ - (lib.mkRemovedOptionModule [ "services" "mysql" "pidDir" ] "Don't wait for pidfiles, describe dependencies through systemd.") - (lib.mkRemovedOptionModule [ "services" "mysql" "rootPassword" ] "Use socket authentication or set the password outside of the nix store.") - (lib.mkRemovedOptionModule [ "services" "mysql" "extraOptions" ] "Use services.mysql.settings.mysqld instead.") - (lib.mkRemovedOptionModule [ "services" "mysql" "bind" ] "Use services.mysql.settings.mysqld.bind-address instead.") - (lib.mkRemovedOptionModule [ "services" "mysql" "port" ] "Use services.mysql.settings.mysqld.port instead.") + (lib.mkRemovedOptionModule [ + "services" + "mysql" + "pidDir" + ] "Don't wait for pidfiles, describe dependencies through systemd.") + (lib.mkRemovedOptionModule [ + "services" + "mysql" + "rootPassword" + ] "Use socket authentication or set the password outside of the nix store.") + (lib.mkRemovedOptionModule [ + "services" + "mysql" + "extraOptions" + ] "Use services.mysql.settings.mysqld instead.") + (lib.mkRemovedOptionModule [ + "services" + "mysql" + "bind" + ] "Use services.mysql.settings.mysqld.bind-address instead.") + (lib.mkRemovedOptionModule [ + "services" + "mysql" + "port" + ] "Use services.mysql.settings.mysqld.port instead.") ]; ###### interface @@ -106,7 +130,7 @@ in settings = lib.mkOption { type = format.type; - default = {}; + default = { }; description = '' MySQL configuration. Refer to , @@ -137,25 +161,27 @@ in }; initialDatabases = lib.mkOption { - type = lib.types.listOf (lib.types.submodule { - options = { - name = lib.mkOption { - type = lib.types.str; - description = '' - The name of the database to create. - ''; + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + description = '' + The name of the database to create. + ''; + }; + schema = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + The initial schema of the database; if null (the default), + an empty database is created. + ''; + }; }; - schema = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - description = '' - The initial schema of the database; if null (the default), - an empty database is created. - ''; - }; - }; - }); - default = []; + } + ); + default = [ ]; description = '' List of database names and their initial schemas that should be used to create databases on the first startup of MySQL. The schema attribute is optional: If not specified, an empty database is created. @@ -176,7 +202,7 @@ in ensureDatabases = lib.mkOption { type = lib.types.listOf lib.types.str; - default = []; + default = [ ]; description = '' Ensures that the specified databases exist. This option will never delete existing databases, especially not when the value of this @@ -190,39 +216,41 @@ in }; ensureUsers = lib.mkOption { - type = lib.types.listOf (lib.types.submodule { - options = { - name = lib.mkOption { - type = lib.types.str; - description = '' - Name of the user to ensure. - ''; - }; - ensurePermissions = lib.mkOption { - type = lib.types.attrsOf lib.types.str; - default = {}; - description = '' - Permissions to ensure for the user, specified as attribute set. - The attribute names specify the database and tables to grant the permissions for, - separated by a dot. You may use wildcards here. - The attribute values specfiy the permissions to grant. - You may specify one or multiple comma-separated SQL privileges here. + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.mkOption { + type = lib.types.str; + description = '' + Name of the user to ensure. + ''; + }; + ensurePermissions = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = '' + Permissions to ensure for the user, specified as attribute set. + The attribute names specify the database and tables to grant the permissions for, + separated by a dot. You may use wildcards here. + The attribute values specfiy the permissions to grant. + You may specify one or multiple comma-separated SQL privileges here. - For more information on how to specify the target - and on which privileges exist, see the - [GRANT syntax](https://mariadb.com/kb/en/library/grant/). - The attributes are used as `GRANT ''${attrName} ON ''${attrValue}`. - ''; - example = lib.literalExpression '' - { - "database.*" = "ALL PRIVILEGES"; - "*.*" = "SELECT, LOCK TABLES"; - } - ''; + For more information on how to specify the target + and on which privileges exist, see the + [GRANT syntax](https://mariadb.com/kb/en/library/grant/). + The attributes are used as `GRANT ''${attrName} ON ''${attrValue}`. + ''; + example = lib.literalExpression '' + { + "database.*" = "ALL PRIVILEGES"; + "*.*" = "SELECT, LOCK TABLES"; + } + ''; + }; }; - }; - }); - default = []; + } + ); + default = [ ]; description = '' Ensures that the specified users exist and have at least the ensured permissions. The MySQL users will be identified using Unix socket authentication. This authenticates the Unix user with the @@ -251,7 +279,11 @@ in replication = { role = lib.mkOption { - type = lib.types.enum [ "master" "slave" "none" ]; + type = lib.types.enum [ + "master" + "slave" + "none" + ]; default = "none"; description = "Role of the MySQL server instance."; }; @@ -292,14 +324,13 @@ in }; - ###### implementation config = lib.mkIf cfg.enable { - services.mysql.dataDir = - lib.mkDefault (if lib.versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql" - else "/var/mysql"); + services.mysql.dataDir = lib.mkDefault ( + if lib.versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql" else "/var/mysql" + ); services.mysql.settings.mysqld = lib.mkMerge [ { @@ -311,7 +342,11 @@ in log-bin-index = "mysql-bin-${toString cfg.replication.serverId}.index"; relay-log = "mysql-relay-bin"; server-id = cfg.replication.serverId; - binlog-ignore-db = [ "information_schema" "performance_schema" "mysql" ]; + binlog-ignore-db = [ + "information_schema" + "performance_schema" + "mysql" + ]; }) (lib.mkIf (!isMariaDB) { plugin-load-add = [ "auth_socket.so" ]; @@ -355,17 +390,21 @@ in 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 - ''; + 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 @@ -379,52 +418,55 @@ in 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 '' - ${lib.optionalString (!hasNotify) '' - # Wait until the MySQL server is available for use - while [ ! -e /run/mysqld/mysqld.sock ] - do - echo "MySQL daemon not yet started. Waiting for 1 second..." - sleep 1 - done - ''} + postStart = + let + # The super user account to use on *first* run of MySQL server + superUser = if isMariaDB then cfg.user else "root"; + in + '' + ${lib.optionalString (!hasNotify) '' + # Wait until the MySQL server is available for use + while [ ! -e /run/mysqld/mysqld.sock ] + do + echo "MySQL daemon not yet started. Waiting for 1 second..." + 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 + 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 - ${lib.concatMapStrings (database: '' - # Create initial databases - if ! test -e "${cfg.dataDir}/${database.name}"; then - echo "Creating initial database: ${database.name}" - ( echo 'create database `${database.name}`;' + ${lib.concatMapStrings (database: '' + # Create initial databases + if ! test -e "${cfg.dataDir}/${database.name}"; then + echo "Creating initial database: ${database.name}" + ( echo 'create database `${database.name}`;' - ${lib.optionalString (database.schema != null) '' - echo 'use `${database.name}`;' + ${lib.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} + # 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} - ${lib.optionalString (cfg.replication.role == "master") - '' + ${lib.optionalString (cfg.replication.role == "master") '' # Set up the replication master ( echo "use mysql;" @@ -434,8 +476,7 @@ in ) | ${cfg.package}/bin/mysql -u ${superUser} -N ''} - ${lib.optionalString (cfg.replication.role == "slave") - '' + ${lib.optionalString (cfg.replication.role == "slave") '' # Set up the replication slave ( echo "stop slave;" @@ -444,34 +485,36 @@ in ) | ${cfg.package}/bin/mysql -u ${superUser} -N ''} - ${lib.optionalString (cfg.initialScript != null) - '' + ${lib.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 + rm ${cfg.dataDir}/mysql_init + fi - ${lib.optionalString (cfg.ensureDatabases != []) '' - ( - ${lib.concatMapStrings (database: '' - echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;" - '') cfg.ensureDatabases} - ) | ${cfg.package}/bin/mysql -N - ''} + ${lib.optionalString (cfg.ensureDatabases != [ ]) '' + ( + ${lib.concatMapStrings (database: '' + echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;" + '') cfg.ensureDatabases} + ) | ${cfg.package}/bin/mysql -N + ''} - ${lib.concatMapStrings (user: - '' - ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};" - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (database: permission: '' - echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';" - '') user.ensurePermissions)} + ${lib.concatMapStrings (user: '' + ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${ + if isMariaDB then "unix_socket" else "auth_socket" + };" + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList (database: permission: '' + echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';" + '') user.ensurePermissions + )} ) | ${cfg.package}/bin/mysql -N '') cfg.ensureUsers} - ''; + ''; serviceConfig = lib.mkMerge [ { @@ -500,7 +543,11 @@ in ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; - RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; LockPersonality = true; MemoryDenyWriteExecute = true; RestrictRealtime = true; @@ -516,4 +563,6 @@ in ]; }; }; + + meta.maintainers = [ lib.maintainers._6543 ]; }