about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/databases/mysql.nix
diff options
context:
space:
mode:
authorAlyssa Ross <hi@alyssa.is>2021-01-10 07:13:44 +0000
committerAlyssa Ross <hi@alyssa.is>2021-01-12 14:07:16 +0000
commite2698550456abba83c6dcd5d5e5a9990a0b96f8a (patch)
tree79a56f0df3fa55e470d84b4dff6059fbf487ec18 /nixpkgs/nixos/modules/services/databases/mysql.nix
parent1cdc42df888dc98c347e03bd942ed9825a55bcb3 (diff)
parent84d74ae9c9cbed73274b8e4e00be14688ffc93fe (diff)
downloadnixlib-e2698550456abba83c6dcd5d5e5a9990a0b96f8a.tar
nixlib-e2698550456abba83c6dcd5d5e5a9990a0b96f8a.tar.gz
nixlib-e2698550456abba83c6dcd5d5e5a9990a0b96f8a.tar.bz2
nixlib-e2698550456abba83c6dcd5d5e5a9990a0b96f8a.tar.lz
nixlib-e2698550456abba83c6dcd5d5e5a9990a0b96f8a.tar.xz
nixlib-e2698550456abba83c6dcd5d5e5a9990a0b96f8a.tar.zst
nixlib-e2698550456abba83c6dcd5d5e5a9990a0b96f8a.zip
Merge commit '84d74ae9c9cbed73274b8e4e00be14688ffc93fe'
Diffstat (limited to 'nixpkgs/nixos/modules/services/databases/mysql.nix')
-rw-r--r--nixpkgs/nixos/modules/services/databases/mysql.nix260
1 files changed, 135 insertions, 125 deletions
diff --git a/nixpkgs/nixos/modules/services/databases/mysql.nix b/nixpkgs/nixos/modules/services/databases/mysql.nix
index 2e8c5b7640b2..7d0a3f9afc48 100644
--- a/nixpkgs/nixos/modules/services/databases/mysql.nix
+++ b/nixpkgs/nixos/modules/services/databases/mysql.nix
@@ -6,12 +6,10 @@ let
 
   cfg = config.services.mysql;
 
-  mysql = cfg.package;
-
-  isMariaDB = lib.getName mysql == lib.getName pkgs.mariadb;
+  isMariaDB = lib.getName cfg.package == lib.getName pkgs.mariadb;
 
   mysqldOptions =
-    "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${mysql}";
+    "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}";
 
   settingsFile = pkgs.writeText "my.cnf" (
     generators.toINI { listsAsDuplicateKeys = true; } cfg.settings +
@@ -22,7 +20,7 @@ in
 
 {
   imports = [
-    (mkRemovedOptionModule [ "services" "mysql" "pidDir" ] "Don't wait for pidfiles, describe dependencies through systemd")
+    (mkRemovedOptionModule [ "services" "mysql" "pidDir" ] "Don't wait for pidfiles, describe dependencies through systemd.")
     (mkRemovedOptionModule [ "services" "mysql" "rootPassword" ] "Use socket authentication or set the password outside of the nix store.")
   ];
 
@@ -46,25 +44,31 @@ in
         type = types.nullOr types.str;
         default = null;
         example = literalExample "0.0.0.0";
-        description = "Address to bind to. The default is to bind to all addresses";
+        description = "Address to bind to. The default is to bind to all addresses.";
       };
 
       port = mkOption {
         type = types.int;
         default = 3306;
-        description = "Port of MySQL";
+        description = "Port of MySQL.";
       };
 
       user = mkOption {
         type = types.str;
         default = "mysql";
-        description = "User account under which MySQL runs";
+        description = "User account under which MySQL runs.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "mysql";
+        description = "Group under which MySQL runs.";
       };
 
       dataDir = mkOption {
         type = types.path;
         example = "/var/lib/mysql";
-        description = "Location where MySQL stores its table files";
+        description = "Location where MySQL stores its table files.";
       };
 
       configFile = mkOption {
@@ -171,7 +175,7 @@ in
       initialScript = mkOption {
         type = types.nullOr types.path;
         default = null;
-        description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database";
+        description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database.";
       };
 
       ensureDatabases = mkOption {
@@ -259,33 +263,33 @@ in
         serverId = mkOption {
           type = types.int;
           default = 1;
-          description = "Id of the MySQL server instance. This number must be unique for each instance";
+          description = "Id of the MySQL server instance. This number must be unique for each instance.";
         };
 
         masterHost = mkOption {
           type = types.str;
-          description = "Hostname of the MySQL master server";
+          description = "Hostname of the MySQL master server.";
         };
 
         slaveHost = mkOption {
           type = types.str;
-          description = "Hostname of the MySQL slave server";
+          description = "Hostname of the MySQL slave server.";
         };
 
         masterUser = mkOption {
           type = types.str;
-          description = "Username of the MySQL replication user";
+          description = "Username of the MySQL replication user.";
         };
 
         masterPassword = mkOption {
           type = types.str;
-          description = "Password of the MySQL replication user";
+          description = "Password of the MySQL replication user.";
         };
 
         masterPort = mkOption {
           type = types.int;
           default = 3306;
-          description = "Port number on which the MySQL master server runs";
+          description = "Port number on which the MySQL master server runs.";
         };
       };
     };
@@ -317,29 +321,33 @@ in
         binlog-ignore-db = [ "information_schema" "performance_schema" "mysql" ];
       })
       (mkIf (!isMariaDB) {
-        plugin-load-add = optional (cfg.ensureUsers != []) "auth_socket.so";
+        plugin-load-add = "auth_socket.so";
       })
     ];
 
