mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-19 17:01:25 -07:00
Initial commit
This commit is contained in:
commit
aebc1b03fd
4861 changed files with 812621 additions and 0 deletions
330
plugins/module_utils/postgres.py
Normal file
330
plugins/module_utils/postgres.py
Normal file
|
@ -0,0 +1,330 @@
|
|||
# This code is part of Ansible, but is an independent component.
|
||||
# This particular file snippet, and this file snippet only, is BSD licensed.
|
||||
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
||||
# still belong to the author of the module, and may assign their own license
|
||||
# to the complete work.
|
||||
#
|
||||
# Copyright (c), Ted Timmons <ted@timmons.me>, 2017.
|
||||
# Most of this was originally added by other creators in the postgresql_user module.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
psycopg2 = None # This line needs for unit tests
|
||||
try:
|
||||
import psycopg2
|
||||
HAS_PSYCOPG2 = True
|
||||
except ImportError:
|
||||
HAS_PSYCOPG2 = False
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils.six import iteritems
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
|
||||
def postgres_common_argument_spec():
|
||||
"""
|
||||
Return a dictionary with connection options.
|
||||
|
||||
The options are commonly used by most of PostgreSQL modules.
|
||||
"""
|
||||
return dict(
|
||||
login_user=dict(default='postgres'),
|
||||
login_password=dict(default='', no_log=True),
|
||||
login_host=dict(default=''),
|
||||
login_unix_socket=dict(default=''),
|
||||
port=dict(type='int', default=5432, aliases=['login_port']),
|
||||
ssl_mode=dict(default='prefer', choices=['allow', 'disable', 'prefer', 'require', 'verify-ca', 'verify-full']),
|
||||
ca_cert=dict(aliases=['ssl_rootcert']),
|
||||
)
|
||||
|
||||
|
||||
def ensure_required_libs(module):
|
||||
"""Check required libraries."""
|
||||
if not HAS_PSYCOPG2:
|
||||
module.fail_json(msg=missing_required_lib('psycopg2'))
|
||||
|
||||
if module.params.get('ca_cert') and LooseVersion(psycopg2.__version__) < LooseVersion('2.4.3'):
|
||||
module.fail_json(msg='psycopg2 must be at least 2.4.3 in order to use the ca_cert parameter')
|
||||
|
||||
|
||||
def connect_to_db(module, conn_params, autocommit=False, fail_on_conn=True):
|
||||
"""Connect to a PostgreSQL database.
|
||||
|
||||
Return psycopg2 connection object.
|
||||
|
||||
Args:
|
||||
module (AnsibleModule) -- object of ansible.module_utils.basic.AnsibleModule class
|
||||
conn_params (dict) -- dictionary with connection parameters
|
||||
|
||||
Kwargs:
|
||||
autocommit (bool) -- commit automatically (default False)
|
||||
fail_on_conn (bool) -- fail if connection failed or just warn and return None (default True)
|
||||
"""
|
||||
ensure_required_libs(module)
|
||||
|
||||
db_connection = None
|
||||
try:
|
||||
db_connection = psycopg2.connect(**conn_params)
|
||||
if autocommit:
|
||||
if LooseVersion(psycopg2.__version__) >= LooseVersion('2.4.2'):
|
||||
db_connection.set_session(autocommit=True)
|
||||
else:
|
||||
db_connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
|
||||
|
||||
# Switch role, if specified:
|
||||
if module.params.get('session_role'):
|
||||
cursor = db_connection.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
||||
|
||||
try:
|
||||
cursor.execute('SET ROLE "%s"' % module.params['session_role'])
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Could not switch role: %s" % to_native(e))
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
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')
|
||||
|
||||
if fail_on_conn:
|
||||
module.fail_json(msg="unable to connect to database: %s" % to_native(e))
|
||||
else:
|
||||
module.warn("PostgreSQL server is unavailable: %s" % to_native(e))
|
||||
db_connection = None
|
||||
|
||||
except Exception as e:
|
||||
if fail_on_conn:
|
||||
module.fail_json(msg="unable to connect to database: %s" % to_native(e))
|
||||
else:
|
||||
module.warn("PostgreSQL server is unavailable: %s" % to_native(e))
|
||||
db_connection = None
|
||||
|
||||
return db_connection
|
||||
|
||||
|
||||
def exec_sql(obj, query, query_params=None, ddl=False, add_to_executed=True, dont_exec=False):
|
||||
"""Execute SQL.
|
||||
|
||||
Auxiliary function for PostgreSQL user classes.
|
||||
|
||||
Returns a query result if possible or True/False if ddl=True arg was passed.
|
||||
It necessary for statements that don't return any result (like DDL queries).
|
||||
|
||||
Args:
|
||||
obj (obj) -- must be an object of a user class.
|
||||
The object must have module (AnsibleModule class object) and
|
||||
cursor (psycopg cursor object) attributes
|
||||
query (str) -- SQL query to execute
|
||||
|
||||
Kwargs:
|
||||
query_params (dict or tuple) -- Query parameters to prevent SQL injections,
|
||||
could be a dict or tuple
|
||||
ddl (bool) -- must return True or False instead of rows (typical for DDL queries)
|
||||
(default False)
|
||||
add_to_executed (bool) -- append the query to obj.executed_queries attribute
|
||||
dont_exec (bool) -- used with add_to_executed=True to generate a query, add it
|
||||
to obj.executed_queries list and return True (default False)
|
||||
"""
|
||||
|
||||
if dont_exec:
|
||||
# This is usually needed to return queries in check_mode
|
||||
# without execution
|
||||
query = obj.cursor.mogrify(query, query_params)
|
||||
if add_to_executed:
|
||||
obj.executed_queries.append(query)
|
||||
|
||||
return True
|
||||
|
||||
try:
|
||||
if query_params is not None:
|
||||
obj.cursor.execute(query, query_params)
|
||||
else:
|
||||
obj.cursor.execute(query)
|
||||
|
||||
if add_to_executed:
|
||||
if query_params is not None:
|
||||
obj.executed_queries.append(obj.cursor.mogrify(query, query_params))
|
||||
else:
|
||||
obj.executed_queries.append(query)
|
||||
|
||||
if not ddl:
|
||||
res = obj.cursor.fetchall()
|
||||
return res
|
||||
return True
|
||||
except Exception as e:
|
||||
obj.module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e)))
|
||||
return False
|
||||
|
||||
|
||||
def get_conn_params(module, params_dict, warn_db_default=True):
|
||||
"""Get connection parameters from the passed dictionary.
|
||||
|
||||
Return a dictionary with parameters to connect to PostgreSQL server.
|
||||
|
||||
Args:
|
||||
module (AnsibleModule) -- object of ansible.module_utils.basic.AnsibleModule class
|
||||
params_dict (dict) -- dictionary with variables
|
||||
|
||||
Kwargs:
|
||||
warn_db_default (bool) -- warn that the default DB is used (default True)
|
||||
"""
|
||||
# To use defaults values, keyword arguments must be absent, so
|
||||
# check which values are empty and don't include in the return dictionary
|
||||
params_map = {
|
||||
"login_host": "host",
|
||||
"login_user": "user",
|
||||
"login_password": "password",
|
||||
"port": "port",
|
||||
"ssl_mode": "sslmode",
|
||||
"ca_cert": "sslrootcert"
|
||||
}
|
||||
|
||||
# Might be different in the modules:
|
||||
if params_dict.get('db'):
|
||||
params_map['db'] = 'database'
|
||||
elif params_dict.get('database'):
|
||||
params_map['database'] = 'database'
|
||||
elif params_dict.get('login_db'):
|
||||
params_map['login_db'] = 'database'
|
||||
else:
|
||||
if warn_db_default:
|
||||
module.warn('Database name has not been passed, '
|
||||
'used default database to connect to.')
|
||||
|
||||
kw = dict((params_map[k], v) for (k, v) in iteritems(params_dict)
|
||||
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 params_dict["login_unix_socket"] != "":
|
||||
kw["host"] = params_dict["login_unix_socket"]
|
||||
|
||||
return kw
|
||||
|
||||
|
||||
class PgMembership(object):
|
||||
def __init__(self, module, cursor, groups, target_roles, fail_on_role=True):
|
||||
self.module = module
|
||||
self.cursor = cursor
|
||||
self.target_roles = [r.strip() for r in target_roles]
|
||||
self.groups = [r.strip() for r in groups]
|
||||
self.executed_queries = []
|
||||
self.granted = {}
|
||||
self.revoked = {}
|
||||
self.fail_on_role = fail_on_role
|
||||
self.non_existent_roles = []
|
||||
self.changed = False
|
||||
self.__check_roles_exist()
|
||||
|
||||
def grant(self):
|
||||
for group in self.groups:
|
||||
self.granted[group] = []
|
||||
|
||||
for role in self.target_roles:
|
||||
# If role is in a group now, pass:
|
||||
if self.__check_membership(group, role):
|
||||
continue
|
||||
|
||||
query = 'GRANT "%s" TO "%s"' % (group, role)
|
||||
self.changed = exec_sql(self, query, ddl=True)
|
||||
|
||||
if self.changed:
|
||||
self.granted[group].append(role)
|
||||
|
||||
return self.changed
|
||||
|
||||
def revoke(self):
|
||||
for group in self.groups:
|
||||
self.revoked[group] = []
|
||||
|
||||
for role in self.target_roles:
|
||||
# If role is not in a group now, pass:
|
||||
if not self.__check_membership(group, role):
|
||||
continue
|
||||
|
||||
query = 'REVOKE "%s" FROM "%s"' % (group, role)
|
||||
self.changed = exec_sql(self, query, ddl=True)
|
||||
|
||||
if self.changed:
|
||||
self.revoked[group].append(role)
|
||||
|
||||
return self.changed
|
||||
|
||||
def __check_membership(self, src_role, dst_role):
|
||||
query = ("SELECT ARRAY(SELECT b.rolname FROM "
|
||||
"pg_catalog.pg_auth_members m "
|
||||
"JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) "
|
||||
"WHERE m.member = r.oid) "
|
||||
"FROM pg_catalog.pg_roles r "
|
||||
"WHERE r.rolname = %(dst_role)s")
|
||||
|
||||
res = exec_sql(self, query, query_params={'dst_role': dst_role}, add_to_executed=False)
|
||||
membership = []
|
||||
if res:
|
||||
membership = res[0][0]
|
||||
|
||||
if not membership:
|
||||
return False
|
||||
|
||||
if src_role in membership:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def __check_roles_exist(self):
|
||||
existent_groups = self.__roles_exist(self.groups)
|
||||
existent_roles = self.__roles_exist(self.target_roles)
|
||||
|
||||
for group in self.groups:
|
||||
if group not in existent_groups:
|
||||
if self.fail_on_role:
|
||||
self.module.fail_json(msg="Role %s does not exist" % group)
|
||||
else:
|
||||
self.module.warn("Role %s does not exist, pass" % group)
|
||||
self.non_existent_roles.append(group)
|
||||
|
||||
for role in self.target_roles:
|
||||
if role not in existent_roles:
|
||||
if self.fail_on_role:
|
||||
self.module.fail_json(msg="Role %s does not exist" % role)
|
||||
else:
|
||||
self.module.warn("Role %s does not exist, pass" % role)
|
||||
|
||||
if role not in self.groups:
|
||||
self.non_existent_roles.append(role)
|
||||
|
||||
else:
|
||||
if self.fail_on_role:
|
||||
self.module.exit_json(msg="Role role '%s' is a member of role '%s'" % (role, role))
|
||||
else:
|
||||
self.module.warn("Role role '%s' is a member of role '%s', pass" % (role, role))
|
||||
|
||||
# Update role lists, excluding non existent roles:
|
||||
self.groups = [g for g in self.groups if g not in self.non_existent_roles]
|
||||
|
||||
self.target_roles = [r for r in self.target_roles if r not in self.non_existent_roles]
|
||||
|
||||
def __roles_exist(self, roles):
|
||||
tmp = ["'" + x + "'" for x in roles]
|
||||
query = "SELECT rolname FROM pg_roles WHERE rolname IN (%s)" % ','.join(tmp)
|
||||
return [x[0] for x in exec_sql(self, query, add_to_executed=False)]
|
Loading…
Add table
Add a link
Reference in a new issue