summaryrefslogtreecommitdiffstats
path: root/nixos/modules/services/cluster
diff options
context:
space:
mode:
authorPaul-Henri Froidmont <git.contact-57n2p@froidmont.org>2022-08-08 14:25:30 +0200
committerpennae <82953136+pennae@users.noreply.github.com>2022-08-16 18:57:15 +0200
commitd2ce318bd4676a45acb9c0cb3836c0d230648ef3 (patch)
tree526017f80cda7fc239b74e90b3f42a58801f4f27 /nixos/modules/services/cluster
parenta5abd926845e1d888ed3c03935a05cf4a7138087 (diff)
nixos/patroni: init
Diffstat (limited to 'nixos/modules/services/cluster')
-rw-r--r--nixos/modules/services/cluster/patroni/default.nix268
1 files changed, 268 insertions, 0 deletions
diff --git a/nixos/modules/services/cluster/patroni/default.nix b/nixos/modules/services/cluster/patroni/default.nix
new file mode 100644
index 000000000000..1685351e48d3
--- /dev/null
+++ b/nixos/modules/services/cluster/patroni/default.nix
@@ -0,0 +1,268 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+ cfg = config.services.patroni;
+ defaultUser = "patroni";
+ defaultGroup = "patroni";
+ format = pkgs.formats.yaml { };
+
+ #boto doesn't support python 3.10 yet
+ patroni = pkgs.patroni.override { pythonPackages = pkgs.python39Packages; };
+
+ configFileName = "patroni-${cfg.scope}-${cfg.name}.yaml";
+ configFile = format.generate configFileName cfg.settings;
+in
+{
+ options.services.patroni = {
+
+ enable = mkEnableOption "Patroni";
+
+ postgresqlPackage = mkOption {
+ type = types.package;
+ example = literalExpression "pkgs.postgresql_14";
+ description = mdDoc ''
+ PostgreSQL package to use.
+ Plugins can be enabled like this `pkgs.postgresql_14.withPackages (p: [ p.pg_safeupdate p.postgis ])`.
+ '';
+ };
+
+ postgresqlDataDir = mkOption {
+ type = types.path;
+ defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.patroni.postgresqlPackage.psqlSchema}"'';
+ example = "/var/lib/postgresql/14";
+ default = "/var/lib/postgresql/${cfg.postgresqlPackage.psqlSchema}";
+ description = mdDoc ''
+ The data directory for PostgreSQL. If left as the default value
+ this directory will automatically be created before the PostgreSQL server starts, otherwise
+ the sysadmin is responsible for ensuring the directory exists with appropriate ownership
+ and permissions.
+ '';
+ };
+
+ postgresqlPort = mkOption {
+ type = types.port;
+ default = 5432;
+ description = mdDoc ''
+ The port on which PostgreSQL listens.
+ '';
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = defaultUser;
+ example = "postgres";
+ description = mdDoc ''
+ The user for the service. If left as the default value this user will automatically be created,
+ otherwise the sysadmin is responsible for ensuring the user exists.
+ '';
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = defaultGroup;
+ example = "postgres";
+ description = mdDoc ''
+ The group for the service. If left as the default value this group will automatically be created,
+ otherwise the sysadmin is responsible for ensuring the group exists.
+ '';
+ };
+
+ dataDir = mkOption {
+ type = types.path;
+ default = "/var/lib/patroni";
+ description = mdDoc ''
+ Folder where Patroni data will be written, used by Raft as well if enabled.
+ '';
+ };
+
+ scope = mkOption {
+ type = types.str;
+ example = "cluster1";
+ description = mdDoc ''
+ Cluster name.
+ '';
+ };
+
+ name = mkOption {
+ type = types.str;
+ example = "node1";
+ description = mdDoc ''
+ The name of the host. Must be unique for the cluster.
+ '';
+ };
+
+ namespace = mkOption {
+ type = types.str;
+ default = "/service";
+ description = mdDoc ''
+ Path within the configuration store where Patroni will keep information about the cluster.
+ '';
+ };
+
+ nodeIp = mkOption {
+ type = types.str;
+ example = "192.168.1.1";
+ description = mdDoc ''
+ IP address of this node.
+ '';
+ };
+
+ otherNodesIps = mkOption {
+ type = types.listOf types.string;
+ example = [ "192.168.1.2" "192.168.1.3" ];
+ description = mdDoc ''
+ IP addresses of the other nodes.
+ '';
+ };
+
+ restApiPort = mkOption {
+ type = types.port;
+ default = 8008;
+ description = mdDoc ''
+ The port on Patroni's REST api listens.
+ '';
+ };
+
+ raft = mkOption {
+ type = types.bool;
+ default = false;
+ description = mdDoc ''
+ This will configure Patroni to use its own RAFT implementation instead of using a dedicated DCS.
+ '';
+ };
+
+ raftPort = mkOption {
+ type = types.port;
+ default = 5010;
+ description = mdDoc ''
+ The port on which RAFT listens.
+ '';
+ };
+
+ softwareWatchdog = mkOption {
+ type = types.bool;
+ default = false;
+ description = mdDoc ''
+ This will configure Patroni to use the software watchdog built into the Linux kernel
+ as described in the [documentation](https://patroni.readthedocs.io/en/latest/watchdog.html#setting-up-software-watchdog-on-linux).
+ '';
+ };
+
+ settings = mkOption {
+ type = format.type;
+ default = { };
+ description = mdDoc ''
+ The primary patroni configuration. See the [documentation](https://patroni.readthedocs.io/en/latest/SETTINGS.html)
+ for possible values.
+ Secrets should be passed in by using the `environmentFiles` option.
+ '';
+ };
+
+ environmentFiles = mkOption {
+ type = with types; attrsOf (nullOr (oneOf [ str path package ]));
+ default = { };
+ example = {
+ PATRONI_REPLICATION_PASSWORD = "/secret/file";
+ PATRONI_SUPERUSER_PASSWORD = "/secret/file";
+ };
+ description = mdDoc "Environment variables made available to Patroni as files content, useful for providing secrets from files.";
+ };
+ };
+
+ config = mkIf cfg.enable {
+
+ services.patroni.settings = {
+ scope = cfg.scope;
+ name = cfg.name;
+ namespace = cfg.namespace;
+
+ restapi = {
+ listen = "${cfg.nodeIp}:${toString cfg.restApiPort}";
+ connect_address = "${cfg.nodeIp}:${toString cfg.restApiPort}";
+ };
+
+ raft = mkIf cfg.raft {
+ data_dir = "${cfg.dataDir}/raft";
+ self_addr = "${cfg.nodeIp}:5010";
+ partner_addrs = map (ip: ip + ":5010") cfg.otherNodesIps;
+ };
+
+ postgresql = {
+ listen = "${cfg.nodeIp}:${toString cfg.postgresqlPort}";
+ connect_address = "${cfg.nodeIp}:${toString cfg.postgresqlPort}";
+ data_dir = cfg.postgresqlDataDir;
+ bin_dir = "${cfg.postgresqlPackage}/bin";
+ pgpass = "${cfg.dataDir}/pgpass";
+ };
+
+ watchdog = mkIf cfg.softwareWatchdog {
+ mode = "required";
+ device = "/dev/watchdog";
+ safety_margin = 5;
+ };
+ };
+
+
+ users = {
+ users = mkIf (cfg.user == defaultUser) {
+ patroni = {
+ group = cfg.group;
+ isSystemUser = true;
+ };
+ };
+ groups = mkIf (cfg.group == defaultGroup) {
+ patroni = { };
+ };
+ };
+
+ systemd.services = {
+ patroni = {
+ description = "Runners to orchestrate a high-availability PostgreSQL";
+
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ script = ''
+ ${concatStringsSep "\n" (attrValues (mapAttrs (name: path: ''export ${name}="$(< ${escapeShellArg path})"'') cfg.environmentFiles))}
+ exec ${patroni}/bin/patroni ${configFile}
+ '';
+
+ serviceConfig = mkMerge [
+ {
+ User = cfg.user;
+ Group = cfg.group;
+ Type = "simple";
+ Restart = "on-failure";
+ TimeoutSec = 30;
+ ExecReload = "${pkgs.coreutils}/bin/kill -s HUP $MAINPID";
+ KillMode = "process";
+ }
+ (mkIf (cfg.postgresqlDataDir == "/var/lib/postgresql/${cfg.postgresqlPackage.psqlSchema}" && cfg.dataDir == "/var/lib/patroni") {
+ StateDirectory = "patroni patroni/raft postgresql postgresql/${cfg.postgresqlPackage.psqlSchema}";
+ StateDirectoryMode = "0750";
+ })
+ ];
+ };
+ };
+
+ boot.kernelModules = mkIf cfg.softwareWatchdog [ "softdog" ];
+
+ services.udev.extraRules = mkIf cfg.softwareWatchdog ''
+ KERNEL=="watchdog", OWNER="${cfg.user}", GROUP="${cfg.group}", MODE="0600"
+ '';
+
+ environment.systemPackages = [
+ patroni
+ cfg.postgresqlPackage
+ (mkIf cfg.raft pkgs.python310Packages.pysyncobj)
+ ];
+
+ environment.etc."${configFileName}".source = configFile;
+
+ environment.sessionVariables = {
+ PATRONICTL_CONFIG_FILE = "/etc/${configFileName}";
+ };
+ };
+
+ meta.maintainers = [ maintainers.phfroidmont ];
+}