aboutsummaryrefslogtreecommitdiff
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.nix425
1 files changed, 425 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 00000000000..df74cfc9a26
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/databases/mysql.nix
@@ -0,0 +1,425 @@
+{ 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);
+
+ mysqldOptions =
+ "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${mysql}";
+ # For MySQL 5.7+, --insecure creates the root user without password
+ # (earlier versions and MariaDB do this by default).
+ installOptions =
+ "${mysqldOptions} ${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";
+ };
+
+ 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.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";
+ };
+
+ 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";
+ };
+ }
+ ]
+ '';
+ };
+
+ 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.tmpfiles.rules = [
+ "d '${cfg.dataDir}' 0700 ${cfg.user} mysql -"
+ ];
+
+ 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
+ ${mysql}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${installOptions}
+ touch /tmp/mysql_init
+ fi
+ '';
+
+ serviceConfig = {
+ User = cfg.user;
+ Group = "mysql";
+ Type = if hasNotify then "notify" else "simple";
+ RuntimeDirectory = "mysqld";
+ RuntimeDirectoryMode = "0755";
+ # 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 /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 != 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 "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
+ # 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
+ ''}
+
+ 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}
+ '';
+ in
+ # ensureDatbases & ensureUsers depends on this script being run as root
+ # when the user has secured their mysql install
+ "+${setupScript}";
+ };
+ };
+
+ };
+
+}