aboutsummaryrefslogtreecommitdiff
path: root/modules/private/databases
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2019-12-05 00:51:45 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2019-12-05 00:51:45 +0100
commit9f6a78629aad1d22dc8b928860fd05eb40f07352 (patch)
tree25c918ccd80eeb3e04d5d0c3ed4815ff3de5dcae /modules/private/databases
parent304a7dac48405d63a5d20466d4afc7ba04d11164 (diff)
downloadNix-9f6a78629aad1d22dc8b928860fd05eb40f07352.tar.gz
Nix-9f6a78629aad1d22dc8b928860fd05eb40f07352.tar.zst
Nix-9f6a78629aad1d22dc8b928860fd05eb40f07352.zip
Add mysql replication
Diffstat (limited to 'modules/private/databases')
-rw-r--r--modules/private/databases/default.nix4
-rw-r--r--modules/private/databases/mariadb.nix53
-rw-r--r--modules/private/databases/mariadb_replication.nix237
-rw-r--r--modules/private/databases/postgresql_replication.nix4
-rw-r--r--modules/private/databases/utils.nix30
5 files changed, 317 insertions, 11 deletions
diff --git a/modules/private/databases/default.nix b/modules/private/databases/default.nix
index d1d6a2b..7371410 100644
--- a/modules/private/databases/default.nix
+++ b/modules/private/databases/default.nix
@@ -21,6 +21,10 @@ in
21 inherit (myconfig.env.ldap) host base; 21 inherit (myconfig.env.ldap) host base;
22 inherit (myconfig.env.databases.mysql.pam) dn filter password; 22 inherit (myconfig.env.databases.mysql.pam) dn filter password;
23 }; 23 };
24 replicationLdapConfig = {
25 inherit (myconfig.env.ldap) host base;
26 inherit (myconfig.env.ldap.eldiron) dn password;
27 };
24 credentials.root = myconfig.env.databases.mysql.systemUsers.root; 28 credentials.root = myconfig.env.databases.mysql.systemUsers.root;
25 }; 29 };
26 30
diff --git a/modules/private/databases/mariadb.nix b/modules/private/databases/mariadb.nix
index 4293f02..632306c 100644
--- a/modules/private/databases/mariadb.nix
+++ b/modules/private/databases/mariadb.nix
@@ -34,6 +34,17 @@ in {
34 }; 34 };
35 }; 35 };
36 }; 36 };
37 replicationLdapConfig = lib.mkOption {
38 description = "LDAP configuration to allow replication";
39 type = lib.types.submodule {
40 options = {
41 host = lib.mkOption { type = lib.types.str; };
42 base = lib.mkOption { type = lib.types.str; };
43 dn = lib.mkOption { type = lib.types.str; };
44 password = lib.mkOption { type = lib.types.str; };
45 };
46 };
47 };
37 dataDir = lib.mkOption { 48 dataDir = lib.mkOption {
38 type = lib.types.path; 49 type = lib.types.path;
39 default = "/var/lib/mysql"; 50 default = "/var/lib/mysql";
@@ -72,6 +83,13 @@ in {
72 # User identified by LDAP: 83 # User identified by LDAP:
73 # CREATE USER foo@% IDENTIFIED VIA pam USING 'mysql' REQUIRE SSL; 84 # CREATE USER foo@% IDENTIFIED VIA pam USING 'mysql' REQUIRE SSL;
74 # CREATE USER foo@localhost IDENTIFIED VIA pam USING 'mysql'; 85 # CREATE USER foo@localhost IDENTIFIED VIA pam USING 'mysql';
86
87 # To create a user (host) for replication:
88 # CREATE USER 'host'@'%' IDENTIFIED VIA pam USING 'mysql_replication' REQUIRE SSL;
89 # GRANT REPLICATION SLAVE, REPLICATION CLIENT, RELOAD, LOCK TABLES, SELECT, SHOW VIEW ON *.* TO 'host'@'%';
90 # (the lock/select grant permits to let the replication host handle
91 # the initial fetch of the database)
92 # % should be valid for both localhost (for cron dumps) and the origin host.
75 services.mysql = { 93 services.mysql = {
76 enable = true; 94 enable = true;
77 package = cfg.package; 95 package = cfg.package;
@@ -80,6 +98,10 @@ in {
80 ssl_ca = ${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt 98 ssl_ca = ${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
81 ssl_key = ${config.security.acme.directory}/mysql/key.pem 99 ssl_key = ${config.security.acme.directory}/mysql/key.pem
82 ssl_cert = ${config.security.acme.directory}/mysql/fullchain.pem 100 ssl_cert = ${config.security.acme.directory}/mysql/fullchain.pem
101
102 # for replication
103 log-bin=mariadb-bin
104 server-id=1
83 ''; 105 '';
84 }; 106 };
85 107
@@ -120,17 +142,22 @@ in {
120 ssl start_tls 142 ssl start_tls
121 ''; 143 '';
122 } 144 }
145 {
146 dest = "mysql/pam_replication";
147 permissions = "0400";
148 user = "mysql";
149 group = "mysql";
150 text = with cfg.replicationLdapConfig; ''
151 host ${host}
152 base ${base}
153 binddn ${dn}
154 bindpw ${password}
155 pam_login_attribute cn
156 ssl start_tls
157 '';
158 }
123 ]; 159 ];
124 160
125 services.cron = {
126 enable = true;
127 systemCronJobs = [
128 ''
129 30 1,13 * * * root ${cfg.package}/bin/mysqldump --defaults-file=${config.secrets.location}/mysql/mysqldump --all-databases > ${cfg.dataDir}/backup.sql
130 ''
131 ];
132 };
133
134 security.pam.services = let 161 security.pam.services = let
135 pam_ldap = "${pkgs.pam_ldap}/lib/security/pam_ldap.so"; 162 pam_ldap = "${pkgs.pam_ldap}/lib/security/pam_ldap.so";
136 in [ 163 in [
@@ -142,8 +169,14 @@ in {
142 account required ${pam_ldap} config=${config.secrets.location}/mysql/pam 169 account required ${pam_ldap} config=${config.secrets.location}/mysql/pam
143 ''; 170 '';
144 } 171 }
172 {
173 name = "mysql_replication";
174 text = ''
175 auth required ${pam_ldap} config=${config.secrets.location}/mysql/pam_replication
176 account required ${pam_ldap} config=${config.secrets.location}/mysql/pam_replication
177 '';
178 }
145 ]; 179 ];
146 180
147 }; 181 };
148} 182}
149
diff --git a/modules/private/databases/mariadb_replication.nix b/modules/private/databases/mariadb_replication.nix
new file mode 100644
index 0000000..23648bb
--- /dev/null
+++ b/modules/private/databases/mariadb_replication.nix
@@ -0,0 +1,237 @@
1{ pkgs, config, lib, ... }:
2let
3 cfg = config.myServices.databasesReplication.mariadb;
4in
5{
6 options.myServices.databasesReplication.mariadb = {
7 enable = lib.mkEnableOption "Enable mariadb replication";
8 base = lib.mkOption {
9 type = lib.types.path;
10 description = ''
11 Base path to put the replications
12 '';
13 };
14 hosts = lib.mkOption {
15 default = {};
16 description = ''
17 Hosts to backup
18 '';
19 type = lib.types.attrsOf (lib.types.submodule {
20 options = {
21 package = lib.mkOption {
22 type = lib.types.package;
23 default = pkgs.mariadb;
24 description = ''
25 Mariadb package for this host
26 '';
27 };
28 serverId = lib.mkOption {
29 type = lib.types.int;
30 description = ''
31 Server id to use for replication cluster (must be unique among the cluster!)
32 '';
33 };
34 host = lib.mkOption {
35 type = lib.types.str;
36 description = ''
37 Host to connect to
38 '';
39 };
40 port = lib.mkOption {
41 type = lib.types.str;
42 description = ''
43 Port to connect to
44 '';
45 };
46 user = lib.mkOption {
47 type = lib.types.str;
48 description = ''
49 User to connect as
50 '';
51 };
52 password = lib.mkOption {
53 type = lib.types.str;
54 description = ''
55 Password to use
56 '';
57 };
58 dumpUser = lib.mkOption {
59 type = lib.types.str;
60 description = ''
61 User who can do a dump
62 '';
63 };
64 dumpPassword = lib.mkOption {
65 type = lib.types.str;
66 description = ''
67 Password for the dump user
68 '';
69 };
70 };
71 });
72 };
73 };
74
75 config = lib.mkIf cfg.enable {
76 users.users.mysql = {
77 description = "MySQL server user";
78 group = "mysql";
79 uid = config.ids.uids.mysql;
80 extraGroups = [ "keys" ];
81 };
82 users.groups.mysql.gid = config.ids.gids.mysql;
83
84 secrets.keys = lib.flatten (lib.mapAttrsToList (name: hcfg: [
85 {
86 dest = "mysql_replication/${name}/slave_init_commands";
87 user = "mysql";
88 group = "mysql";
89 permissions = "0400";
90 text = ''
91 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;
92 START SLAVE;
93 '';
94 }
95 {
96 dest = "mysql_replication/${name}/mysqldump_remote";
97 permissions = "0400";
98 user = "root";
99 group = "root";
100 text = ''
101 [mysqldump]
102 user = ${hcfg.user}
103 password = ${hcfg.password}
104 '';
105 }
106 {
107 dest = "mysql_replication/${name}/mysqldump";
108 permissions = "0400";
109 user = "root";
110 group = "root";
111 text = ''
112 [mysqldump]
113 user = ${hcfg.dumpUser}
114 password = ${hcfg.dumpPassword}
115 '';
116 }
117 ]) cfg.hosts);
118
119 services.cron = {
120 enable = true;
121 systemCronJobs = lib.flatten (lib.mapAttrsToList (name: hcfg:
122 let
123 dataDir = "${cfg.base}/${name}/mysql";
124 backupDir = "${cfg.base}/${name}/mysql_backup";
125 backup_script = pkgs.writeScript "backup_mysql_${name}" ''
126 #!${pkgs.stdenv.shell}
127
128 set -euo pipefail
129
130 ${hcfg.package}/bin/mysqldump \
131 --defaults-file=${config.secrets.location}/mysql_replication/${name}/mysqldump \
132 -S /run/mysqld_${name}/mysqld.sock \
133 --gtid \
134 --master-data \
135 --flush-privileges \
136 --all-databases > ${backupDir}/$(${pkgs.coreutils}/bin/date -Iseconds).sql
137 '';
138 u = pkgs.callPackage ./utils.nix {};
139 cleanup_script = pkgs.writeScript "cleanup_mysql_${name}" (u.exponentialDumps backupDir);
140 in [
141 "0 22,4,10,16 * * * root ${backup_script}"
142 "0 3 * * * root ${cleanup_script}"
143 ]) cfg.hosts);
144 };
145
146 system.activationScripts = lib.attrsets.mapAttrs' (name: hcfg:
147 lib.attrsets.nameValuePair "mysql_replication_${name}" {
148 deps = [ "users" "groups" ];
149 text = ''
150 install -m 0700 -o mysql -g mysql -d ${cfg.base}/${name}/mysql
151 install -m 0700 -o mysql -g mysql -d ${cfg.base}/${name}/mysql_backup
152 '';
153 }) cfg.hosts;
154
155 environment.etc = lib.attrsets.mapAttrs' (name: hcfg:
156 lib.attrsets.nameValuePair "mysql/${name}_my.cnf" {
157 text = ''
158 [mysqld]
159 socket = /run/mysqld_${name}/mysqld.sock
160 datadir = ${cfg.base}/${name}/mysql/
161 log-bin = mariadb-bin
162 server-id = ${builtins.toString hcfg.serverId}
163 '';
164 }
165 ) cfg.hosts;
166
167 systemd.services = lib.attrsets.mapAttrs' (name: hcfg:
168 let
169 dataDir = "${cfg.base}/${name}/mysql";
170 in
171 lib.attrsets.nameValuePair "mysql_backup_${name}" {
172 description = "Mysql replication for ${name}";
173 wantedBy = [ "multi-user.target" ];
174 after = [ "network.target" ];
175 restartTriggers = [ config.environment.etc."mysql/${name}_my.cnf".source ];
176 unitConfig.RequiresMountsFor = dataDir;
177
178 preStart = ''
179 if ! test -e ${dataDir}/mysql; then
180 ${hcfg.package}/bin/mysqldump \
181 --defaults-file=${config.secrets.location}/mysql_replication/${name}/mysqldump_remote \
182 -h ${hcfg.host} \
183 -P ${hcfg.port} \
184 --ssl \
185 --gtid \
186 --flush-privileges \
187 --master-data \
188 --all-databases > ${dataDir}/initial.sql
189
190 ${hcfg.package}/bin/mysql_install_db \
191 --defaults-file=/etc/mysql/${name}_my.cnf \
192 --user=mysql \
193 --datadir=${dataDir} \
194 --basedir=${hcfg.package}
195 fi
196 '';
197
198 serviceConfig = {
199 User = "mysql";
200 Group = "mysql";
201 RuntimeDirectory = "mysqld_${name}";
202 RuntimeDirectoryMode = "0755";
203 SupplementaryGroups = "keys";
204 PermissionsStartOnly = true;
205 Type = "notify";
206
207 ExecStart = "${hcfg.package}/bin/mysqld --defaults-file=/etc/mysql/${name}_my.cnf --user=mysql --datadir=${dataDir} --basedir=${hcfg.package}";
208 ExecStartPost =
209 let
210 sql_before = pkgs.writeText "mysql-initial-before" ''
211 DROP DATABASE test;
212 '';
213 setupScript = pkgs.writeScript "mysql-setup" ''
214 #!${pkgs.runtimeShell} -e
215
216 if test -e ${dataDir}/initial.sql; then
217 cat \
218 ${sql_before} \
219 ${dataDir}/initial.sql \
220 ${config.secrets.location}/mysql_replication/${name}/slave_init_commands \
221 | ${hcfg.package}/bin/mysql \
222 --defaults-file=/etc/mysql/${name}_my.cnf \
223 -S /run/mysqld_${name}/mysqld.sock \
224 --user=root
225 rm -f ${dataDir}/initial.sql
226 fi
227 '';
228 in
229 "+${setupScript}";
230 # initial dump can take a long time
231 TimeoutStartSec="infinity";
232 TimeoutStopSec = 120;
233 };
234 }) cfg.hosts;
235 };
236}
237
diff --git a/modules/private/databases/postgresql_replication.nix b/modules/private/databases/postgresql_replication.nix
index 145fcac..cc32c2b 100644
--- a/modules/private/databases/postgresql_replication.nix
+++ b/modules/private/databases/postgresql_replication.nix
@@ -115,9 +115,11 @@ in
115 115
116 ${hcfg.package}/bin/pg_dumpall -h ${dataDir} -f ${backupDir}/$(${pkgs.coreutils}/bin/date -Iseconds).sql 116 ${hcfg.package}/bin/pg_dumpall -h ${dataDir} -f ${backupDir}/$(${pkgs.coreutils}/bin/date -Iseconds).sql
117 ''; 117 '';
118 u = pkgs.callPackage ./utils.nix {};
119 cleanup_script = pkgs.writeScript "cleanup_postgresql_${name}" (u.keepLastNDumps backupDir 12);
118 in [ 120 in [
119 "0 22,4,10,16 * * * postgres ${backup_script}" 121 "0 22,4,10,16 * * * postgres ${backup_script}"
120 "0 3 * * * postgres ${pkgs.coreutils}/bin/rm -f $(${pkgs.coreutils}/bin/ls -1 ${backupDir}/*.sql | ${pkgs.coreutils}/bin/sort -r | ${pkgs.gnused}/bin/sed -e '1,12d')" 122 "0 3 * * * postgres ${cleanup_script}"
121 ]) cfg.hosts); 123 ]) cfg.hosts);
122 }; 124 };
123 125
diff --git a/modules/private/databases/utils.nix b/modules/private/databases/utils.nix
new file mode 100644
index 0000000..c296f49
--- /dev/null
+++ b/modules/private/databases/utils.nix
@@ -0,0 +1,30 @@
1{ pkgs }:
2{
3 keepLastNDumps = backupDir: n: ''
4 #!${pkgs.stdenv.shell}
5
6 cd ${backupDir}
7 ${pkgs.coreutils}/bin/rm -f \
8 $(${pkgs.coreutils}/bin/ls -1 *.sql \
9 | ${pkgs.coreutils}/bin/sort -r \
10 | ${pkgs.gnused}/bin/sed -e '1,${builtins.toString n}d')
11 '';
12 exponentialDumps = backupDir: let
13 log2rotateSrc = builtins.fetchGit {
14 url = "https://github.com/avian2/pylog2rotate";
15 ref = "master";
16 rev = "061f0564757289d3bea553b16f8fd5c4a0319c5e";
17 };
18 log2rotate = pkgs.writeScript "log2rotate" ''
19 #!${pkgs.python3}/bin/python
20
21 ${builtins.readFile "${log2rotateSrc}/log2rotate.py"}
22 '';
23 in ''
24 #!${pkgs.stdenv.shell}
25
26 cd ${backupDir}
27 ${pkgs.coreutils}/bin/rm -f $(ls -1 *.sql | grep -v 'T22:' | sort -r | sed -e '1,12d')
28 ${pkgs.coreutils}/bin/rm -f $(ls -1 *T22*.sql | ${log2rotate} --skip 7 --fuzz 7 --delete --format='%Y-%m-%dT%H:%M:%S+00:00.sql')
29 '';
30}