From 4da6d8cbf9ae9d590f012c526f2c6f11997746ab Mon Sep 17 00:00:00 2001 From: Andrey Klychkov Date: Tue, 2 Jul 2019 16:24:46 +0300 Subject: [PATCH] postgresql_ext: add version parameter (#58381) * postgresql_ext: add version new option * postgresql_ext: add version new option, fix ssl tests * postgresql_ext: add version new option, fix tests * postgresql_ext: add version new option, fix examples * postgresql_ext: add version new option, fix the doc * postgresql_ext: add version new option, fix examples * postgresql_ext: add version new option, fix typo in tests --- .../database/postgresql/postgresql_ext.py | 191 ++++++++-- .../targets/postgresql/tasks/main.yml | 3 + .../tasks/postgresql_ext_version_opt.yml | 331 ++++++++++++++++++ .../targets/postgresql/tasks/ssl.yml | 42 +-- .../setup_postgresql_db/files/dummy--1.0.sql | 2 + .../setup_postgresql_db/files/dummy--2.0.sql | 2 + .../setup_postgresql_db/files/dummy--3.0.sql | 2 + .../setup_postgresql_db/files/dummy.control | 3 + .../setup_postgresql_db/tasks/main.yml | 30 ++ 9 files changed, 563 insertions(+), 43 deletions(-) create mode 100644 test/integration/targets/postgresql/tasks/postgresql_ext_version_opt.yml create mode 100644 test/integration/targets/setup_postgresql_db/files/dummy--1.0.sql create mode 100644 test/integration/targets/setup_postgresql_db/files/dummy--2.0.sql create mode 100644 test/integration/targets/setup_postgresql_db/files/dummy--3.0.sql create mode 100644 test/integration/targets/setup_postgresql_db/files/dummy.control diff --git a/lib/ansible/modules/database/postgresql/postgresql_ext.py b/lib/ansible/modules/database/postgresql/postgresql_ext.py index f37672a2d1b..a589cfe0aaa 100644 --- a/lib/ansible/modules/database/postgresql/postgresql_ext.py +++ b/lib/ansible/modules/database/postgresql/postgresql_ext.py @@ -79,6 +79,15 @@ options: type: str aliases: [ ssl_rootcert ] version_added: '2.8' + version: + description: + - Extension version to add or update to. Has effect with I(state=present) only. + - If not specified, the latest extension version will be created. + - It can't downgrade an extension version. + When version downgrade is needed, remove the extension and create new one with appropriate version. + - Set I(version=latest) to update the extension to the latest available version. + type: str + version_added: '2.9' notes: - The default authentication assumes that you are either logging in as or sudo'ing to the C(postgres) account on the host. @@ -92,6 +101,8 @@ requirements: [ psycopg2 ] author: - Daniel Schep (@dschep) - Thomas O'Donnell (@andytom) +- Sandro Santilli (@strk) +- Andrew Klychkov (@Andersson007) extends_documentation_fragment: postgres ''' @@ -122,6 +133,18 @@ EXAMPLES = r''' db: acme cascade: yes state: absent + +- name: Create extension foo of version 1.2 or update it if it's already created + postgresql_ext: + db: acme + name: foo + version: 1.2 + +- name: Assuming extension foo is created, update it to the latest version + postgresql_ext: + db: acme + name: foo + version: latest ''' RETURN = r''' @@ -135,6 +158,8 @@ query: import traceback +from distutils.version import LooseVersion + try: from psycopg2.extras import DictCursor except ImportError: @@ -180,18 +205,81 @@ def ext_delete(cursor, ext, cascade): return False -def ext_create(cursor, ext, schema, cascade): - if not ext_exists(cursor, ext): - query = "CREATE EXTENSION \"%s\"" % ext - if schema: - query += " WITH SCHEMA \"%s\"" % schema - if cascade: - query += " CASCADE" - cursor.execute(query) - executed_queries.append(query) - return True +def ext_update_version(cursor, ext, version): + """Update extension version. + + Return True if success. + + Args: + cursor (cursor) -- cursor object of psycopg2 library + ext (str) -- extension name + version (str) -- extension version + """ + if version != 'latest': + query = ("ALTER EXTENSION \"%s\" UPDATE TO '%s'" % (ext, version)) else: - return False + query = ("ALTER EXTENSION \"%s\" UPDATE" % ext) + cursor.execute(query) + executed_queries.append(query) + return True + + +def ext_create(cursor, ext, schema, cascade, version): + query = "CREATE EXTENSION \"%s\"" % ext + if schema: + query += " WITH SCHEMA \"%s\"" % schema + if version: + query += " VERSION '%s'" % version + if cascade: + query += " CASCADE" + cursor.execute(query) + executed_queries.append(query) + return True + + +def ext_get_versions(cursor, ext): + """ + Get the current created extension version and available versions. + + Return tuple (current_version, [list of available versions]). + + Note: the list of available versions contains only versions + that higher than the current created version. + If the extension is not created, this list will contain all + available versions. + + Args: + cursor (cursor) -- cursor object of psycopg2 library + ext (str) -- extension name + """ + + # 1. Get the current extension version: + query = ("SELECT extversion FROM pg_catalog.pg_extension " + "WHERE extname = '%s'" % ext) + + current_version = '0' + cursor.execute(query) + res = cursor.fetchone() + if res: + current_version = res[0] + + # 2. Get available versions: + query = ("SELECT version FROM pg_available_extension_versions " + "WHERE name = '%s'" % ext) + cursor.execute(query) + res = cursor.fetchall() + + available_versions = [] + if res: + # Make the list of available versions: + for line in res: + if LooseVersion(line[0]) > LooseVersion(current_version): + available_versions.append(line['version']) + + if current_version == '0': + current_version = False + + return (current_version, available_versions) # =========================================== # Module execution. @@ -207,6 +295,7 @@ def main(): state=dict(type="str", default="present", choices=["absent", "present"]), cascade=dict(type="bool", default=False), session_role=dict(type="str"), + version=dict(type="str"), ) module = AnsibleModule( @@ -218,24 +307,82 @@ def main(): schema = module.params["schema"] state = module.params["state"] cascade = module.params["cascade"] + version = module.params["version"] changed = False + if version and state == 'absent': + module.warn("Parameter version is ignored when state=absent") + conn_params = get_conn_params(module, module.params) db_connection = connect_to_db(module, conn_params, autocommit=True) cursor = db_connection.cursor(cursor_factory=DictCursor) try: - if module.check_mode: - if state == "present": - changed = not ext_exists(cursor, ext) - elif state == "absent": - changed = ext_exists(cursor, ext) - else: - if state == "absent": - changed = ext_delete(cursor, ext, cascade) - - elif state == "present": - changed = ext_create(cursor, ext, schema, cascade) + # Get extension info and available versions: + curr_version, available_versions = ext_get_versions(cursor, ext) + + if state == "present": + if version == 'latest': + if available_versions: + version = available_versions[-1] + else: + version = '' + + if version: + # If the specific version is passed and it is not available for update: + if version not in available_versions: + if not curr_version: + module.fail_json(msg="Passed version '%s' is not available" % version) + + elif LooseVersion(curr_version) == LooseVersion(version): + changed = False + + else: + module.fail_json(msg="Passed version '%s' is lower than " + "the current created version '%s' or " + "the passed version is not available" % (version, curr_version)) + + # If the specific version is passed and it is higher that the current version: + if curr_version and version: + if LooseVersion(curr_version) < LooseVersion(version): + if module.check_mode: + changed = True + else: + changed = ext_update_version(cursor, ext, version) + + # If the specific version is passed and it is created now: + if curr_version == version: + changed = False + + # If the ext doesn't exist and installed: + elif not curr_version and available_versions: + if module.check_mode: + changed = True + else: + changed = ext_create(cursor, ext, schema, cascade, version) + + # If version is not passed: + else: + if not curr_version: + # If the ext doesn't exist and it's installed: + if available_versions: + if module.check_mode: + changed = True + else: + changed = ext_create(cursor, ext, schema, cascade, version) + + # If the ext doesn't exist and not installed: + else: + module.fail_json(msg="Extension %s is not installed" % ext) + + elif state == "absent": + if curr_version: + if module.check_mode: + changed = True + else: + changed = ext_delete(cursor, ext, cascade) + else: + changed = False except Exception as e: db_connection.close() diff --git a/test/integration/targets/postgresql/tasks/main.yml b/test/integration/targets/postgresql/tasks/main.yml index c760b83428e..584b074a6c3 100644 --- a/test/integration/targets/postgresql/tasks/main.yml +++ b/test/integration/targets/postgresql/tasks/main.yml @@ -851,6 +851,9 @@ - include: postgresql_ext.yml when: postgres_version_resp.stdout is version('9.1', '>=') and ansible_distribution == 'Fedora' +- include: postgresql_ext_version_opt.yml + when: ansible_distribution == 'Ubuntu' + # Test postgresql_slot module. # Physical replication slots are available from PostgreSQL 9.4 - include: postgresql_slot.yml diff --git a/test/integration/targets/postgresql/tasks/postgresql_ext_version_opt.yml b/test/integration/targets/postgresql/tasks/postgresql_ext_version_opt.yml new file mode 100644 index 00000000000..2efd4f01591 --- /dev/null +++ b/test/integration/targets/postgresql/tasks/postgresql_ext_version_opt.yml @@ -0,0 +1,331 @@ +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# Tests for postgresql_ext version option + +- vars: + test_ext: dummy + test_schema: schema1 + task_parameters: &task_parameters + become_user: '{{ pg_user }}' + become: True + register: result + pg_parameters: &pg_parameters + login_user: '{{ pg_user }}' + login_db: postgres + + block: + # Preparation: + - name: postgresql_ext_version - create schema schema1 + <<: *task_parameters + postgresql_schema: + <<: *pg_parameters + name: "{{ test_schema }}" + + # Do tests: + - name: postgresql_ext_version - create extension of specific version, check mode + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + schema: "{{ test_schema }}" + version: 1.0 + check_mode: yes + + - assert: + that: + - result.changed == true + + - name: postgresql_ext_version - check that nothing was actually changed + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}'" + + - assert: + that: + - result.rowcount == 0 + + - name: postgresql_ext_version - create extension of specific version + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + schema: "{{ test_schema }}" + version: 1.0 + + - assert: + that: + - result.changed == true + - result.queries == ["CREATE EXTENSION \"{{ test_ext }}\" WITH SCHEMA \"{{ test_schema }}\" VERSION '1.0'"] + + - name: postgresql_ext_version - check + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}' AND extversion = '1.0'" + + - assert: + that: + - result.rowcount == 1 + + - name: postgresql_ext_version - try to create extension of the same version again in check_mode + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + schema: "{{ test_schema }}" + version: 1.0 + check_mode: yes + + - assert: + that: + - result.changed == false + + - name: postgresql_ext_version - check + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}' AND extversion = '1.0'" + + - assert: + that: + - result.rowcount == 1 + + - name: postgresql_ext_version - try to create extension of the same version again in actual mode + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + schema: "{{ test_schema }}" + version: 1.0 + + - assert: + that: + - result.changed == false + + - name: postgresql_ext_version - check + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}' AND extversion = '1.0'" + + - assert: + that: + - result.rowcount == 1 + + - name: postgresql_ext_version - update the extension to the next version in check_mode + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + schema: "{{ test_schema }}" + version: 2.0 + check_mode: yes + + - assert: + that: + - result.changed == true + + - name: postgresql_ext_version - check, the version must be 1.0 + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}' AND extversion = '1.0'" + + - assert: + that: + - result.rowcount == 1 + + - name: postgresql_ext_version - update the extension to the next version + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + schema: "{{ test_schema }}" + version: 2.0 + + - assert: + that: + - result.changed == true + - result.queries == ["ALTER EXTENSION \"{{ test_ext }}\" UPDATE TO '2.0'"] + + - name: postgresql_ext_version - check, the version must be 2.0 + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}' AND extversion = '2.0'" + + - assert: + that: + - result.rowcount == 1 + + - name: postgresql_ext_version - check that version won't be changed if version won't be passed + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + schema: "{{ test_schema }}" + + - assert: + that: + - result.changed == false + + - name: postgresql_ext_version - check, the version must be 2.0 + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}' AND extversion = '2.0'" + + - assert: + that: + - result.rowcount == 1 + + - name: postgresql_ext_version - update the extension to the latest version + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + schema: "{{ test_schema }}" + version: latest + + - assert: + that: + - result.changed == true + - result.queries == ["ALTER EXTENSION \"{{ test_ext }}\" UPDATE TO '3.0'"] + + - name: postgresql_ext_version - check + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}' AND extversion = '3.0'" + + - assert: + that: + - result.rowcount == 1 + + - name: postgresql_ext_version - try to update the extension to the latest version again + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + schema: "{{ test_schema }}" + version: latest + + - assert: + that: + - result.changed == false + + - name: postgresql_ext_version - try to downgrade the extension version, must fail + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + schema: "{{ test_schema }}" + version: 1.0 + ignore_errors: yes + + - assert: + that: + - result.failed == true + + - name: postgresql_ext_version - drop the extension in check_mode + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + state: absent + check_mode: yes + + - assert: + that: + - result.changed == true + + - name: postgresql_ext_version - check that extension exists + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}' AND extversion = '3.0'" + + - assert: + that: + - result.rowcount == 1 + + - name: postgresql_ext_version - drop the extension in actual mode + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + state: absent + + - assert: + that: + - result.changed == true + + - name: postgresql_ext_version - check that extension doesn't exist after the prev step + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}'" + + - assert: + that: + - result.rowcount == 0 + + - name: postgresql_ext_version - try to drop the non-existent extension again + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + state: absent + + - assert: + that: + - result.changed == false + + - name: postgresql_ext_version - create the extension without passing version + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + + - assert: + that: + - result.changed == true + - result.queries == ["CREATE EXTENSION \"{{ test_ext }}\""] + + - name: postgresql_ext_version - check + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT 1 FROM pg_extension WHERE extname = '{{ test_ext }}' AND extversion = '3.0'" + + - assert: + that: + - result.rowcount == 1 + + - name: postgresql_ext_version - try to install non-existent version + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: non_existent + ignore_errors: yes + + - assert: + that: + - result.failed == true + - result.msg == "Extension non_existent is not installed" + + # Cleanup: + - name: postgresql_ext_version - drop the extension + <<: *task_parameters + postgresql_ext: + <<: *pg_parameters + name: "{{ test_ext }}" + state: absent + + - name: postgresql_ext_version - drop the schema + <<: *task_parameters + postgresql_schema: + <<: *pg_parameters + name: "{{ test_schema }}" + state: absent diff --git a/test/integration/targets/postgresql/tasks/ssl.yml b/test/integration/targets/postgresql/tasks/ssl.yml index d6ec2544e28..3a5e5e4da08 100644 --- a/test/integration/targets/postgresql/tasks/ssl.yml +++ b/test/integration/targets/postgresql/tasks/ssl.yml @@ -28,43 +28,43 @@ package: name=openssl state=present - name: postgresql SSL - create certs 1 - become_user: "{{ pg_user }}" + become_user: root become: yes shell: 'openssl req -new -nodes -text -out ~{{ pg_user }}/root.csr \ -keyout ~{{ pg_user }}/root.key -subj "/CN=localhost.local"' -- name: postgresql SSL - set right permissions to root.key - become_user: "{{ pg_user }}" - become: yes - file: - path: '~{{ pg_user }}/root.key' - mode: 0770 - -- name: postgresql SSL - create certs 3 - become_user: "{{ pg_user }}" +- name: postgresql SSL - create certs 2 + become_user: root become: yes shell: 'openssl x509 -req -in ~{{ pg_user }}/root.csr -text -days 3650 \ -extensions v3_ca -signkey ~{{ pg_user }}/root.key -out ~{{ pg_user }}/root.crt' -- name: postgresql SSL - create certs 4 - become_user: "{{ pg_user }}" +- name: postgresql SSL - create certs 3 + become_user: root become: yes shell: 'openssl req -new -nodes -text -out ~{{ pg_user }}/server.csr \ -keyout ~{{ pg_user }}/server.key -subj "/CN=localhost.local"' -- name: postgresql SSL - set right permissions to server.key - become_user: "{{ pg_user }}" - become: yes - file: - path: '~{{ pg_user }}/server.key' - mode: 0770 - -- name: postgresql SSL - create certs 5 - become_user: "{{ pg_user }}" +- name: postgresql SSL - create certs 4 + become_user: root become: yes shell: 'openssl x509 -req -in ~{{ pg_user }}/server.csr -text -days 365 \ -CA ~{{ pg_user }}/root.crt -CAkey ~{{ pg_user }}/root.key -CAcreateserial -out server.crt' +- name: postgresql SSL - set right permissions to files + become_user: root + become: yes + file: + path: '{{ item }}' + mode: 0600 + owner: '{{ pg_user }}' + group: '{{ pg_user }}' + with_items: + - '~{{ pg_user }}/root.key' + - '~{{ pg_user }}/server.key' + - '~{{ pg_user }}/root.crt' + - '~{{ pg_user }}/server.csr' + - name: postgresql SSL - enable SSL become_user: "{{ pg_user }}" become: yes diff --git a/test/integration/targets/setup_postgresql_db/files/dummy--1.0.sql b/test/integration/targets/setup_postgresql_db/files/dummy--1.0.sql new file mode 100644 index 00000000000..53c79666b47 --- /dev/null +++ b/test/integration/targets/setup_postgresql_db/files/dummy--1.0.sql @@ -0,0 +1,2 @@ +CREATE OR REPLACE FUNCTION dummy_display_ext_version() +RETURNS text LANGUAGE SQL AS 'SELECT (''1.0'')::text'; diff --git a/test/integration/targets/setup_postgresql_db/files/dummy--2.0.sql b/test/integration/targets/setup_postgresql_db/files/dummy--2.0.sql new file mode 100644 index 00000000000..227ba1b4c4d --- /dev/null +++ b/test/integration/targets/setup_postgresql_db/files/dummy--2.0.sql @@ -0,0 +1,2 @@ +CREATE OR REPLACE FUNCTION dummy_display_ext_version() +RETURNS text LANGUAGE SQL AS 'SELECT (''2.0'')::text'; diff --git a/test/integration/targets/setup_postgresql_db/files/dummy--3.0.sql b/test/integration/targets/setup_postgresql_db/files/dummy--3.0.sql new file mode 100644 index 00000000000..7d6a60e543a --- /dev/null +++ b/test/integration/targets/setup_postgresql_db/files/dummy--3.0.sql @@ -0,0 +1,2 @@ +CREATE OR REPLACE FUNCTION dummy_display_ext_version() +RETURNS text LANGUAGE SQL AS 'SELECT (''3.0'')::text'; diff --git a/test/integration/targets/setup_postgresql_db/files/dummy.control b/test/integration/targets/setup_postgresql_db/files/dummy.control new file mode 100644 index 00000000000..4f8553c2271 --- /dev/null +++ b/test/integration/targets/setup_postgresql_db/files/dummy.control @@ -0,0 +1,3 @@ +comment = 'dummy extension used to test postgresql_ext Ansible module' +default_version = '3.0' +relocatable = true diff --git a/test/integration/targets/setup_postgresql_db/tasks/main.yml b/test/integration/targets/setup_postgresql_db/tasks/main.yml index 86a6818c0d9..0fad4c44208 100644 --- a/test/integration/targets/setup_postgresql_db/tasks/main.yml +++ b/test/integration/targets/setup_postgresql_db/tasks/main.yml @@ -145,3 +145,33 @@ - name: restart postgresql service service: name={{ postgresql_service }} state=restarted + +######################## +# Setup dummy extension: +- name: copy control file for dummy ext + copy: + src: dummy.control + dest: "/usr/share/postgresql/{{ pg_ver }}/extension/dummy.control" + mode: 0444 + when: ansible_os_family == 'Debian' + +- name: copy version files for dummy ext + copy: + src: "{{ item }}" + dest: "/usr/share/postgresql/{{ pg_ver }}/extension/{{ item }}" + mode: 0444 + with_items: + - dummy--1.0.sql + - dummy--2.0.sql + - dummy--3.0.sql + when: ansible_os_family == 'Debian' + +- name: add update paths + file: + path: "/usr/share/postgresql/{{ pg_ver }}/extension/{{ item }}" + mode: 0444 + state: touch + with_items: + - dummy--1.0--2.0.sql + - dummy--2.0--3.0.sql + when: ansible_os_family == 'Debian'