#!/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()