diff --git a/lib/ansible/modules/database/postgresql/postgresql_facts.py b/lib/ansible/modules/database/postgresql/postgresql_facts.py new file mode 100644 index 00000000000..d4affbb753e --- /dev/null +++ b/lib/ansible/modules/database/postgresql/postgresql_facts.py @@ -0,0 +1,1016 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, 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_facts +short_description: Gather facts about PostgreSQL servers +description: +- Gathers facts about PostgreSQL servers. +version_added: "2.8" +options: + filter: + description: + - Limit collected facts 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). + - 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 facts. + - If you pass including and excluding values to the filter, for example, I(filter=!settings,ver), + the excluding values will be ignored. + type: list + db: + description: + - Name of database to connect. + type: str + aliases: + - login_db + port: + description: + - Database port to connect. + type: int + default: 5432 + aliases: + - login_port + session_role: + 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 + 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. +- login_user or session_role must be able to read from pg_authid. +- To avoid "Peer authentication failed for user postgres" error, + use postgres user as a I(become_user). +- 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''' +# Display facts from postgres hosts. +# ansible postgres -m postgresql_facts + +# Display only databases and roles facts from all hosts using shell-style wildcards: +# ansible all -m postgresql_facts -a 'filter=dat*,rol*' + +# Display only replications and repl_slots facts from standby hosts using shell-style wildcards: +# ansible standby -m postgresql_facts -a 'filter=repl*' + +# Display all facts from databases hosts except settings: +# ansible databases -m postgresql_facts -a 'filter=!settings' + +- name: Collect PostgreSQL version and extensions + become: yes + become_user: postgres + postgresql_facts: + filter: ver*,ext* + +- name: Collect all facts except settings and roles + become: yes + become_user: postgres + postgresql_facts: + filter: "!settings,!roles" + +# On FreeBSD with PostgreSQL 9.5 version and lower use pgsql user to become +# and pass "postgres" as a database to connect to +- name: Collect tablespaces and repl_slots facts + become: yes + become_user: pgsql + postgresql_facts: + db: postgres + filter: + - tablesp* + - repl_sl* + +- name: Collect all facts except databases + become: yes + become_user: postgres + postgresql_facts: + filter: + - "!databases" +''' + +RETURN = r''' +version: + description: Database server version U(https://www.postgresql.org/support/versioning/). + returned: always + type: dict + sample: { "version": { "major": 10, "minor": 6 } } + contains: + major: + description: Major server version. + returned: always + type: int + sample: 11 + minor: + description: Minor server version. + returned: always + type: int + sample: 1 +databases: + description: Information about databases. + returned: always + type: dict + sample: + - { "postgres": { "access_priv": "", "collate": "en_US.UTF-8", + "ctype": "en_US.UTF-8", "encoding": "UTF8", "owner": "postgres", "size": "7997 kB" } } + contains: + database_name: + description: Database name. + returned: always + type: dict + sample: template1 + contains: + access_priv: + description: Database access privileges. + returned: always + type: str + sample: "=c/postgres_npostgres=CTc/postgres" + collate: + description: + - Database collation U(https://www.postgresql.org/docs/current/collation.html). + returned: always + type: str + sample: en_US.UTF-8 + ctype: + description: + - Database LC_CTYPE U(https://www.postgresql.org/docs/current/multibyte.html). + returned: always + type: str + sample: en_US.UTF-8 + encoding: + description: + - Database encoding U(https://www.postgresql.org/docs/current/multibyte.html). + returned: always + type: str + sample: UTF8 + owner: + description: + - Database owner U(https://www.postgresql.org/docs/current/sql-createdatabase.html). + returned: always + type: str + sample: postgres + size: + description: Database size in bytes. + returned: always + type: str + sample: 8189415 + extensions: + description: + - Extensions U(https://www.postgresql.org/docs/current/sql-createextension.html). + returned: always + type: dict + sample: + - { "plpgsql": { "description": "PL/pgSQL procedural language", + "extversion": { "major": 1, "minor": 0 } } } + contains: + extdescription: + description: Extension description. + returned: if existent + type: str + sample: PL/pgSQL procedural language + extversion: + description: Extension description. + returned: always + type: dict + contains: + major: + description: Extension major version. + returned: always + type: int + sample: 1 + minor: + description: Extension minor version. + returned: always + type: int + sample: 0 + nspname: + description: Namespace where the extension is. + returned: always + type: str + sample: pg_catalog + languages: + description: Procedural languages U(https://www.postgresql.org/docs/current/xplang.html). + returned: always + type: dict + sample: { "sql": { "lanacl": "", "lanowner": "postgres" } } + contains: + lanacl: + description: + - Language access privileges + U(https://www.postgresql.org/docs/current/catalog-pg-language.html). + returned: always + type: str + sample: "{postgres=UC/postgres,=U/postgres}" + lanowner: + description: + - Language owner U(https://www.postgresql.org/docs/current/catalog-pg-language.html). + returned: always + type: str + sample: postgres + namespaces: + description: + - Namespaces (schema) U(https://www.postgresql.org/docs/current/sql-createschema.html). + returned: always + type: dict + sample: { "pg_catalog": { "nspacl": "{postgres=UC/postgres,=U/postgres}", "nspowner": "postgres" } } + contains: + nspacl: + description: + - Access privileges U(https://www.postgresql.org/docs/current/catalog-pg-namespace.html). + returned: always + type: str + sample: "{postgres=UC/postgres,=U/postgres}" + nspowner: + description: + - Schema owner U(https://www.postgresql.org/docs/current/catalog-pg-namespace.html). + returned: always + type: str + sample: postgres +repl_slots: + description: + - Replication slots (available in 9.4 and later) + U(https://www.postgresql.org/docs/current/catalog-pg-replication-slots.html). + returned: if existent + type: dict + sample: { "slot0": { "active": false, "database": null, "plugin": null, "slot_type": "physical" } } + contains: + active: + description: + - True means that a receiver has connected to it, and it is currently reserving archives. + returned: always + type: bool + sample: true + database: + description: Database name this slot is associated with, or null. + returned: always + type: str + sample: acme + plugin: + description: + - Base name of the shared object containing the output plugin + this logical slot is using, or null for physical slots. + returned: always + type: str + sample: pgoutput + slot_type: + description: The slot type - physical or logical. + returned: always + type: str + sample: logical +replications: + description: + - Information about the current replications by process PIDs + U(https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-STATS-VIEWS-TABLE). + returned: if pg_stat_replication view existent + type: dict + sample: + - { 76580: { "app_name": "standby1", "backend_start": "2019-02-03 00:14:33.908593+03", + "client_addr": "10.10.10.2", "client_hostname": "", "state": "streaming", "usename": "postgres" } } + contains: + usename: + description: + - Name of the user logged into this WAL sender process ('usename' is a column name in pg_stat_replication view). + returned: always + type: str + sample: replication_user + app_name: + description: Name of the application that is connected to this WAL sender. + returned: if existent + type: str + sample: acme_srv + client_addr: + description: + - IP address of the client connected to this WAL sender. + - If this field is null, it indicates that the client is connected + via a Unix socket on the server machine. + returned: always + type: str + sample: 10.0.0.101 + client_hostname: + description: + - Host name of the connected client, as reported by a reverse DNS lookup of client_addr. + - This field will only be non-null for IP connections, and only when log_hostname is enabled. + returned: always + type: str + sample: dbsrv1 + backend_start: + description: Time when this process was started, i.e., when the client connected to this WAL sender. + returned: always + type: str + sample: "2019-02-03 00:14:33.908593+03" + state: + description: Current WAL sender state. + returned: always + type: str + sample: streaming +tablespaces: + description: + - Information about tablespaces U(https://www.postgresql.org/docs/current/catalog-pg-tablespace.html). + returned: always + type: dict + sample: + - { "test": { "spcacl": "{postgres=C/postgres,andreyk=C/postgres}", "spcoptions": [ "seq_page_cost=1" ], + "spcowner": "postgres" } } + contains: + spcacl: + description: Tablespace access privileges. + returned: always + type: str + sample: "{postgres=C/postgres,andreyk=C/postgres}" + spcoptions: + description: Tablespace-level options. + returned: always + type: list + sample: [ "seq_page_cost=1" ] + spcowner: + description: Owner of the tablespace. + returned: always + type: str + sample: test_user +roles: + description: + - Information about roles U(https://www.postgresql.org/docs/current/user-manag.html). + returned: always + type: dict + sample: + - { "test_role": { "canlogin": true, "member_of": [ "user_ro" ], "superuser": false, + "valid_until": "9999-12-31T23:59:59.999999+00:00" } } + contains: + canlogin: + description: Login privilege U(https://www.postgresql.org/docs/current/role-attributes.html). + returned: always + type: bool + sample: true + member_of: + description: + - Role membership U(https://www.postgresql.org/docs/current/role-membership.html). + returned: always + type: list + sample: [ "read_only_users" ] + superuser: + description: User is a superuser or not. + returned: always + type: bool + sample: false + valid_until: + description: + - Password expiration date U(https://www.postgresql.org/docs/current/sql-alterrole.html). + returned: always + type: str + sample: "9999-12-31T23:59:59.999999+00:00" +pending_restart_settings: + description: + - List of settings that are pending restart to be set. + returned: always + type: list + sample: [ "shared_buffers" ] +settings: + description: + - Information about run-time server parameters + U(https://www.postgresql.org/docs/current/view-pg-settings.html). + returned: always + type: dict + sample: + - { "work_mem": { "boot_val": "4096", "context": "user", "max_val": "2147483647", + "min_val": "64", "setting": "8192", "sourcefile": "/var/lib/pgsql/10/data/postgresql.auto.conf", + "unit": "kB", "vartype": "integer", "val_in_bytes": 4194304 } } + contains: + setting: + description: Current value of the parameter. + returned: always + type: str + sample: 49152 + unit: + description: Implicit unit of the parameter. + returned: always + type: str + sample: kB + boot_val: + description: + - Parameter value assumed at server startup if the parameter is not otherwise set. + returned: always + type: str + sample: 4096 + min_val: + description: + - Minimum allowed value of the parameter (null for non-numeric values). + returned: always + type: str + sample: 64 + max_val: + description: + - Maximum allowed value of the parameter (null for non-numeric values). + returned: always + type: str + sample: 2147483647 + sourcefile: + description: + - Configuration file the current value was set in. + - Null for values set from sources other than configuration files, + or when examined by a user who is neither a superuser or a member of pg_read_all_settings. + - Helpful when using include directives in configuration files. + returned: always + type: str + sample: /var/lib/pgsql/10/data/postgresql.auto.conf + context: + description: + - Context required to set the parameter's value. + - For more information see U(https://www.postgresql.org/docs/current/view-pg-settings.html). + returned: always + type: str + sample: user + vartype: + description: + - Parameter type (bool, enum, integer, real, or string). + returned: always + type: str + sample: integer + val_in_bytes: + description: + - Current value of the parameter in bytes. + returned: if supported + type: int + sample: 2147483647 + pretty_val: + description: + - Value presented in the pretty form. + returned: always + type: str + sample: 2MB + pending_restart: + description: + - True if the value has been changed in the configuration file but needs a restart; or false otherwise. + - Returns only if C(settings) is passed. + returned: always + type: bool + sample: false +''' + +from fnmatch import fnmatch + +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 PgDbConn(object): + def __init__(self, module, params_dict, session_role): + self.params_dict = params_dict + self.module = module + self.db_conn = None + self.session_role = session_role + self.cursor = None + + def connect(self): + try: + self.db_conn = psycopg2.connect(**self.params_dict) + self.cursor = self.db_conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + + # Switch role, if specified: + if self.session_role: + try: + self.cursor.execute('SET ROLE %s' % self.session_role) + except Exception as e: + self.module.fail_json(msg="Could not switch role: %s" % to_native(e)) + + return self.cursor + + except TypeError as e: + if 'sslrootcert' in e.args[0]: + self.module.fail_json(msg='PostgreSQL server must be at least version 8.4 ' + 'to support sslrootcert') + self.module.fail_json(msg="Unable to connect to database: %s" % to_native(e)) + + except Exception as e: + self.module.fail_json(msg="Unable to connect to database: %s" % to_native(e)) + + def reconnect(self, dbname): + self.db_conn.close() + + self.params_dict['database'] = dbname + return self.connect() + + +class PgClusterFacts(object): + def __init__(self, module, db_conn_obj): + self.module = module + self.db_obj = db_conn_obj + self.cursor = db_conn_obj.connect() + self.pg_facts = { + "version": {}, + "tablespaces": {}, + "databases": {}, + "replications": {}, + "repl_slots": {}, + "settings": {}, + "roles": {}, + "pending_restart_settings": [], + } + + def collect(self, val_list=False): + subset_map = { + "version": self.get_pg_version, + "tablespaces": self.get_tablespaces, + "databases": self.get_db_info, + "replications": self.get_repl_info, + "repl_slots": self.get_rslot_info, + "settings": self.get_settings, + "roles": self.get_role_info, + } + + incl_list = [] + excl_list = [] + # Notice: incl_list and excl_list + # don't make sense together, therefore, + # if incl_list is not empty, we collect + # only values from it: + if val_list: + for i in val_list: + if i[0] != '!': + incl_list.append(i) + else: + excl_list.append(i.lstrip('!')) + + if incl_list: + for s in subset_map: + for i in incl_list: + if fnmatch(s, i): + subset_map[s]() + break + elif excl_list: + found = False + # Collect info: + for s in subset_map: + for e in excl_list: + if fnmatch(s, e): + found = True + + if not found: + subset_map[s]() + else: + found = False + + # Default behaviour, if include or exclude is not passed: + else: + # Just collect info for each item: + for s in subset_map: + subset_map[s]() + + return self.pg_facts + + def get_tablespaces(self): + """ + Get information about tablespaces. + """ + # Check spcoption exists: + opt = self.__exec_sql("SELECT column_name " + "FROM information_schema.columns " + "WHERE table_name = 'pg_tablespace' " + "AND column_name = 'spcoptions'") + + if not opt: + query = ("SELECT s.spcname, a.rolname, s.spcacl " + "FROM pg_tablespace AS s " + "JOIN pg_authid AS a ON s.spcowner = a.oid") + else: + query = ("SELECT s.spcname, a.rolname, s.spcacl, s.spcoptions " + "FROM pg_tablespace AS s " + "JOIN pg_authid AS a ON s.spcowner = a.oid") + + res = self.__exec_sql(query) + ts_dict = {} + for i in res: + ts_name = i[0] + ts_info = dict( + spcowner=i[1], + spcacl=i[2] if i[2] else '', + ) + if opt: + ts_info['spcoptions'] = i[3] if i[3] else [] + + ts_dict[ts_name] = ts_info + + self.pg_facts["tablespaces"] = ts_dict + + def get_ext_info(self): + """ + Get information about existing extensions. + """ + # Check that pg_extension exists: + res = self.__exec_sql("SELECT EXISTS (SELECT 1 FROM " + "information_schema.tables " + "WHERE table_name = 'pg_extension')") + if not res[0][0]: + return True + + query = ("SELECT e.extname, e.extversion, n.nspname, c.description " + "FROM pg_catalog.pg_extension AS e " + "LEFT JOIN pg_catalog.pg_namespace AS n " + "ON n.oid = e.extnamespace " + "LEFT JOIN pg_catalog.pg_description AS c " + "ON c.objoid = e.oid " + "AND c.classoid = 'pg_catalog.pg_extension'::pg_catalog.regclass") + res = self.__exec_sql(query) + ext_dict = {} + for i in res: + ext_ver = i[1].split('.') + + ext_dict[i[0]] = dict( + extversion=dict( + major=int(ext_ver[0]), + minor=int(ext_ver[1]), + ), + nspname=i[2], + description=i[3], + ) + + return ext_dict + + def get_role_info(self): + """ + Get information about roles (in PgSQL groups and users are roles). + """ + query = ("SELECT r.rolname, r.rolsuper, r.rolcanlogin, " + "r.rolvaliduntil, " + "ARRAY(SELECT b.rolname " + "FROM pg_catalog.pg_auth_members AS m " + "JOIN pg_catalog.pg_roles AS b ON (m.roleid = b.oid) " + "WHERE m.member = r.oid) AS memberof " + "FROM pg_catalog.pg_roles AS r " + "WHERE r.rolname !~ '^pg_'") + + res = self.__exec_sql(query) + rol_dict = {} + for i in res: + rol_dict[i[0]] = dict( + superuser=i[1], + canlogin=i[2], + valid_until=i[3] if i[3] else '', + member_of=i[4] if i[4] else [], + ) + + self.pg_facts["roles"] = rol_dict + + def get_rslot_info(self): + """ + Get information about replication slots if exist. + """ + # Check that pg_replication_slots exists: + res = self.__exec_sql("SELECT EXISTS (SELECT 1 FROM " + "information_schema.tables " + "WHERE table_name = 'pg_replication_slots')") + if not res[0][0]: + return True + + query = ("SELECT slot_name, plugin, slot_type, database, " + "active FROM pg_replication_slots") + res = self.__exec_sql(query) + + # If there is no replication: + if not res: + return True + + rslot_dict = {} + for i in res: + rslot_dict[i[0]] = dict( + plugin=i[1], + slot_type=i[2], + database=i[3], + active=i[4], + ) + + self.pg_facts["repl_slots"] = rslot_dict + + def get_settings(self): + """ + Get server settings. + """ + # Check pending restart column exists: + pend_rest_col_exists = self.__exec_sql("SELECT 1 FROM information_schema.columns " + "WHERE table_name = 'pg_settings' " + "AND column_name = 'pending_restart'") + if not pend_rest_col_exists: + query = ("SELECT name, setting, unit, context, vartype, " + "boot_val, min_val, max_val, sourcefile " + "FROM pg_settings") + else: + query = ("SELECT name, setting, unit, context, vartype, " + "boot_val, min_val, max_val, sourcefile, pending_restart " + "FROM pg_settings") + + res = self.__exec_sql(query) + + set_dict = {} + for i in res: + val_in_bytes = None + setting = i[1] + if i[2]: + unit = i[2] + else: + unit = '' + + if unit == 'kB': + val_in_bytes = int(setting) * 1024 + + elif unit == '8kB': + val_in_bytes = int(setting) * 1024 * 8 + + elif unit == 'MB': + val_in_bytes = int(setting) * 1024 * 1024 + + if val_in_bytes is not None and val_in_bytes < 0: + val_in_bytes = 0 + + setting_name = i[0] + pretty_val = self.__get_pretty_val(setting_name) + + pending_restart = None + if pend_rest_col_exists: + pending_restart = i[9] + + set_dict[setting_name] = dict( + setting=setting, + unit=unit, + context=i[3], + vartype=i[4], + boot_val=i[5] if i[5] else '', + min_val=i[6] if i[6] else '', + max_val=i[7] if i[7] else '', + sourcefile=i[8] if i[8] else '', + pretty_val=pretty_val, + ) + if val_in_bytes is not None: + set_dict[setting_name]['val_in_bytes'] = val_in_bytes + + if pending_restart is not None: + set_dict[setting_name]['pending_restart'] = pending_restart + if pending_restart: + self.pg_facts["pending_restart_settings"].append(setting_name) + + self.pg_facts["settings"] = set_dict + + def get_repl_info(self): + """ + Get information about replication if the server is a master. + """ + # Check that pg_replication_slots exists: + res = self.__exec_sql("SELECT EXISTS (SELECT 1 FROM " + "information_schema.tables " + "WHERE table_name = 'pg_stat_replication')") + if not res[0][0]: + return True + + query = ("SELECT r.pid, a.rolname, r.application_name, r.client_addr, " + "r.client_hostname, r.backend_start::text, r.state " + "FROM pg_stat_replication AS r " + "JOIN pg_authid AS a ON r.usesysid = a.oid") + res = self.__exec_sql(query) + + # If there is no replication: + if not res: + return True + + repl_dict = {} + for i in res: + repl_dict[i[0]] = dict( + usename=i[1], + app_name=i[2] if i[2] else '', + client_addr=i[3], + client_hostname=i[4] if i[4] else '', + backend_start=i[5], + state=i[6], + ) + + self.pg_facts["replications"] = repl_dict + + def get_lang_info(self): + """ + Get information about current supported languages. + """ + query = ("SELECT l.lanname, a.rolname, l.lanacl " + "FROM pg_language AS l " + "JOIN pg_authid AS a ON l.lanowner = a.oid") + res = self.__exec_sql(query) + lang_dict = {} + for i in res: + lang_dict[i[0]] = dict( + lanowner=i[1], + lanacl=i[2] if i[2] else '', + ) + + return lang_dict + + def get_namespaces(self): + """ + Get information about namespaces. + """ + query = ("SELECT n.nspname, a.rolname, n.nspacl " + "FROM pg_catalog.pg_namespace AS n " + "JOIN pg_authid AS a ON a.oid = n.nspowner") + res = self.__exec_sql(query) + + nsp_dict = {} + for i in res: + nsp_dict[i[0]] = dict( + nspowner=i[1], + nspacl=i[2] if i[2] else '', + ) + + return nsp_dict + + def get_pg_version(self): + query = "SELECT version()" + raw = self.__exec_sql(query)[0][0] + raw = raw.split()[1].split('.') + self.pg_facts["version"] = dict( + major=int(raw[0]), + minor=int(raw[1]), + ) + + def get_db_info(self): + # Following query returns: + # Name, Owner, Encoding, Collate, Ctype, Access Priv, Size + query = ("SELECT d.datname, " + "pg_catalog.pg_get_userbyid(d.datdba), " + "pg_catalog.pg_encoding_to_char(d.encoding), " + "d.datcollate, " + "d.datctype, " + "pg_catalog.array_to_string(d.datacl, E'\n'), " + "CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') " + "THEN pg_catalog.pg_database_size(d.datname)::text " + "ELSE 'No Access' END, " + "t.spcname " + "FROM pg_catalog.pg_database AS d " + "JOIN pg_catalog.pg_tablespace t ON d.dattablespace = t.oid " + "WHERE d.datname != 'template0'") + + res = self.__exec_sql(query) + + db_dict = {} + for i in res: + db_dict[i[0]] = dict( + owner=i[1], + encoding=i[2], + collate=i[3], + ctype=i[4], + access_priv=i[5] if i[5] else '', + size=i[6], + ) + + for datname in db_dict: + self.cursor = self.db_obj.reconnect(datname) + db_dict[datname]['namespaces'] = self.get_namespaces() + db_dict[datname]['extensions'] = self.get_ext_info() + db_dict[datname]['languages'] = self.get_lang_info() + + self.pg_facts["databases"] = db_dict + + def __get_pretty_val(self, setting): + return self.__exec_sql("SHOW %s" % setting)[0][0] + + 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 psycopg2.ProgrammingError as e: + self.module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e))) + self.cursor.close() + return False + +# =========================================== +# Module execution. +# + + +def main(): + argument_spec = postgres_common_argument_spec() + argument_spec.update( + db=dict(type='str', aliases=['login_db']), + port=dict(type='int', default=5432, aliases=['login_port']), + filter=dict(type='list'), + ssl_mode=dict(type='str', default='prefer', choices=['allow', 'disable', 'prefer', 'require', 'verify-ca', 'verify-full']), + ssl_rootcert=dict(type='str'), + session_role=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") + + filter_ = module.params["filter"] + sslrootcert = module.params["ssl_rootcert"] + session_role = module.params["session_role"] + + # 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"] == "" 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: + module.fail_json(msg='psycopg2 must be at least 2.4.3 in order ' + 'to user the ssl_rootcert parameter') + + db_conn_obj = PgDbConn(module, kw, session_role) + + # Do job: + pg_facts = PgClusterFacts(module, db_conn_obj) + + module.exit_json(**pg_facts.collect(filter_)) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/postgresql/tasks/main.yml b/test/integration/targets/postgresql/tasks/main.yml index 2f35a61fd9b..3e0b47b541c 100644 --- a/test/integration/targets/postgresql/tasks/main.yml +++ b/test/integration/targets/postgresql/tasks/main.yml @@ -780,6 +780,9 @@ # Test postgresql_privs - include: postgresql_privs.yml +# Test postgresql_facts module: +- include: postgresql_facts.yml + # 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_facts.yml b/test/integration/targets/postgresql/tasks/postgresql_facts.yml new file mode 100644 index 00000000000..77d79f3cd6d --- /dev/null +++ b/test/integration/targets/postgresql/tasks/postgresql_facts.yml @@ -0,0 +1,108 @@ +# Test code for the postgresql_facts 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_facts - 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 + +- name: postgresql_facts - test return values and session_role param + become_user: "{{ pg_user }}" + become: yes + postgresql_facts: + db: "{{ db_default }}" + login_user: "{{ pg_user }}" + session_role: session_superuser + register: result + ignore_errors: yes + +- assert: + that: + - result.version != {} + - result.databases.{{ db_default }}.collate + - result.databases.{{ db_default }}.languages + - result.databases.{{ db_default }}.namespaces + - result.databases.{{ db_default }}.extensions + - result.settings + - result.tablespaces + - result.roles + +- name: postgresql_facts - check filter param passed by list + become_user: "{{ pg_user }}" + become: yes + postgresql_facts: + db: "{{ db_default }}" + login_user: "{{ pg_user }}" + filter: + - ver* + - rol* + register: result + ignore_errors: yes + +- assert: + that: + - result.version != {} + - result.roles + - result.databases == {} + - result.repl_slots == {} + - result.replications == {} + - result.settings == {} + - result.tablespaces == {} + +- name: postgresql_facts - check filter param passed by string + become_user: "{{ pg_user }}" + become: yes + postgresql_facts: + db: "{{ db_default }}" + filter: ver*,role* + login_user: "{{ pg_user }}" + register: result + ignore_errors: yes + +- assert: + that: + - result.version != {} + - result.roles + - result.databases == {} + - result.repl_slots == {} + - result.replications == {} + - result.settings == {} + - result.tablespaces == {} + +- name: postgresql_facts - check filter param passed by string + become_user: "{{ pg_user }}" + become: yes + postgresql_facts: + db: "{{ db_default }}" + filter: ver* + login_user: "{{ pg_user }}" + register: result + ignore_errors: yes + +- assert: + that: + - result.version + - result.roles == {} + +- name: postgresql_facts - check excluding filter param passed by list + become_user: "{{ pg_user }}" + become: yes + postgresql_facts: + db: "{{ db_default }}" + filter: + - "!ver*" + - "!rol*" + login_user: "{{ pg_user }}" + register: result + ignore_errors: yes + +- assert: + that: + - result.version == {} + - result.roles == {} + - result.databases