about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/databases/mysql.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/modules/services/databases/mysql.nix')
-rw-r--r--nixpkgs/nixos/modules/services/databases/mysql.nix440
1 files changed, 440 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/services/databases/mysql.nix b/nixpkgs/nixos/modules/services/databases/mysql.nix
new file mode 100644
index 000000000000..89291d4438ff
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/databases/mysql.nix
@@ -0,0 +1,440 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.mysql;
+
+  mysql = cfg.package;
+
+  isMariaDB =
+    let
+      pName = _p: (builtins.parseDrvName (_p.name)).name;
+    in pName mysql == pName pkgs.mariadb;
+  isMysqlAtLeast57 =
+    let
+      pName = _p: (builtins.parseDrvName (_p.name)).name;
+    in (pName mysql == pName pkgs.mysql57)
+       && ((builtins.compareVersions mysql.version "5.7") >= 0);
+
+  pidFile = "${cfg.pidDir}/mysqld.pid";
+
+  mysqldAndInstallOptions =
+    "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${mysql}";
+  mysqldOptions =
+    "${mysqldAndInstallOptions} --pid-file=${pidFile}";
+  # For MySQL 5.7+, --insecure creates the root user without password
+  # (earlier versions and MariaDB do this by default).
+  installOptions =
+    "${mysqldAndInstallOptions} ${lib.optionalString isMysqlAtLeast57 "--insecure"}";
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.mysql = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "
+          Whether to enable the MySQL server.
+        ";
+      };
+
+      package = mkOption {
+        type = types.package;
+        example = literalExample "pkgs.mysql";
+        description = "
+          Which MySQL derivation to use. MariaDB packages are supported too.
+        ";
+      };
+
+      bind = mkOption {
+        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";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 3306;
+        description = "Port of MySQL";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "mysql";
+        description = "User account under which MySQL runs";
+      };
+
+      dataDir = mkOption {
+        type = types.path;
+        example = "/var/lib/mysql";
+        description = "Location where MySQL stores its table files";
+      };
+
+      pidDir = mkOption {
+        default = "/run/mysqld";
+        description = "Location of the file which stores the PID of the MySQL server";
+      };
+
+      extraOptions = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          key_buffer_size = 6G
+          table_cache = 1600
+          log-error = /var/log/mysql_err.log
+        '';
+        description = ''
+          Provide extra options to the MySQL configuration file.
+
+          Please note, that these options are added to the
+          <literal>[mysqld]</literal> section so you don't need to explicitly
+          state it again.
+        '';
+      };
+
+      initialDatabases = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            name = mkOption {
+              type = types.str;
+              description = ''
+                The name of the database to create.
+              '';
+            };
+            schema = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = ''
+                The initial schema of the database; if null (the default),
+                an empty database is created.
+              '';
+            };
+          };
+        });
+        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.
+        '';
+        example = [
+          { name = "foodatabase"; schema = literalExample "./foodatabase.sql"; }
+          { name = "bardatabase"; }
+        ];
+      };
+
+      initialScript = mkOption {
+        type = types.nullOr types.lines;
+        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";
+      };
+
+      ensureDatabases = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          Ensures that the specified databases exist.
+          This option will never delete existing databases, especially not when the value of this
+          option is changed. This means that databases created once through this option or
+          otherwise have to be removed manually.
+        '';
+        example = [
+          "nextcloud"
+          "matomo"
+        ];
+      };
+
+      ensureUsers = mkOption {
+        type = types.listOf (types.submodule {
+          options = {
+            name = mkOption {
+              type = types.str;
+              description = ''
+                Name of the user to ensure.
+              '';
+            };
+            ensurePermissions = mkOption {
+              type = types.attrsOf 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
+                <link xlink:href="https://mariadb.com/kb/en/library/grant/">GRANT syntax</link>.
+                The attributes are used as <code>GRANT ''${attrName} ON ''${attrValue}</code>.
+              '';
+              example = literalExample ''
+                {
+                  "database.*" = "ALL PRIVILEGES";
+                  "*.*" = "SELECT, LOCK TABLES";
+                }
+              '';
+            };
+          };
+        });
+        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
+          same name only, and that without the need for a password.
+          This option will never delete existing users or remove permissions, especially not when the value of this
+          option is changed. This means that users created and permissions assigned once through this option or
+          otherwise have to be removed manually.
+        '';
+        example = literalExample ''
+          [
+            {
+              name = "nextcloud";
+              ensurePermissions = {
+                "nextcloud.*" = "ALL PRIVILEGES";
+              };
+            }
+            {
+              name = "backup";
+              ensurePermissions = {
+                "*.*" = "SELECT, LOCK TABLES";
+              };
+            }
+          ]
+        '';
+      };
+
+      # FIXME: remove this option; it's a really bad idea.
+      rootPassword = mkOption {
+        default = null;
+        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.";
+      };
+
+      replication = {
+        role = mkOption {
+          type = types.enum [ "master" "slave" "none" ];
+          default = "none";
+          description = "Role of the MySQL server instance.";
+        };
+
+        serverId = mkOption {
+          type = types.int;
+          default = 1;
+          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";
+        };
+
+        slaveHost = mkOption {
+          type = types.str;
+          description = "Hostname of the MySQL slave server";
+        };
+
+        masterUser = mkOption {
+          type = types.str;
+          description = "Username of the MySQL replication user";
+        };
+
+        masterPassword = mkOption {
+          type = types.str;
+          description = "Password of the MySQL replication user";
+        };
+
+        masterPort = mkOption {
+          type = types.int;
+          default = 3306;
+          description = "Port number on which the MySQL master server runs";
+        };
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.mysql.enable {
+
+    services.mysql.dataDir =
+      mkDefault (if versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql"
+                 else "/var/mysql");
+
+    users.users.mysql = {
+      description = "MySQL server user";
+      group = "mysql";
+      uid = config.ids.uids.mysql;
+    };
+
+    users.groups.mysql.gid = config.ids.gids.mysql;
+
+    environment.systemPackages = [mysql];
+
+    environment.etc."my.cnf".text =
+    ''
+      [mysqld]
+      port = ${toString cfg.port}
+      datadir = ${cfg.dataDir}
+      ${optionalString (cfg.bind != null) "bind-address = ${cfg.bind}" }
+      ${optionalString (cfg.replication.role == "master" || cfg.replication.role == "slave") "log-bin=mysql-bin"}
+      ${optionalString (cfg.replication.role == "master" || cfg.replication.role == "slave") "server-id = ${toString cfg.replication.serverId}"}
+      ${optionalString (cfg.ensureUsers != [])
+      ''
+        plugin-load-add = auth_socket.so
+      ''}
+      ${cfg.extraOptions}
+    '';
+
+    systemd.services.mysql = let
+      hasNotify = (cfg.package == pkgs.mariadb);
+    in {
+        description = "MySQL Server";
+
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        restartTriggers = [ config.environment.etc."my.cnf".source ];
+
+        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 ! test -e ${cfg.dataDir}/mysql; then
+                mkdir -m 0700 -p ${cfg.dataDir}
+                chown -R ${cfg.user} ${cfg.dataDir}
+                ${mysql}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${installOptions}
+                touch /tmp/mysql_init
+            fi
+
+            mkdir -m 0755 -p ${cfg.pidDir}
+            chown -R ${cfg.user} ${cfg.pidDir}
+          '';
+
+        serviceConfig = {
+          Type = if hasNotify then "notify" else "simple";
+          RuntimeDirectory = "mysqld";
+          # 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";
+        };
+
+        postStart = ''
+          ${lib.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 /tmp/mysql_init ]
+            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") ''
+                          echo 'use `${database.name}`;'
+
+                          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 "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
+                  ''}
+
+                ${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;"
+                    ) | ${mysql}/bin/mysql -u root -N
+                  ''}
+
+                ${optionalString (cfg.initialScript != null)
+                  ''
+                    # Execute initial script
+                    cat ${cfg.initialScript} | ${mysql}/bin/mysql -u root -N
+                  ''}
+
+                ${optionalString (cfg.rootPassword != null)
+                  ''
+                    # Change root password
+
+                    ( echo "use mysql;"
+                      echo "update user set Password=password('$(cat ${cfg.rootPassword})') where User='root';"
+                      echo "flush privileges;"
+                    ) | ${mysql}/bin/mysql -u root -N
+                  ''}
+
+              rm /tmp/mysql_init
+            fi
+
+            ${optionalString (cfg.ensureDatabases != []) ''
+              (
+              ${concatMapStrings (database: ''
+                echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
+              '') cfg.ensureDatabases}
+              ) | ${mysql}/bin/mysql -u root -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}
+
+          ''; # */
+      };
+
+  };
+
+}