diff --git a/lib/ansible/modules/database/postgresql/postgresql_ext.py b/lib/ansible/modules/database/postgresql/postgresql_ext.py index f5b37560f31..48bca26b61c 100644 --- a/lib/ansible/modules/database/postgresql/postgresql_ext.py +++ b/lib/ansible/modules/database/postgresql/postgresql_ext.py @@ -1,130 +1,174 @@ #!/usr/bin/python # -*- coding: utf-8 -*- + # Copyright: Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type - ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} - -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: postgresql_ext -short_description: Add or remove PostgreSQL extensions from a database. +short_description: Add or remove PostgreSQL extensions from a database description: - - Add or remove PostgreSQL extensions from a database. -version_added: "1.9" +- Add or remove PostgreSQL extensions from a database. +version_added: '1.9' options: name: description: - - name of the extension to add or remove + - Name of the extension to add or remove. required: true + type: str db: description: - - name of the database to add or remove the extension to/from + - Name of the database to add or remove the extension to/from. required: true + type: str + aliases: + - login_db schema: description: - - name of the schema to add the extension to - version_added: "2.8" + - Name of the schema to add the extension to. + version_added: '2.8' + type: str login_user: description: - - The username used to authenticate with + - The username used to authenticate with. + type: str login_password: description: - - The password used to authenticate with + - The password used to authenticate with. + type: str login_host: description: - - Host running the database + - Host running the database. + type: str default: localhost login_unix_socket: description: - - Path to a Unix domain socket for local connections. + - Path to a Unix domain socket for local connections. + type: str version_added: '2.8' ssl_mode: description: - - Determines whether or with what priority a secure SSL TCP/IP connection - will be negotiated with the server. - - See U(https://www.postgresql.org/docs/current/static/libpq-ssl.html) for - more information on the modes. - - Default of C(prefer) matches libpq default. + - Determines whether or with what priority a secure SSL TCP/IP connection + will be negotiated with the server. + - See U(https://www.postgresql.org/docs/current/static/libpq-ssl.html) for + more information on the modes. + - Default of C(prefer) matches libpq default. default: prefer - choices: ["disable", "allow", "prefer", "require", "verify-ca", "verify-full"] + choices: [allow, disable, prefer, require, verify-ca, verify-full] + type: str version_added: '2.8' ssl_rootcert: description: - - Specifies the name of a file containing SSL certificate authority (CA) - certificate(s). If the file exists, the server's certificate will be - verified to be signed by one of these authorities. + - Specifies the name of a file containing SSL certificate authority (CA) + certificate(s). If the file exists, the server's certificate will be + verified to be signed by one of these authorities. + type: path version_added: '2.8' port: description: - - Database port to connect to. + - Database port to connect to. default: 5432 + type: int session_role: - version_added: "2.8" - description: | - Switch to session_role after connecting. The specified session_role must be a role that the current login_user is a member of. - Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally. + description: + - Switch to session_role after connecting. + - The specified session_role must be a role that the current login_user is a member of. + - Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally. + type: str + version_added: '2.8' state: description: - - The database extension state + - The database extension state. default: present - choices: [ "present", "absent" ] + choices: [ absent, present ] + type: str cascade: description: - - Automatically install/remove any extensions that this extension depends on - that are not already installed/removed (supported since PostgreSQL 9.6). + - Automatically install/remove any extensions that this extension depends on + that are not already installed/removed (supported since PostgreSQL 9.6). type: bool default: no version_added: '2.8' notes: - - The default authentication assumes that you are either logging in as or sudo'ing to the C(postgres) account on the host. - - This module uses I(psycopg2), a Python PostgreSQL database adapter. You must ensure that psycopg2 is installed on - the host before using this module. If the remote host is the PostgreSQL server (which is the default case), then PostgreSQL must also be installed - on the remote host. For Ubuntu-based systems, install the C(postgresql), C(libpq-dev), and C(python-psycopg2) packages on the remote host before using - this module. +- The default authentication assumes that you are either logging in as + or sudo'ing to the C(postgres) account on the host. +- This module uses I(psycopg2), a Python PostgreSQL database adapter. +- You must ensure that psycopg2 is installed on the host before using this module. +- If the remote host is the PostgreSQL server (which is the default case), + then PostgreSQL must also be installed on the remote host. +- For Ubuntu-based systems, install the C(postgresql), C(libpq-dev), + and C(python-psycopg2) packages on the remote host before using this module. requirements: [ psycopg2 ] author: - - "Daniel Schep (@dschep)" - - "Thomas O'Donnell (@andytom)" +- Daniel Schep (@dschep) +- Thomas O'Donnell (@andytom) ''' -EXAMPLES = ''' -# Adds postgis to the database "acme" -- postgresql_ext: +EXAMPLES = r''' +- name: Adds postgis extension to the database acme in the schema foo + postgresql_ext: + name: postgis + db: acme + schema: foo + +- name: Removes postgis extension to the database acme + postgresql_ext: name: postgis db: acme - schema: extensions + state: absent -# Adds earthdistance to the database "template1" -- postgresql_ext: +- name: Adds earthdistance extension to the database template1 cascade + postgresql_ext: name: earthdistance db: template1 cascade: true + +# In the example below, if earthdistance extension is installed, +# it will be removed too because it depends on cube: +- name: Removes cube extension from the database acme cascade + postgresql_ext: + name: cube + db: acme + cascade: yes + state: absent +''' + +RETURN = r''' +query: + description: List of executed queries. + returned: always + type: list + sample: ["DROP EXTENSION \"acme\""] + ''' + import traceback PSYCOPG2_IMP_ERR = None try: import psycopg2 import psycopg2.extras + HAS_PSYCOPG2 = True except ImportError: PSYCOPG2_IMP_ERR = traceback.format_exc() - postgresqldb_found = False -else: - postgresqldb_found = True + HAS_PSYCOPG2 = False from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.postgres import postgres_common_argument_spec from ansible.module_utils.six import iteritems from ansible.module_utils._text import to_native from ansible.module_utils.database import pg_quote_identifier +executed_queries = [] + class NotSupportedError(Exception): pass @@ -146,6 +190,7 @@ def ext_delete(cursor, ext, cascade): if cascade: query += " CASCADE" cursor.execute(query) + executed_queries.append(query) return True else: return False @@ -159,6 +204,7 @@ def ext_create(cursor, ext, schema, cascade): if cascade: query += " CASCADE" cursor.execute(query) + executed_queries.append(query) return True else: return False @@ -169,27 +215,26 @@ def ext_create(cursor, ext, schema, cascade): def main(): + argument_spec = postgres_common_argument_spec() + argument_spec.update( + db=dict(type="str", required=True, aliases=["login_db"]), + port=dict(type="int", default=5432, aliases=["login_port"]), + ext=dict(type="str", required=True, aliases=['name']), + schema=dict(type="str"), + state=dict(type="str", default="present", choices=["absent", "present"]), + cascade=dict(type='bool', default=False), + ssl_mode=dict(type='str', default='prefer', choices=[ + 'disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full']), + ssl_rootcert=dict(type="path", default=None), + session_role=dict(type="str"), + ) + module = AnsibleModule( - argument_spec=dict( - login_user=dict(default="postgres"), - login_password=dict(default="", no_log=True), - login_host=dict(default=""), - login_unix_socket=dict(default=""), - port=dict(default="5432"), - db=dict(required=True), - ext=dict(required=True, aliases=['name']), - schema=dict(default=""), - state=dict(default="present", choices=["absent", "present"]), - cascade=dict(type='bool', default=False), - ssl_mode=dict(default='prefer', choices=[ - 'disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full']), - ssl_rootcert=dict(default=None), - session_role=dict(), - ), - supports_check_mode=True + argument_spec=argument_spec, + supports_check_mode=True, ) - if not postgresqldb_found: + if not HAS_PSYCOPG2: module.fail_json(msg=missing_required_lib('psycopg2'), exception=PSYCOPG2_IMP_ERR) db = module.params["db"] @@ -233,8 +278,7 @@ def main(): db_connection.set_isolation_level(psycopg2 .extensions .ISOLATION_LEVEL_AUTOCOMMIT) - cursor = db_connection.cursor( - cursor_factory=psycopg2.extras.DictCursor) + cursor = db_connection.cursor(cursor_factory=psycopg2.extras.DictCursor) except TypeError as e: if 'sslrootcert' in e.args[0]: @@ -268,7 +312,7 @@ def main(): except Exception as e: module.fail_json(msg="Database query failed: %s" % to_native(e), exception=traceback.format_exc()) - module.exit_json(changed=changed, db=db, ext=ext) + module.exit_json(changed=changed, db=db, ext=ext, queries=executed_queries) if __name__ == '__main__': diff --git a/test/integration/targets/postgresql/tasks/main.yml b/test/integration/targets/postgresql/tasks/main.yml index 8665d305b01..8704ca73259 100644 --- a/test/integration/targets/postgresql/tasks/main.yml +++ b/test/integration/targets/postgresql/tasks/main.yml @@ -784,13 +784,23 @@ # Test postgresql_privs - include: postgresql_privs.yml -# Test postgresql_facts module: +# Test postgresql_facts module - include: postgresql_facts.yml # Test default_privs with target_role - include: test_target_role.yml when: postgres_version_resp.stdout is version('9.1', '>=') +# Test postgresql_ext. +# pg_extension system view is available from PG 9.1. +# The tests are restricted by Fedora because there will be errors related with +# attempts to change the environment during postgis installation or +# missing postgis package in repositories. +# Anyway, these tests completely depend on Postgres version, +# not specific distributions. +- include: postgresql_ext.yml + when: postgres_version_resp.stdout is version('9.1', '>=') and ansible_distribution == 'Fedora' + # dump/restore tests per format # ============================================================ - include: state_dump_restore.yml test_fixture=user file=dbdata.sql diff --git a/test/integration/targets/postgresql/tasks/postgresql_ext.yml b/test/integration/targets/postgresql/tasks/postgresql_ext.yml new file mode 100644 index 00000000000..e24c8a5471b --- /dev/null +++ b/test/integration/targets/postgresql/tasks/postgresql_ext.yml @@ -0,0 +1,227 @@ +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Create test schema: +- name: postgresql_ext - install postgis + package: name=postgis state=present + when: ansible_os_family != "Windows" + +- name: postgresql_ext - install postgis RedHat + win_package: name=postgis state=present + when: ansible_os_family == "Windows" + +- name: postgresql_ext - create schema schema1 + become_user: "{{ pg_user }}" + become: yes + postgresql_schema: + database: postgres + name: schema1 + state: present + +- name: postgresql_ext - drop extension if exists + become_user: "{{ pg_user }}" + become: yes + postgresql_query: + db: postgres + query: "DROP EXTENSION IF EXISTS postgis" + ignore_errors: yes + +############## +# Start tests: + +# Create extension in check_mode, also check aliases for db and port params: +- name: postgresql_ext - create extension postgis in check_mode + become_user: "{{ pg_user }}" + become: yes + postgresql_ext: + login_db: postgres + login_port: 5432 + name: postgis + check_mode: yes + ignore_errors: yes + register: result + +- assert: + that: + - result.changed == true + - result.queries == [] + +# Check that extension doesn't exist after the previous step, rowcount must be 0 +- name: postgresql_ext - check that extension doesn't exist after the previous step + become_user: "{{ pg_user }}" + become: yes + postgresql_query: + db: postgres + query: "SELECT extname FROM pg_extension WHERE extname='postgis'" + ignore_errors: yes + register: result + +- assert: + that: + - result.rowcount == 0 + +# Create extension postgis, also check aliases for db and port params +- name: postgresql_ext - create extension postgis + become_user: "{{ pg_user }}" + become: yes + postgresql_ext: + login_db: postgres + login_port: 5432 + name: postgis + ignore_errors: yes + register: result + +- assert: + that: + - result.changed == true + - result.queries == ['CREATE EXTENSION "postgis"'] + +# Check that extension exists after the previous step, rowcount must be 1 +- name: postgresql_ext - check that extension exists after the previous step + become_user: "{{ pg_user }}" + become: yes + postgresql_query: + db: postgres + query: "SELECT extname FROM pg_extension WHERE extname='postgis'" + ignore_errors: yes + register: result + +- assert: + that: + - result.rowcount == 1 + +# Drop extension postgis: +- name: postgresql_ext - drop extension postgis + become_user: "{{ pg_user }}" + become: yes + postgresql_ext: + db: postgres + name: postgis + state: absent + ignore_errors: yes + register: result + +- assert: + that: + - result.changed == true + - result.queries == ['DROP EXTENSION "postgis"'] + +# Check that extension doesn't exist after the previous step, rowcount must be 0 +- name: postgresql_ext - check that extension doesn't exist after the previous step + become_user: "{{ pg_user }}" + become: yes + postgresql_query: + db: postgres + query: "SELECT extname FROM pg_extension WHERE extname='postgis'" + ignore_errors: yes + register: result + +- assert: + that: + - result.rowcount == 0 + +# Create extension postgis in particular schema +- name: postgresql_ext - create extension postgis + become_user: "{{ pg_user }}" + become: yes + postgresql_ext: + db: postgres + name: postgis + schema: schema1 + ignore_errors: yes + register: result + +- assert: + that: + - result.changed == true + - result.queries == ['CREATE EXTENSION "postgis" WITH SCHEMA "schema1"'] + +# Check that extension exists after the previous step, rowcount must be 1 +- name: postgresql_ext - check that extension exists after the previous step + become_user: "{{ pg_user }}" + become: yes + postgresql_query: + db: postgres + query: | + SELECT extname FROM pg_extension AS e LEFT JOIN pg_catalog.pg_namespace AS n + ON n.oid = e.extnamespace WHERE e.extname='postgis' AND n.nspname='schema1' + ignore_errors: yes + register: result + +- assert: + that: + - result.rowcount == 1 + +# +# Check cascade option. For creation it's available from PG 9.6. +# I couldn't check it for two or more extension in one time +# because most of the common extensions are available in postgresql-contrib package +# that tries to change the default python interpreter and fails during tests respectively. +# Anyway, that's enough to be sure that the proper SQL was exequted. +# + +# Drop extension cascade +- name: postgresql_ext - drop extension postgis cascade + become_user: "{{ pg_user }}" + become: yes + postgresql_ext: + db: postgres + name: postgis + state: absent + cascade: yes + ignore_errors: yes + register: result + +- assert: + that: + - result.changed == true + - result.queries == ['DROP EXTENSION "postgis" CASCADE'] + +# Check that extension doesn't exist after the previous step, rowcount must be 0 +- name: postgresql_ext - check that extension doesn't exist after the previous step + become_user: "{{ pg_user }}" + become: yes + postgresql_query: + db: postgres + query: "SELECT extname FROM pg_extension WHERE extname='postgis'" + ignore_errors: yes + register: result + +- assert: + that: + - result.rowcount == 0 + +# Create extension postgis cascade. +# CASCADE for CREATE command is available from PG 9.6 +- name: postgresql_ext - create extension postgis cascade + become_user: "{{ pg_user }}" + become: yes + postgresql_ext: + db: postgres + name: postgis + cascade: yes + ignore_errors: yes + register: result + when: postgres_version_resp.stdout is version('9.6', '<=') + +- assert: + that: + - result.changed == true + - result.queries == ['CREATE EXTENSION "postgis" CASCADE"'] + when: postgres_version_resp.stdout is version('9.6', '<=') + +# Check that extension exists after the previous step, rowcount must be 1 +- name: postgresql_ext - check that extension exists after the previous step + become_user: "{{ pg_user }}" + become: yes + postgresql_query: + db: postgres + query: "SELECT extname FROM pg_extension WHERE extname='postgis'" + ignore_errors: yes + register: result + when: postgres_version_resp.stdout is version('9.6', '<=') + +- assert: + that: + - result.rowcount == 1 + when: postgres_version_resp.stdout is version('9.6', '<=')