···379380- [`system.stateVersion`](#opt-system.stateVersion) is now validated and must be in the `"YY.MM"` format, ideally corresponding to a prior NixOS release.
381000000000000000382383- [`services.geoclue2`](#opt-services.geoclue2.enable) now has an `enableStatic` option, which allows the NixOS configuration to specify a fixed location for GeoClue to use.
384
···379380- [`system.stateVersion`](#opt-system.stateVersion) is now validated and must be in the `"YY.MM"` format, ideally corresponding to a prior NixOS release.
381382+- `services.mysql` now supports easy cluster setup via [`services.mysql.galeraCluster`](#opt-services.mysql.galeraCluster.enable) option.
383+384+ Example:
385+386+ ```nix
387+ services.mysql = {
388+ enable = true;
389+ galeraCluster = {
390+ enable = true;
391+ localName = "Node 1";
392+ localAddress = "galera_01";
393+ nodeAddresses = [ "galera_01" "galera_02" "galera_03"];
394+ };
395+ };
396+ ```
397398- [`services.geoclue2`](#opt-services.geoclue2.enable) now has an `enableStatic` option, which allows the NixOS configuration to specify a fixed location for GeoClue to use.
399
+169-5
nixos/modules/services/databases/mysql.nix
···320 description = "Port number on which the MySQL master server runs.";
321 };
322 };
00000000000000000000000000000000000000000000000000000000000000000000000000000323 };
324325 };
···327 ###### implementation
328329 config = lib.mkIf cfg.enable {
0000000000000000000000000000330331 services.mysql.dataDir = lib.mkDefault (
332 if lib.versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql" else "/var/mysql"
···351 (lib.mkIf (!isMariaDB) {
352 plugin-load-add = [ "auth_socket.so" ];
353 })
0000000000354 ];
35500000000000000000000356 users.users = lib.optionalAttrs (cfg.user == "mysql") {
357 mysql = {
358 description = "MySQL server user";
···384385 unitConfig.RequiresMountsFor = cfg.dataDir;
386387- path = [
388- # Needed for the mysql_install_db command in the preStart script
389- # which calls the hostname command.
390- pkgs.nettools
391- ];
000000000000000000392393 preStart =
394 if isMariaDB then
···581 })
582 ];
583 };
00000000000584 };
585586 meta.maintainers = [ lib.maintainers._6543 ];
···320 description = "Port number on which the MySQL master server runs.";
321 };
322 };
323+324+ galeraCluster = {
325+ enable = lib.mkEnableOption "MariaDB Galera Cluster";
326+327+ package = lib.mkOption {
328+ type = lib.types.package;
329+ description = "The MariaDB Galera package that provides the shared library 'libgalera_smm.so' required for cluster functionality.";
330+ default = lib.literalExpression "pkgs.mariadb-galera";
331+ };
332+333+ name = lib.mkOption {
334+ type = lib.types.str;
335+ description = "The logical name of the Galera cluster. All nodes in the same cluster must use the same name.";
336+ default = "galera";
337+ };
338+339+ sstMethod = lib.mkOption {
340+ type = lib.types.enum [
341+ "rsync"
342+ "mariabackup"
343+ ];
344+ description = "Method for the initial state transfer (wsrep_sst_method) when a node joins the cluster. Be aware that rsync needs SSH keys to be generated and authorized on all nodes!";
345+ default = "rsync";
346+ example = "mariabackup";
347+ };
348+349+ localName = lib.mkOption {
350+ type = lib.types.str;
351+ description = "The unique name that identifies this particular node within the cluster. Each node must have a different name.";
352+ example = "node1";
353+ };
354+355+ localAddress = lib.mkOption {
356+ type = lib.types.str;
357+ description = "IP address or hostname of this node that will be used for cluster communication. Must be reachable by all other nodes.";
358+ example = "1.2.3.4";
359+ default = cfg.galeraCluster.localName;
360+ defaultText = lib.literalExpression "config.services.mysql.galeraCluster.localName";
361+ };
362+363+ nodeAddresses = lib.mkOption {
364+ type = lib.types.listOf lib.types.str;
365+ description = "IP addresses or hostnames of all nodes in the cluster, including this node. This is used to construct the default clusterAddress connection string.";
366+ example = lib.literalExpression ''["10.0.0.10" "10.0.0.20" "10.0.0.30"]'';
367+ default = [ ];
368+ };
369+370+ clusterPassword = lib.mkOption {
371+ type = lib.types.str;
372+ description = "Optional password for securing cluster communications. If provided, it will be used in the clusterAddress for authentication between nodes.";
373+ example = "SomePassword";
374+ default = "";
375+ };
376+377+ clusterAddress = lib.mkOption {
378+ type = lib.types.str;
379+ description = "Full Galera cluster connection string. If nodeAddresses is set, this will be auto-generated, but you can override it with a custom value. Format is typically 'gcomm://node1,node2,node3' with optional parameters.";
380+ example = "gcomm://10.0.0.10,10.0.0.20,10.0.0.30?gmcast.seg=1:SomePassword";
381+ default =
382+ if (cfg.galeraCluster.nodeAddresses == [ ]) then
383+ ""
384+ else
385+ "gcomm://${builtins.concatStringsSep "," cfg.galeraCluster.nodeAddresses}"
386+ + lib.optionalString (
387+ cfg.galeraCluster.clusterPassword != ""
388+ ) "?gmcast.seg=1:${cfg.galeraCluster.clusterPassword}";
389+ defaultText = lib.literalExpression ''
390+ if (config.services.mysql.galeraCluster.nodeAddresses == [ ]) then
391+ ""
392+ else
393+ "gcomm://''${builtins.concatStringsSep \",\" config.services.mysql.galeraCluster.nodeAddresses}"
394+ + lib.optionalString (config.services.mysql.galeraCluster.clusterPassword != "")
395+ "?gmcast.seg=1:''${config.services.mysql.galeraCluster.clusterPassword}"
396+ '';
397+ };
398+399+ };
400 };
401402 };
···404 ###### implementation
405406 config = lib.mkIf cfg.enable {
407+ assertions = [
408+ {
409+ assertion = !cfg.galeraCluster.enable || isMariaDB;
410+ message = "'services.mysql.galeraCluster.enable' expect services.mysql.package to be an mariadb variant";
411+ }
412+ {
413+ assertion =
414+ !cfg.galeraCluster.enable
415+ || (
416+ cfg.galeraCluster.localAddress != ""
417+ && (cfg.galeraCluster.nodeAddresses != [ ] || cfg.galeraCluster.clusterAddress != "")
418+ );
419+ message = "mariadb galera cluster is enabled but the localAddress and (nodeAddresses or clusterAddress) are not set";
420+ }
421+ {
422+ assertion = !(cfg.galeraCluster.clusterAddress != "" && cfg.galeraCluster.clusterPassword != "");
423+ message = "mariadb galera clusterPassword is set but overwritten by clusterAddress";
424+ }
425+ {
426+ assertion =
427+ !(
428+ cfg.galeraCluster.enable
429+ && cfg.galeraCluster.nodeAddresses != [ ]
430+ && cfg.galeraCluster.clusterAddress != ""
431+ );
432+ message = "When services.mysql.galeraCluster.clusterAddress is set, setting services.mysql.galeraCluster.nodeAddresses is redundant and will be overwritten by clusterAddress. Choose one approach.";
433+ }
434+ ];
435436 services.mysql.dataDir = lib.mkDefault (
437 if lib.versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql" else "/var/mysql"
···456 (lib.mkIf (!isMariaDB) {
457 plugin-load-add = [ "auth_socket.so" ];
458 })
459+ (lib.mkIf cfg.galeraCluster.enable {
460+ # Ensure Only InnoDB is used as galera clusters can only work with them
461+ enforce_storage_engine = "InnoDB";
462+ default_storage_engine = "InnoDB";
463+464+ # galera only support this binlog format
465+ binlog-format = "ROW";
466+467+ bind_address = lib.mkDefault "0.0.0.0";
468+ })
469 ];
470471+ services.mysql.settings.galera = lib.optionalAttrs cfg.galeraCluster.enable {
472+ wsrep_on = "ON";
473+ wsrep_debug = lib.mkDefault "NONE";
474+ wsrep_retry_autocommit = lib.mkDefault "3";
475+ wsrep_provider = "${cfg.galeraCluster.package}/lib/galera/libgalera_smm.so";
476+477+ wsrep_cluster_name = cfg.galeraCluster.name;
478+ wsrep_cluster_address = cfg.galeraCluster.clusterAddress;
479+480+ wsrep_node_address = cfg.galeraCluster.localAddress;
481+ wsrep_node_name = "${cfg.galeraCluster.localName}";
482+483+ # SST method using rsync
484+ wsrep_sst_method = lib.mkDefault cfg.galeraCluster.sstMethod;
485+ wsrep_sst_auth = lib.mkDefault "check_repl:check_pass";
486+487+ binlog_format = "ROW";
488+ innodb_autoinc_lock_mode = 2;
489+ };
490+491 users.users = lib.optionalAttrs (cfg.user == "mysql") {
492 mysql = {
493 description = "MySQL server user";
···519520 unitConfig.RequiresMountsFor = cfg.dataDir;
521522+ path =
523+ [
524+ # Needed for the mysql_install_db command in the preStart script
525+ # which calls the hostname command.
526+ pkgs.nettools
527+ ]
528+ # tools 'wsrep_sst_rsync' needs
529+ ++ lib.optionals cfg.galeraCluster.enable [
530+ cfg.package
531+ pkgs.bash
532+ pkgs.gawk
533+ pkgs.gnutar
534+ pkgs.gzip
535+ pkgs.inetutils
536+ pkgs.iproute2
537+ pkgs.netcat
538+ pkgs.procps
539+ pkgs.pv
540+ pkgs.rsync
541+ pkgs.socat
542+ pkgs.stunnel
543+ pkgs.which
544+ ];
545546 preStart =
547 if isMariaDB then
···734 })
735 ];
736 };
737+738+ # Open firewall ports for MySQL (and Galera)
739+ networking.firewall.allowedTCPPorts = lib.optionals cfg.galeraCluster.enable [
740+ 3306 # MySQL
741+ 4567 # Galera Cluster
742+ 4568 # Galera IST
743+ 4444 # SST
744+ ];
745+ networking.firewall.allowedUDPPorts = lib.optionals cfg.galeraCluster.enable [
746+ 4567 # Galera Cluster
747+ ];
748 };
749750 meta.maintainers = [ lib.maintainers._6543 ];