at 25.11-pre 14 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 { 57 global = normalize (cfg.settings // flattenWithIndex cfg.repos "repo"); 58 } 59 // lib.mapAttrs ( 60 _: cfg': normalize (cfg'.settings // flattenWithIndex cfg'.instances "pg") 61 ) cfg.stanzas; 62 63 namedJobs = lib.listToAttrs ( 64 lib.flatten ( 65 lib.mapAttrsToList ( 66 stanza: 67 { jobs, ... }: 68 lib.mapAttrsToList ( 69 job: attrs: lib.nameValuePair "pgbackrest-${stanza}-${job}" (attrs // { inherit stanza job; }) 70 ) jobs 71 ) cfg.stanzas 72 ) 73 ); 74 75 disabledOption = lib.mkOption { 76 default = null; 77 readOnly = true; 78 internal = true; 79 }; 80 81 secretPathOption = 82 with lib.types; 83 lib.mkOption { 84 type = nullOr (pathWith { 85 inStore = false; 86 absolute = true; 87 }); 88 default = null; 89 internal = true; 90 }; 91in 92 93{ 94 meta = { 95 maintainers = with lib.maintainers; [ wolfgangwalther ]; 96 }; 97 98 # TODO: Add enableServer option and corresponding pgBackRest TLS server service. 99 # TODO: Allow command-specific options 100 # TODO: Write wrapper around pgbackrest to turn --repo=<name> into --repo=<number> 101 # The following two are dependent on improvements upstream: 102 # https://github.com/pgbackrest/pgbackrest/issues/2621 103 # TODO: Add support for more repository types 104 # TODO: Support passing encryption key safely 105 options.services.pgbackrest = { 106 enable = lib.mkEnableOption "pgBackRest"; 107 108 repos = lib.mkOption { 109 type = 110 with lib.types; 111 attrsOf ( 112 submodule ( 113 { config, name, ... }: 114 let 115 setHostForType = 116 type: 117 if name == "localhost" then 118 null 119 # "posix" is the default repo type, which uses the -host option. 120 # Other types use prefixed options, for example -sftp-host. 121 else if config.type or "posix" != type then 122 null 123 else 124 name; 125 in 126 { 127 freeformType = settingsType; 128 129 options.host = lib.mkOption { 130 type = nullOr str; 131 default = setHostForType "posix"; 132 defaultText = lib.literalExpression "name"; 133 description = "Repository host when operating remotely"; 134 }; 135 136 options.sftp-host = lib.mkOption { 137 type = nullOr str; 138 default = setHostForType "sftp"; 139 defaultText = lib.literalExpression "name"; 140 description = "SFTP repository host"; 141 }; 142 143 options.sftp-private-key-file = lib.mkOption { 144 type = nullOr (pathWith { 145 inStore = false; 146 absolute = true; 147 }); 148 default = null; 149 description = '' 150 SFTP private key file. 151 152 The file must be accessible by both the pgbackrest and the postgres users. 153 ''; 154 }; 155 156 # The following options should not be used; they would store secrets in the store. 157 options.azure-key = disabledOption; 158 options.cipher-pass = disabledOption; 159 options.s3-key = disabledOption; 160 options.s3-key-secret = disabledOption; 161 options.s3-kms-key-id = disabledOption; # unsure whether that's a secret or not 162 options.s3-sse-customer-key = disabledOption; # unsure whether that's a secret or not 163 options.s3-token = disabledOption; 164 options.sftp-private-key-passphrase = disabledOption; 165 166 # The following options are not fully supported / tested, yet, but point to files with secrets. 167 # Users can already set those options, but we'll force non-store paths. 168 options.gcs-key = secretPathOption; 169 options.host-cert-file = secretPathOption; 170 options.host-key-file = secretPathOption; 171 } 172 ) 173 ); 174 default = { }; 175 description = '' 176 An attribute set of repositories as described in: 177 <https://pgbackrest.org/configuration.html#section-repository> 178 179 Each repository defaults to set `repo-host` to the attribute's name. 180 The special value "localhost" will unset `repo-host`. 181 182 ::: {.note} 183 The prefix `repoNNN-` is added automatically. 184 Example: Use `path` instead of `repo1-path`. 185 ::: 186 ''; 187 example = lib.literalExpression '' 188 { 189 localhost.path = "/var/lib/backup"; 190 "backup.example.com".host-type = "tls"; 191 } 192 ''; 193 }; 194 195 stanzas = lib.mkOption { 196 type = 197 with lib.types; 198 attrsOf (submodule { 199 options = { 200 jobs = lib.mkOption { 201 type = lib.types.attrsOf ( 202 lib.types.submodule { 203 options.schedule = lib.mkOption { 204 type = lib.types.str; 205 description = '' 206 When or how often the backup should run. 207 Must be in the format described in {manpage}`systemd.time(7)`. 208 ''; 209 }; 210 211 options.type = lib.mkOption { 212 type = lib.types.str; 213 description = '' 214 Backup type as described in: 215 <https://pgbackrest.org/command.html#command-backup/category-command/option-type> 216 ''; 217 }; 218 } 219 ); 220 default = { }; 221 description = '' 222 Backups jobs to schedule for this stanza as described in: 223 <https://pgbackrest.org/user-guide.html#quickstart/schedule-backup> 224 ''; 225 example = lib.literalExpression '' 226 { 227 weekly = { schedule = "Sun, 6:30"; type = "full"; }; 228 daily = { schedule = "Mon..Sat, 6:30"; type = "diff"; }; 229 } 230 ''; 231 }; 232 233 instances = lib.mkOption { 234 type = 235 with lib.types; 236 attrsOf ( 237 submodule ( 238 { name, ... }: 239 { 240 freeformType = settingsType; 241 options.host = lib.mkOption { 242 type = nullOr str; 243 default = if name == "localhost" then null else name; 244 defaultText = lib.literalExpression ''if name == "localhost" then null else name''; 245 description = "PostgreSQL host for operating remotely."; 246 }; 247 248 # The following options are not fully supported / tested, yet, but point to files with secrets. 249 # Users can already set those options, but we'll force non-store paths. 250 options.host-cert-file = secretPathOption; 251 options.host-key-file = secretPathOption; 252 } 253 ) 254 ); 255 default = { }; 256 description = '' 257 An attribute set of database instances as described in: 258 <https://pgbackrest.org/configuration.html#section-stanza> 259 260 Each instance defaults to set `pg-host` to the attribute's name. 261 The special value "localhost" will unset `pg-host`. 262 263 ::: {.note} 264 The prefix `pgNNN-` is added automatically. 265 Example: Use `user` instead of `pg1-user`. 266 ::: 267 ''; 268 example = lib.literalExpression '' 269 { 270 localhost.database = "app"; 271 "postgres.example.com".port = "5433"; 272 } 273 ''; 274 }; 275 276 settings = lib.mkOption { 277 type = lib.types.submodule { 278 freeformType = settingsType; 279 280 # The following options are not fully supported / tested, yet, but point to files with secrets. 281 # Users can already set those options, but we'll force non-store paths. 282 options.tls-server-cert-file = secretPathOption; 283 options.tls-server-key-file = secretPathOption; 284 }; 285 default = { }; 286 description = '' 287 An attribute set of options as described in: 288 <https://pgbackrest.org/configuration.html> 289 290 All options can be used. 291 Repository options should be set via [`repos`](#opt-services.pgbackrest.repos) instead. 292 Stanza options should be set via [`instances`](#opt-services.pgbackrest.stanzas._name_.instances) instead. 293 ''; 294 example = lib.literalExpression '' 295 { 296 process-max = 2; 297 } 298 ''; 299 }; 300 }; 301 }); 302 default = { }; 303 description = '' 304 An attribute set of stanzas as described in: 305 <https://pgbackrest.org/user-guide.html#quickstart/configure-stanza> 306 ''; 307 }; 308 309 settings = lib.mkOption { 310 type = lib.types.submodule { 311 freeformType = settingsType; 312 313 # The following options are not fully supported / tested, yet, but point to files with secrets. 314 # Users can already set those options, but we'll force non-store paths. 315 options.tls-server-cert-file = secretPathOption; 316 options.tls-server-key-file = secretPathOption; 317 }; 318 default = { }; 319 description = '' 320 An attribute set of options as described in: 321 <https://pgbackrest.org/configuration.html> 322 323 All globally available options, i.e. all except stanza options, can be used. 324 Repository options should be set via [`repos`](#opt-services.pgbackrest.repos) instead. 325 ''; 326 example = lib.literalExpression '' 327 { 328 process-max = 2; 329 } 330 ''; 331 }; 332 }; 333 334 config = lib.mkIf cfg.enable ( 335 lib.mkMerge [ 336 { 337 services.pgbackrest.settings = { 338 log-level-console = lib.mkDefault "info"; 339 log-level-file = lib.mkDefault "off"; 340 cmd-ssh = lib.getExe pkgs.openssh; 341 }; 342 343 environment.systemPackages = [ pkgs.pgbackrest ]; 344 environment.etc."pgbackrest/pgbackrest.conf".source = 345 settingsFormat.generate "pgbackrest.conf" fullConfig; 346 347 users.users.pgbackrest = { 348 name = "pgbackrest"; 349 group = "pgbackrest"; 350 description = "pgBackRest service user"; 351 isSystemUser = true; 352 useDefaultShell = true; 353 createHome = true; 354 home = cfg.repos.localhost.path or "/var/lib/pgbackrest"; 355 }; 356 users.groups.pgbackrest = { }; 357 358 systemd.services = lib.mapAttrs ( 359 _: 360 { 361 stanza, 362 job, 363 type, 364 ... 365 }: 366 { 367 description = "pgBackRest job ${job} for stanza ${stanza}"; 368 369 serviceConfig = { 370 User = "pgbackrest"; 371 Group = "pgbackrest"; 372 Type = "oneshot"; 373 # stanza-create is idempotent, so safe to always run 374 ExecStartPre = "${lib.getExe pkgs.pgbackrest} --stanza='${stanza}' stanza-create"; 375 ExecStart = "${lib.getExe pkgs.pgbackrest} --stanza='${stanza}' backup --type='${type}'"; 376 }; 377 } 378 ) namedJobs; 379 380 systemd.timers = lib.mapAttrs ( 381 name: 382 { 383 stanza, 384 job, 385 schedule, 386 ... 387 }: 388 { 389 description = "pgBackRest job ${job} for stanza ${stanza}"; 390 wantedBy = [ "timers.target" ]; 391 after = [ "network-online.target" ]; 392 wants = [ "network-online.target" ]; 393 timerConfig = { 394 OnCalendar = schedule; 395 Persistent = true; 396 Unit = "${name}.service"; 397 }; 398 } 399 ) namedJobs; 400 } 401 402 # The default stanza is set up for the local postgresql instance. 403 # It does not backup automatically, the systemd timer still needs to be set. 404 (lib.mkIf config.services.postgresql.enable { 405 services.pgbackrest.stanzas.default = { 406 settings.cmd = lib.getExe pkgs.pgbackrest; 407 instances.localhost = { 408 path = config.services.postgresql.dataDir; 409 user = "postgres"; 410 }; 411 }; 412 services.postgresql.identMap = '' 413 postgres pgbackrest postgres 414 ''; 415 services.postgresql.initdbArgs = [ "--allow-group-access" ]; 416 users.users.pgbackrest.extraGroups = [ "postgres" ]; 417 418 services.postgresql.settings = { 419 archive_command = ''${lib.getExe pkgs.pgbackrest} --stanza=default archive-push "%p"''; 420 archive_mode = lib.mkDefault "on"; 421 }; 422 users.groups.pgbackrest.members = [ "postgres" ]; 423 }) 424 ] 425 ); 426}