diff --git a/lib/ansible/modules/database/postgresql/postgresql_ping.py b/lib/ansible/modules/database/postgresql/postgresql_ping.py new file mode 100644 index 00000000000..f1af0366552 --- /dev/null +++ b/lib/ansible/modules/database/postgresql/postgresql_ping.py @@ -0,0 +1,242 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Andrew Klychkov (@Andersson007) +# 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 = r''' +--- +module: postgresql_ping +short_description: Check remote PostgreSQL server availability +description: +- Simple module to check remote PostgreSQL server availability. +version_added: "2.8" +options: + db: + description: + - Name of database to connect. + type: str + port: + description: + - Database port to connect. + type: int + default: 5432 + login_user: + description: + - User (role) used to authenticate with PostgreSQL. + type: str + default: postgres + login_password: + description: + - Password used to authenticate with PostgreSQL. + type: str + login_host: + description: + - Host running PostgreSQL. + type: str + login_unix_socket: + description: + - Path to a Unix domain socket for local connections. + type: str + 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. + type: str + choices: [ allow, disable, prefer, require, verify-ca, verify-full ] + default: prefer + 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. + type: str +notes: +- The default authentication assumes that you are either logging in as or + sudo'ing to the postgres account on the host. +- This module uses 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 postgresql, libpq-dev, and python-psycopg2 packages + on the remote host before using this module. +requirements: [ psycopg2 ] +author: +- Andrew Klychkov (@Andersson007) +''' + +EXAMPLES = r''' +# PostgreSQL ping dbsrv server from the shell: +# ansible dbsrv -m postgresql_ping + +# In the example below you need to generate sertificates previously. +# See https://www.postgresql.org/docs/current/libpq-ssl.html for more information. +- name: PostgreSQL ping dbsrv server using not default credentials and ssl + postgresql_ping: + db: protected_db + login_host: dbsrv + login_user: secret + login_password: secret_pass + ssl_rootcert: /root/root.crt + ssl_mode: verify-full +''' + +RETURN = r''' +is_available: + description: PostgreSQL server availability. + returned: always + type: bool + sample: true +server_version: + description: PostgreSQL server version. + returned: always + type: dict + sample: { major: 10, minor: 1 } +''' + + +try: + import psycopg2 + HAS_PSYCOPG2 = True +except ImportError: + HAS_PSYCOPG2 = False + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.database import SQLParseError +from ansible.module_utils.postgres import postgres_common_argument_spec +from ansible.module_utils._text import to_native +from ansible.module_utils.six import iteritems + + +# =========================================== +# PostgreSQL module specific support methods. +# + + +class PgPing(object): + def __init__(self, module, cursor): + self.module = module + self.cursor = cursor + self.is_available = False + self.version = {} + + def do(self): + self.get_pg_version() + return (self.is_available, self.version) + + def get_pg_version(self): + query = "SELECT version()" + raw = self.__exec_sql(query)[0][0] + if raw: + self.is_available = True + raw = raw.split()[1].split('.') + self.version = dict( + major=int(raw[0]), + minor=int(raw[1]), + ) + + def __exec_sql(self, query): + try: + self.cursor.execute(query) + res = self.cursor.fetchall() + if res: + return res + except SQLParseError as e: + self.module.fail_json(msg=to_native(e)) + self.cursor.close() + except Exception as e: + self.module.warn("PostgreSQL server is unavailable: %s" % to_native(e)) + + return False + +# =========================================== +# Module execution. +# + + +def main(): + argument_spec = postgres_common_argument_spec() + argument_spec.update( + db=dict(type='str'), + ssl_mode=dict(type='str', default='prefer', choices=['allow', 'disable', 'prefer', 'require', 'verify-ca', 'verify-full']), + ssl_rootcert=dict(type='str'), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + if not HAS_PSYCOPG2: + module.fail_json(msg="The python psycopg2 module is required") + + sslrootcert = module.params["ssl_rootcert"] + + # To use defaults values, keyword arguments must be absent, so + # check which values are empty and don't include in the **kw + # dictionary + params_map = { + "login_host": "host", + "login_user": "user", + "login_password": "password", + "port": "port", + "db": "database", + "ssl_mode": "sslmode", + "ssl_rootcert": "sslrootcert" + } + kw = dict((params_map[k], v) for (k, v) in iteritems(module.params) + if k in params_map and v != "" and v is not None) + + # If a login_unix_socket is specified, incorporate it here. + is_localhost = "host" not in kw or kw["host"] is None or kw["host"] == "localhost" + if is_localhost and module.params["login_unix_socket"] != "": + kw["host"] = module.params["login_unix_socket"] + + if psycopg2.__version__ < '2.4.3' and sslrootcert is not None: + module.fail_json(msg='psycopg2 must be at least 2.4.3 in order ' + 'to user the ssl_rootcert parameter') + + # Set some default values: + cursor = False + db_connection = False + result = dict( + changed=False, + is_available=False, + server_version=dict(), + ) + + try: + db_connection = psycopg2.connect(**kw) + cursor = db_connection.cursor(cursor_factory=psycopg2.extras.DictCursor) + except TypeError as e: + if 'sslrootcert' in e.args[0]: + module.fail_json(msg='Postgresql server must be at least ' + 'version 8.4 to support sslrootcert') + module.fail_json(msg="unable to connect to database: %s" % to_native(e)) + except Exception as e: + module.warn("PostgreSQL server is unavailable: %s" % to_native(e)) + + # Do job: + pg_ping = PgPing(module, cursor) + if cursor: + # If connection established: + result["is_available"], result["server_version"] = pg_ping.do() + db_connection.rollback() + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/postgresql/defaults/main.yml b/test/integration/targets/postgresql/defaults/main.yml index af5a5fbe4da..339a76e72d3 100644 --- a/test/integration/targets/postgresql/defaults/main.yml +++ b/test/integration/targets/postgresql/defaults/main.yml @@ -4,7 +4,8 @@ db_name: 'ansible_db' db_user1: 'ansible_db_user1' db_user2: 'ansible_db_user2' db_user3: 'ansible_db_user3' +db_default: 'postgres' tmp_dir: '/tmp' db_session_role1: 'session_role1' -db_session_role2: 'session_role2' \ No newline at end of file +db_session_role2: 'session_role2' diff --git a/test/integration/targets/postgresql/tasks/main.yml b/test/integration/targets/postgresql/tasks/main.yml index 3f35d44ee82..e8036335688 100644 --- a/test/integration/targets/postgresql/tasks/main.yml +++ b/test/integration/targets/postgresql/tasks/main.yml @@ -771,6 +771,9 @@ # Test postgresql_query module - include: postgresql_query.yml +# Verify postgresql_ping module +- include: postgresql_ping.yml db_name_nonexist=fake_db + # 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_ping.yml b/test/integration/targets/postgresql/tasks/postgresql_ping.yml new file mode 100644 index 00000000000..eb9e75aa836 --- /dev/null +++ b/test/integration/targets/postgresql/tasks/postgresql_ping.yml @@ -0,0 +1,35 @@ +# Test code for the postgresql_ping module +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: postgresql_ping - test return values + become_user: "{{ pg_user }}" + become: yes + postgresql_ping: + db: "{{ db_default }}" + login_user: "{{ pg_user }}" + register: result + ignore_errors: yes + +- assert: + that: + - result.is_available == true + - result.server_version != {} + - result.server_version.major != false + - result.server_version.minor != false + - result.changed == false + +- name: postgresql_ping - check ping of non-existing database doesn't return anything + become_user: "{{ pg_user }}" + become: yes + postgresql_ping: + db: "{{ db_name_nonexist }}" + login_user: "{{ pg_user }}" + register: result + ignore_errors: yes + +- assert: + that: + - result.is_available == false + - result.server_version == {} + - result.changed == false