nixos/services.mysql: add galera cluster options (#388978)

And add release notes for new option.

Co-authored-by: Arne Keller <arne.keller@posteo.de>

authored by 6543 Arne Keller and committed by GitHub cac3bdab 59a953f5

+199 -47
+15
nixos/doc/manual/release-notes/rl-2505.section.md
··· 379 380 - [`system.stateVersion`](#opt-system.stateVersion) is now validated and must be in the `"YY.MM"` format, ideally corresponding to a prior NixOS release. 381 382 383 - [`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
··· 379 380 - [`system.stateVersion`](#opt-system.stateVersion) is now validated and must be in the `"YY.MM"` format, ideally corresponding to a prior NixOS release. 381 382 + - `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 + ``` 397 398 - [`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 }; 323 }; 324 325 }; ··· 327 ###### implementation 328 329 config = lib.mkIf cfg.enable { 330 331 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 }) 354 ]; 355 356 users.users = lib.optionalAttrs (cfg.user == "mysql") { 357 mysql = { 358 description = "MySQL server user"; ··· 384 385 unitConfig.RequiresMountsFor = cfg.dataDir; 386 387 - path = [ 388 - # Needed for the mysql_install_db command in the preStart script 389 - # which calls the hostname command. 390 - pkgs.nettools 391 - ]; 392 393 preStart = 394 if isMariaDB then ··· 581 }) 582 ]; 583 }; 584 }; 585 586 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 }; 401 402 }; ··· 404 ###### implementation 405 406 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 + ]; 435 436 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 ]; 470 471 + 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"; ··· 519 520 unitConfig.RequiresMountsFor = cfg.dataDir; 521 522 + 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 + ]; 545 546 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 }; 749 750 meta.maintainers = [ lib.maintainers._6543 ];
+15 -42
nixos/tests/mysql/mariadb-galera.nix
··· 58 extraHosts = lib.concatMapStringsSep "\n" (i: "192.168.1.${toString i} galera_0${toString i}") ( 59 lib.range 1 6 60 ); 61 - firewall.allowedTCPPorts = [ 62 - 3306 63 - 4444 64 - 4567 65 - 4568 66 - ]; 67 - firewall.allowedUDPPorts = [ 4567 ]; 68 - }; 69 - systemd.services.mysql = with pkgs; { 70 - path = with pkgs; [ 71 - bash 72 - gawk 73 - gnutar 74 - gzip 75 - inetutils 76 - iproute2 77 - netcat 78 - procps 79 - pv 80 - rsync 81 - socat 82 - stunnel 83 - which 84 - ]; 85 }; 86 services.mysql = { 87 enable = true; ··· 101 FLUSH PRIVILEGES; 102 '' 103 ); 104 settings = { 105 - mysqld = { 106 - bind_address = "0.0.0.0"; 107 - }; 108 galera = { 109 - wsrep_on = "ON"; 110 wsrep_debug = "NONE"; 111 - wsrep_retry_autocommit = "3"; 112 - wsrep_provider = "${galeraPackage}/lib/galera/libgalera_smm.so"; 113 - wsrep_cluster_address = 114 - "gcomm://" 115 - + lib.optionalString (id == 2 || id == 3) "galera_01,galera_02,galera_03" 116 - + lib.optionalString (id == 5 || id == 6) "galera_04,galera_05,galera_06"; 117 - wsrep_cluster_name = "galera"; 118 - wsrep_node_address = address; 119 - wsrep_node_name = "galera_0${toString id}"; 120 - wsrep_sst_method = method; 121 - wsrep_sst_auth = "check_repl:check_pass"; 122 - binlog_format = "ROW"; 123 - enforce_storage_engine = "InnoDB"; 124 - innodb_autoinc_lock_mode = "2"; 125 }; 126 }; 127 };
··· 58 extraHosts = lib.concatMapStringsSep "\n" (i: "192.168.1.${toString i} galera_0${toString i}") ( 59 lib.range 1 6 60 ); 61 }; 62 services.mysql = { 63 enable = true; ··· 77 FLUSH PRIVILEGES; 78 '' 79 ); 80 + 81 + galeraCluster = { 82 + enable = true; 83 + package = galeraPackage; 84 + sstMethod = method; 85 + 86 + localAddress = address; 87 + localName = "galera_0${toString id}"; 88 + 89 + clusterAddress = 90 + "gcomm://" 91 + + lib.optionalString (id == 2 || id == 3) "galera_01,galera_02,galera_03" 92 + + lib.optionalString (id == 5 || id == 6) "galera_04,galera_05,galera_06"; 93 + }; 94 + 95 settings = { 96 galera = { 97 wsrep_debug = "NONE"; 98 }; 99 }; 100 };