diff --git a/changelogs/fragments/67464-postgresql_info_add_collecting_subscription_info.yml b/changelogs/fragments/67464-postgresql_info_add_collecting_subscription_info.yml new file mode 100644 index 00000000000..d6f14d4a1c0 --- /dev/null +++ b/changelogs/fragments/67464-postgresql_info_add_collecting_subscription_info.yml @@ -0,0 +1,2 @@ +minor_changes: +- postgresql_info - add collection info about replication subscriptions (https://github.com/ansible/ansible/pull/67464). diff --git a/lib/ansible/modules/database/postgresql/postgresql_info.py b/lib/ansible/modules/database/postgresql/postgresql_info.py index 196d8ce9769..73e1925eed4 100644 --- a/lib/ansible/modules/database/postgresql/postgresql_info.py +++ b/lib/ansible/modules/database/postgresql/postgresql_info.py @@ -26,7 +26,7 @@ options: - Limit the collected information by comma separated string or YAML list. - Allowable values are C(version), C(databases), C(settings), C(tablespaces), C(roles), - C(replications), C(repl_slots). + C(replications), C(repl_slots), C(subscriptions) (since 2.10). - By default, collects all subsets. - You can use shell-style (fnmatch) wildcard to pass groups of values (see Examples). - You can use '!' before value (for example, C(!settings)) to exclude it from the information. @@ -453,6 +453,15 @@ settings: returned: always type: bool sample: false +subscriptions: + description: + - Information about replication subscriptions (available for PostgreSQL 10 and higher) + U(https://www.postgresql.org/docs/current/logical-replication-subscription.html). + - Content depends on PostgreSQL server version. + returned: if configured + type: dict + sample: + - {"acme_db": {"my_subscription": {"ownername": "postgres", "subenabled": true, "subpublications": ["first_publication"]}}} ''' from fnmatch import fnmatch @@ -470,6 +479,7 @@ from ansible.module_utils.postgres import ( get_conn_params, postgres_common_argument_spec, ) +from ansible.module_utils.six import iteritems from ansible.module_utils._text import to_native @@ -533,6 +543,7 @@ class PgClusterInfo(object): "repl_slots": {}, "settings": {}, "roles": {}, + "subscriptions": {}, "pending_restart_settings": [], } @@ -546,6 +557,7 @@ class PgClusterInfo(object): "repl_slots": self.get_rslot_info, "settings": self.get_settings, "roles": self.get_role_info, + "subscriptions": self.get_subscr_info, } incl_list = [] @@ -588,6 +600,36 @@ class PgClusterInfo(object): return self.pg_info + def get_subscr_info(self): + """Get subscription statistics and fill out self.pg_info dictionary.""" + if self.cursor.connection.server_version < 10000: + return + + query = ("SELECT s.*, r.rolname AS ownername, d.datname AS dbname " + "FROM pg_catalog.pg_subscription s " + "JOIN pg_catalog.pg_database d " + "ON s.subdbid = d.oid " + "JOIN pg_catalog.pg_roles AS r " + "ON s.subowner = r.oid") + + result = self.__exec_sql(query) + + if result: + result = [dict(row) for row in result] + else: + return + + for elem in result: + if not self.pg_info['subscriptions'].get(elem['dbname']): + self.pg_info['subscriptions'][elem['dbname']] = {} + + if not self.pg_info['subscriptions'][elem['dbname']].get(elem['subname']): + self.pg_info['subscriptions'][elem['dbname']][elem['subname']] = {} + + for key, val in iteritems(elem): + if key not in ('subname', 'dbname'): + self.pg_info['subscriptions'][elem['dbname']][elem['subname']][key] = val + def get_tablespaces(self): """Get information about tablespaces.""" # Check spcoption exists: diff --git a/test/integration/targets/postgresql_info/aliases b/test/integration/targets/postgresql_info/aliases index 6e19e26ba97..55ffced079d 100644 --- a/test/integration/targets/postgresql_info/aliases +++ b/test/integration/targets/postgresql_info/aliases @@ -1,4 +1,9 @@ destructive -shippable/posix/group4 +shippable/posix/group1 skip/aix skip/osx +skip/centos +skip/freebsd +skip/rhel +skip/opensuse +skip/fedora diff --git a/test/integration/targets/postgresql_info/defaults/main.yml b/test/integration/targets/postgresql_info/defaults/main.yml index 73eb55ae255..000532aef2f 100644 --- a/test/integration/targets/postgresql_info/defaults/main.yml +++ b/test/integration/targets/postgresql_info/defaults/main.yml @@ -1,2 +1,15 @@ --- +pg_user: postgres db_default: postgres +master_port: 5433 +replica_port: 5434 + +test_table1: acme1 +test_pub: first_publication +test_pub2: second_publication +replication_role: logical_replication +replication_pass: alsdjfKJKDf1# +test_db: acme_db +test_subscription: test +test_subscription2: test2 +conn_timeout: 100 diff --git a/test/integration/targets/postgresql_info/meta/main.yml b/test/integration/targets/postgresql_info/meta/main.yml index 4ce5a5837ba..d72e4d23c7e 100644 --- a/test/integration/targets/postgresql_info/meta/main.yml +++ b/test/integration/targets/postgresql_info/meta/main.yml @@ -1,2 +1,2 @@ dependencies: - - setup_postgresql_db + - setup_postgresql_replication diff --git a/test/integration/targets/postgresql_info/tasks/main.yml b/test/integration/targets/postgresql_info/tasks/main.yml index 43d2c9ba21d..eec057ca18b 100644 --- a/test/integration/targets/postgresql_info/tasks/main.yml +++ b/test/integration/targets/postgresql_info/tasks/main.yml @@ -1,2 +1,7 @@ +# For testing getting publication and subscription info +- import_tasks: setup_publication.yml + when: ansible_distribution == 'Ubuntu' and ansible_distribution_major_version >= '18' + # Initial CI tests of postgresql_info module - import_tasks: postgresql_info_initial.yml + when: ansible_distribution == 'Ubuntu' and ansible_distribution_major_version >= '18' diff --git a/test/integration/targets/postgresql_info/tasks/postgresql_info_initial.yml b/test/integration/targets/postgresql_info/tasks/postgresql_info_initial.yml index e6ca117c70d..a2401c472e6 100644 --- a/test/integration/targets/postgresql_info/tasks/postgresql_info_initial.yml +++ b/test/integration/targets/postgresql_info/tasks/postgresql_info_initial.yml @@ -1,28 +1,67 @@ -# Test code for the postgresql_info module -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -- name: postgresql_info - create role to check session_role - become_user: "{{ pg_user }}" - become: yes - postgresql_user: - db: "{{ db_default }}" - login_user: "{{ pg_user }}" - name: session_superuser - role_attr_flags: SUPERUSER +- vars: + task_parameters: &task_parameters + become_user: '{{ pg_user }}' + become: yes + register: result + pg_parameters: &pg_parameters + login_user: '{{ pg_user }}' + login_db: '{{ db_default }}' -- name: postgresql_info - test return values and session_role param - become_user: "{{ pg_user }}" - become: yes - postgresql_info: - db: "{{ db_default }}" - login_user: "{{ pg_user }}" - session_role: session_superuser - register: result - ignore_errors: yes + block: -- assert: - that: + - name: Create test subscription + <<: *task_parameters + postgresql_subscription: + <<: *pg_parameters + login_port: '{{ replica_port }}' + name: '{{ test_subscription }}' + login_db: '{{ test_db }}' + state: present + publications: '{{ test_pub }}' + connparams: + host: 127.0.0.1 + port: '{{ master_port }}' + user: '{{ replication_role }}' + password: '{{ replication_pass }}' + dbname: '{{ test_db }}' + + - name: Create test subscription + <<: *task_parameters + postgresql_subscription: + <<: *pg_parameters + login_port: '{{ replica_port }}' + name: '{{ test_subscription2 }}' + login_db: '{{ test_db }}' + state: present + publications: '{{ test_pub2 }}' + connparams: + host: 127.0.0.1 + port: '{{ master_port }}' + user: '{{ replication_role }}' + password: '{{ replication_pass }}' + dbname: '{{ test_db }}' + + - name: postgresql_info - create role to check session_role + <<: *task_parameters + postgresql_user: + <<: *pg_parameters + login_port: '{{ replica_port }}' + login_user: "{{ pg_user }}" + name: session_superuser + role_attr_flags: SUPERUSER + + - name: postgresql_info - test return values and session_role param + <<: *task_parameters + postgresql_info: + <<: *pg_parameters + login_port: '{{ replica_port }}' + session_role: session_superuser + + - assert: + that: - result.version != {} - result.databases.{{ db_default }}.collate - result.databases.{{ db_default }}.languages @@ -31,21 +70,39 @@ - result.settings - result.tablespaces - result.roles + - result.subscriptions.{{ test_db }}.{{ test_subscription }} + - result.subscriptions.{{ test_db }}.{{ test_subscription2 }} -- name: postgresql_info - check filter param passed by list - become_user: "{{ pg_user }}" - become: yes - postgresql_info: - db: "{{ db_default }}" - login_user: "{{ pg_user }}" - filter: - - ver* - - rol* - register: result - ignore_errors: yes + - name: postgresql_info - check filter param passed by list + <<: *task_parameters + postgresql_info: + <<: *pg_parameters + login_port: '{{ replica_port }}' + filter: + - ver* + - rol* + - subscr* -- assert: - that: + - assert: + that: + - result.version != {} + - result.roles + - result.subscriptions.{{ test_db }}.{{ test_subscription }} != {} + - result.subscriptions.{{ test_db }}.{{ test_subscription2 }} != {} + - result.databases == {} + - result.repl_slots == {} + - result.replications == {} + - result.settings == {} + - result.tablespaces == {} + + - name: postgresql_info - check filter param passed by string + <<: *task_parameters + postgresql_info: + <<: *pg_parameters + filter: ver*,role* + + - assert: + that: - result.version != {} - result.roles - result.databases == {} @@ -54,55 +111,27 @@ - result.settings == {} - result.tablespaces == {} -- name: postgresql_info - check filter param passed by string - become_user: "{{ pg_user }}" - become: yes - postgresql_info: - db: "{{ db_default }}" - filter: ver*,role* - login_user: "{{ pg_user }}" - register: result - ignore_errors: yes + - name: postgresql_info - check filter param passed by string + <<: *task_parameters + postgresql_info: + <<: *pg_parameters + filter: ver* -- assert: - that: - - result.version != {} - - result.roles - - result.databases == {} - - result.repl_slots == {} - - result.replications == {} - - result.settings == {} - - result.tablespaces == {} - -- name: postgresql_info - check filter param passed by string - become_user: "{{ pg_user }}" - become: yes - postgresql_info: - db: "{{ db_default }}" - filter: ver* - login_user: "{{ pg_user }}" - register: result - ignore_errors: yes - -- assert: - that: + - assert: + that: - result.version - result.roles == {} -- name: postgresql_info - check excluding filter param passed by list - become_user: "{{ pg_user }}" - become: yes - postgresql_info: - db: "{{ db_default }}" - filter: + - name: postgresql_info - check excluding filter param passed by list + <<: *task_parameters + postgresql_info: + <<: *pg_parameters + filter: - "!ver*" - "!rol*" - login_user: "{{ pg_user }}" - register: result - ignore_errors: yes -- assert: - that: + - assert: + that: - result.version == {} - result.roles == {} - result.databases diff --git a/test/integration/targets/postgresql_info/tasks/setup_publication.yml b/test/integration/targets/postgresql_info/tasks/setup_publication.yml new file mode 100644 index 00000000000..0d7df0d73ea --- /dev/null +++ b/test/integration/targets/postgresql_info/tasks/setup_publication.yml @@ -0,0 +1,61 @@ +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# Preparation for further tests of postgresql_subscription module. + +- vars: + task_parameters: &task_parameters + become_user: '{{ pg_user }}' + become: yes + register: result + pg_parameters: &pg_parameters + login_user: '{{ pg_user }}' + login_db: '{{ test_db }}' + + block: + - name: Create test db + <<: *task_parameters + postgresql_db: + login_user: '{{ pg_user }}' + login_port: '{{ master_port }}' + maintenance_db: '{{ db_default }}' + name: '{{ test_db }}' + + - name: Create test role + <<: *task_parameters + postgresql_user: + <<: *pg_parameters + login_port: '{{ master_port }}' + name: '{{ replication_role }}' + password: '{{ replication_pass }}' + role_attr_flags: LOGIN,REPLICATION + + - name: Create test table + <<: *task_parameters + postgresql_table: + <<: *pg_parameters + login_port: '{{ master_port }}' + name: '{{ test_table1 }}' + columns: + - id int + + - name: Master - dump schema + <<: *task_parameters + shell: pg_dumpall -p '{{ master_port }}' -s > /tmp/schema.sql + + - name: Replicat restore schema + <<: *task_parameters + shell: psql -p '{{ replica_port }}' -f /tmp/schema.sql + + - name: Create publication + <<: *task_parameters + postgresql_publication: + <<: *pg_parameters + login_port: '{{ master_port }}' + name: '{{ test_pub }}' + + - name: Create publication + <<: *task_parameters + postgresql_publication: + <<: *pg_parameters + login_port: '{{ master_port }}' + name: '{{ test_pub2 }}'