From e9c8bd299be467dd86a75a72312a3804f3ef5076 Mon Sep 17 00:00:00 2001 From: ace Date: Wed, 8 Feb 2023 02:39:20 +0300 Subject: [PATCH] first commit --- .gitignore | 1 + README.md | 5 + defaults/main.yaml | 37 ++++++ handlers/main.yaml | 8 ++ meta/main.yaml | 1 + tasks/Debian/main.yaml | 69 +++++++++++ tasks/RedHat/cacert.yaml | 177 ++++++++++++++++++++++++++++ tasks/RedHat/install.yaml | 45 +++++++ tasks/RedHat/main.yaml | 44 +++++++ tasks/RedHat/python36.yaml | 15 +++ tasks/RedHat/python39.yaml | 21 ++++ tasks/config.yaml | 38 ++++++ tasks/dynamic_config.yaml | 38 ++++++ tasks/main.yaml | 45 +++++++ templates/patroni.yaml.j2 | 234 +++++++++++++++++++++++++++++++++++++ vars/Debian.yaml | 21 ++++ vars/RedHat.yaml | 64 ++++++++++ vars/main.yaml | 154 ++++++++++++++++++++++++ 18 files changed, 1017 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 defaults/main.yaml create mode 100644 handlers/main.yaml create mode 100644 meta/main.yaml create mode 100644 tasks/Debian/main.yaml create mode 100644 tasks/RedHat/cacert.yaml create mode 100644 tasks/RedHat/install.yaml create mode 100644 tasks/RedHat/main.yaml create mode 100644 tasks/RedHat/python36.yaml create mode 100644 tasks/RedHat/python39.yaml create mode 100644 tasks/config.yaml create mode 100644 tasks/dynamic_config.yaml create mode 100644 tasks/main.yaml create mode 100644 templates/patroni.yaml.j2 create mode 100644 vars/Debian.yaml create mode 100644 vars/RedHat.yaml create mode 100644 vars/main.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba6d390 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.galaxy_install_info diff --git a/README.md b/README.md new file mode 100644 index 0000000..b256a7f --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +Setup Patroni cluster for RHEL8 +Supported PostgreSQL versions: + - 13 + - 14 + - 15 diff --git a/defaults/main.yaml b/defaults/main.yaml new file mode 100644 index 0000000..4d07368 --- /dev/null +++ b/defaults/main.yaml @@ -0,0 +1,37 @@ +# Patroni common options +patroni_version: "2.1.7" +patroni_install_repo: yes + +# Patroni cluster options +patroni_cluster_name: "patroni-cluster" +patroni_namespace: "/service/" + +# Patroni PostgreSQL common options +patroni_postgresql_version: "14.6" +patroni_postgresql_install_repo: yes + +# Patroni superuser auth +patroni_superuser_username: "postgres" +patroni_superuser_password: "postgres" + +# Patroni replication auth +patroni_replication_username: "replicator" +patroni_replication_password: "replicator" + +# Patroni REST API options +patroni_restapi_username: "admin" +patroni_restapi_password: "admin" +patroni_restapi_listen_port: 8008 + +# Patroni SSL options +patroni_ssl: no +patroni_ssl_path: "/etc/patroni/ssl" +patroni_self_signed_cert: no +patroni_self_signed_cert_name: "cert" + +## DCS options. Consul or Etcd +patroni_dcs_exists: no +patroni_dcs_type: "consul" + +## Backup options +patroni_wal_g_install: no diff --git a/handlers/main.yaml b/handlers/main.yaml new file mode 100644 index 0000000..0c82ab3 --- /dev/null +++ b/handlers/main.yaml @@ -0,0 +1,8 @@ +- block: + - name: Restart Patroni + throttle: 1 + ansible.builtin.systemd: + state: reloaded + daemon_reload: yes + name: patroni + when: "not patroni_postgresql_setup.changed or not patroni_enable_and_start.changed" diff --git a/meta/main.yaml b/meta/main.yaml new file mode 100644 index 0000000..32cf5dd --- /dev/null +++ b/meta/main.yaml @@ -0,0 +1 @@ +dependencies: [] diff --git a/tasks/Debian/main.yaml b/tasks/Debian/main.yaml new file mode 100644 index 0000000..9c67274 --- /dev/null +++ b/tasks/Debian/main.yaml @@ -0,0 +1,69 @@ +- name: Add gpg package + apt: + name: gpg + +- name: Add gpg keys for patroni + ansible.builtin.apt_key: + url: "{{ item.url }}" + keyring: "{{ item.keyring }}" + loop: "{{ patroni_apt_key }}" + +- name: Add Patroni repository + apt_repository: + repo: "{{ item.repo }}" + state: present + filename: "{{ item.filename }}" + update_cache: yes + loop: "{{ patroni_apt_repository }}" + +- name: Check if PostgreSQL is installed + ansible.builtin.shell: dpkg-query -l {{ patroni_postgresql_package_name }} 2>&1 | grep {{ patroni_postgresql_version }} + ignore_errors: True + register: is_patroni_postgresql + changed_when: is_patroni_postgresql.rc != 0 + failed_when: False + +- name: Mask PostgreSQL before install + ansible.builtin.systemd: + name: "{{ patroni_postgresql_package_name }}" + masked: yes + when: is_patroni_postgresql.rc != 0 + +- name: "Install {{ patroni_postgresql_package_name }}-{{ patroni_postgresql_version }}" + apt: + name: "{{ patroni_postgresql_package }}" + update_cache: yes + register: patroni_postgresql_setup + when: is_patroni_postgresql.rc != 0 + +- name: Unmask PostgreSQL after install + ansible.builtin.systemd: + name: "{{ patroni_postgresql_package_name }}" + masked: no + when: is_patroni_postgresql.rc != 0 + +- name: Check if Patroni is installed + ansible.builtin.shell: dpkg-query -l {{ patroni_package_name }} 2>&1 | grep {{ patroni_version }} + ignore_errors: True + register: is_patroni + changed_when: is_patroni.rc != 0 + failed_when: False + +- name: Mask Patroni before install + ansible.builtin.systemd: + name: "{{ patroni_package_name }}" + masked: yes + when: is_patroni.rc != 0 + +- name: "Install {{ patroni_package_name }}-{{ patroni_version }}" + apt: + name: "{{ patroni_package }}" + update_cache: yes + register: patroni_setup + when: is_patroni.rc != 0 + +- name: Unmask Patroni after install + ansible.builtin.systemd: + name: "{{ patroni_package_name }}" + masked: no + when: is_patroni.rc != 0 diff --git a/tasks/RedHat/cacert.yaml b/tasks/RedHat/cacert.yaml new file mode 100644 index 0000000..e0fa1c6 --- /dev/null +++ b/tasks/RedHat/cacert.yaml @@ -0,0 +1,177 @@ +- name: Check if ssl dir exist + file: + name: "{{ patroni_ssl_path }}" + state: directory + owner: postgres + group: postgres + when: patroni_ssl + +- name: Add predefined ssl cert for Patroni + copy: + src: "{{ patroni_cert_name }}" + dest: "{{ patroni_ssl_path }}/{{ patroni_cert_name }}" + owner: postgres + group: postgres + notify: Restart Patroni + when: patroni_cert is defined + +- name: Generate OpenSSL key and cert for Patroni + when: "inventory_hostname == groups.patroni|first" + block: + - name: Generate an OpenSSL private CA key with the default values (4096 bits, RSA) + community.crypto.openssl_privatekey: + path: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.key" + owner: postgres + group: postgres + when: patroni_cert is not defined + register: patroni_ca_key_gen + + - name: Generate an OpenSSL Certificate Signing Request + community.crypto.openssl_csr: + path: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.csr" + privatekey_path: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.key" + use_common_name_for_san: false + basic_constraints: + - 'CA:TRUE' + basic_constraints_critical: yes + key_usage: + - keyCertSign + key_usage_critical: true + common_name: "CA-{{ patroni_self_signed_cert_name }}" + owner: postgres + group: postgres + register: patroni_ca_csr + + - name: Generate a Self Signed OpenSSL CA certificate + community.crypto.x509_certificate: + path: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.crt" + csr_path: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.csr" + privatekey_path: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.key" + provider: selfsigned + owner: postgres + group: postgres + when: patroni_cert is not defined + register: patroni_ca_cert_gen + + - name: Generate an OpenSSL private client key with the default values (4096 bits, RSA) + community.crypto.openssl_privatekey: + path: "{{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.key" + owner: postgres + group: postgres + when: patroni_cert is not defined + register: patroni_key_gen + + - name: Generate subject_alt_ips + set_fact: + client_subject_alt_ips: "{{ groups.patroni | map('extract', hostvars, ['ansible_host']) | map('regex_replace', '^', 'IP:') | list }}" + + - name: Print alt subjects ips + debug: + msg: "{{ client_subject_alt_ips }}" + verbosity: 2 + + - name: Generate subject_alt_names + set_fact: + client_subject_alt_names: "{{ groups.patroni | map('extract', hostvars, ['inventory_hostname']) | map('regex_replace', '^', 'DNS:') | list }}" + + - name: Print alt subjects names + debug: + msg: "{{ client_subject_alt_names }}" + verbosity: 2 + + - name: Generate an OpenSSL Certificate Signing Request for client + community.crypto.openssl_csr: + path: "{{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.csr" + privatekey_path: "{{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.key" + common_name: "{{ patroni_self_signed_cert_name }}" + subject_alt_name: "{{ (( groups.patroni | map('extract', hostvars, ['ansible_host']) | map('regex_replace', '^', 'IP:') | list | default([])) + (groups.patroni | map('extract', hostvars, ['inventory_hostname']) | map('regex_replace', '^', 'DNS:') | list | default([]))) }}" + owner: postgres + group: postgres + register: patroni_csr + + - name: Generate an OpenSSL certificate for client signed with your own CA certificate + community.crypto.x509_certificate: + path: "{{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.crt" + csr_path: "{{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.csr" + ownca_path: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.crt" + ownca_privatekey_path: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.key" + provider: ownca + owner: postgres + group: postgres + register: patroni_cert + + - name: Get CA cert content + slurp: + src: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.crt" + register: patroni_ca_cert_b64 + + - name: Get CA key content + slurp: + src: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.key" + register: patroni_ca_key_b64 + + - name: Get client cert content + slurp: + src: "{{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.crt" + register: patroni_cert_b64 + + - name: Get client key content + slurp: + src: "{{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.key" + register: patroni_key_b64 + + - name: Set facts about key and cert + set_fact: + patroni_ca_key: "{{ patroni_ca_key_b64.content | b64decode }}" + patroni_ca_cert: "{{ patroni_ca_cert_b64.content | b64decode }}" + patroni_key: "{{ patroni_key_b64.content | b64decode }}" + patroni_cert: "{{ patroni_cert_b64.content | b64decode }}" + delegate_to: "{{ item }}" + delegate_facts: true + run_once: true + loop: "{{ groups.patroni }}" + +- name: Put Patroni CA OpenSSL key + copy: + content: "{{ patroni_ca_key }}" + dest: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.key" + owner: postgres + group: postgres + mode: 0600 + notify: Restart Patroni + +- name: Put Patroni CA OpenSSL cert + copy: + content: "{{ patroni_ca_cert }}" + dest: "{{ patroni_ssl_path }}/CA-{{ patroni_self_signed_cert_name }}.crt" + owner: postgres + group: postgres + notify: Restart Patroni + +- name: Put Patroni CA OpenSSL cert to PKI + copy: + content: "{{ patroni_ca_cert }}" + dest: "/etc/pki/ca-trust/source/anchors/CA-{{ patroni_self_signed_cert_name }}.crt" + register: ca_trust_anchors + notify: Restart Patroni + +- name: Update CA trust + shell: update-ca-trust extract + when: ca_trust_anchors.changed + +- name: Put Patroni OpenSSL key + copy: + content: "{{ patroni_key }}" + dest: "{{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.key" + owner: postgres + group: postgres + mode: 0600 + notify: Restart Patroni + +- name: Put Patroni OpenSSL cert + copy: + content: "{{ patroni_cert }}" + dest: "{{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.crt" + owner: postgres + group: postgres + notify: Restart Patroni diff --git a/tasks/RedHat/install.yaml b/tasks/RedHat/install.yaml new file mode 100644 index 0000000..577960d --- /dev/null +++ b/tasks/RedHat/install.yaml @@ -0,0 +1,45 @@ +- name: Install Patroni {{ patroni_version }} with Python 3.6 + when: "patroni_use_python39 is not defined or not patroni_use_python39" + block: + - name: Add Patroni repository + dnf: + name: "{{ patroni_repo_package }}" + state: present + disable_gpg_check: yes + + - name: "Install Patroni {{ patroni_version }} with Python 3.6" + dnf: + name: "{{ item }}" + state: present + disable_gpg_check: yes + update_cache: yes + loop: + - "{{ patroni_default_packages }}" + register: patroni_setup + +- name: Install Patroni {{ patroni_version }} with Python 3.9 + when: + - patroni_use_python39 is defined + - patroni_use_python39 + block: + - name: Enable Python 3.9 module + shell: dnf module enable -y python39 + register: enable_python39_module + changed_when: "'Nothing to do' not in enable_python39_module.stdout" + + - name: Install Python 3.9 + dnf: + name: python39 + state: present + + - name: Set python3.9 as python + shell: alternatives --set python /usr/bin/python3.9 + register: set_python39_as_python + changed_when: false + failed_when: set_python39_as_python.stdout != '' + + - name: "Install Patroni {{ patroni_version }} with Python 3.9" + dnf: + name: "{{ patroni_python39_packages }}" + disablerepo: pgdg* + state: latest diff --git a/tasks/RedHat/main.yaml b/tasks/RedHat/main.yaml new file mode 100644 index 0000000..e8416af --- /dev/null +++ b/tasks/RedHat/main.yaml @@ -0,0 +1,44 @@ +- name: Disable PostgreSQL module + shell: dnf module disable -y postgresql + register: disable_postgresql_module + changed_when: "'Nothing to do' not in disable_postgresql_module.stdout" + +- name: "Install EPEL" + dnf: + name: "epel-release" + state: present + +- name: Add PostgreSQL repository + dnf: + name: "{{ patroni_postgresql_repo_package }}" + state: present + disable_gpg_check: yes + when: patroni_postgresql_install_repo + +- name: "Install PostgreSQL {{ patroni_postgresql_version }}" + dnf: + name: "{{ item }}" + state: present + disable_gpg_check: yes + loop: + - "{{ patroni_postgresql_packages }}" + register: patroni_postgresql_setup + +- name: Manage TLS/SSL certificates + include_tasks: cacert.yaml + when: patroni_ssl + +- name: Add WAL-G to patroni_create_replica_methods + set_fact: + patroni_create_replica_methods: "{{ patroni_create_replica_methods + [ 'wal_g' ] }}" + when: patroni_wal_g_install + +- name: Include WAL-G role + ansible.builtin.include_role: + name: wal-g + vars: + wal_g_pg: yes + when: patroni_wal_g_install + +- name: Include Patroni {{ patroni_version }} install + include_tasks: install.yaml diff --git a/tasks/RedHat/python36.yaml b/tasks/RedHat/python36.yaml new file mode 100644 index 0000000..1ce706b --- /dev/null +++ b/tasks/RedHat/python36.yaml @@ -0,0 +1,15 @@ +- name: Add Patroni repository + dnf: + name: "{{ patroni_repo_package }}" + state: present + disable_gpg_check: yes + +- name: "Install Patroni {{ patroni_version }} with Python 3.6" + dnf: + name: "{{ item }}" + state: present + disable_gpg_check: yes + update_cache: yes + loop: + - "{{ patroni_default_packages }}" + register: patroni_setup diff --git a/tasks/RedHat/python39.yaml b/tasks/RedHat/python39.yaml new file mode 100644 index 0000000..ababa05 --- /dev/null +++ b/tasks/RedHat/python39.yaml @@ -0,0 +1,21 @@ +- name: Enable Python 3.9 module + shell: dnf module enable -y python39 + register: enable_python39_module + changed_when: "'Nothing to do' not in enable_python39_module.stdout" + +- name: Install Python 3.9 + dnf: + name: python39 + state: present + +- name: Set python3.9 as python + shell: alternatives --set python /usr/bin/python3.9 + register: set_python39_as_python + changed_when: false + failed_when: set_python39_as_python.stdout != '' + +- name: "Install Patroni {{ patroni_version }} with Python 3.9" + dnf: + name: "{{ patroni_python39_packages }}" + disablerepo: pgdg* + state: latest diff --git a/tasks/config.yaml b/tasks/config.yaml new file mode 100644 index 0000000..e616962 --- /dev/null +++ b/tasks/config.yaml @@ -0,0 +1,38 @@ +--- +- name: Merge user options for PostgreSQL part of Patroni config + set_fact: + patroni_postgresql_parameters_combined: "{{ patroni_postgresql_default_parameters | combine(patroni_postgresql_parameters|default({}), recursive=true) }}" + +- name: Template Patroni default config + set_fact: + patroni_default_config: "{{ lookup('template', 'patroni.yaml.j2') | from_yaml }}" + +- name: Print Patroni default config + debug: + msg: "{{ patroni_default_config }}" + verbosity: 2 + +- name: Merge user options with default Patroni config + set_fact: + patroni_config_combined: "{{ patroni_default_config | combine(patroni_config|default({}), recursive=true) }}" + +- name: Create folder for Patroni config + file: + name: "{{ item }}" + state: directory + owner: "postgres" + group: "postgres" + loop: + - /etc/patroni + - /var/log/postgresql + - /var/lib/pgsql_stats_tmp + +- name: Propagate Patroni config + copy: + content: "{{ patroni_config_combined | to_nice_yaml }}" + dest: "/etc/patroni/patroni.yml" + owner: "postgres" + group: "postgres" + mode: 0644 + register: patroni_config_file + notify: Restart Patroni diff --git a/tasks/dynamic_config.yaml b/tasks/dynamic_config.yaml new file mode 100644 index 0000000..926adcb --- /dev/null +++ b/tasks/dynamic_config.yaml @@ -0,0 +1,38 @@ +--- +- name: Propagate Patroni dynamic config + copy: + content: "{{ patroni_postgresql_dynamic_parameters | to_nice_json }}" + dest: "/etc/patroni/dynamic.json" + owner: "postgres" + group: "postgres" + mode: 0644 + register: patroni_dynamic_config_file + +- name: Dynamic configuration parameters for Patroni + when: patroni_dynamic_config_file.changed + block: + - name: Find Patroni master + ansible.builtin.uri: + url: "{{ 'https://' if patroni_ssl else 'http://' }}{{ hostvars[inventory_hostname]['ansible_default_ipv4']['address'] }}:{{ patroni_restapi_listen_port }}" + method: GET + body_format: json + status_code: + - 200 + - 503 + register: patroni_role + + - name: Set fact about roles + set_fact: + role: "{{ patroni_role.json.role }}" + + - name: Apply dynamic configuration parameters for Patroni + ansible.builtin.uri: + url: "{{ 'https://' if patroni_ssl else 'http://' }}{{ hostvars[inventory_hostname]['ansible_default_ipv4']['address'] }}:{{ patroni_restapi_listen_port }}/config" + user: "{{ patroni_restapi_username }}" + password: "{{ patroni_restapi_password }}" + method: PATCH + body: "{{ patroni_postgresql_dynamic_parameters | to_nice_json }}" + force_basic_auth: yes + when: + - "hostvars[inventory_hostname]['role'] == 'master'" + diff --git a/tasks/main.yaml b/tasks/main.yaml new file mode 100644 index 0000000..e6869bf --- /dev/null +++ b/tasks/main.yaml @@ -0,0 +1,45 @@ +- name: Make sure handlers are flushed immediately + meta: flush_handlers + +- name: Load a variable file based on the OS type + include_vars: "{{ lookup('first_found', params) }}" + vars: + params: + files: + - "{{ ansible_facts['distribution'] }}.yaml" + - "{{ ansible_facts['os_family'] }}.yaml" + paths: + - "vars" + tags: always, patroni_vars + +- name: Task name + stat: + path: /etc/patroni/patroni.yaml + register: patroni_config + +- name: "Install Patroni for {{ ansible_facts['os_family'] }}" + include_tasks: "{{ ansible_facts['os_family'] }}/main.yaml" + tags: patroni_install + +- name: "Bootstrap Patroni configuration" + include_tasks: config.yaml + when: not patroni_config.stat.exists + tags: patroni_configure + +- name: Enable and start patroni + systemd: + daemon_reload: true + name: patroni + enabled: true + state: started + masked: no + register: patroni_enable_and_start + tags: patroni, patroni_start + +- name: "Dynamic Patroni configuration" + include_tasks: dynamic_config.yaml + when: + #- patroni_config.stat.exists + - patroni_postgresql_dynamic_parameters + tags: patroni_dynamic_configure + diff --git a/templates/patroni.yaml.j2 b/templates/patroni.yaml.j2 new file mode 100644 index 0000000..28c33c2 --- /dev/null +++ b/templates/patroni.yaml.j2 @@ -0,0 +1,234 @@ +#jinja2: trim_blocks:True,lstrip_blocks:True +--- +scope: {{ patroni_cluster_name }} +name: {{ ansible_hostname }} +namespace: {{ patroni_namespace }} + +{% if patroni_log_destination == 'logfile' %} +log: + level: {{ patroni_log_level |upper }} + traceback_level: {{ patroni_log_traceback_level |upper }} + format: {{ patroni_log_format |quote }} + dateformat: {{ patroni_log_dateformat |quote }} + max_queue_size: {{ patroni_log_max_queue_size |int }} + dir: {{ patroni_log_dir }} + file_num: {{ patroni_log_file_num |int }} + file_size: {{ patroni_log_file_size |int }} + loggers: + patroni.postmaster: {{ patroni_log_loggers_patroni_postmaster |upper }} + urllib3: {{ patroni_log_loggers_urllib3 |upper }} +{% endif %} + +ctl: + certfile: {{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.crt + keyfile: {{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.key + cafile: {{ patroni_ssl_path }}/CA-cert.crt + +restapi: + listen: 0.0.0.0:{{ patroni_restapi_listen_port }} + connect_address: {{ hostvars[inventory_hostname]['ansible_host'] }}:{{ patroni_restapi_listen_port }} +{% if patroni_ssl|bool %} + certfile: {{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.crt + keyfile: {{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.key + cafile: {{ patroni_ssl_path }}/CA-cert.crt + authentication: + username: {{ patroni_restapi_username }} + password: {{ patroni_restapi_password }} +{% endif %} + +{% if not patroni_dcs_exists|bool and patroni_dcs_type == 'etcd' %} +etcd: + hosts: {% for host in groups['etcd'] %}{{ hostvars[host]['ansible_host'] }}:2379{% if not loop.last %},{% endif %}{% endfor %} +{% endif %} +{% if patroni_dcs_exists|bool and patroni_dcs_type == 'etcd' %} +etcd: + hosts: {% for etcd_hosts in patroni_etcd_hosts %}{{etcd_hosts.host}}:{{etcd_hosts.port}}{% if not loop.last %},{% endif %}{% endfor %} +{% endif %} +{% if patroni_dcs_exists|bool and patroni_dcs_type == 'consul' %} +consul: + hosts: {% for consul_hosts in patroni_consul_hosts %}{{consul_hosts.host}}:{{consul_hosts.port}}{% if not loop.last %},{% endif %}{% endfor %} +{% endif %} +{% if not patroni_dcs_exists|bool and patroni_dcs_type == 'consul' %} +consul: + host: localhost +{% endif %} + +bootstrap: + method: {{ patroni_cluster_bootstrap_method }} +{% if patroni_cluster_bootstrap_method == 'wal-g' %} + wal-g: + command: {{ wal_g_patroni_cluster_bootstrap_command }} + no_params: True + recovery_conf: + recovery_target_action: promote + recovery_target_timeline: latest + restore_command: {{ wal_g_pg_binary_name }} wal-fetch %f %p +{% endif %} +{% if patroni_cluster_bootstrap_method == 'pg_probackup' %} + pg_probackup: + command: {{ pg_probackup_patroni_cluster_bootstrap_command }} + no_params: true +{% endif %} + dcs: + ttl: {{ patroni_ttl |d(30, true) |int }} + loop_wait: {{ patroni_loop_wait |d(10, true) |int }} + retry_timeout: {{ patroni_retry_timeout |d(10, true) |int }} + maximum_lag_on_failover: {{ patroni_maximum_lag_on_failover |d(1048576, true) |int }} + master_start_timeout: {{ patroni_master_start_timeout |d(300, true) |int }} + synchronous_mode: {{ patroni_synchronous_mode |string |d(false, true) |lower }} + synchronous_mode_strict: {{ patroni_synchronous_mode_strict |string |d(false, true) |lower }} + synchronous_node_count: {{ patroni_synchronous_node_count |d(1, true) |int }} + {% if patroni_standby_cluster.host is defined and patroni_standby_cluster.host | length > 0 %} + standby_cluster: + host: {{ patroni_standby_cluster.host }} + port: {{ patroni_standby_cluster.port }} + {% if patroni_standby_cluster.primary_slot_name is defined and patroni_standby_cluster.primary_slot_name | length > 0 %} + primary_slot_name: {{ patroni_standby_cluster.primary_slot_name }} + {% endif %} + {% if patroni_standby_cluster.restore_command is defined and patroni_standby_cluster.restore_command | length > 0 %} + restore_command: {{ patroni_standby_cluster.restore_command }} + {% endif %} + {% if patroni_standby_cluster.recovery_min_apply_delay is defined and patroni_standby_cluster.recovery_min_apply_delay | length > 0 %} + recovery_min_apply_delay: {{ patroni_standby_cluster.recovery_min_apply_delay }} + {% endif %} + {% endif %} + postgresql: + use_pg_rewind: {{ patroni_postgresql_use_pg_rewind |string |d(false, true) |lower }} + use_slots: true + parameters: {{ patroni_postgresql_parameters_combined }} + + initdb: # List options to be passed on to initdb + - encoding: {{ patroni_postgresql_encoding }} + - locale: {{ patroni_postgresql_locale }} + - data-checksums + + pg_hba: # Add following lines to pg_hba.conf after running 'initdb' +{% if patroni_ssl|bool %} +{% for host in groups.patroni %} + - hostssl all all {{ hostvars[host]['ansible_host'] }}/32 {{ patroni_postgresql_password_encryption_algorithm }} +{% endfor %} +{% else %} +{% for host in groups.patroni %} + - host all all {{ hostvars[host]['ansible_host'] }}/32 {{ patroni_postgresql_password_encryption_algorithm }} +{% endfor %} +{% endif %} +{% for host in groups.patroni %} + - host replication {{ patroni_replication_username }} {{ hostvars[host]['ansible_host'] }}/32 {{ patroni_postgresql_password_encryption_algorithm }} +{% endfor %} + - hostssl all all 127.0.0.1/32 {{ patroni_postgresql_password_encryption_algorithm }} + - hostssl all all 0.0.0.0/0 scram-sha-256 + - host replication replicator 0.0.0.0/0 scram-sha-256 + + +postgresql: + listen: {{ hostvars[inventory_hostname]['ansible_host'] }},127.0.0.1:{{ patroni_postgresql_port }} + connect_address: {{ hostvars[inventory_hostname]['ansible_host'] }}:{{ patroni_postgresql_port }} + use_unix_socket: true + data_dir: /var/lib/pgsql/{{ patroni_postgresql_major_version }}/data + bin_dir: /usr/pgsql-{{ patroni_postgresql_major_version }}/bin + config_dir: /var/lib/pgsql/{{ patroni_postgresql_major_version }}/data + pgpass: /var/lib/pgsql/.pgpass_patroni + authentication: + replication: + username: {{ patroni_replication_username }} + password: {{ patroni_replication_password }} +{% if patroni_ssl|bool %} + sslcert: {{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.crt + sslkey: {{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name}}.key +{% endif %} + superuser: + username: {{ patroni_superuser_username }} + password: {{ patroni_superuser_password }} +{% if patroni_ssl|bool %} + sslcert: {{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.crt + sslkey: {{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name}}.key +{% endif %} +# rewind: # Has no effect on postgres 10 and lower +# username: rewind_user +# password: rewind_password + parameters: + unix_socket_directories: {{ patroni_postgresql_unix_socket_dir }} +{% if patroni_postgresql_stats_temp_directory_path is defined and patroni_postgresql_stats_temp_directory_path != 'none' %} + stats_temp_directory: {{ patroni_postgresql_stats_temp_directory_path }} +{% endif %} +{% if patroni_ssl|bool %} + ssl: on + ssl_cert_file: {{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name }}.crt + ssl_key_file: {{ patroni_ssl_path }}/{{ patroni_self_signed_cert_name}}.key +{% endif %} + + +{% if patroni_postgresql_pg_ident is defined and patroni_postgresql_pg_ident | length > 0 %} + pg_ident: + {% for ident in patroni_postgresql_pg_ident %} + - {{ ident.mapname }} {{ ident.system_username }} {{ ident.pg_username }} + {% endfor %} +{% endif %} + + remove_data_directory_on_rewind_failure: {{ patroni_remove_data_directory_on_rewind_failure |string |d(false, true) |lower }} + remove_data_directory_on_diverged_timelines: {{ patroni_remove_data_directory_on_diverged_timelines |string |d(false, true) |lower }} + +# callbacks: +# on_start: +# on_stop: +# on_restart: +# on_reload: +# on_role_change: + + create_replica_methods: +{% if patroni_create_replica_methods is defined and patroni_create_replica_methods | length > 0 %} + {% for create_replica_method in patroni_create_replica_methods %} + - {{ create_replica_method }} + {% endfor %} + {% if 'pgbackrest' in patroni_create_replica_methods %} + pgbackrest: + {% for item in pgbackrest %} + {{ item.option }}: {{ item.value }} + {% endfor %} + {% endif %} + {% if 'wal_g' in patroni_create_replica_methods %} + wal_g: + {% for item in wal_g %} + {{ item.option }}: {{ item.value }} + {% endfor %} + {% endif %} + {% if 'basebackup' in patroni_create_replica_methods %} + basebackup: + {% for item in basebackup %} + {{ item.option }}: '{{ item.value }}' + {% endfor %} + {% if patroni_postgresql_major_version is version('10', '>=') and patroni_postgresql_wal_dir is defined and patroni_postgresql_wal_dir | length > 0 %} + waldir: {{ patroni_postgresql_wal_dir }} + {% endif %} + {% endif %} + {% if 'pg_probackup' in patroni_create_replica_methods %} + pg_probackup: + {% for item in pg_probackup %} + {{ item.option }}: {{ item.value }} + {% endfor %} + {% endif %} +{% else %} + - basebackup + basebackup: + max-rate: '100M' +{% endif %} + +{% if patroni_postgresql_restore_command is defined and patroni_postgresql_restore_command | length > 0 %} + recovery_conf: + restore_command: {{ patroni_postgresql_restore_command }} +{% endif %} + +watchdog: + mode: off # Allowed values: off, automatic, required + device: /dev/watchdog + safety_margin: 5 + +tags: + nofailover: false + noloadbalance: false + clonefrom: false + nosync: false + + # specify a node to replicate from (cascading replication) +# replicatefrom: (node name) + diff --git a/vars/Debian.yaml b/vars/Debian.yaml new file mode 100644 index 0000000..6482d76 --- /dev/null +++ b/vars/Debian.yaml @@ -0,0 +1,21 @@ +patroni_apt_key: + - name: org.postgresql.gpg + url: "https://www.postgresql.org/media/keys/ACCC4CF8.asc" + keyring: /etc/apt/trusted.gpg.d/org.postgresql.gpg +patroni_apt_repository: + - repo: deb http://apt.postgresql.org/pub/repos/apt {{ ansible_distribution_release }}-pgdg main + filename: postgresql + +patroni_postgresql_package: "{{ patroni_postgresql_package_name }}-{{ patroni_postgresql_major_version }}={{ patroni_postgresql_version }}" +patroni_package: "{{ patroni_package_name }}={{ patroni_version }}" + + +# PostgreSQL variables +patroni_postgresql_cluster_name: "main" +patroni_postgresql_data_dir: "{{ patroni_postgresql_home_dir }}/{{ patroni_postgresql_major_version }}/{{ patroni_postgresql_cluster_name }}" # You can specify custom data dir path +patroni_postgresql_wal_dir: "" # custom WAL dir path (symlink will be created) [optional] +patroni_postgresql_conf_dir: "/etc/postgresql/{{ patroni_postgresql_major_version }}/{{ patroni_postgresql_cluster_name }}" +patroni_postgresql_bin_dir: "/usr/lib/postgresql/{{ patroni_postgresql_major_version }}/bin" +patroni_postgresql_log_dir: "/var/log/postgresql" +patroni_postgresql_unix_socket_dir: "/var/run/postgresql" +patroni_postgresql_home_dir: "/var/lib/postgresql" diff --git a/vars/RedHat.yaml b/vars/RedHat.yaml new file mode 100644 index 0000000..6ab259c --- /dev/null +++ b/vars/RedHat.yaml @@ -0,0 +1,64 @@ +patroni_postgresql_repo_package: "https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm" +patroni_postgresql_packages: + - "{{ patroni_postgresql_package_name }}{{ patroni_postgresql_major_version }}-server-{{ patroni_postgresql_version }}" + - "{{ patroni_postgresql_package_name }}{{ patroni_postgresql_major_version }}-contrib-{{ patroni_postgresql_version }}" + +patroni_repo_package: "https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm" +patroni_default_packages: + - "{{ patroni_package_name }}-{{ patroni_version }}" + - "{{ patroni_package_name }}-consul-{{ patroni_version }}" + - "{{ patroni_package_name }}-etcd-{{ patroni_version }}" + - "{{ patroni_package_name }}-zookeeper-{{ patroni_version }}" + +patroni_python39_packages: + - "{{ patroni_package_name }}-{{ patroni_version }}" + - python39-consul + - python39-etcd + - python3.9 + - python39-six + - python39-aio-pika + - python39-aiodns + - python39-aiohttp + - python39-aiormq + - python39-aiosignal + - python39-async-timeout + - python39-attrs + - python39-brotli + - python39-cchardet + - python39-certifi + - python39-cffi + - python39-chardet + - python39-charset-normalizer + - python39-click + - python39-consul + - python39-dnspython + - python39-dotenv + - python39-etcd + - python39-frozenlist + - python39-idna + - python39-multidict + - python39-pamqp + - python39-ply + - python39-prettytable + - python39-psutil + - python39-psycopg2 + - python39-pycares + - python39-pycparser + - python39-pydantic + - python39-pysocks + - python39-pyyaml + - python39-requests + - python39-typing-extensions + - python39-urllib3 + - python39-wcwidth + - python39-yarl + - python39-ydiff + - python39-dateutil + +patroni_postgresql_data_dir: "{{ patroni_postgresql_home_dir }}/{{ patroni_postgresql_major_version }}/data" # You can specify custom data dir path +patroni_postgresql_wal_dir: "" # custom WAL dir path (symlink will be created) [optional] +patroni_postgresql_conf_dir: "{{ patroni_postgresql_data_dir }}" +patroni_postgresql_bin_dir: "/usr/pgsql-{{ patroni_postgresql_version }}/bin" +patroni_postgresql_log_dir: "/var/log/postgresql" +patroni_postgresql_unix_socket_dir: "/var/run/postgresql" +patroni_postgresql_home_dir: "/var/lib/pgsql" diff --git a/vars/main.yaml b/vars/main.yaml new file mode 100644 index 0000000..3ec850e --- /dev/null +++ b/vars/main.yaml @@ -0,0 +1,154 @@ +patroni_postgresql_package_name: "postgresql" +patroni_postgresql_major_version: "{{ patroni_postgresql_version | split('.') | first }}" +patroni_postgresql_minor_version: "{{ patroni_postgresql_version | split('.') | last }}" + +patroni_package_name: patroni + +patroni_synchronous_mode: false # or 'true' for enable synchronous database replication +patroni_synchronous_mode_strict: false # if 'true' then block all client writes to the master, when a synchronous replica is not available +patroni_synchronous_node_count: 2 # number of synchronous standby databases + +patroni_postgresql_port: "5432" +patroni_postgresql_encoding: "UTF8" # for bootstrap only (initdb) +patroni_postgresql_locale: "en_US.UTF-8" # for bootstrap only (initdb) +patroni_postgresql_data_checksums: true # for bootstrap only (initdb) +patroni_postgresql_password_encryption_algorithm: "scram-sha-256" + +patroni_postgresql_unix_socket_dir: "/var/run/postgresql" +patroni_postgresql_stats_temp_directory_path: "/var/lib/pgsql_stats_tmp" + +patroni_postgresql_use_pg_rewind: true # or 'false' +# try to use pg_rewind on the former leader when it joins cluster as a replica. + +patroni_remove_data_directory_on_rewind_failure: false # or 'true' (if use_pg_rewind: 'true') +# avoid removing the data directory on an unsuccessful rewind +# if 'true', Patroni will remove the PostgreSQL data directory and recreate the replica. + +patroni_remove_data_directory_on_diverged_timelines: false # or 'true' +# if 'true', Patroni will remove the PostgreSQL data directory and recreate the replica +# if it notices that timelines are diverging and the former master can not start streaming from the new master. + +# https://patroni.readthedocs.io/en/latest/replica_bootstrap.html#bootstrap +patroni_cluster_bootstrap_method: "initdb" # or "wal-g", "pgbackrest", "pg_probackup" + +# https://patroni.readthedocs.io/en/latest/replica_bootstrap.html#building-replicas +patroni_create_replica_methods: +# - pgbackrest +# - wal_g +# - pg_probackup + - basebackup + +# Dynamic configuration +patroni_postgresql_dynamic_parameters: {} + +# postgresql parameters to bootstrap dcs (are parameters for example) +patroni_postgresql_default_parameters: + max_connections: "1000" + superuser_reserved_connections: "5" + password_encryption: "{{ patroni_postgresql_password_encryption_algorithm }}" + max_locks_per_transaction: "64" + max_prepared_transactions: "0" + huge_pages: "try" + shared_buffers: "512MB" + work_mem: "128MB" + maintenance_work_mem: "256MB" + effective_cache_size: "4GB" + checkpoint_timeout: "15min" + checkpoint_completion_target: "0.9" + min_wal_size: "2GB" + max_wal_size: "8GB" + wal_buffers: "32MB" + default_statistics_target: "1000" + seq_page_cost: "1" + random_page_cost: "4" + effective_io_concurrency: "2" + synchronous_commit: "on" + autovacuum: "on" + autovacuum_max_workers: "5" + autovacuum_vacuum_scale_factor: "0.01" + autovacuum_analyze_scale_factor: "0.01" + autovacuum_vacuum_cost_limit: "500" + autovacuum_vacuum_cost_delay: "2" + autovacuum_naptime: "1s" + max_files_per_process: "4096" + archive_mode: "on" + archive_timeout: "1800s" + archive_command: "cd ." + wal_level: "replica" + wal_keep_size: "2GB" + max_wal_senders: "10" + max_replication_slots: "10" + hot_standby: "on" + wal_log_hints: "on" + wal_compression: "on" + shared_preload_libraries: "pg_stat_statements,auto_explain" + pg_stat_statements.max: "10000" + pg_stat_statements.track: "all" + pg_stat_statements.track_utility: "false" + pg_stat_statements.save: "true" + auto_explain.log_min_duration: "10s" + auto_explain.log_analyze: "true" + auto_explain.log_buffers: "true" + auto_explain.log_timing: "false" + auto_explain.log_triggers: "true" + auto_explain.log_verbose: "true" + auto_explain.log_nested_statements: "true" + track_io_timing: "on" + log_lock_waits: "on" + log_temp_files: "0" + track_activities: "on" + track_counts: "on" + track_functions: "all" + log_checkpoints: "on" + logging_collector: "on" + log_truncate_on_rotation: "on" + log_rotation_age: "1d" + log_rotation_size: "0" + log_line_prefix: "'%t [%p-%l] %r %q%u@%d '" + log_filename: "'postgresql-%a.log'" + log_directory: "{{ patroni_postgresql_log_dir }}" + hot_standby_feedback: "on" + max_standby_streaming_delay: "30s" + wal_receiver_status_interval: "10s" + idle_in_transaction_session_timeout: "10min" + jit: "off" +# archive_command: "wal-g wal-push %p" +# archive_command: "pgbackrest --stanza={{ pgbackrest_stanza }} archive-push %p" +# max_worker_processes: "24" +# max_parallel_workers: "12" +# max_parallel_workers_per_gather: "2" +# max_parallel_maintenance_workers: "2" +# old_snapshot_threshold: "60min" + +# list of lines that Patroni will use to generate pg_ident.conf +patroni_postgresql_pg_ident: [] +# - {mapname: "main", system_username: "postgres", pg_username: "backup"} +# - {mapname: "", system_username: "", pg_username: ""} + +# Extended variables (optional) +patroni_ttl: 30 +patroni_loop_wait: 10 +patroni_retry_timeout: 10 +patroni_maximum_lag_on_failover: 1048576 +patroni_master_start_timeout: 300 + +# https://patroni.readthedocs.io/en/latest/replica_bootstrap.html#standby-cluster +patroni_standby_cluster: + host: "" # an address of remote master + port: "5432" # a port of remote master +# primary_slot_name: "" # which slot on the remote master to use for replication (optional) +# restore_command: "" # command to restore WAL records from the remote master to standby leader (optional) +# recovery_min_apply_delay: "" # how long to wait before actually apply WAL records on a standby leader (optional) + +patroni_log_destination: stderr # or 'logfile' +# if patroni_log_destination: logfile +patroni_log_dir: /var/log/patroni +patroni_log_level: info +patroni_log_traceback_level: error +patroni_log_format: "%(asctime)s %(levelname)s: %(message)s" +patroni_log_dateformat: "" +patroni_log_max_queue_size: 1000 +patroni_log_file_num: 4 +patroni_log_file_size: 25000000 # bytes +patroni_log_loggers_patroni_postmaster: warning +patroni_log_loggers_urllib3: warning # or 'debug'