at master 16 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.services.pgbackrest; 10 11 settingsFormat = pkgs.formats.ini { 12 listsAsDuplicateKeys = true; 13 }; 14 15 # pgBackRest "options" 16 settingsType = 17 with lib.types; 18 attrsOf (oneOf [ 19 bool 20 ints.unsigned 21 str 22 (attrsOf str) 23 (listOf str) 24 ]); 25 26 # Applied to both repoNNN-* and pgNNN-* options in global and stanza sections. 27 flattenWithIndex = 28 attrs: prefix: 29 lib.concatMapAttrs ( 30 name: 31 let 32 index = lib.lists.findFirstIndex (n: n == name) null (lib.attrNames attrs); 33 index1 = index + 1; 34 in 35 lib.mapAttrs' (option: lib.nameValuePair "${prefix}${toString index1}-${option}") 36 ) attrs; 37 38 # Remove nulls, turn attrsets into lists and bools into y/n 39 normalize = 40 x: 41 lib.pipe x [ 42 (lib.filterAttrs (_: v: v != null)) 43 (lib.mapAttrs (_: v: if lib.isAttrs v then lib.mapAttrsToList (n': v': "${n'}=${v'}") v else v)) 44 (lib.mapAttrs ( 45 _: v: 46 if v == true then 47 "y" 48 else if v == false then 49 "n" 50 else 51 v 52 )) 53 ]; 54 55 fullConfig = { 56 global = normalize (cfg.settings // flattenWithIndex cfg.repos "repo"); 57 } 58 // lib.mapAttrs' ( 59 cmd: settings: lib.nameValuePair "global:${cmd}" (normalize settings) 60 ) cfg.commands 61 // lib.mapAttrs ( 62 _: cfg': normalize (cfg'.settings // flattenWithIndex cfg'.instances "pg") 63 ) cfg.stanzas; 64 65 namedJobs = lib.listToAttrs ( 66 lib.flatten ( 67 lib.mapAttrsToList ( 68 stanza: 69 { jobs, ... }: 70 lib.mapAttrsToList ( 71 job: attrs: lib.nameValuePair "pgbackrest-${stanza}-${job}" (attrs // { inherit stanza job; }) 72 ) jobs 73 ) cfg.stanzas 74 ) 75 ); 76 77 disabledOption = lib.mkOption { 78 default = null; 79 readOnly = true; 80 internal = true; 81 }; 82 83 secretPathOption = 84 with lib.types; 85 lib.mkOption { 86 type = nullOr (pathWith { 87 inStore = false; 88 absolute = true; 89 }); 90 default = null; 91 internal = true; 92 }; 93in 94 95{ 96 meta = { 97 maintainers = with lib.maintainers; [ wolfgangwalther ]; 98 }; 99 100 # TODO: Add enableServer option and corresponding pgBackRest TLS server service. 101 # TODO: Write wrapper around pgbackrest to turn --repo=<name> into --repo=<number> 102 # The following two are dependent on improvements upstream: 103 # https://github.com/pgbackrest/pgbackrest/issues/2621 104 # TODO: Add support for more repository types 105 # TODO: Support passing encryption key safely 106 options.services.pgbackrest = { 107 enable = lib.mkEnableOption "pgBackRest"; 108 109 repos = lib.mkOption { 110 type = 111 with lib.types; 112 attrsOf ( 113 submodule ( 114 { config, name, ... }: 115 let 116 setHostForType = 117 type: 118 if name == "localhost" then 119 null 120 # "posix" is the default repo type, which uses the -host option. 121 # Other types use prefixed options, for example -sftp-host. 122 else if config.type or "posix" != type then 123 null 124 else 125 name; 126 in 127 { 128 freeformType = settingsType; 129 130 options.host = lib.mkOption { 131 type = nullOr str; 132 default = setHostForType "posix"; 133 defaultText = lib.literalExpression "name"; 134 description = "Repository host when operating remotely"; 135 }; 136 137 options.sftp-host = lib.mkOption { 138 type = nullOr str; 139 default = setHostForType "sftp"; 140 defaultText = lib.literalExpression "name"; 141 description = "SFTP repository host"; 142 }; 143 144 options.sftp-private-key-file = lib.mkOption { 145 type = nullOr (pathWith { 146 inStore = false; 147 absolute = true; 148 }); 149 default = null; 150 description = '' 151 SFTP private key file. 152 153 The file must be accessible by both the pgbackrest and the postgres users. 154 ''; 155 }; 156 157 # The following options should not be used; they would store secrets in the store. 158 options.azure-key = disabledOption; 159 options.cipher-pass = disabledOption; 160 options.s3-key = disabledOption; 161 options.s3-key-secret = disabledOption; 162 options.s3-kms-key-id = disabledOption; # unsure whether that's a secret or not 163 options.s3-sse-customer-key = disabledOption; # unsure whether that's a secret or not 164 options.s3-token = disabledOption; 165 options.sftp-private-key-passphrase = disabledOption; 166 167 # The following options are not fully supported / tested, yet, but point to files with secrets. 168 # Users can already set those options, but we'll force non-store paths. 169 options.gcs-key = secretPathOption; 170 options.host-cert-file = secretPathOption; 171 options.host-key-file = secretPathOption; 172 } 173 ) 174 ); 175 default = { }; 176 description = '' 177 An attribute set of repositories as described in: 178 <https://pgbackrest.org/configuration.html#section-repository> 179 180 Each repository defaults to set `repo-host` to the attribute's name. 181 The special value "localhost" will unset `repo-host`. 182 183 ::: {.note} 184 The prefix `repoNNN-` is added automatically. 185 Example: Use `path` instead of `repo1-path`. 186 ::: 187 ''; 188 example = lib.literalExpression '' 189 { 190 localhost.path = "/var/lib/backup"; 191 "backup.example.com".host-type = "tls"; 192 } 193 ''; 194 }; 195 196 stanzas = lib.mkOption { 197 type = 198 with lib.types; 199 attrsOf (submodule { 200 options = { 201 jobs = lib.mkOption { 202 type = lib.types.attrsOf ( 203 lib.types.submodule { 204 options.schedule = lib.mkOption { 205 type = lib.types.str; 206 description = '' 207 When or how often the backup should run. 208 Must be in the format described in {manpage}`systemd.time(7)`. 209 ''; 210 }; 211 212 options.type = lib.mkOption { 213 type = lib.types.str; 214 description = '' 215 Backup type as described in: 216 <https://pgbackrest.org/command.html#command-backup/category-command/option-type> 217 ''; 218 }; 219 } 220 ); 221 default = { }; 222 description = '' 223 Backups jobs to schedule for this stanza as described in: 224 <https://pgbackrest.org/user-guide.html#quickstart/schedule-backup> 225 ''; 226 example = lib.literalExpression '' 227 { 228 weekly = { schedule = "Sun, 6:30"; type = "full"; }; 229 daily = { schedule = "Mon..Sat, 6:30"; type = "diff"; }; 230 } 231 ''; 232 }; 233 234 instances = lib.mkOption { 235 type = 236 with lib.types; 237 attrsOf ( 238 submodule ( 239 { name, ... }: 240 { 241 freeformType = settingsType; 242 options.host = lib.mkOption { 243 type = nullOr str; 244 default = if name == "localhost" then null else name; 245 defaultText = lib.literalExpression ''if name == "localhost" then null else name''; 246 description = "PostgreSQL host for operating remotely."; 247 }; 248 249 # The following options are not fully supported / tested, yet, but point to files with secrets. 250 # Users can already set those options, but we'll force non-store paths. 251 options.host-cert-file = secretPathOption; 252 options.host-key-file = secretPathOption; 253 } 254 ) 255 ); 256 default = { }; 257 description = '' 258 An attribute set of database instances as described in: 259 <https://pgbackrest.org/configuration.html#section-stanza> 260 261 Each instance defaults to set `pg-host` to the attribute's name. 262 The special value "localhost" will unset `pg-host`. 263 264 ::: {.note} 265 The prefix `pgNNN-` is added automatically. 266 Example: Use `user` instead of `pg1-user`. 267 ::: 268 ''; 269 example = lib.literalExpression '' 270 { 271 localhost.database = "app"; 272 "postgres.example.com".port = "5433"; 273 } 274 ''; 275 }; 276 277 settings = lib.mkOption { 278 type = lib.types.submodule { 279 freeformType = settingsType; 280 281 # The following options are not fully supported / tested, yet, but point to files with secrets. 282 # Users can already set those options, but we'll force non-store paths. 283 options.tls-server-cert-file = secretPathOption; 284 options.tls-server-key-file = secretPathOption; 285 }; 286 default = { }; 287 description = '' 288 An attribute set of options as described in: 289 <https://pgbackrest.org/configuration.html> 290 291 All options can be used. 292 Repository options should be set via [`repos`](#opt-services.pgbackrest.repos) instead. 293 Stanza options should be set via [`instances`](#opt-services.pgbackrest.stanzas._name_.instances) instead. 294 ''; 295 example = lib.literalExpression '' 296 { 297 process-max = 2; 298 } 299 ''; 300 }; 301 }; 302 }); 303 default = { }; 304 description = '' 305 An attribute set of stanzas as described in: 306 <https://pgbackrest.org/user-guide.html#quickstart/configure-stanza> 307 ''; 308 }; 309 310 settings = lib.mkOption { 311 type = lib.types.submodule { 312 freeformType = settingsType; 313 314 # The following options are not fully supported / tested, yet, but point to files with secrets. 315 # Users can already set those options, but we'll force non-store paths. 316 options.tls-server-cert-file = secretPathOption; 317 options.tls-server-key-file = secretPathOption; 318 }; 319 default = { }; 320 description = '' 321 An attribute set of options as described in: 322 <https://pgbackrest.org/configuration.html> 323 324 All globally available options, i.e. all except stanza options, can be used. 325 Repository options should be set via [`repos`](#opt-services.pgbackrest.repos) instead. 326 ''; 327 example = lib.literalExpression '' 328 { 329 process-max = 2; 330 } 331 ''; 332 }; 333 334 commands = 335 lib.genAttrs 336 [ 337 # List of commands from https://pgbackrest.org/command.html: 338 "annotate" 339 "archive-get" 340 "archive-push" 341 "backup" 342 "check" 343 "expire" 344 "help" 345 "info" 346 "repo-get" 347 "repo-ls" 348 "restore" 349 "server" 350 "server-ping" 351 "stanza-create" 352 "stanza-delete" 353 "stanza-upgrade" 354 "start" 355 "stop" 356 "verify" 357 "version" 358 ] 359 ( 360 command: 361 lib.mkOption { 362 type = lib.types.submodule { 363 freeformType = settingsType; 364 365 # The following options are not fully supported / tested, yet, but point to files with secrets. 366 # Users can already set those options, but we'll force non-store paths. 367 options.tls-server-cert-file = secretPathOption; 368 options.tls-server-key-file = secretPathOption; 369 }; 370 default = { }; 371 description = '' 372 Options for the '${command}' command. 373 374 An attribute set of options as described in: 375 <https://pgbackrest.org/configuration.html> 376 377 All globally available options, i.e. all except stanza options, can be used. 378 Repository options should be set via [`repos`](#opt-services.pgbackrest.repos) instead. 379 ''; 380 } 381 ); 382 }; 383 384 config = lib.mkIf cfg.enable ( 385 lib.mkMerge [ 386 { 387 services.pgbackrest.settings = { 388 log-level-console = lib.mkDefault "info"; 389 log-level-file = lib.mkDefault "off"; 390 cmd-ssh = lib.getExe pkgs.openssh; 391 }; 392 393 environment.systemPackages = [ pkgs.pgbackrest ]; 394 environment.etc."pgbackrest/pgbackrest.conf".source = 395 settingsFormat.generate "pgbackrest.conf" fullConfig; 396 397 users.users.pgbackrest = { 398 name = "pgbackrest"; 399 group = "pgbackrest"; 400 description = "pgBackRest service user"; 401 isSystemUser = true; 402 useDefaultShell = true; 403 createHome = true; 404 home = cfg.repos.localhost.path or "/var/lib/pgbackrest"; 405 }; 406 users.groups.pgbackrest = { }; 407 408 systemd.services = lib.mapAttrs ( 409 _: 410 { 411 stanza, 412 job, 413 type, 414 ... 415 }: 416 { 417 description = "pgBackRest job ${job} for stanza ${stanza}"; 418 419 serviceConfig = { 420 User = "pgbackrest"; 421 Group = "pgbackrest"; 422 Type = "oneshot"; 423 # stanza-create is idempotent, so safe to always run 424 ExecStartPre = "${lib.getExe pkgs.pgbackrest} --stanza='${stanza}' stanza-create"; 425 ExecStart = "${lib.getExe pkgs.pgbackrest} --stanza='${stanza}' backup --type='${type}'"; 426 }; 427 } 428 ) namedJobs; 429 430 systemd.timers = lib.mapAttrs ( 431 name: 432 { 433 stanza, 434 job, 435 schedule, 436 ... 437 }: 438 { 439 description = "pgBackRest job ${job} for stanza ${stanza}"; 440 wantedBy = [ "timers.target" ]; 441 after = [ "network-online.target" ]; 442 wants = [ "network-online.target" ]; 443 timerConfig = { 444 OnCalendar = schedule; 445 Persistent = true; 446 Unit = "${name}.service"; 447 }; 448 } 449 ) namedJobs; 450 } 451 452 # The default stanza is set up for the local postgresql instance. 453 # It does not backup automatically, the systemd timer still needs to be set. 454 (lib.mkIf config.services.postgresql.enable { 455 services.pgbackrest.stanzas.default = { 456 settings.cmd = lib.getExe pkgs.pgbackrest; 457 instances.localhost = { 458 path = config.services.postgresql.dataDir; 459 user = "postgres"; 460 }; 461 }; 462 # If PostgreSQL runs on the same machine, any restore will have to be done with that user. 463 # Keeping the lock file in a directory writeable by the postgres user prevents errors. 464 services.pgbackrest.commands.restore.lock-path = "/tmp/postgresql"; 465 services.postgresql.identMap = '' 466 postgres pgbackrest postgres 467 ''; 468 services.postgresql.initdbArgs = [ "--allow-group-access" ]; 469 users.users.pgbackrest.extraGroups = [ "postgres" ]; 470 471 services.postgresql.settings = { 472 archive_command = ''${lib.getExe pkgs.pgbackrest} --stanza=default archive-push "%p"''; 473 archive_mode = lib.mkDefault "on"; 474 }; 475 users.groups.pgbackrest.members = [ "postgres" ]; 476 }) 477 ] 478 ); 479}