-    users.users.mysql = {
-      description = "MySQL server user";
-      group = "mysql";
-      uid = config.ids.uids.mysql;
+    users.users = optionalAttrs (cfg.user == "mysql") {
+      mysql = {
+        description = "MySQL server user";
+        group = cfg.group;
+        uid = config.ids.uids.mysql;
+      };
     };
 
-    users.groups.mysql.gid = config.ids.gids.mysql;
+    users.groups = optionalAttrs (cfg.group == "mysql") {
+      mysql.gid = config.ids.gids.mysql;
+    };
 
-    environment.systemPackages = [mysql];
+    environment.systemPackages = [ cfg.package ];
 
     environment.etc."my.cnf".source = cfg.configFile;
 
     systemd.tmpfiles.rules = [
-      "d '${cfg.dataDir}' 0700 ${cfg.user} mysql - -"
-      "z '${cfg.dataDir}' 0700 ${cfg.user} mysql - -"
+      "d '${cfg.dataDir}' 0700 '${cfg.user}' '${cfg.group}' - -"
+      "z '${cfg.dataDir}' 0700 '${cfg.user}' '${cfg.group}' - -"
     ];
 
     systemd.services.mysql = let
-      hasNotify = (cfg.package == pkgs.mariadb);
+      hasNotify = isMariaDB;
     in {
         description = "MySQL Server";
 
@@ -357,125 +365,127 @@ in
 
         preStart = if isMariaDB then ''
           if ! test -e ${cfg.dataDir}/mysql; then
-            ${mysql}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
+            ${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
-            ${mysql}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
+            ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
             touch ${cfg.dataDir}/mysql_init
           fi
         '';
 
-        serviceConfig = {
-          Type = if hasNotify then "notify" else "simple";
-          Restart = "on-abort";
-          RestartSec = "5s";
-          # The last two environment variables are used for starting Galera clusters
-          ExecStart = "${mysql}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION";
-          ExecStartPost =
-            let
-              setupScript = pkgs.writeScript "mysql-setup" ''
-                #!${pkgs.runtimeShell} -e
-
-                ${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 ]
+        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
-                    ${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
-                            ''}
-                          ) | ${mysql}/bin/mysql -u root -N
-                      fi
-                    '') cfg.initialDatabases}
-
-                    ${optionalString (cfg.replication.role == "master")
-                      ''
-                        # Set up the replication master
+                    echo "Tried 30 times, giving up..."
+                    exit 1
+                fi
 
-                        ( 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}';"
-                        ) | ${mysql}/bin/mysql -u root -N
+                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 == "slave")
-                      ''
-                        # Set up the replication slave
+              ${optionalString (cfg.replication.role == "master")
+                ''
+                  # Set up the replication master
 
-                        ( 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;"
-                        ) | ${mysql}/bin/mysql -u root -N
-                      ''}
+                  ( 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.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} | ${mysql}/bin/mysql -u root -N
-                      ''}
+              ${optionalString (cfg.replication.role == "slave")
+                ''
+                  # Set up the replication slave
 
-                    rm ${cfg.dataDir}/mysql_init
-                fi
+                  ( 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.ensureDatabases != []) ''
-                  (
-                  ${concatMapStrings (database: ''
-                    echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
-                  '') cfg.ensureDatabases}
-                  ) | ${mysql}/bin/mysql -u root -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
                 ''}
 
-                ${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)}
-                    ) | ${mysql}/bin/mysql -u root -N
-                  '') cfg.ensureUsers}
-              '';
-            in
-              # ensureDatbases & ensureUsers depends on this script being run as root
-              # when the user has secured their mysql install
-              "+${setupScript}";
+              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
+            '') cfg.ensureUsers}
+        '';
+
+        serviceConfig = {
+          Type = if hasNotify then "notify" else "simple";
+          Restart = "on-abort";
+          RestartSec = "5s";
+          # The last two environment variables are used for starting Galera clusters
+          ExecStart = "${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION";
           # User and group
           User = cfg.user;
-          Group = "mysql";
+          Group = cfg.group;
           # Runtime directory and mode
           RuntimeDirectory = "mysqld";
           RuntimeDirectoryMode = "0755";