2020-03-09 10:11:07 +01:00
|
|
|
#!/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
|
|
|
|
|
|
|
|
DOCUMENTATION = r'''
|
|
|
|
---
|
|
|
|
module: postgresql_db
|
|
|
|
short_description: Add or remove PostgreSQL databases from a remote host.
|
|
|
|
description:
|
|
|
|
- Add or remove PostgreSQL databases from a remote host.
|
|
|
|
options:
|
|
|
|
name:
|
|
|
|
description:
|
|
|
|
- Name of the database to add or remove
|
|
|
|
type: str
|
|
|
|
required: true
|
|
|
|
aliases: [ db ]
|
|
|
|
port:
|
|
|
|
description:
|
|
|
|
- Database port to connect (if needed)
|
|
|
|
type: int
|
|
|
|
default: 5432
|
|
|
|
aliases:
|
|
|
|
- login_port
|
|
|
|
owner:
|
|
|
|
description:
|
|
|
|
- Name of the role to set as owner of the database
|
|
|
|
type: str
|
|
|
|
template:
|
|
|
|
description:
|
|
|
|
- Template used to create the database
|
|
|
|
type: str
|
|
|
|
encoding:
|
|
|
|
description:
|
|
|
|
- Encoding of the database
|
|
|
|
type: str
|
|
|
|
lc_collate:
|
|
|
|
description:
|
|
|
|
- Collation order (LC_COLLATE) to use in the database. Must match collation order of template database unless C(template0) is used as template.
|
|
|
|
type: str
|
|
|
|
lc_ctype:
|
|
|
|
description:
|
|
|
|
- Character classification (LC_CTYPE) to use in the database (e.g. lower, upper, ...) Must match LC_CTYPE of template database unless C(template0)
|
|
|
|
is used as template.
|
|
|
|
type: str
|
|
|
|
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
|
|
|
|
state:
|
|
|
|
description:
|
|
|
|
- The database state.
|
|
|
|
- C(present) implies that the database should be created if necessary.
|
|
|
|
- C(absent) implies that the database should be removed if present.
|
|
|
|
- C(dump) requires a target definition to which the database will be backed up. (Added in Ansible 2.4)
|
|
|
|
Note that in some PostgreSQL versions of pg_dump, which is an embedded PostgreSQL utility and is used by the module,
|
|
|
|
returns rc 0 even when errors occurred (e.g. the connection is forbidden by pg_hba.conf, etc.),
|
|
|
|
so the module returns changed=True but the dump has not actually been done. Please, be sure that your version of
|
|
|
|
pg_dump returns rc 1 in this case.
|
|
|
|
- C(restore) also requires a target definition from which the database will be restored. (Added in Ansible 2.4)
|
|
|
|
- The format of the backup will be detected based on the target name.
|
|
|
|
- Supported compression formats for dump and restore include C(.pgc), C(.bz2), C(.gz) and C(.xz)
|
|
|
|
- Supported formats for dump and restore include C(.sql) and C(.tar)
|
|
|
|
type: str
|
|
|
|
choices: [ absent, dump, present, restore ]
|
|
|
|
default: present
|
|
|
|
target:
|
|
|
|
description:
|
|
|
|
- File to back up or restore from.
|
|
|
|
- Used when I(state) is C(dump) or C(restore).
|
|
|
|
type: path
|
|
|
|
target_opts:
|
|
|
|
description:
|
|
|
|
- Further arguments for pg_dump or pg_restore.
|
|
|
|
- Used when I(state) is C(dump) or C(restore).
|
|
|
|
type: str
|
|
|
|
maintenance_db:
|
|
|
|
description:
|
|
|
|
- The value specifies the initial database (which is also called as maintenance DB) that Ansible connects to.
|
|
|
|
type: str
|
|
|
|
default: postgres
|
|
|
|
conn_limit:
|
|
|
|
description:
|
|
|
|
- Specifies the database connection limit.
|
|
|
|
type: str
|
|
|
|
tablespace:
|
|
|
|
description:
|
|
|
|
- The tablespace to set for the database
|
|
|
|
U(https://www.postgresql.org/docs/current/sql-alterdatabase.html).
|
|
|
|
- If you want to move the database back to the default tablespace,
|
|
|
|
explicitly set this to pg_default.
|
|
|
|
type: path
|
|
|
|
dump_extra_args:
|
|
|
|
description:
|
|
|
|
- Provides additional arguments when I(state) is C(dump).
|
|
|
|
- Cannot be used with dump-file-format-related arguments like ``--format=d``.
|
|
|
|
type: str
|
2020-06-13 15:01:19 +02:00
|
|
|
version_added: '0.2.0'
|
2020-04-20 08:38:35 +02:00
|
|
|
trust_input:
|
|
|
|
description:
|
2020-05-09 08:25:12 +02:00
|
|
|
- If C(no), check whether values of parameters I(owner), I(conn_limit), I(encoding),
|
|
|
|
I(db), I(template), I(tablespace), I(session_role) are potentially dangerous.
|
|
|
|
- It makes sense to use C(yes) only when SQL injections via the parameters are possible.
|
2020-04-20 08:38:35 +02:00
|
|
|
type: bool
|
|
|
|
default: yes
|
2020-06-13 15:01:19 +02:00
|
|
|
version_added: '0.2.0'
|
2020-03-09 10:11:07 +01:00
|
|
|
seealso:
|
|
|
|
- name: CREATE DATABASE reference
|
|
|
|
description: Complete reference of the CREATE DATABASE command documentation.
|
|
|
|
link: https://www.postgresql.org/docs/current/sql-createdatabase.html
|
|
|
|
- name: DROP DATABASE reference
|
|
|
|
description: Complete reference of the DROP DATABASE command documentation.
|
|
|
|
link: https://www.postgresql.org/docs/current/sql-dropdatabase.html
|
|
|
|
- name: pg_dump reference
|
|
|
|
description: Complete reference of pg_dump documentation.
|
|
|
|
link: https://www.postgresql.org/docs/current/app-pgdump.html
|
|
|
|
- name: pg_restore reference
|
|
|
|
description: Complete reference of pg_restore documentation.
|
|
|
|
link: https://www.postgresql.org/docs/current/app-pgrestore.html
|
2020-06-25 11:09:13 +02:00
|
|
|
- module: community.general.postgresql_tablespace
|
|
|
|
- module: community.general.postgresql_info
|
|
|
|
- module: community.general.postgresql_ping
|
2020-03-09 10:11:07 +01:00
|
|
|
notes:
|
|
|
|
- State C(dump) and C(restore) don't require I(psycopg2) since version 2.8.
|
|
|
|
author: "Ansible Core Team"
|
|
|
|
extends_documentation_fragment:
|
|
|
|
- community.general.postgres
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
EXAMPLES = r'''
|
|
|
|
- name: Create a new database with name "acme"
|
|
|
|
postgresql_db:
|
|
|
|
name: acme
|
|
|
|
|
|
|
|
# Note: If a template different from "template0" is specified, encoding and locale settings must match those of the template.
|
|
|
|
- name: Create a new database with name "acme" and specific encoding and locale # settings.
|
|
|
|
postgresql_db:
|
|
|
|
name: acme
|
|
|
|
encoding: UTF-8
|
|
|
|
lc_collate: de_DE.UTF-8
|
|
|
|
lc_ctype: de_DE.UTF-8
|
|
|
|
template: template0
|
|
|
|
|
|
|
|
# Note: Default limit for the number of concurrent connections to a specific database is "-1", which means "unlimited"
|
|
|
|
- name: Create a new database with name "acme" which has a limit of 100 concurrent connections
|
|
|
|
postgresql_db:
|
|
|
|
name: acme
|
|
|
|
conn_limit: "100"
|
|
|
|
|
|
|
|
- name: Dump an existing database to a file
|
|
|
|
postgresql_db:
|
|
|
|
name: acme
|
|
|
|
state: dump
|
|
|
|
target: /tmp/acme.sql
|
|
|
|
|
|
|
|
- name: Dump an existing database to a file excluding the test table
|
|
|
|
postgresql_db:
|
|
|
|
name: acme
|
|
|
|
state: dump
|
|
|
|
target: /tmp/acme.sql
|
|
|
|
dump_extra_args: --exclude-table=test
|
|
|
|
|
|
|
|
- name: Dump an existing database to a file (with compression)
|
|
|
|
postgresql_db:
|
|
|
|
name: acme
|
|
|
|
state: dump
|
|
|
|
target: /tmp/acme.sql.gz
|
|
|
|
|
|
|
|
- name: Dump a single schema for an existing database
|
|
|
|
postgresql_db:
|
|
|
|
name: acme
|
|
|
|
state: dump
|
|
|
|
target: /tmp/acme.sql
|
|
|
|
target_opts: "-n public"
|
|
|
|
|
2020-05-20 20:07:21 +02:00
|
|
|
- name: Dump only table1 and table2 from the acme database
|
|
|
|
postgresql_db:
|
|
|
|
name: acme
|
|
|
|
state: dump
|
|
|
|
target: /tmp/table1_table2.sql
|
|
|
|
target_opts: "-t table1 -t table2"
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
# Note: In the example below, if database foo exists and has another tablespace
|
|
|
|
# the tablespace will be changed to foo. Access to the database will be locked
|
|
|
|
# until the copying of database files is finished.
|
|
|
|
- name: Create a new database called foo in tablespace bar
|
|
|
|
postgresql_db:
|
|
|
|
name: foo
|
|
|
|
tablespace: bar
|
|
|
|
'''
|
|
|
|
|
|
|
|
RETURN = r'''
|
|
|
|
executed_commands:
|
|
|
|
description: List of commands which tried to run.
|
|
|
|
returned: always
|
|
|
|
type: list
|
|
|
|
sample: ["CREATE DATABASE acme"]
|
2020-06-13 15:01:19 +02:00
|
|
|
version_added: '0.2.0'
|
2020-03-09 10:11:07 +01:00
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
import subprocess
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
try:
|
|
|
|
import psycopg2
|
|
|
|
import psycopg2.extras
|
|
|
|
except ImportError:
|
|
|
|
HAS_PSYCOPG2 = False
|
|
|
|
else:
|
|
|
|
HAS_PSYCOPG2 = True
|
|
|
|
|
|
|
|
import ansible_collections.community.general.plugins.module_utils.postgres as pgutils
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
2020-04-20 08:38:35 +02:00
|
|
|
from ansible_collections.community.general.plugins.module_utils.database import (
|
|
|
|
check_input,
|
|
|
|
SQLParseError,
|
|
|
|
)
|
2020-03-09 10:11:07 +01:00
|
|
|
from ansible.module_utils.six import iteritems
|
|
|
|
from ansible.module_utils.six.moves import shlex_quote
|
|
|
|
from ansible.module_utils._text import to_native
|
|
|
|
|
|
|
|
executed_commands = []
|
|
|
|
|
|
|
|
|
|
|
|
class NotSupportedError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
# ===========================================
|
|
|
|
# PostgreSQL module specific support methods.
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
|
def set_owner(cursor, db, owner):
|
2020-04-20 08:38:35 +02:00
|
|
|
query = 'ALTER DATABASE "%s" OWNER TO "%s"' % (db, owner)
|
2020-03-09 10:11:07 +01:00
|
|
|
executed_commands.append(query)
|
|
|
|
cursor.execute(query)
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def set_conn_limit(cursor, db, conn_limit):
|
2020-04-20 08:38:35 +02:00
|
|
|
query = 'ALTER DATABASE "%s" CONNECTION LIMIT %s' % (db, conn_limit)
|
2020-03-09 10:11:07 +01:00
|
|
|
executed_commands.append(query)
|
|
|
|
cursor.execute(query)
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def get_encoding_id(cursor, encoding):
|
|
|
|
query = "SELECT pg_char_to_encoding(%(encoding)s) AS encoding_id;"
|
|
|
|
cursor.execute(query, {'encoding': encoding})
|
|
|
|
return cursor.fetchone()['encoding_id']
|
|
|
|
|
|
|
|
|
|
|
|
def get_db_info(cursor, db):
|
|
|
|
query = """
|
|
|
|
SELECT rolname AS owner,
|
|
|
|
pg_encoding_to_char(encoding) AS encoding, encoding AS encoding_id,
|
|
|
|
datcollate AS lc_collate, datctype AS lc_ctype, pg_database.datconnlimit AS conn_limit,
|
|
|
|
spcname AS tablespace
|
|
|
|
FROM pg_database
|
|
|
|
JOIN pg_roles ON pg_roles.oid = pg_database.datdba
|
|
|
|
JOIN pg_tablespace ON pg_tablespace.oid = pg_database.dattablespace
|
|
|
|
WHERE datname = %(db)s
|
|
|
|
"""
|
|
|
|
cursor.execute(query, {'db': db})
|
|
|
|
return cursor.fetchone()
|
|
|
|
|
|
|
|
|
|
|
|
def db_exists(cursor, db):
|
|
|
|
query = "SELECT * FROM pg_database WHERE datname=%(db)s"
|
|
|
|
cursor.execute(query, {'db': db})
|
|
|
|
return cursor.rowcount == 1
|
|
|
|
|
|
|
|
|
|
|
|
def db_delete(cursor, db):
|
|
|
|
if db_exists(cursor, db):
|
2020-04-20 08:38:35 +02:00
|
|
|
query = 'DROP DATABASE "%s"' % db
|
2020-03-09 10:11:07 +01:00
|
|
|
executed_commands.append(query)
|
|
|
|
cursor.execute(query)
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace):
|
|
|
|
params = dict(enc=encoding, collate=lc_collate, ctype=lc_ctype, conn_limit=conn_limit, tablespace=tablespace)
|
|
|
|
if not db_exists(cursor, db):
|
2020-04-20 08:38:35 +02:00
|
|
|
query_fragments = ['CREATE DATABASE "%s"' % db]
|
2020-03-09 10:11:07 +01:00
|
|
|
if owner:
|
|
|
|
query_fragments.append('OWNER "%s"' % owner)
|
|
|
|
if template:
|
2020-04-20 08:38:35 +02:00
|
|
|
query_fragments.append('TEMPLATE "%s"' % template)
|
2020-03-09 10:11:07 +01:00
|
|
|
if encoding:
|
|
|
|
query_fragments.append('ENCODING %(enc)s')
|
|
|
|
if lc_collate:
|
|
|
|
query_fragments.append('LC_COLLATE %(collate)s')
|
|
|
|
if lc_ctype:
|
|
|
|
query_fragments.append('LC_CTYPE %(ctype)s')
|
|
|
|
if tablespace:
|
2020-04-20 08:38:35 +02:00
|
|
|
query_fragments.append('TABLESPACE "%s"' % tablespace)
|
2020-03-09 10:11:07 +01:00
|
|
|
if conn_limit:
|
|
|
|
query_fragments.append("CONNECTION LIMIT %(conn_limit)s" % {"conn_limit": conn_limit})
|
|
|
|
query = ' '.join(query_fragments)
|
|
|
|
executed_commands.append(cursor.mogrify(query, params))
|
|
|
|
cursor.execute(query, params)
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
db_info = get_db_info(cursor, db)
|
|
|
|
if (encoding and get_encoding_id(cursor, encoding) != db_info['encoding_id']):
|
|
|
|
raise NotSupportedError(
|
|
|
|
'Changing database encoding is not supported. '
|
|
|
|
'Current encoding: %s' % db_info['encoding']
|
|
|
|
)
|
|
|
|
elif lc_collate and lc_collate != db_info['lc_collate']:
|
|
|
|
raise NotSupportedError(
|
|
|
|
'Changing LC_COLLATE is not supported. '
|
|
|
|
'Current LC_COLLATE: %s' % db_info['lc_collate']
|
|
|
|
)
|
|
|
|
elif lc_ctype and lc_ctype != db_info['lc_ctype']:
|
|
|
|
raise NotSupportedError(
|
|
|
|
'Changing LC_CTYPE is not supported.'
|
|
|
|
'Current LC_CTYPE: %s' % db_info['lc_ctype']
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
changed = False
|
|
|
|
|
|
|
|
if owner and owner != db_info['owner']:
|
|
|
|
changed = set_owner(cursor, db, owner)
|
|
|
|
|
|
|
|
if conn_limit and conn_limit != str(db_info['conn_limit']):
|
|
|
|
changed = set_conn_limit(cursor, db, conn_limit)
|
|
|
|
|
|
|
|
if tablespace and tablespace != db_info['tablespace']:
|
|
|
|
changed = set_tablespace(cursor, db, tablespace)
|
|
|
|
|
|
|
|
return changed
|
|
|
|
|
|
|
|
|
|
|
|
def db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace):
|
|
|
|
if not db_exists(cursor, db):
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
db_info = get_db_info(cursor, db)
|
|
|
|
if (encoding and get_encoding_id(cursor, encoding) != db_info['encoding_id']):
|
|
|
|
return False
|
|
|
|
elif lc_collate and lc_collate != db_info['lc_collate']:
|
|
|
|
return False
|
|
|
|
elif lc_ctype and lc_ctype != db_info['lc_ctype']:
|
|
|
|
return False
|
|
|
|
elif owner and owner != db_info['owner']:
|
|
|
|
return False
|
|
|
|
elif conn_limit and conn_limit != str(db_info['conn_limit']):
|
|
|
|
return False
|
|
|
|
elif tablespace and tablespace != db_info['tablespace']:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def db_dump(module, target, target_opts="",
|
|
|
|
db=None,
|
|
|
|
dump_extra_args=None,
|
|
|
|
user=None,
|
|
|
|
password=None,
|
|
|
|
host=None,
|
|
|
|
port=None,
|
|
|
|
**kw):
|
|
|
|
|
|
|
|
flags = login_flags(db, host, port, user, db_prefix=False)
|
|
|
|
cmd = module.get_bin_path('pg_dump', True)
|
|
|
|
comp_prog_path = None
|
|
|
|
|
|
|
|
if os.path.splitext(target)[-1] == '.tar':
|
|
|
|
flags.append(' --format=t')
|
|
|
|
elif os.path.splitext(target)[-1] == '.pgc':
|
|
|
|
flags.append(' --format=c')
|
|
|
|
if os.path.splitext(target)[-1] == '.gz':
|
|
|
|
if module.get_bin_path('pigz'):
|
|
|
|
comp_prog_path = module.get_bin_path('pigz', True)
|
|
|
|
else:
|
|
|
|
comp_prog_path = module.get_bin_path('gzip', True)
|
|
|
|
elif os.path.splitext(target)[-1] == '.bz2':
|
|
|
|
comp_prog_path = module.get_bin_path('bzip2', True)
|
|
|
|
elif os.path.splitext(target)[-1] == '.xz':
|
|
|
|
comp_prog_path = module.get_bin_path('xz', True)
|
|
|
|
|
|
|
|
cmd += "".join(flags)
|
|
|
|
|
|
|
|
if dump_extra_args:
|
|
|
|
cmd += " {0} ".format(dump_extra_args)
|
|
|
|
|
|
|
|
if target_opts:
|
|
|
|
cmd += " {0} ".format(target_opts)
|
|
|
|
|
|
|
|
if comp_prog_path:
|
|
|
|
# Use a fifo to be notified of an error in pg_dump
|
|
|
|
# Using shell pipe has no way to return the code of the first command
|
|
|
|
# in a portable way.
|
|
|
|
fifo = os.path.join(module.tmpdir, 'pg_fifo')
|
|
|
|
os.mkfifo(fifo)
|
|
|
|
cmd = '{1} <{3} > {2} & {0} >{3}'.format(cmd, comp_prog_path, shlex_quote(target), fifo)
|
|
|
|
else:
|
|
|
|
cmd = '{0} > {1}'.format(cmd, shlex_quote(target))
|
|
|
|
|
|
|
|
return do_with_password(module, cmd, password)
|
|
|
|
|
|
|
|
|
|
|
|
def db_restore(module, target, target_opts="",
|
|
|
|
db=None,
|
|
|
|
user=None,
|
|
|
|
password=None,
|
|
|
|
host=None,
|
|
|
|
port=None,
|
|
|
|
**kw):
|
|
|
|
|
|
|
|
flags = login_flags(db, host, port, user)
|
|
|
|
comp_prog_path = None
|
|
|
|
cmd = module.get_bin_path('psql', True)
|
|
|
|
|
|
|
|
if os.path.splitext(target)[-1] == '.sql':
|
|
|
|
flags.append(' --file={0}'.format(target))
|
|
|
|
|
|
|
|
elif os.path.splitext(target)[-1] == '.tar':
|
|
|
|
flags.append(' --format=Tar')
|
|
|
|
cmd = module.get_bin_path('pg_restore', True)
|
|
|
|
|
|
|
|
elif os.path.splitext(target)[-1] == '.pgc':
|
|
|
|
flags.append(' --format=Custom')
|
|
|
|
cmd = module.get_bin_path('pg_restore', True)
|
|
|
|
|
|
|
|
elif os.path.splitext(target)[-1] == '.gz':
|
|
|
|
comp_prog_path = module.get_bin_path('zcat', True)
|
|
|
|
|
|
|
|
elif os.path.splitext(target)[-1] == '.bz2':
|
|
|
|
comp_prog_path = module.get_bin_path('bzcat', True)
|
|
|
|
|
|
|
|
elif os.path.splitext(target)[-1] == '.xz':
|
|
|
|
comp_prog_path = module.get_bin_path('xzcat', True)
|
|
|
|
|
|
|
|
cmd += "".join(flags)
|
|
|
|
if target_opts:
|
|
|
|
cmd += " {0} ".format(target_opts)
|
|
|
|
|
|
|
|
if comp_prog_path:
|
|
|
|
env = os.environ.copy()
|
|
|
|
if password:
|
|
|
|
env = {"PGPASSWORD": password}
|
|
|
|
p1 = subprocess.Popen([comp_prog_path, target], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
p2 = subprocess.Popen(cmd, stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=env)
|
|
|
|
(stdout2, stderr2) = p2.communicate()
|
|
|
|
p1.stdout.close()
|
|
|
|
p1.wait()
|
|
|
|
if p1.returncode != 0:
|
|
|
|
stderr1 = p1.stderr.read()
|
|
|
|
return p1.returncode, '', stderr1, 'cmd: ****'
|
|
|
|
else:
|
|
|
|
return p2.returncode, '', stderr2, 'cmd: ****'
|
|
|
|
else:
|
|
|
|
cmd = '{0} < {1}'.format(cmd, shlex_quote(target))
|
|
|
|
|
|
|
|
return do_with_password(module, cmd, password)
|
|
|
|
|
|
|
|
|
|
|
|
def login_flags(db, host, port, user, db_prefix=True):
|
|
|
|
"""
|
|
|
|
returns a list of connection argument strings each prefixed
|
|
|
|
with a space and quoted where necessary to later be combined
|
|
|
|
in a single shell string with `"".join(rv)`
|
|
|
|
|
|
|
|
db_prefix determines if "--dbname" is prefixed to the db argument,
|
|
|
|
since the argument was introduced in 9.3.
|
|
|
|
"""
|
|
|
|
flags = []
|
|
|
|
if db:
|
|
|
|
if db_prefix:
|
|
|
|
flags.append(' --dbname={0}'.format(shlex_quote(db)))
|
|
|
|
else:
|
|
|
|
flags.append(' {0}'.format(shlex_quote(db)))
|
|
|
|
if host:
|
|
|
|
flags.append(' --host={0}'.format(host))
|
|
|
|
if port:
|
|
|
|
flags.append(' --port={0}'.format(port))
|
|
|
|
if user:
|
|
|
|
flags.append(' --username={0}'.format(user))
|
|
|
|
return flags
|
|
|
|
|
|
|
|
|
|
|
|
def do_with_password(module, cmd, password):
|
|
|
|
env = {}
|
|
|
|
if password:
|
|
|
|
env = {"PGPASSWORD": password}
|
|
|
|
executed_commands.append(cmd)
|
|
|
|
rc, stderr, stdout = module.run_command(cmd, use_unsafe_shell=True, environ_update=env)
|
|
|
|
return rc, stderr, stdout, cmd
|
|
|
|
|
|
|
|
|
|
|
|
def set_tablespace(cursor, db, tablespace):
|
2020-04-20 08:38:35 +02:00
|
|
|
query = 'ALTER DATABASE "%s" SET TABLESPACE "%s"' % (db, tablespace)
|
2020-03-09 10:11:07 +01:00
|
|
|
executed_commands.append(query)
|
|
|
|
cursor.execute(query)
|
|
|
|
return True
|
|
|
|
|
|
|
|
# ===========================================
|
|
|
|
# Module execution.
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
argument_spec = pgutils.postgres_common_argument_spec()
|
|
|
|
argument_spec.update(
|
|
|
|
db=dict(type='str', required=True, aliases=['name']),
|
|
|
|
owner=dict(type='str', default=''),
|
|
|
|
template=dict(type='str', default=''),
|
|
|
|
encoding=dict(type='str', default=''),
|
|
|
|
lc_collate=dict(type='str', default=''),
|
|
|
|
lc_ctype=dict(type='str', default=''),
|
|
|
|
state=dict(type='str', default='present', choices=['absent', 'dump', 'present', 'restore']),
|
|
|
|
target=dict(type='path', default=''),
|
|
|
|
target_opts=dict(type='str', default=''),
|
|
|
|
maintenance_db=dict(type='str', default="postgres"),
|
|
|
|
session_role=dict(type='str'),
|
|
|
|
conn_limit=dict(type='str', default=''),
|
|
|
|
tablespace=dict(type='path', default=''),
|
|
|
|
dump_extra_args=dict(type='str', default=None),
|
2020-04-20 08:38:35 +02:00
|
|
|
trust_input=dict(type='bool', default=True),
|
2020-03-09 10:11:07 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
module = AnsibleModule(
|
|
|
|
argument_spec=argument_spec,
|
|
|
|
supports_check_mode=True
|
|
|
|
)
|
|
|
|
|
|
|
|
db = module.params["db"]
|
|
|
|
owner = module.params["owner"]
|
|
|
|
template = module.params["template"]
|
|
|
|
encoding = module.params["encoding"]
|
|
|
|
lc_collate = module.params["lc_collate"]
|
|
|
|
lc_ctype = module.params["lc_ctype"]
|
|
|
|
target = module.params["target"]
|
|
|
|
target_opts = module.params["target_opts"]
|
|
|
|
state = module.params["state"]
|
|
|
|
changed = False
|
|
|
|
maintenance_db = module.params['maintenance_db']
|
|
|
|
session_role = module.params["session_role"]
|
|
|
|
conn_limit = module.params['conn_limit']
|
|
|
|
tablespace = module.params['tablespace']
|
|
|
|
dump_extra_args = module.params['dump_extra_args']
|
2020-04-20 08:38:35 +02:00
|
|
|
trust_input = module.params['trust_input']
|
|
|
|
|
|
|
|
# Check input
|
|
|
|
if not trust_input:
|
|
|
|
# Check input for potentially dangerous elements:
|
|
|
|
check_input(module, owner, conn_limit, encoding, db, template, tablespace, session_role)
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
raw_connection = state in ("dump", "restore")
|
|
|
|
|
|
|
|
if not raw_connection:
|
|
|
|
pgutils.ensure_required_libs(module)
|
|
|
|
|
|
|
|
# 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",
|
|
|
|
"ssl_mode": "sslmode",
|
|
|
|
"ca_cert": "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 target == "":
|
|
|
|
target = "{0}/{1}.sql".format(os.getcwd(), db)
|
|
|
|
target = os.path.expanduser(target)
|
|
|
|
|
|
|
|
if not raw_connection:
|
|
|
|
try:
|
|
|
|
db_connection = psycopg2.connect(database=maintenance_db, **kw)
|
|
|
|
|
|
|
|
# Enable autocommit so we can create databases
|
|
|
|
if psycopg2.__version__ >= '2.4.2':
|
|
|
|
db_connection.autocommit = True
|
|
|
|
else:
|
|
|
|
db_connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
|
|
|
|
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. Exception: {0}'.format(to_native(e)),
|
|
|
|
exception=traceback.format_exc())
|
|
|
|
module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc())
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc())
|
|
|
|
|
|
|
|
if session_role:
|
|
|
|
try:
|
|
|
|
cursor.execute('SET ROLE "%s"' % session_role)
|
|
|
|
except Exception as e:
|
|
|
|
module.fail_json(msg="Could not switch role: %s" % to_native(e), exception=traceback.format_exc())
|
|
|
|
|
|
|
|
try:
|
|
|
|
if module.check_mode:
|
|
|
|
if state == "absent":
|
|
|
|
changed = db_exists(cursor, db)
|
|
|
|
elif state == "present":
|
|
|
|
changed = not db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace)
|
|
|
|
module.exit_json(changed=changed, db=db, executed_commands=executed_commands)
|
|
|
|
|
|
|
|
if state == "absent":
|
|
|
|
try:
|
|
|
|
changed = db_delete(cursor, db)
|
|
|
|
except SQLParseError as e:
|
|
|
|
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
|
|
|
|
|
|
|
elif state == "present":
|
|
|
|
try:
|
|
|
|
changed = db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace)
|
|
|
|
except SQLParseError as e:
|
|
|
|
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
|
|
|
|
|
|
|
elif state in ("dump", "restore"):
|
|
|
|
method = state == "dump" and db_dump or db_restore
|
|
|
|
try:
|
|
|
|
if state == 'dump':
|
|
|
|
rc, stdout, stderr, cmd = method(module, target, target_opts, db, dump_extra_args, **kw)
|
|
|
|
else:
|
|
|
|
rc, stdout, stderr, cmd = method(module, target, target_opts, db, **kw)
|
|
|
|
|
|
|
|
if rc != 0:
|
|
|
|
module.fail_json(msg=stderr, stdout=stdout, rc=rc, cmd=cmd)
|
|
|
|
else:
|
|
|
|
module.exit_json(changed=True, msg=stdout, stderr=stderr, rc=rc, cmd=cmd,
|
|
|
|
executed_commands=executed_commands)
|
|
|
|
except SQLParseError as e:
|
|
|
|
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
|
|
|
|
|
|
|
except NotSupportedError as e:
|
|
|
|
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
|
|
|
|
except SystemExit:
|
|
|
|
# Avoid catching this on Python 2.4
|
|
|
|
raise
|
|
|
|
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, executed_commands=executed_commands)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|