+{ pkgs, config, lib, ... }:
+let
+ cfg = config.myServices.databasesReplication.mariadb;
+in
+{
+ options.myServices.databasesReplication.mariadb = {
+ enable = lib.mkEnableOption "Enable mariadb replication";
+ base = lib.mkOption {
+ type = lib.types.path;
+ description = ''
+ Base path to put the replications
+ '';
+ };
+ hosts = lib.mkOption {
+ default = {};
+ description = ''
+ Hosts to backup
+ '';
+ type = lib.types.attrsOf (lib.types.submodule {
+ options = {
+ package = lib.mkOption {
+ type = lib.types.package;
+ default = pkgs.mariadb;
+ description = ''
+ Mariadb package for this host
+ '';
+ };
+ serverId = lib.mkOption {
+ type = lib.types.int;
+ description = ''
+ Server id to use for replication cluster (must be unique among the cluster!)
+ '';
+ };
+ host = lib.mkOption {
+ type = lib.types.str;
+ description = ''
+ Host to connect to
+ '';
+ };
+ port = lib.mkOption {
+ type = lib.types.str;
+ description = ''
+ Port to connect to
+ '';
+ };
+ user = lib.mkOption {
+ type = lib.types.str;
+ description = ''
+ User to connect as
+ '';
+ };
+ password = lib.mkOption {
+ type = lib.types.str;
+ description = ''
+ Password to use
+ '';
+ };
+ dumpUser = lib.mkOption {
+ type = lib.types.str;
+ description = ''
+ User who can do a dump
+ '';
+ };
+ dumpPassword = lib.mkOption {
+ type = lib.types.str;
+ description = ''
+ Password for the dump user
+ '';
+ };
+ };
+ });
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ users.users.mysql = {
+ description = "MySQL server user";
+ group = "mysql";
+ uid = config.ids.uids.mysql;
+ extraGroups = [ "keys" ];
+ };
+ users.groups.mysql.gid = config.ids.gids.mysql;
+
+ secrets.keys = lib.flatten (lib.mapAttrsToList (name: hcfg: [
+ {
+ dest = "mysql_replication/${name}/slave_init_commands";
+ user = "mysql";
+ group = "mysql";
+ permissions = "0400";
+ text = ''
+ CHANGE MASTER TO master_host="${hcfg.host}", master_port=${hcfg.port}, master_user="${hcfg.user}", master_password="${hcfg.password}", master_ssl=1, master_use_gtid=slave_pos;
+ START SLAVE;
+ '';
+ }
+ {
+ dest = "mysql_replication/${name}/mysqldump_remote";
+ permissions = "0400";
+ user = "root";
+ group = "root";
+ text = ''
+ [mysqldump]
+ user = ${hcfg.user}
+ password = ${hcfg.password}
+ '';
+ }
+ {
+ dest = "mysql_replication/${name}/mysqldump";
+ permissions = "0400";
+ user = "root";
+ group = "root";
+ text = ''
+ [mysqldump]
+ user = ${hcfg.dumpUser}
+ password = ${hcfg.dumpPassword}
+ '';
+ }
+ ]) cfg.hosts);
+
+ services.cron = {
+ enable = true;
+ systemCronJobs = lib.flatten (lib.mapAttrsToList (name: hcfg:
+ let
+ dataDir = "${cfg.base}/${name}/mysql";
+ backupDir = "${cfg.base}/${name}/mysql_backup";
+ backup_script = pkgs.writeScript "backup_mysql_${name}" ''
+ #!${pkgs.stdenv.shell}
+
+ set -euo pipefail
+
+ ${hcfg.package}/bin/mysqldump \
+ --defaults-file=${config.secrets.location}/mysql_replication/${name}/mysqldump \
+ -S /run/mysqld_${name}/mysqld.sock \
+ --gtid \
+ --master-data \
+ --flush-privileges \
+ --all-databases > ${backupDir}/$(${pkgs.coreutils}/bin/date -Iseconds).sql
+ '';
+ u = pkgs.callPackage ./utils.nix {};
+ cleanup_script = pkgs.writeScript "cleanup_mysql_${name}" (u.exponentialDumps backupDir);
+ in [
+ "0 22,4,10,16 * * * root ${backup_script}"
+ "0 3 * * * root ${cleanup_script}"
+ ]) cfg.hosts);
+ };
+
+ system.activationScripts = lib.attrsets.mapAttrs' (name: hcfg:
+ lib.attrsets.nameValuePair "mysql_replication_${name}" {
+ deps = [ "users" "groups" ];
+ text = ''
+ install -m 0700 -o mysql -g mysql -d ${cfg.base}/${name}/mysql
+ install -m 0700 -o mysql -g mysql -d ${cfg.base}/${name}/mysql_backup
+ '';
+ }) cfg.hosts;
+
+ environment.etc = lib.attrsets.mapAttrs' (name: hcfg:
+ lib.attrsets.nameValuePair "mysql/${name}_my.cnf" {
+ text = ''
+ [mysqld]
+ socket = /run/mysqld_${name}/mysqld.sock
+ datadir = ${cfg.base}/${name}/mysql/
+ log-bin = mariadb-bin
+ server-id = ${builtins.toString hcfg.serverId}
+ '';
+ }
+ ) cfg.hosts;
+
+ systemd.services = lib.attrsets.mapAttrs' (name: hcfg:
+ let
+ dataDir = "${cfg.base}/${name}/mysql";
+ in
+ lib.attrsets.nameValuePair "mysql_backup_${name}" {
+ description = "Mysql replication for ${name}";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+ restartTriggers = [ config.environment.etc."mysql/${name}_my.cnf".source ];
+ unitConfig.RequiresMountsFor = dataDir;
+
+ preStart = ''
+ if ! test -e ${dataDir}/mysql; then
+ ${hcfg.package}/bin/mysqldump \
+ --defaults-file=${config.secrets.location}/mysql_replication/${name}/mysqldump_remote \
+ -h ${hcfg.host} \
+ -P ${hcfg.port} \
+ --ssl \
+ --gtid \
+ --flush-privileges \
+ --master-data \
+ --all-databases > ${dataDir}/initial.sql
+
+ ${hcfg.package}/bin/mysql_install_db \
+ --defaults-file=/etc/mysql/${name}_my.cnf \
+ --user=mysql \
+ --datadir=${dataDir} \
+ --basedir=${hcfg.package}
+ fi
+ '';
+
+ serviceConfig = {
+ User = "mysql";
+ Group = "mysql";
+ RuntimeDirectory = "mysqld_${name}";
+ RuntimeDirectoryMode = "0755";
+ SupplementaryGroups = "keys";
+ PermissionsStartOnly = true;
+ Type = "notify";
+
+ ExecStart = "${hcfg.package}/bin/mysqld --defaults-file=/etc/mysql/${name}_my.cnf --user=mysql --datadir=${dataDir} --basedir=${hcfg.package}";
+ ExecStartPost =
+ let
+ sql_before = pkgs.writeText "mysql-initial-before" ''
+ DROP DATABASE test;
+ '';
+ setupScript = pkgs.writeScript "mysql-setup" ''
+ #!${pkgs.runtimeShell} -e
+
+ if test -e ${dataDir}/initial.sql; then
+ cat \
+ ${sql_before} \
+ ${dataDir}/initial.sql \
+ ${config.secrets.location}/mysql_replication/${name}/slave_init_commands \
+ | ${hcfg.package}/bin/mysql \
+ --defaults-file=/etc/mysql/${name}_my.cnf \
+ -S /run/mysqld_${name}/mysqld.sock \
+ --user=root
+ rm -f ${dataDir}/initial.sql
+ fi
+ '';
+ in
+ "+${setupScript}";
+ # initial dump can take a long time
+ TimeoutStartSec="infinity";
+ TimeoutStopSec = 120;
+ };
+ }) cfg.hosts;
+ };
+}
+