├── .gitignore
├── ansible.cfg
├── inventory.yml
├── playbooks
├── module_utils
│ ├── scramp
│ │ ├── __init__.py
│ │ ├── utils.py
│ │ ├── LICENSE
│ │ └── _version.py
│ ├── pg8000
│ │ ├── exceptions.py
│ │ ├── LICENSE
│ │ ├── __init__.py
│ │ └── native.py
│ └── postgres.py
├── library
│ ├── postgresql_ping.py
│ ├── postgresql_schema.py
│ ├── postgresql_slot.py
│ ├── postgresql_query.py
│ ├── postgresql_membership.py
│ ├── postgresql_lang.py
│ ├── postgresql_copy.py
│ ├── postgresql_ext.py
│ ├── postgresql_set.py
│ ├── postgresql_owner.py
│ ├── postgresql_tablespace.py
│ └── postgresql_idx.py
└── test.yml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /*pass*
2 | /originals/**
3 | /playbooks/connection_plugins/**
4 | **/__pycache__/**
5 |
6 |
--------------------------------------------------------------------------------
/ansible.cfg:
--------------------------------------------------------------------------------
1 | [defaults]
2 | stdout_callback = yaml
3 | callable_plugins = yaml,profile_tasks
4 | callback_whitelist = profile_tasks
5 |
--------------------------------------------------------------------------------
/inventory.yml:
--------------------------------------------------------------------------------
1 | ---
2 | all:
3 | children:
4 | databases:
5 | hosts:
6 | localhost:
7 | ansible_connection: local
8 | ansible_python_interpreter: /usr/bin/python3
9 |
10 |
--------------------------------------------------------------------------------
/playbooks/module_utils/scramp/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | from .core import (
4 | ScramClient, ScramException, ScramMechanism, make_channel_binding)
5 |
6 | __all__ = [ScramClient, ScramMechanism, ScramException, make_channel_binding]
7 |
--------------------------------------------------------------------------------
/playbooks/module_utils/scramp/utils.py:
--------------------------------------------------------------------------------
1 | import hmac as hmaca
2 | from base64 import b64decode, b64encode
3 |
4 |
5 | def hmac(hf, key, msg):
6 | return hmaca.new(key, msg=msg, digestmod=hf).digest()
7 |
8 |
9 | def h(hf, msg):
10 | return hf(msg).digest()
11 |
12 |
13 | def hi(hf, password, salt, iterations):
14 | u = ui = hmac(hf, password, salt + b'\x00\x00\x00\x01')
15 | for i in range(iterations - 1):
16 | ui = hmac(hf, password, ui)
17 | u = xor(u, ui)
18 | return u
19 |
20 |
21 | def xor(bytes1, bytes2):
22 | return bytes(a ^ b for a, b in zip(bytes1, bytes2))
23 |
24 |
25 | def b64enc(binary):
26 | return b64encode(binary).decode('utf8')
27 |
28 |
29 | def b64dec(string):
30 | return b64decode(string)
31 |
32 |
33 | def uenc(string):
34 | return string.encode('utf-8')
35 |
--------------------------------------------------------------------------------
/playbooks/module_utils/pg8000/exceptions.py:
--------------------------------------------------------------------------------
1 | class Error(Exception):
2 | """Generic exception that is the base exception of all other error
3 | exceptions.
4 |
5 | This exception is part of the `DBAPI 2.0 specification
6 | `_.
7 | """
8 |
9 | pass
10 |
11 |
12 | class InterfaceError(Error):
13 | """Generic exception raised for errors that are related to the database
14 | interface rather than the database itself. For example, if the interface
15 | attempts to use an SSL connection but the server refuses, an InterfaceError
16 | will be raised.
17 |
18 | This exception is part of the `DBAPI 2.0 specification
19 | `_.
20 | """
21 |
22 | pass
23 |
24 |
25 | class DatabaseError(Error):
26 | """Generic exception raised for errors that are related to the database.
27 |
28 | This exception is part of the `DBAPI 2.0 specification
29 | `_.
30 | """
31 |
32 | pass
33 |
--------------------------------------------------------------------------------
/playbooks/module_utils/scramp/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Tony Locke
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/playbooks/module_utils/pg8000/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2007-2009, Mathieu Fenniak
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are
6 | met:
7 |
8 | * Redistributions of source code must retain the above copyright notice,
9 | this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 | * The name of the author may not be used to endorse or promote products
14 | derived from this software without specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 | POSSIBILITY OF SUCH DAMAGE.
27 |
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # English
2 |
3 | ## What is it?
4 |
5 | Ansible modules for PostgreSQL - plain drop-in for your project.
6 |
7 | ## What is this for?
8 |
9 | If you ever had to install psycopg2, you've definitely came across that dependency hell: libraries, libraries and libraries - tons of them. This project nulls out said dependency hassle: everything works out-of-the-box, you don't have to install anything. The guest star here is "pg8000": pure-Python PostgreSQL driver, with minor modifications to support its loading and usage by Ansible.
10 |
11 | ## Requirements
12 |
13 | * CentOS/RHEL 7.x;
14 | * Ansible >=2.9.x;
15 | * Python >=3.6.x;
16 | * PostgreSQL >=10.x (this was debugged with Postgres 12, though)
17 |
18 | ## Quick start guide
19 |
20 | 1. Copy to your project directory:
21 | 1. playbooks/library/*
22 | 2. playbooks/module_utils/pg8000/*
23 | 3. playbooks/module_utils/scramp/*
24 |
25 | 2. You're done, please don't forget to perform a wild dance (well, latter was a joke, just in case :-)
26 |
27 | ## How to send a donation to the author
28 |
29 | If you want to thank the author - [this is a donate link](https://yoomoney.ru/to/410011277351108). Any sum is happily accepted.
30 |
31 | ## Legal information
32 |
33 | This project is conceived and performed by me, Sergey Pechenko, on my own will, out of working hours, using own hardware.
34 |
35 | Copyright: (С) 2021, Sergey Pechenko
36 |
37 | This project includes the changes in the "postgres.py" Ansible module, which has own copyright:
38 |
39 | (c) Ted Timmons , 2017 (BSD-licensed)
40 |
41 | Other Ansible modules included with or without my changes carry their own licenses and copyright - please see the exact module for author names
42 |
43 | This project also uses MIT-licensed components as follows:
44 |
45 | * pg8000 (c) 2007-2009, Mathieu Fenniak
46 |
47 | * scramp (C) 2019 Tony Locke
48 |
49 | Portions for these components that provide possibility for Ansible to load and run them are also (C) 2021, Sergey Pechenko.
50 |
51 | ## License
52 |
53 | GPLv3+ (please see LICENSE)
54 |
55 | ## Contact
56 |
57 | You can ask your questions about this plugin at the [Ansible chat](https://t.me/pro_ansible), or [PM me](https://t.me/tnt4brain)
58 |
59 |
60 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_ping.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2018, Andrew Klychkov (@Andersson007)
5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 |
7 | # Contribution:
8 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
9 | # Welcome to https://t.me/pro_ansible for discussion and support
10 | # License: please see above
11 |
12 | from __future__ import absolute_import, division, print_function
13 | __metaclass__ = type
14 |
15 | ANSIBLE_METADATA = {
16 | 'metadata_version': '1.1',
17 | 'status': ['preview'],
18 | 'supported_by': 'community'
19 | }
20 |
21 | DOCUMENTATION = r'''
22 | ---
23 | module: postgresql_ping
24 | short_description: Check remote PostgreSQL server availability
25 | description:
26 | - Simple module to check remote PostgreSQL server availability.
27 | version_added: '2.8'
28 | options:
29 | db:
30 | description:
31 | - Name of a database to connect to.
32 | type: str
33 | aliases:
34 | - login_db
35 | seealso:
36 | - module: postgresql_info
37 | author:
38 | - Andrew Klychkov (@Andersson007)
39 | extends_documentation_fragment: postgres
40 | '''
41 |
42 | EXAMPLES = r'''
43 | # PostgreSQL ping dbsrv server from the shell:
44 | # ansible dbsrv -m postgresql_ping
45 |
46 | # In the example below you need to generate certificates previously.
47 | # See https://www.postgresql.org/docs/current/libpq-ssl.html for more information.
48 | - name: PostgreSQL ping dbsrv server using not default credentials and ssl
49 | postgresql_ping:
50 | db: protected_db
51 | login_host: dbsrv
52 | login_user: secret
53 | login_password: secret_pass
54 | ca_cert: /root/root.crt
55 | ssl_mode: verify-full
56 | '''
57 |
58 | RETURN = r'''
59 | is_available:
60 | description: PostgreSQL server availability.
61 | returned: always
62 | type: bool
63 | sample: true
64 | server_version:
65 | description: PostgreSQL server version.
66 | returned: always
67 | type: dict
68 | sample: { major: 10, minor: 1 }
69 | '''
70 |
71 |
72 | from ansible.module_utils.basic import AnsibleModule
73 | from ansible.module_utils.postgres import (
74 | connect_to_db,
75 | exec_sql,
76 | get_conn_params,
77 | postgres_common_argument_spec,
78 | )
79 |
80 |
81 | # ===========================================
82 | # PostgreSQL module specific support methods.
83 | #
84 |
85 |
86 | class PgPing(object):
87 | def __init__(self, module, cursor):
88 | self.module = module
89 | self.cursor = cursor
90 | self.is_available = False
91 | self.version = {}
92 |
93 | def do(self):
94 | self.get_pg_version()
95 | return (self.is_available, self.version)
96 |
97 | def get_pg_version(self):
98 | query = "SELECT version()"
99 | raw = exec_sql(self, query, add_to_executed=False)[0][0]
100 | if raw:
101 | self.is_available = True
102 | raw = raw.split()[1].split('.')
103 | self.version = dict(
104 | major=int(raw[0]),
105 | minor=int(raw[1]),
106 | )
107 |
108 |
109 | # ===========================================
110 | # Module execution.
111 | #
112 |
113 |
114 | def main():
115 | argument_spec = postgres_common_argument_spec()
116 | argument_spec.update(
117 | db=dict(type='str', aliases=['login_db']),
118 | )
119 | module = AnsibleModule(
120 | argument_spec=argument_spec,
121 | supports_check_mode=True,
122 | )
123 |
124 | # Set some default values:
125 | cursor = False
126 | db_connection = False
127 | result = dict(
128 | changed=False,
129 | is_available=False,
130 | server_version=dict(),
131 | )
132 |
133 | conn_params = get_conn_params(module, module.params, warn_db_default=False)
134 | db_connection = connect_to_db(module, conn_params, fail_on_conn=False)
135 |
136 | if db_connection is not None:
137 | cursor = db_connection.cursor()
138 |
139 | # Do job:
140 | pg_ping = PgPing(module, cursor)
141 | if cursor:
142 | # If connection established:
143 | result["is_available"], result["server_version"] = pg_ping.do()
144 | db_connection.rollback()
145 |
146 | module.exit_json(**result)
147 |
148 |
149 | if __name__ == '__main__':
150 | main()
151 |
--------------------------------------------------------------------------------
/playbooks/module_utils/pg8000/__init__.py:
--------------------------------------------------------------------------------
1 | from .legacy import (
2 | BIGINTEGER,
3 | BINARY,
4 | BOOLEAN,
5 | BOOLEAN_ARRAY,
6 | BYTES,
7 | Binary,
8 | CHAR,
9 | CHAR_ARRAY,
10 | Connection,
11 | Cursor,
12 | DATE,
13 | DATETIME,
14 | DECIMAL,
15 | DECIMAL_ARRAY,
16 | DataError,
17 | DatabaseError,
18 | Date,
19 | DateFromTicks,
20 | Error,
21 | FLOAT,
22 | FLOAT_ARRAY,
23 | INET,
24 | INT2VECTOR,
25 | INTEGER,
26 | INTEGER_ARRAY,
27 | INTERVAL,
28 | IntegrityError,
29 | InterfaceError,
30 | InternalError,
31 | JSON,
32 | JSONB,
33 | MACADDR,
34 | NAME,
35 | NAME_ARRAY,
36 | NULLTYPE,
37 | NUMBER,
38 | NotSupportedError,
39 | OID,
40 | OperationalError,
41 | PGInterval,
42 | ProgrammingError,
43 | ROWID,
44 | STRING,
45 | TEXT,
46 | TEXT_ARRAY,
47 | TIME,
48 | TIMEDELTA,
49 | TIMESTAMP,
50 | TIMESTAMPTZ,
51 | Time,
52 | TimeFromTicks,
53 | Timestamp,
54 | TimestampFromTicks,
55 | UNKNOWN,
56 | UUID_TYPE,
57 | VARCHAR,
58 | VARCHAR_ARRAY,
59 | Warning,
60 | XID,
61 | pginterval_in,
62 | pginterval_out,
63 | timedelta_in,
64 | )
65 |
66 |
67 |
68 | from ._version import get_versions
69 |
70 | __version__ = get_versions()["version"]
71 | del get_versions
72 |
73 | # Copyright (c) 2007-2009, Mathieu Fenniak
74 | # Copyright (c) The Contributors
75 | # All rights reserved.
76 | #
77 | # Redistribution and use in source and binary forms, with or without
78 | # modification, are permitted provided that the following conditions are
79 | # met:
80 | #
81 | # * Redistributions of source code must retain the above copyright notice,
82 | # this list of conditions and the following disclaimer.
83 | # * Redistributions in binary form must reproduce the above copyright notice,
84 | # this list of conditions and the following disclaimer in the documentation
85 | # and/or other materials provided with the distribution.
86 | # * The name of the author may not be used to endorse or promote products
87 | # derived from this software without specific prior written permission.
88 | #
89 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
90 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
91 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
92 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
93 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
94 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
95 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
96 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
97 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
98 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
99 | # POSSIBILITY OF SUCH DAMAGE.
100 |
101 | __author__ = "Mathieu Fenniak"
102 |
103 | # Contribution:
104 | # pg8000 driver adaptation for Ansible drop-in
105 | # (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
106 | # Welcome to https://t.me/pro_ansible for discussion and support
107 | # License: please see above
108 |
109 | __authors__ = ["Mathieu Fenniak", "Sergey Pechenko"]
110 |
111 | def connect(
112 | user,
113 | host="localhost",
114 | database=None,
115 | port=5432,
116 | password=None,
117 | source_address=None,
118 | unix_sock=None,
119 | ssl_context=None,
120 | timeout=None,
121 | tcp_keepalive=True,
122 | application_name=None,
123 | replication=None,
124 | ):
125 |
126 | return Connection(
127 | user,
128 | host=host,
129 | database=database,
130 | port=port,
131 | password=password,
132 | source_address=source_address,
133 | unix_sock=unix_sock,
134 | ssl_context=ssl_context,
135 | timeout=timeout,
136 | tcp_keepalive=tcp_keepalive,
137 | application_name=application_name,
138 | replication=replication,
139 | )
140 |
141 |
142 | apilevel = "2.0"
143 | """The DBAPI level supported, currently "2.0".
144 |
145 | This property is part of the `DBAPI 2.0 specification
146 | `_.
147 | """
148 |
149 | threadsafety = 1
150 | """Integer constant stating the level of thread safety the DBAPI interface
151 | supports. This DBAPI module supports sharing of the module only. Connections
152 | and cursors my not be shared between threads. This gives pg8000 a threadsafety
153 | value of 1.
154 |
155 | This property is part of the `DBAPI 2.0 specification
156 | `_.
157 | """
158 |
159 | paramstyle = "format"
160 |
161 |
162 | __all__ = [
163 | "BIGINTEGER",
164 | "BINARY",
165 | "BOOLEAN",
166 | "BOOLEAN_ARRAY",
167 | "BYTES",
168 | "Binary",
169 | "CHAR",
170 | "CHAR_ARRAY",
171 | "Connection",
172 | "Cursor",
173 | "DATE",
174 | "DATETIME",
175 | "DECIMAL",
176 | "DECIMAL_ARRAY",
177 | "DataError",
178 | "DatabaseError",
179 | "Date",
180 | "DateFromTicks",
181 | "Error",
182 | "FLOAT",
183 | "FLOAT_ARRAY",
184 | "INET",
185 | "INT2VECTOR",
186 | "INTEGER",
187 | "INTEGER_ARRAY",
188 | "INTERVAL",
189 | "IntegrityError",
190 | "InterfaceError",
191 | "InternalError",
192 | "JSON",
193 | "JSONB",
194 | "MACADDR",
195 | "NAME",
196 | "NAME_ARRAY",
197 | "NULLTYPE",
198 | "NUMBER",
199 | "NotSupportedError",
200 | "OID",
201 | "OperationalError",
202 | "PGInterval",
203 | "ProgrammingError",
204 | "ROWID",
205 | "STRING",
206 | "TEXT",
207 | "TEXT_ARRAY",
208 | "TIME",
209 | "TIMEDELTA",
210 | "TIMESTAMP",
211 | "TIMESTAMPTZ",
212 | "Time",
213 | "TimeFromTicks",
214 | "Timestamp",
215 | "TimestampFromTicks",
216 | "UNKNOWN",
217 | "UUID_TYPE",
218 | "VARCHAR",
219 | "VARCHAR_ARRAY",
220 | "Warning",
221 | "XID",
222 | "connect",
223 | "pginterval_in",
224 | "pginterval_out",
225 | "timedelta_in",
226 | ]
227 |
--------------------------------------------------------------------------------
/playbooks/test.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - hosts: all
3 | gather_facts: no
4 | become_user: postgres
5 | become: true
6 | tasks: []
7 | # - name: Create database
8 | # postgresql_db:
9 | # name: "icinga2"
10 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
11 | # - name: "Create functions (loop)"
12 | # postgresql_query:
13 | # db: "icinga2"
14 | # query: "{{ item }}"
15 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
16 | # with_items:
17 | # - "DROP FUNCTION IF EXISTS from_unixtime(bigint);"
18 | # - >
19 | # CREATE FUNCTION from_unixtime(bigint) RETURNS timestamp AS $$
20 | # SELECT to_timestamp($1) AT TIME ZONE 'UTC' AS result
21 | # $$ LANGUAGE sql;
22 | # - "DROP FUNCTION IF EXISTS unix_timestamp(timestamp WITH TIME ZONE);"
23 | # - >
24 | # CREATE OR REPLACE FUNCTION unix_timestamp(timestamp) RETURNS bigint AS '
25 | # SELECT CAST(EXTRACT(EPOCH FROM $1) AS bigint) AS result;
26 | # ' LANGUAGE sql;
27 | # - >
28 | # CREATE OR REPLACE FUNCTION updatedbversion(version_i TEXT) RETURNS void AS $$
29 | # BEGIN
30 | # IF EXISTS( SELECT * FROM icinga_dbversion WHERE name='idoutils')
31 | # THEN
32 | # UPDATE icinga_dbversion
33 | # SET version=version_i, modify_time=NOW()
34 | # WHERE name='idoutils';
35 | # ELSE
36 | # INSERT INTO icinga_dbversion (dbversion_id, name, version, create_time, modify_time) VALUES ('1', 'idoutils', version_i, NOW(), NOW());
37 | # END IF;
38 | # RETURN;
39 | # END;
40 | # $$ LANGUAGE plpgsql;
41 | # - name: "Create table #1"
42 | # postgresql_table:
43 | # name: icinga_acknowledgements
44 | # columns:
45 | # - 'acknowledgement_id bigserial'
46 | # - 'instance_id bigint default 0'
47 | # - 'entry_time timestamp'
48 | # - 'entry_time_usec INTEGER default 0'
49 | # - 'acknowledgement_type INTEGER default 0'
50 | # - 'object_id bigint default 0'
51 | # - 'state INTEGER default 0'
52 | # - "author_name TEXT default ''"
53 | # - "comment_data TEXT default ''"
54 | # - 'is_sticky INTEGER default 0'
55 | # - 'persistent_comment INTEGER default 0'
56 | # - 'notify_contacts INTEGER default 0'
57 | # - 'end_time timestamp'
58 | # - 'CONSTRAINT PK_acknowledgement_id PRIMARY KEY (acknowledgement_id)'
59 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
60 | # - name: "Create table #2"
61 | # postgresql_table:
62 | # db: icinga2
63 | # name: icinga_commands
64 | # columns:
65 | # - 'command_id bigserial'
66 | # - 'instance_id bigint default 0'
67 | # - 'config_type INTEGER default 0'
68 | # - 'object_id bigint default 0'
69 | # - "command_line TEXT default ''"
70 | # - 'config_hash varchar(64) DEFAULT NULL'
71 | # - 'CONSTRAINT PK_command_id PRIMARY KEY (command_id)'
72 | # - 'CONSTRAINT UQ_commands UNIQUE (instance_id,object_id,config_type)'
73 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
74 | # - name: "Create user 1"
75 | # postgresql_user:
76 | # name: testuser
77 | # db: icinga2
78 | # priv: ALL
79 | # password: 'THAT'
80 | # # does not work
81 | # encrypted: true
82 | # role_attr_flags: superuser
83 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
84 | # - name: "Create group 1"
85 | # postgresql_user:
86 | # name: read_only
87 | # db: icinga2
88 | # priv: ALL
89 | # role_attr_flags: NOLOGIN
90 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
91 | # When got bitten by SELinux,
92 | # set system_u:object_r:postgresql_db_t:s0 on target dir
93 | # this will fix things
94 | # - name: "Create tablespace"
95 | # postgresql_tablespace:
96 | # name: this_test
97 | # state: absent
98 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
99 | # location: /tmp/_pg_test_tablespace_
100 | # - name: "Create sequence"
101 | # postgresql_sequence:
102 | # name: serial
103 | # start: 101
104 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
105 | # - name: "Create sequence #2"
106 | # postgresql_sequence:
107 | # name: serial
108 | # start: 101
109 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
110 | # - name: "Delete sequence #2"
111 | # postgresql_sequence:
112 | # name: serial
113 | # state: absent
114 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
115 | # - name: "Grant privileges"
116 | # postgresql_privs:
117 | # database: icinga2
118 | # state: present
119 | # privs: ALL
120 | # type: table
121 | # objs: icinga_commands
122 | # schema: public
123 | # roles: testuser
124 | # grant_option: no
125 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
126 | # - name: "Create schema"
127 | # postgresql_schema:
128 | # db: icinga2
129 | # name: synthezoid
130 | # state: present
131 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
132 | # - name: "Pingy"
133 | # postgresql_ping:
134 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
135 | # - name: "Info"
136 | # postgresql_info:
137 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
138 | # - name: "Adds pg_trgm extension"
139 | # postgresql_ext:
140 | # name: pg_trgm
141 | # db: icinga2
142 | # schema: synthezoid
143 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
144 | # - name: "Add index"
145 | # postgresql_idx:
146 | # login_unix_socket: "/var/run/postgresql/.s.PGSQL.5432"
147 | # db: icinga2
148 | # table: "icinga_commands"
149 | # idxname: "test"
150 | # columns:
151 | # - "command_id"
152 | # state: present
153 | # - name: "pg_hda.conf content"
154 | # postgresql_pg_hba:
155 | # dest: /var/opt/rh/rh-postgresql12/lib/pgsql/data/pg_hba.conf
156 | # - name: Add language plpython
157 | # postgresql_lang:
158 | # db: icinga2
159 | # lang: plpythonu
160 | # state: present
161 | # trust: yes
162 | # force_trust: yes
163 | # - name: Add slot
164 | # postgresql_slot:
165 | # slot_name: physical_gg
166 | # db: icinga2
167 | # state: present
168 | # - name: Set wal_log_hints parameter to default value (remove parameter from postgresql.auto.conf)
169 | # postgresql_set:
170 | # name: wal_log_hints
171 | # value: default
172 | # - name: Give ownership
173 | # postgresql_owner:
174 | # db: icinga2
175 | # obj_name: icinga_commands
176 | # obj_type: table
177 | # new_owner: testuser
178 | # - name: Tets publication
179 | # postgresql_publication:
180 | # db: icinga2
181 | # name: test_pub
182 | # state: present
183 | # tables:
184 | # - icinga_commands
185 | # - name: Copy data from acme table to file /tmp/data.txt in text format, TAB-separated
186 | # postgresql_copy:
187 | # src: icinga_commands
188 | # copy_to: /tmp/data.txt
189 | # - name: Grant role read_only to test user
190 | # postgresql_membership:
191 | # group: read_only
192 | # target_roles:
193 | # - testuser
194 | # state: present
--------------------------------------------------------------------------------
/playbooks/module_utils/pg8000/native.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 | from .converters import (
4 | BIGINT,
5 | BOOLEAN,
6 | BOOLEAN_ARRAY,
7 | BYTES,
8 | CHAR,
9 | CHAR_ARRAY,
10 | DATE,
11 | FLOAT,
12 | FLOAT_ARRAY,
13 | INET,
14 | INT2VECTOR,
15 | INTEGER,
16 | INTEGER_ARRAY,
17 | INTERVAL,
18 | JSON,
19 | JSONB,
20 | MACADDR,
21 | NAME,
22 | NAME_ARRAY,
23 | NULLTYPE,
24 | NUMERIC,
25 | NUMERIC_ARRAY,
26 | OID,
27 | PGInterval,
28 | STRING,
29 | TEXT,
30 | TEXT_ARRAY,
31 | TIME,
32 | TIMESTAMP,
33 | TIMESTAMPTZ,
34 | UNKNOWN,
35 | UUID_TYPE,
36 | VARCHAR,
37 | VARCHAR_ARRAY,
38 | XID,
39 | make_params,
40 | )
41 | from .core import CoreConnection
42 | from .exceptions import DatabaseError, Error, InterfaceError
43 |
44 |
45 | # Copyright (c) 2007-2009, Mathieu Fenniak
46 | # Copyright (c) The Contributors
47 | # All rights reserved.
48 | #
49 | # Redistribution and use in source and binary forms, with or without
50 | # modification, are permitted provided that the following conditions are
51 | # met:
52 | #
53 | # * Redistributions of source code must retain the above copyright notice,
54 | # this list of conditions and the following disclaimer.
55 | # * Redistributions in binary form must reproduce the above copyright notice,
56 | # this list of conditions and the following disclaimer in the documentation
57 | # and/or other materials provided with the distribution.
58 | # * The name of the author may not be used to endorse or promote products
59 | # derived from this software without specific prior written permission.
60 | #
61 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
62 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
63 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
64 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
65 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
66 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
67 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
68 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
69 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
70 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
71 | # POSSIBILITY OF SUCH DAMAGE.
72 |
73 | __author__ = "Mathieu Fenniak"
74 |
75 | # Contribution:
76 | # pg8000 driver adaptation for Ansible drop-in
77 | # (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
78 | # Welcome to https://t.me/pro_ansible for discussion and support
79 | # License: please see above
80 |
81 | __authors__ = ["Mathieu Fenniak", "Sergey Pechenko"]
82 |
83 | def to_statement(query):
84 | OUTSIDE = 0 # outside quoted string
85 | INSIDE_SQ = 1 # inside single-quote string '...'
86 | INSIDE_QI = 2 # inside quoted identifier "..."
87 | INSIDE_ES = 3 # inside escaped single-quote string, E'...'
88 | INSIDE_PN = 4 # inside parameter name eg. :name
89 | INSIDE_CO = 5 # inside inline comment eg. --
90 |
91 | in_quote_escape = False
92 | placeholders = []
93 | output_query = []
94 | state = OUTSIDE
95 | prev_c = None
96 | for i, c in enumerate(query):
97 | if i + 1 < len(query):
98 | next_c = query[i + 1]
99 | else:
100 | next_c = None
101 |
102 | if state == OUTSIDE:
103 | if c == "'":
104 | output_query.append(c)
105 | if prev_c == "E":
106 | state = INSIDE_ES
107 | else:
108 | state = INSIDE_SQ
109 | elif c == '"':
110 | output_query.append(c)
111 | state = INSIDE_QI
112 | elif c == "-":
113 | output_query.append(c)
114 | if prev_c == "-":
115 | state = INSIDE_CO
116 | elif c == ":" and next_c not in ":=" and prev_c != ":":
117 | state = INSIDE_PN
118 | placeholders.append("")
119 | else:
120 | output_query.append(c)
121 |
122 | elif state == INSIDE_SQ:
123 | if c == "'":
124 | if in_quote_escape:
125 | in_quote_escape = False
126 | else:
127 | if next_c == "'":
128 | in_quote_escape = True
129 | else:
130 | state = OUTSIDE
131 | output_query.append(c)
132 |
133 | elif state == INSIDE_QI:
134 | if c == '"':
135 | state = OUTSIDE
136 | output_query.append(c)
137 |
138 | elif state == INSIDE_ES:
139 | if c == "'" and prev_c != "\\":
140 | # check for escaped single-quote
141 | state = OUTSIDE
142 | output_query.append(c)
143 |
144 | elif state == INSIDE_PN:
145 | placeholders[-1] += c
146 | if next_c is None or (not next_c.isalnum() and next_c != "_"):
147 | state = OUTSIDE
148 | try:
149 | pidx = placeholders.index(placeholders[-1], 0, -1)
150 | output_query.append("$" + str(pidx + 1))
151 | del placeholders[-1]
152 | except ValueError:
153 | output_query.append("$" + str(len(placeholders)))
154 |
155 | elif state == INSIDE_CO:
156 | output_query.append(c)
157 | if c == "\n":
158 | state = OUTSIDE
159 |
160 | prev_c = c
161 |
162 | for reserved in ("types", "stream"):
163 | if reserved in placeholders:
164 | raise InterfaceError(
165 | "The name '" + reserved + "' can't be used as a placeholder "
166 | "because it's used for another purpose."
167 | )
168 |
169 | def make_vals(args):
170 | vals = []
171 | for p in placeholders:
172 | try:
173 | vals.append(args[p])
174 | except KeyError:
175 | raise InterfaceError(
176 | "There's a placeholder '" + p + "' in the query, but "
177 | "no matching keyword argument."
178 | )
179 | return tuple(vals)
180 |
181 | return "".join(output_query), make_vals
182 |
183 |
184 | class Connection(CoreConnection):
185 | def __init__(self, *args, **kwargs):
186 | super().__init__(*args, **kwargs)
187 | self._context = None
188 |
189 | @property
190 | def columns(self):
191 | context = self._context
192 | if context is None:
193 | return None
194 | return context.columns
195 |
196 | @property
197 | def row_count(self):
198 | context = self._context
199 | if context is None:
200 | return None
201 | return context.row_count
202 |
203 | def run(self, sql, stream=None, types=None, **params):
204 | statement, make_vals = to_statement(sql)
205 | if types is None:
206 | oids = None
207 | else:
208 | oids = make_vals(defaultdict(lambda: None, types))
209 |
210 | self._context = self.execute_unnamed(
211 | statement, make_vals(params), input_oids=oids, stream=stream
212 | )
213 | return self._context.rows
214 |
215 | def prepare(self, sql):
216 | return PreparedStatement(self, sql)
217 |
218 |
219 | class PreparedStatement:
220 | def __init__(self, con, sql):
221 | self.con = con
222 | self.statement, self.make_vals = to_statement(sql)
223 | self.name_map = {}
224 |
225 | @property
226 | def columns(self):
227 | return self._context.columns
228 |
229 | def run(self, stream=None, **params):
230 | oids, params = make_params(self.con.py_types, self.make_vals(params))
231 |
232 | try:
233 | name_bin, columns, input_funcs = self.name_map[oids]
234 | except KeyError:
235 | name_bin, columns, input_funcs = self.name_map[
236 | oids
237 | ] = self.con.prepare_statement(self.statement, oids)
238 |
239 | self._context = self.con.execute_named(name_bin, params, columns, input_funcs)
240 |
241 | return self._context.rows
242 |
243 | def close(self):
244 | for statement_name_bin, _, _ in self.name_map.values():
245 | self.con.close_prepared_statement(statement_name_bin)
246 |
247 | self.name_map.clear()
248 |
249 |
250 | __all__ = [
251 | "BIGINT",
252 | "BOOLEAN",
253 | "BOOLEAN_ARRAY",
254 | "BYTES",
255 | "CHAR",
256 | "CHAR_ARRAY",
257 | "DATE",
258 | "DatabaseError",
259 | "Error",
260 | "FLOAT",
261 | "FLOAT_ARRAY",
262 | "INET",
263 | "INT2VECTOR",
264 | "INTEGER",
265 | "INTEGER_ARRAY",
266 | "INTERVAL",
267 | "InterfaceError",
268 | "JSON",
269 | "JSONB",
270 | "MACADDR",
271 | "NAME",
272 | "NAME_ARRAY",
273 | "NULLTYPE",
274 | "NUMERIC",
275 | "NUMERIC_ARRAY",
276 | "OID",
277 | "PGInterval",
278 | "STRING",
279 | "TEXT",
280 | "TEXT_ARRAY",
281 | "TIME",
282 | "TIMESTAMP",
283 | "TIMESTAMPTZ",
284 | "UNKNOWN",
285 | "UUID_TYPE",
286 | "VARCHAR",
287 | "VARCHAR_ARRAY",
288 | "XID",
289 | ]
290 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_schema.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2016, Ansible Project
5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 |
7 | # Contribution:
8 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
9 | # Welcome to https://t.me/pro_ansible for discussion and support
10 | # License: please see above
11 |
12 | from __future__ import absolute_import, division, print_function
13 | __metaclass__ = type
14 |
15 | ANSIBLE_METADATA = {'metadata_version': '1.1',
16 | 'status': ['preview'],
17 | 'supported_by': 'community'}
18 |
19 | DOCUMENTATION = r'''
20 | ---
21 | module: postgresql_schema
22 | short_description: Add or remove PostgreSQL schema
23 | description:
24 | - Add or remove PostgreSQL schema.
25 | version_added: '2.3'
26 | options:
27 | name:
28 | description:
29 | - Name of the schema to add or remove.
30 | required: true
31 | type: str
32 | aliases:
33 | - schema
34 | database:
35 | description:
36 | - Name of the database to connect to and add or remove the schema.
37 | type: str
38 | default: postgres
39 | aliases:
40 | - db
41 | - login_db
42 | owner:
43 | description:
44 | - Name of the role to set as owner of the schema.
45 | type: str
46 | session_role:
47 | version_added: '2.8'
48 | description:
49 | - Switch to session_role after connecting.
50 | - The specified session_role must be a role that the current login_user is a member of.
51 | - Permissions checking for SQL commands is carried out as though the session_role
52 | were the one that had logged in originally.
53 | type: str
54 | state:
55 | description:
56 | - The schema state.
57 | type: str
58 | default: present
59 | choices: [ absent, present ]
60 | cascade_drop:
61 | description:
62 | - Drop schema with CASCADE to remove child objects.
63 | type: bool
64 | default: false
65 | version_added: '2.8'
66 | ssl_mode:
67 | description:
68 | - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
69 | - See https://www.postgresql.org/docs/current/static/libpq-ssl.html for more information on the modes.
70 | - Default of C(prefer) matches libpq default.
71 | type: str
72 | default: prefer
73 | choices: [ allow, disable, prefer, require, verify-ca, verify-full ]
74 | version_added: '2.8'
75 | ca_cert:
76 | description:
77 | - Specifies the name of a file containing SSL certificate authority (CA) certificate(s).
78 | - If the file exists, the server's certificate will be verified to be signed by one of these authorities.
79 | type: str
80 | aliases: [ ssl_rootcert ]
81 | version_added: '2.8'
82 | seealso:
83 | - name: PostgreSQL schemas
84 | description: General information about PostgreSQL schemas.
85 | link: https://www.postgresql.org/docs/current/ddl-schemas.html
86 | - name: CREATE SCHEMA reference
87 | description: Complete reference of the CREATE SCHEMA command documentation.
88 | link: https://www.postgresql.org/docs/current/sql-createschema.html
89 | - name: ALTER SCHEMA reference
90 | description: Complete reference of the ALTER SCHEMA command documentation.
91 | link: https://www.postgresql.org/docs/current/sql-alterschema.html
92 | - name: DROP SCHEMA reference
93 | description: Complete reference of the DROP SCHEMA command documentation.
94 | link: https://www.postgresql.org/docs/current/sql-dropschema.html
95 | author:
96 | - Flavien Chantelot (@Dorn-)
97 | - Thomas O'Donnell (@andytom)
98 | extends_documentation_fragment: postgres
99 | '''
100 |
101 | EXAMPLES = r'''
102 | - name: Create a new schema with name acme in test database
103 | postgresql_schema:
104 | db: test
105 | name: acme
106 |
107 | - name: Create a new schema acme with a user bob who will own it
108 | postgresql_schema:
109 | name: acme
110 | owner: bob
111 |
112 | - name: Drop schema "acme" with cascade
113 | postgresql_schema:
114 | name: acme
115 | state: absent
116 | cascade_drop: yes
117 | '''
118 |
119 | RETURN = r'''
120 | schema:
121 | description: Name of the schema.
122 | returned: success, changed
123 | type: str
124 | sample: "acme"
125 | queries:
126 | description: List of executed queries.
127 | returned: always
128 | type: list
129 | sample: ["CREATE SCHEMA \"acme\""]
130 | '''
131 |
132 | import traceback
133 |
134 |
135 | from ansible.module_utils.basic import AnsibleModule
136 | from ansible.module_utils.postgres import (
137 | connect_to_db,
138 | get_conn_params,
139 | postgres_common_argument_spec,
140 | )
141 | from ansible.module_utils.database import SQLParseError, pg_quote_identifier
142 | from ansible.module_utils._text import to_native
143 |
144 | executed_queries = []
145 |
146 |
147 | class NotSupportedError(Exception):
148 | pass
149 |
150 |
151 | # ===========================================
152 | # PostgreSQL module specific support methods.
153 | #
154 |
155 | def set_owner(cursor, schema, owner):
156 | query = "ALTER SCHEMA %s OWNER TO %s" % (
157 | pg_quote_identifier(schema, 'schema'),
158 | pg_quote_identifier(owner, 'role'))
159 | cursor.execute(query)
160 | executed_queries.append(query)
161 | return True
162 |
163 |
164 | def get_schema_info(cursor, schema):
165 | query = ("SELECT schema_owner AS owner "
166 | "FROM information_schema.schemata "
167 | "WHERE schema_name = (%s)")
168 | cursor.execute(query, [schema])
169 | return cursor.fetchone()
170 |
171 |
172 | def schema_exists(cursor, schema):
173 | query = ("SELECT schema_name FROM information_schema.schemata "
174 | "WHERE schema_name = (%s)")
175 | cursor.execute(query, [schema])
176 | return cursor.rowcount == 1
177 |
178 |
179 | def schema_delete(cursor, schema, cascade):
180 | if schema_exists(cursor, schema):
181 | query = "DROP SCHEMA %s" % pg_quote_identifier(schema, 'schema')
182 | if cascade:
183 | query += " CASCADE"
184 | cursor.execute(query)
185 | executed_queries.append(query)
186 | return True
187 | else:
188 | return False
189 |
190 |
191 | def schema_create(cursor, schema, owner):
192 | if not schema_exists(cursor, schema):
193 | query_fragments = ['CREATE SCHEMA %s' % pg_quote_identifier(schema, 'schema')]
194 | if owner:
195 | query_fragments.append('AUTHORIZATION %s' % pg_quote_identifier(owner, 'role'))
196 | query = ' '.join(query_fragments)
197 | cursor.execute(query)
198 | executed_queries.append(query)
199 | return True
200 | else:
201 | schema_info = get_schema_info(cursor, schema)
202 | if owner and owner != schema_info['owner']:
203 | return set_owner(cursor, schema, owner)
204 | else:
205 | return False
206 |
207 |
208 | def schema_matches(cursor, schema, owner):
209 | if not schema_exists(cursor, schema):
210 | return False
211 | else:
212 | schema_info = get_schema_info(cursor, schema)
213 | if owner and owner != schema_info['owner']:
214 | return False
215 | else:
216 | return True
217 |
218 | # ===========================================
219 | # Module execution.
220 | #
221 |
222 |
223 | def main():
224 | argument_spec = postgres_common_argument_spec()
225 | argument_spec.update(
226 | schema=dict(type="str", required=True, aliases=['name']),
227 | owner=dict(type="str", default=""),
228 | database=dict(type="str", default="postgres", aliases=["db", "login_db"]),
229 | cascade_drop=dict(type="bool", default=False),
230 | state=dict(type="str", default="present", choices=["absent", "present"]),
231 | session_role=dict(type="str"),
232 | )
233 |
234 | module = AnsibleModule(
235 | argument_spec=argument_spec,
236 | supports_check_mode=True,
237 | )
238 |
239 | schema = module.params["schema"]
240 | owner = module.params["owner"]
241 | state = module.params["state"]
242 | cascade_drop = module.params["cascade_drop"]
243 | changed = False
244 |
245 | conn_params = get_conn_params(module, module.params)
246 | db_connection = connect_to_db(module, conn_params, autocommit=True)
247 | cursor = db_connection.cursor()
248 |
249 | try:
250 | if module.check_mode:
251 | if state == "absent":
252 | changed = not schema_exists(cursor, schema)
253 | elif state == "present":
254 | changed = not schema_matches(cursor, schema, owner)
255 | module.exit_json(changed=changed, schema=schema)
256 |
257 | if state == "absent":
258 | try:
259 | changed = schema_delete(cursor, schema, cascade_drop)
260 | except SQLParseError as e:
261 | module.fail_json(msg=to_native(e), exception=traceback.format_exc())
262 |
263 | elif state == "present":
264 | try:
265 | changed = schema_create(cursor, schema, owner)
266 | except SQLParseError as e:
267 | module.fail_json(msg=to_native(e), exception=traceback.format_exc())
268 | except NotSupportedError as e:
269 | module.fail_json(msg=to_native(e), exception=traceback.format_exc())
270 | except SystemExit:
271 | # Avoid catching this on Python 2.4
272 | raise
273 | except Exception as e:
274 | module.fail_json(msg="Database query failed: %s" % to_native(e), exception=traceback.format_exc())
275 |
276 | db_connection.close()
277 | module.exit_json(changed=changed, schema=schema, queries=executed_queries)
278 |
279 |
280 | if __name__ == '__main__':
281 | main()
282 |
--------------------------------------------------------------------------------
/playbooks/module_utils/postgres.py:
--------------------------------------------------------------------------------
1 | # This code is part of Ansible, but is an independent component.
2 | # This particular file snippet, and this file snippet only, is BSD licensed.
3 | # Modules you write using this snippet, which is embedded dynamically by Ansible
4 | # still belong to the author of the module, and may assign their own license
5 | # to the complete work.
6 | #
7 | # Copyright (c), Ted Timmons , 2017.
8 | # Most of this was originally added by other creators in the postgresql_user module.
9 | # All rights reserved.
10 | #
11 | # Redistribution and use in source and binary forms, with or without modification,
12 | # are permitted provided that the following conditions are met:
13 | #
14 | # * Redistributions of source code must retain the above copyright
15 | # notice, this list of conditions and the following disclaimer.
16 | # * Redistributions in binary form must reproduce the above copyright notice,
17 | # this list of conditions and the following disclaimer in the documentation
18 | # and/or other materials provided with the distribution.
19 | #
20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
23 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
24 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
25 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
28 | # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
30 | # Contribution:
31 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
32 | # Welcome to https://t.me/pro_ansible for discussion and support
33 | # License: please see above
34 |
35 | pg8k = None # This line needs for unit tests (?)
36 | try:
37 | from . import pg8000
38 |
39 | HAS_PG8K = True
40 | pg8000.paramstyle = 'pyformat'
41 | DataError = pg8000.DataError
42 | DatabaseError = pg8000.DatabaseError
43 | IntegrityError = pg8000.IntegrityError
44 | InterfaceError = pg8000.InterfaceError
45 | InternalError = pg8000.InternalError
46 | NotSupportedError = pg8000.NotSupportedError
47 | OperationalError = pg8000.OperationalError
48 | ProgrammingError = pg8000.ProgrammingError
49 | except ImportError:
50 | HAS_PG8K = False
51 |
52 | from ansible.module_utils.basic import missing_required_lib
53 | from ansible.module_utils._text import to_native
54 | from ansible.module_utils.six import iteritems
55 | from distutils.version import LooseVersion
56 |
57 |
58 | def dict_wrap(cursor: pg8000.Cursor, values: list):
59 | return dict(zip([x[0] for x in cursor.description], values))
60 |
61 |
62 | def postgres_common_argument_spec():
63 | """
64 | Return a dictionary with connection options.
65 |
66 | The options are commonly used by most of PostgreSQL modules.
67 | """
68 | return dict(
69 | login_user=dict(default='postgres'),
70 | login_password=dict(default=''), # no_log=True),
71 | login_host=dict(default=''),
72 | login_unix_socket=dict(default=''),
73 | port=dict(type='int', default=5432, aliases=['login_port']),
74 | ssl_mode=dict(default='prefer', choices=['allow', 'disable', 'prefer', 'require', 'verify-ca', 'verify-full']),
75 | ca_cert=dict(aliases=['ssl_rootcert']),
76 | )
77 |
78 |
79 | def ensure_required_libs(module):
80 | """Check required libraries."""
81 | if not HAS_PG8K:
82 | module.fail_json(msg=missing_required_lib('pg8000'))
83 | # broken at the moment
84 | # if module.params.get('ca_cert') and LooseVersion(psycopg2.__version__) < LooseVersion('2.4.3'):
85 | # module.fail_json(msg='psycopg2 must be at least 2.4.3 in order to use the ca_cert parameter')
86 |
87 |
88 | def connect_to_db(module, conn_params, autocommit=False, fail_on_conn=True):
89 | """Connect to a PostgreSQL database.
90 |
91 | Return pg8000 connection object.
92 |
93 | Args:
94 | module (AnsibleModule) -- object of ansible.module_utils.basic.AnsibleModule class
95 | conn_params (dict) -- dictionary with connection parameters
96 |
97 | Kwargs:
98 | autocommit (bool) -- commit automatically (default False)
99 | fail_on_conn (bool) -- fail if connection failed or just warn and return None (default True)
100 | """
101 | ensure_required_libs(module)
102 |
103 | db_connection = None
104 | try:
105 | del conn_params['sslmode']
106 | db_connection = pg8000.connect(**conn_params)
107 | if autocommit:
108 | db_connection.autocommit = True
109 | version_tuple_str = [tple[1] for tple in db_connection.parameter_statuses if tple[0] == b'server_version'][
110 | 0].decode('utf-8')
111 | version_list = (version_tuple_str.split('.') + [0])[0:3]
112 | db_connection.server_version = int(''.join(["{:02}".format(int(x)) for x in version_list]))
113 | # Switch role, if specified:
114 | if module.params.get('session_role'):
115 | cursor = db_connection.cursor()
116 | try:
117 | cursor.execute('SET ROLE %s' % module.params['session_role'])
118 | except Exception as e:
119 | module.fail_json(msg="Could not switch role: %s" % to_native(e))
120 | finally:
121 | cursor.close()
122 |
123 | except TypeError as e:
124 | if 'sslrootcert' in e.args[0]:
125 | module.fail_json(msg='Postgresql server must be at least '
126 | 'version 8.4 to support sslrootcert')
127 |
128 | if fail_on_conn:
129 | module.fail_json(msg="unable to connect to database: %s" % to_native(e))
130 | else:
131 | module.warn("PostgreSQL server is unavailable: %s" % to_native(e))
132 | db_connection = None
133 |
134 | except Exception as e:
135 | if fail_on_conn:
136 | module.fail_json(msg="unable to connect to database: %s" % to_native(e))
137 | else:
138 | module.warn("PostgreSQL server is unavailable: %s" % to_native(e))
139 | db_connection = None
140 |
141 | return db_connection
142 |
143 |
144 | def exec_sql(obj, query, query_params=None, ddl=False, add_to_executed=True, dont_exec=False, **kwargs):
145 | """Execute SQL.
146 |
147 | Auxiliary function for PostgreSQL user classes.
148 |
149 | Returns a query result if possible or True/False if ddl=True arg was passed.
150 | It necessary for statements that don't return any result (like DDL queries).
151 |
152 | Args:
153 | obj (obj) -- must be an object of a user class.
154 | The object must have module (AnsibleModule class object) and
155 | cursor (psycopg cursor object) attributes
156 | query (str) -- SQL query to execute
157 |
158 | Kwargs:
159 | query_params (dict or tuple) -- Query parameters to prevent SQL injections,
160 | could be a dict or tuple
161 | ddl (bool) -- must return True or False instead of rows (typical for DDL queries)
162 | (default False)
163 | add_to_executed (bool) -- append the query to obj.executed_queries attribute
164 | dont_exec (bool) -- used with add_to_executed=True to generate a query, add it
165 | to obj.executed_queries list and return True (default False)
166 | """
167 |
168 | if dont_exec:
169 | # This is usually needed to return queries in check_mode
170 | # without execution
171 | query = obj.cursor.mogrify(query, query_params)
172 | if add_to_executed:
173 | obj.executed_queries.append(query)
174 |
175 | return True
176 |
177 | try:
178 | if query_params is not None:
179 | obj.cursor.execute(query, query_params)
180 | else:
181 | obj.cursor.execute(query)
182 |
183 | if add_to_executed:
184 | if query_params is not None:
185 | obj.executed_queries.append(query % (*query_params,))
186 | else:
187 | obj.executed_queries.append(query)
188 |
189 | if not ddl:
190 | res = obj.cursor.fetchall()
191 | if 'hacky_dict' in kwargs:
192 | tmp_descr = (x[0] for x in obj.cursor.description)
193 | tmp_res = [dict(zip(tmp_descr, x)) for x in res]
194 | return tmp_res
195 | else:
196 | return res
197 | return True
198 | except Exception as e:
199 | obj.module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e)))
200 | return False
201 |
202 |
203 | def get_conn_params(module, params_dict, warn_db_default=True):
204 | """Get connection parameters from the passed dictionary.
205 |
206 | Return a dictionary with parameters to connect to PostgreSQL server.
207 |
208 | Args:
209 | module (AnsibleModule) -- object of ansible.module_utils.basic.AnsibleModule class
210 | params_dict (dict) -- dictionary with variables
211 |
212 | Kwargs:
213 | warn_db_default (bool) -- warn that the default DB is used (default True)
214 | """
215 | # To use defaults values, keyword arguments must be absent, so
216 | # check which values are empty and don't include in the return dictionary
217 | params_map = {
218 | "login_host": "host",
219 | "login_user": "user",
220 | "login_password": "password",
221 | "port": "port",
222 | "ssl_mode": "sslmode",
223 | "ca_cert": "sslrootcert"
224 | }
225 |
226 | # Might be different in the modules:
227 | if params_dict.get('db'):
228 | params_map['db'] = 'database'
229 | elif params_dict.get('database'):
230 | params_map['database'] = 'database'
231 | elif params_dict.get('login_db'):
232 | params_map['login_db'] = 'database'
233 | else:
234 | if warn_db_default:
235 | module.warn('Database name has not been passed, '
236 | 'used default database to connect to.')
237 |
238 | kw = dict((params_map[k], v) for (k, v) in iteritems(params_dict)
239 | if k in params_map and v != '' and v is not None)
240 |
241 | # If a login_unix_socket is specified, incorporate it here.
242 | is_localhost = "host" not in kw or kw["host"] is None or kw["host"] == "localhost"
243 | if is_localhost and params_dict["login_unix_socket"] != "":
244 | kw["unix_sock"] = params_dict["login_unix_socket"]
245 |
246 | return kw
247 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_slot.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2019, John Scalia (@jscalia), Andrew Klychkov (@Andersson007)
5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 |
7 | # Contribution:
8 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
9 | # Welcome to https://t.me/pro_ansible for discussion and support
10 | # License: please see above
11 |
12 | from __future__ import absolute_import, division, print_function
13 | __metaclass__ = type
14 |
15 | ANSIBLE_METADATA = {'metadata_version': '1.1',
16 | 'status': ['preview'],
17 | 'supported_by': 'community'}
18 |
19 | DOCUMENTATION = '''
20 | ---
21 | module: postgresql_slot
22 | short_description: Add or remove replication slots from a PostgreSQL database
23 | description:
24 | - Add or remove physical or logical replication slots from a PostgreSQL database.
25 | version_added: '2.8'
26 |
27 | options:
28 | name:
29 | description:
30 | - Name of the replication slot to add or remove.
31 | type: str
32 | required: yes
33 | aliases:
34 | - slot_name
35 | slot_type:
36 | description:
37 | - Slot type.
38 | type: str
39 | default: physical
40 | choices: [ logical, physical ]
41 | state:
42 | description:
43 | - The slot state.
44 | - I(state=present) implies the slot must be present in the system.
45 | - I(state=absent) implies the I(groups) must be revoked from I(target_roles).
46 | type: str
47 | default: present
48 | choices: [ absent, present ]
49 | immediately_reserve:
50 | description:
51 | - Optional parameter that when C(yes) specifies that the LSN for this replication slot be reserved
52 | immediately, otherwise the default, C(no), specifies that the LSN is reserved on the first connection
53 | from a streaming replication client.
54 | - Is available from PostgreSQL version 9.6.
55 | - Uses only with I(slot_type=physical).
56 | - Mutually exclusive with I(slot_type=logical).
57 | type: bool
58 | default: no
59 | output_plugin:
60 | description:
61 | - All logical slots must indicate which output plugin decoder they're using.
62 | - This parameter does not apply to physical slots.
63 | - It will be ignored with I(slot_type=physical).
64 | type: str
65 | default: "test_decoding"
66 | db:
67 | description:
68 | - Name of database to connect to.
69 | type: str
70 | aliases:
71 | - login_db
72 | session_role:
73 | description:
74 | - Switch to session_role after connecting.
75 | The specified session_role must be a role that the current login_user is a member of.
76 | - Permissions checking for SQL commands is carried out as though
77 | the session_role were the one that had logged in originally.
78 | type: str
79 |
80 | notes:
81 | - Physical replication slots were introduced to PostgreSQL with version 9.4,
82 | while logical replication slots were added beginning with version 10.0.
83 |
84 | seealso:
85 | - name: PostgreSQL pg_replication_slots view reference
86 | description: Complete reference of the PostgreSQL pg_replication_slots view.
87 | link: https://www.postgresql.org/docs/current/view-pg-replication-slots.html
88 | - name: PostgreSQL streaming replication protocol reference
89 | description: Complete reference of the PostgreSQL streaming replication protocol documentation.
90 | link: https://www.postgresql.org/docs/current/protocol-replication.html
91 | - name: PostgreSQL logical replication protocol reference
92 | description: Complete reference of the PostgreSQL logical replication protocol documentation.
93 | link: https://www.postgresql.org/docs/current/protocol-logical-replication.html
94 |
95 | author:
96 | - John Scalia (@jscalia)
97 | - Andrew Klychkov (@Andersson007)
98 | extends_documentation_fragment: postgres
99 | '''
100 |
101 | EXAMPLES = r'''
102 | - name: Create physical_one physical slot if doesn't exist
103 | become_user: postgres
104 | postgresql_slot:
105 | slot_name: physical_one
106 | db: ansible
107 |
108 | - name: Remove physical_one slot if exists
109 | become_user: postgres
110 | postgresql_slot:
111 | slot_name: physical_one
112 | db: ansible
113 | state: absent
114 |
115 | - name: Create logical_one logical slot to the database acme if doesn't exist
116 | postgresql_slot:
117 | name: logical_slot_one
118 | slot_type: logical
119 | state: present
120 | output_plugin: custom_decoder_one
121 | db: "acme"
122 |
123 | - name: Remove logical_one slot if exists from the cluster running on another host and non-standard port
124 | postgresql_slot:
125 | name: logical_one
126 | login_host: mydatabase.example.org
127 | port: 5433
128 | login_user: ourSuperuser
129 | login_password: thePassword
130 | state: absent
131 | '''
132 |
133 | RETURN = r'''
134 | name:
135 | description: Name of the slot
136 | returned: always
137 | type: str
138 | sample: "physical_one"
139 | queries:
140 | description: List of executed queries.
141 | returned: always
142 | type: str
143 | sample: [ "SELECT pg_create_physical_replication_slot('physical_one', False, False)" ]
144 | '''
145 |
146 | from ansible.module_utils.basic import AnsibleModule
147 | from ansible.module_utils.postgres import (
148 | connect_to_db,
149 | exec_sql,
150 | get_conn_params,
151 | postgres_common_argument_spec,
152 | )
153 |
154 |
155 | # ===========================================
156 | # PostgreSQL module specific support methods.
157 | #
158 |
159 | class PgSlot(object):
160 | def __init__(self, module, cursor, name):
161 | self.module = module
162 | self.cursor = cursor
163 | self.name = name
164 | self.exists = False
165 | self.kind = ''
166 | self.__slot_exists()
167 | self.changed = False
168 | self.executed_queries = []
169 | version_tuple_str = [tple[1] for tple in cursor.connection.parameter_statuses if tple[0] == b'server_version'][
170 | 0].decode('utf-8')
171 | version_list = (version_tuple_str.split('.') + [0])[0:3]
172 | self.server_version = int(''.join(["{:02}".format(int(x)) for x in version_list]))
173 |
174 | def create(self, kind='physical', immediately_reserve=False, output_plugin=False, just_check=False):
175 | if self.exists:
176 | if self.kind == kind:
177 | return False
178 | else:
179 | self.module.warn("slot with name '%s' already exists "
180 | "but has another type '%s'" % (self.name, self.kind))
181 | return False
182 |
183 | if just_check:
184 | return None
185 |
186 |
187 | if kind == 'physical':
188 | # Check server version (needs for immedately_reserverd needs 9.6+):
189 | param_list = [self.name]
190 | if self.server_version < 96000:
191 | query = "SELECT pg_create_physical_replication_slot(%s)"
192 | else:
193 | query = "SELECT pg_create_physical_replication_slot(%s, %s)"
194 | param_list.append(immediately_reserve)
195 |
196 | self.changed = exec_sql(self, query,
197 | query_params=param_list,
198 | ddl=True)
199 |
200 | elif kind == 'logical':
201 | query = "SELECT pg_create_logical_replication_slot(%s, %s)"
202 | self.changed = exec_sql(self, query,
203 | query_params=[self.name, output_plugin], ddl=True)
204 |
205 | def drop(self):
206 | if not self.exists:
207 | return False
208 |
209 | query = "SELECT pg_drop_replication_slot(%s)"
210 | self.changed = exec_sql(self, query, query_params=[self.name], ddl=True)
211 |
212 | def __slot_exists(self):
213 | query = "SELECT slot_type FROM pg_replication_slots WHERE slot_name = %s"
214 | res = exec_sql(self, query, query_params=[self.name], add_to_executed=False)
215 | if res:
216 | self.exists = True
217 | self.kind = res[0][0]
218 |
219 |
220 | # ===========================================
221 | # Module execution.
222 | #
223 |
224 |
225 | def main():
226 | argument_spec = postgres_common_argument_spec()
227 | argument_spec.update(
228 | db=dict(type="str", aliases=["login_db"]),
229 | name=dict(type="str", aliases=["slot_name"]),
230 | slot_type=dict(type="str", default="physical", choices=["logical", "physical"]),
231 | immediately_reserve=dict(type="bool", default=False),
232 | session_role=dict(type="str"),
233 | output_plugin=dict(type="str", default="test_decoding"),
234 | state=dict(type="str", default="present", choices=["absent", "present"]),
235 | )
236 |
237 | module = AnsibleModule(
238 | argument_spec=argument_spec,
239 | supports_check_mode=True,
240 | )
241 |
242 | name = module.params["name"]
243 | slot_type = module.params["slot_type"]
244 | immediately_reserve = module.params["immediately_reserve"]
245 | state = module.params["state"]
246 | output_plugin = module.params["output_plugin"]
247 |
248 | if immediately_reserve and slot_type == 'logical':
249 | module.fail_json(msg="Module parameters immediately_reserve and slot_type=logical are mutually exclusive")
250 |
251 | # When slot_type is logical and parameter db is not passed,
252 | # the default database will be used to create the slot and
253 | # the user should know about this.
254 | # When the slot type is physical,
255 | # it doesn't matter which database will be used
256 | # because physical slots are global objects.
257 | if slot_type == 'logical':
258 | warn_db_default = True
259 | else:
260 | warn_db_default = False
261 |
262 | conn_params = get_conn_params(module, module.params, warn_db_default=warn_db_default)
263 | db_connection = connect_to_db(module, conn_params, autocommit=True)
264 | cursor = db_connection.cursor()
265 |
266 | ##################################
267 | # Create an object and do main job
268 | pg_slot = PgSlot(module, cursor, name)
269 |
270 | changed = False
271 |
272 | if module.check_mode:
273 | if state == "present":
274 | if not pg_slot.exists:
275 | changed = True
276 |
277 | pg_slot.create(slot_type, immediately_reserve, output_plugin, just_check=True)
278 |
279 | elif state == "absent":
280 | if pg_slot.exists:
281 | changed = True
282 | else:
283 | if state == "absent":
284 | pg_slot.drop()
285 |
286 | elif state == "present":
287 | pg_slot.create(slot_type, immediately_reserve, output_plugin)
288 |
289 | changed = pg_slot.changed
290 |
291 | db_connection.close()
292 | module.exit_json(changed=changed, name=name, queries=pg_slot.executed_queries)
293 |
294 |
295 | if __name__ == '__main__':
296 | main()
297 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_query.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2017, Felix Archambault
5 | # Copyright: (c) 2019, Andrew Klychkov (@Andersson007)
6 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7 |
8 | # Contribution:
9 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
10 | # Welcome to https://t.me/pro_ansible for discussion and support
11 | # License: please see above
12 |
13 | from __future__ import (absolute_import, division, print_function)
14 | __metaclass__ = type
15 |
16 | ANSIBLE_METADATA = {
17 | 'metadata_version': '1.1',
18 | 'supported_by': 'community',
19 | 'status': ['preview']
20 | }
21 |
22 | DOCUMENTATION = r'''
23 | ---
24 | module: postgresql_query
25 | short_description: Run PostgreSQL queries
26 | description:
27 | - Runs arbitrary PostgreSQL queries.
28 | - Can run queries from SQL script files.
29 | - Does not run against backup files. Use M(postgresql_db) with I(state=restore)
30 | to run queries on files made by pg_dump/pg_dumpall utilities.
31 | version_added: '2.8'
32 | options:
33 | query:
34 | description:
35 | - SQL query to run. Variables can be escaped with psycopg2 syntax U(http://initd.org/psycopg/docs/usage.html).
36 | type: str
37 | positional_args:
38 | description:
39 | - List of values to be passed as positional arguments to the query.
40 | When the value is a list, it will be converted to PostgreSQL array.
41 | - Mutually exclusive with I(named_args).
42 | type: list
43 | named_args:
44 | description:
45 | - Dictionary of key-value arguments to pass to the query.
46 | When the value is a list, it will be converted to PostgreSQL array.
47 | - Mutually exclusive with I(positional_args).
48 | type: dict
49 | path_to_script:
50 | description:
51 | - Path to SQL script on the remote host.
52 | - Returns result of the last query in the script.
53 | - Mutually exclusive with I(query).
54 | type: path
55 | session_role:
56 | description:
57 | - Switch to session_role after connecting. The specified session_role must
58 | be a role that the current login_user is a member of.
59 | - Permissions checking for SQL commands is carried out as though
60 | the session_role were the one that had logged in originally.
61 | type: str
62 | db:
63 | description:
64 | - Name of database to connect to and run queries against.
65 | type: str
66 | aliases:
67 | - login_db
68 | autocommit:
69 | description:
70 | - Execute in autocommit mode when the query can't be run inside a transaction block
71 | (e.g., VACUUM).
72 | - Mutually exclusive with I(check_mode).
73 | type: bool
74 | default: no
75 | version_added: '2.9'
76 | seealso:
77 | - module: postgresql_db
78 | author:
79 | - Felix Archambault (@archf)
80 | - Andrew Klychkov (@Andersson007)
81 | - Will Rouesnel (@wrouesnel)
82 | extends_documentation_fragment: postgres
83 | '''
84 |
85 | EXAMPLES = r'''
86 | - name: Simple select query to acme db
87 | postgresql_query:
88 | db: acme
89 | query: SELECT version()
90 |
91 | - name: Select query to db acme with positional arguments and non-default credentials
92 | postgresql_query:
93 | db: acme
94 | login_user: django
95 | login_password: mysecretpass
96 | query: SELECT * FROM acme WHERE id = %s AND story = %s
97 | positional_args:
98 | - 1
99 | - test
100 |
101 | - name: Select query to test_db with named_args
102 | postgresql_query:
103 | db: test_db
104 | query: SELECT * FROM test WHERE id = %(id_val)s AND story = %(story_val)s
105 | named_args:
106 | id_val: 1
107 | story_val: test
108 |
109 | - name: Insert query to test_table in db test_db
110 | postgresql_query:
111 | db: test_db
112 | query: INSERT INTO test_table (id, story) VALUES (2, 'my_long_story')
113 |
114 | - name: Run queries from SQL script
115 | postgresql_query:
116 | db: test_db
117 | path_to_script: /var/lib/pgsql/test.sql
118 | positional_args:
119 | - 1
120 |
121 | - name: Example of using autocommit parameter
122 | postgresql_query:
123 | db: test_db
124 | query: VACUUM
125 | autocommit: yes
126 |
127 | - name: >
128 | Insert data to the column of array type using positional_args.
129 | Note that we use quotes here, the same as for passing JSON, etc.
130 | postgresql_query:
131 | query: INSERT INTO test_table (array_column) VALUES (%s)
132 | positional_args:
133 | - '{1,2,3}'
134 |
135 | # Pass list and string vars as positional_args
136 | - name: Set vars
137 | set_fact:
138 | my_list:
139 | - 1
140 | - 2
141 | - 3
142 | my_arr: '{1, 2, 3}'
143 |
144 | - name: Select from test table by passing positional_args as arrays
145 | postgresql_query:
146 | query: SELECT * FROM test_array_table WHERE arr_col1 = %s AND arr_col2 = %s
147 | positional_args:
148 | - '{{ my_list }}'
149 | - '{{ my_arr|string }}'
150 | '''
151 |
152 | RETURN = r'''
153 | query:
154 | description: Query that was tried to be executed.
155 | returned: always
156 | type: str
157 | sample: 'SELECT * FROM bar'
158 | statusmessage:
159 | description: Attribute containing the message returned by the command.
160 | returned: always
161 | type: str
162 | sample: 'INSERT 0 1'
163 | query_result:
164 | description:
165 | - List of dictionaries in column:value form representing returned rows.
166 | returned: changed
167 | type: list
168 | sample: [{"Column": "Value1"},{"Column": "Value2"}]
169 | rowcount:
170 | description: Number of affected rows.
171 | returned: changed
172 | type: int
173 | sample: 5
174 | '''
175 |
176 |
177 | from ansible.module_utils.basic import AnsibleModule
178 | from ansible.module_utils.postgres import (
179 | connect_to_db,
180 | get_conn_params,
181 | postgres_common_argument_spec,
182 | )
183 | from ansible.module_utils._text import to_native
184 | from ansible.module_utils.six import iteritems
185 |
186 | # ===========================================
187 | # Module execution.
188 | #
189 |
190 | def list_to_pg_array(elem):
191 | """Convert the passed list to PostgreSQL array
192 | represented as a string.
193 |
194 | Args:
195 | elem (list): List that needs to be converted.
196 |
197 | Returns:
198 | elem (str): String representation of PostgreSQL array.
199 | """
200 | elem = str(elem).strip('[]')
201 | elem = '{' + elem + '}'
202 | return elem
203 |
204 |
205 | def convert_elements_to_pg_arrays(obj):
206 | """Convert list elements of the passed object
207 | to PostgreSQL arrays represented as strings.
208 |
209 | Args:
210 | obj (dict or list): Object whose elements need to be converted.
211 |
212 | Returns:
213 | obj (dict or list): Object with converted elements.
214 | """
215 | if isinstance(obj, dict):
216 | for (key, elem) in iteritems(obj):
217 | if isinstance(elem, list):
218 | obj[key] = list_to_pg_array(elem)
219 |
220 | elif isinstance(obj, list):
221 | for i, elem in enumerate(obj):
222 | if isinstance(elem, list):
223 | obj[i] = list_to_pg_array(elem)
224 |
225 | return obj
226 |
227 |
228 | def main():
229 | argument_spec = postgres_common_argument_spec()
230 | argument_spec.update(
231 | query=dict(type='str'),
232 | db=dict(type='str', aliases=['login_db']),
233 | positional_args=dict(type='list'),
234 | named_args=dict(type='dict'),
235 | session_role=dict(type='str'),
236 | path_to_script=dict(type='path'),
237 | autocommit=dict(type='bool', default=False),
238 | )
239 |
240 | module = AnsibleModule(
241 | argument_spec=argument_spec,
242 | mutually_exclusive=(('positional_args', 'named_args'),),
243 | supports_check_mode=True,
244 | )
245 |
246 | query = module.params["query"]
247 | positional_args = module.params["positional_args"]
248 | named_args = module.params["named_args"]
249 | path_to_script = module.params["path_to_script"]
250 | autocommit = module.params["autocommit"]
251 |
252 | if autocommit and module.check_mode:
253 | module.fail_json(msg="Using autocommit is mutually exclusive with check_mode")
254 |
255 | if positional_args and named_args:
256 | module.fail_json(msg="positional_args and named_args params are mutually exclusive")
257 |
258 | if path_to_script and query:
259 | module.fail_json(msg="path_to_script is mutually exclusive with query")
260 |
261 | if positional_args:
262 | positional_args = convert_elements_to_pg_arrays(positional_args)
263 |
264 | elif named_args:
265 | named_args = convert_elements_to_pg_arrays(named_args)
266 |
267 | if path_to_script:
268 | try:
269 | query = open(path_to_script, 'r').read()
270 | except Exception as e:
271 | module.fail_json(msg="Cannot read file '%s' : %s" % (path_to_script, to_native(e)))
272 |
273 | conn_params = get_conn_params(module, module.params)
274 | db_connection = connect_to_db(module, conn_params, autocommit=autocommit)
275 | cursor = db_connection.cursor()
276 |
277 | # Prepare args:
278 | if module.params.get("positional_args"):
279 | arguments = module.params["positional_args"]
280 | elif module.params.get("named_args"):
281 | arguments = module.params["named_args"]
282 | else:
283 | arguments = []
284 |
285 | # Set defaults:
286 | changed = False
287 |
288 | # Execute query:
289 | try:
290 | cursor.execute(query, arguments)
291 | except Exception as e:
292 | cursor.close()
293 | db_connection.close()
294 | module.fail_json(msg="Cannot execute SQL '%s' %s: %s" % (query, arguments, to_native(e)))
295 | statusmessage = cursor.connection.statusmessage
296 | rowcount = cursor.rowcount
297 | if rowcount != 0:
298 | try:
299 | query_result = [dict(row) for row in cursor.fetchall()]
300 | except db_connection.ProgrammingError as e:
301 | if to_native(e) == 'no results to fetch':
302 | query_result = {}
303 | elif to_native(e) == 'no result set':
304 | query_result = {}
305 | except Exception as e:
306 | module.fail_json(msg="Cannot fetch rows from cursor: %s" % to_native(e))
307 |
308 | if 'SELECT' not in statusmessage:
309 | if 'UPDATE' in statusmessage or 'INSERT' in statusmessage or 'DELETE' in statusmessage:
310 | s = statusmessage.split()
311 | if len(s) == 3:
312 | if statusmessage.split()[2] != '0':
313 | changed = True
314 |
315 | elif len(s) == 2:
316 | if statusmessage.split()[1] != '0':
317 | changed = True
318 |
319 | else:
320 | changed = True
321 |
322 | else:
323 | changed = True
324 | changed = False
325 |
326 | if module.check_mode:
327 | db_connection.rollback()
328 | else:
329 | if not autocommit:
330 | db_connection.commit()
331 |
332 | kw = dict(
333 | changed=changed,
334 | query=query,
335 | statusmessage=statusmessage,
336 | query_result=query_result,
337 | rowcount=rowcount if rowcount >= 0 else 0,
338 | )
339 |
340 | cursor.close()
341 | db_connection.close()
342 |
343 | module.exit_json(**kw)
344 |
345 |
346 | if __name__ == '__main__':
347 | main()
348 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_membership.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2019, Andrew Klychkov (@Andersson007)
5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 |
7 | # Contribution:
8 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
9 | # Welcome to https://t.me/pro_ansible for discussion and support
10 | # License: please see above
11 |
12 | from __future__ import (absolute_import, division, print_function)
13 | __metaclass__ = type
14 |
15 | ANSIBLE_METADATA = {
16 | 'metadata_version': '1.1',
17 | 'supported_by': 'community',
18 | 'status': ['preview']
19 | }
20 |
21 | DOCUMENTATION = r'''
22 | ---
23 | module: postgresql_membership
24 | short_description: Add or remove PostgreSQL roles from groups
25 | description:
26 | - Adds or removes PostgreSQL roles from groups (other roles).
27 | - Users are roles with login privilege.
28 | - Groups are PostgreSQL roles usually without LOGIN privilege.
29 | - "Common use case:"
30 | - 1) add a new group (groups) by M(postgresql_user) module with I(role_attr_flags=NOLOGIN)
31 | - 2) grant them desired privileges by M(postgresql_privs) module
32 | - 3) add desired PostgreSQL users to the new group (groups) by this module
33 | version_added: '2.8'
34 | options:
35 | groups:
36 | description:
37 | - The list of groups (roles) that need to be granted to or revoked from I(target_roles).
38 | required: yes
39 | type: list
40 | elements: str
41 | aliases:
42 | - group
43 | - source_role
44 | - source_roles
45 | target_roles:
46 | description:
47 | - The list of target roles (groups will be granted to them).
48 | required: yes
49 | type: list
50 | elements: str
51 | aliases:
52 | - target_role
53 | - users
54 | - user
55 | fail_on_role:
56 | description:
57 | - If C(yes), fail when group or target_role doesn't exist. If C(no), just warn and continue.
58 | default: yes
59 | type: bool
60 | state:
61 | description:
62 | - Membership state.
63 | - I(state=present) implies the I(groups)must be granted to I(target_roles).
64 | - I(state=absent) implies the I(groups) must be revoked from I(target_roles).
65 | type: str
66 | default: present
67 | choices: [ absent, present ]
68 | db:
69 | description:
70 | - Name of database to connect to.
71 | type: str
72 | aliases:
73 | - login_db
74 | session_role:
75 | description:
76 | - Switch to session_role after connecting.
77 | The specified session_role must be a role that the current login_user is a member of.
78 | - Permissions checking for SQL commands is carried out as though
79 | the session_role were the one that had logged in originally.
80 | type: str
81 | seealso:
82 | - module: postgresql_user
83 | - module: postgresql_privs
84 | - module: postgresql_owner
85 | - name: PostgreSQL role membership reference
86 | description: Complete reference of the PostgreSQL role membership documentation.
87 | link: https://www.postgresql.org/docs/current/role-membership.html
88 | - name: PostgreSQL role attributes reference
89 | description: Complete reference of the PostgreSQL role attributes documentation.
90 | link: https://www.postgresql.org/docs/current/role-attributes.html
91 | author:
92 | - Andrew Klychkov (@Andersson007)
93 | extends_documentation_fragment: postgres
94 | '''
95 |
96 | EXAMPLES = r'''
97 | - name: Grant role read_only to alice and bob
98 | postgresql_membership:
99 | group: read_only
100 | target_roles:
101 | - alice
102 | - bob
103 | state: present
104 |
105 | # you can also use target_roles: alice,bob,etc to pass the role list
106 |
107 | - name: Revoke role read_only and exec_func from bob. Ignore if roles don't exist
108 | postgresql_membership:
109 | groups:
110 | - read_only
111 | - exec_func
112 | target_role: bob
113 | fail_on_role: no
114 | state: absent
115 | '''
116 |
117 | RETURN = r'''
118 | queries:
119 | description: List of executed queries.
120 | returned: always
121 | type: str
122 | sample: [ "GRANT \"user_ro\" TO \"alice\"" ]
123 | granted:
124 | description: Dict of granted groups and roles.
125 | returned: if I(state=present)
126 | type: dict
127 | sample: { "ro_group": [ "alice", "bob" ] }
128 | revoked:
129 | description: Dict of revoked groups and roles.
130 | returned: if I(state=absent)
131 | type: dict
132 | sample: { "ro_group": [ "alice", "bob" ] }
133 | state:
134 | description: Membership state that tried to be set.
135 | returned: always
136 | type: str
137 | sample: "present"
138 | '''
139 |
140 | from ansible.module_utils.basic import AnsibleModule
141 | from ansible.module_utils.database import pg_quote_identifier
142 | from ansible.module_utils.postgres import (
143 | connect_to_db,
144 | exec_sql,
145 | get_conn_params,
146 | postgres_common_argument_spec,
147 | )
148 |
149 |
150 | class PgMembership(object):
151 | def __init__(self, module, cursor, groups, target_roles, fail_on_role):
152 | self.module = module
153 | self.cursor = cursor
154 | self.target_roles = [r.strip() for r in target_roles]
155 | self.groups = [r.strip() for r in groups]
156 | self.executed_queries = []
157 | self.granted = {}
158 | self.revoked = {}
159 | self.fail_on_role = fail_on_role
160 | self.non_existent_roles = []
161 | self.changed = False
162 | self.__check_roles_exist()
163 |
164 | def grant(self):
165 | for group in self.groups:
166 | self.granted[group] = []
167 |
168 | for role in self.target_roles:
169 | # If role is in a group now, pass:
170 | if self.__check_membership(group, role):
171 | continue
172 |
173 | query = "GRANT %s TO %s" % ((pg_quote_identifier(group, 'role'),
174 | (pg_quote_identifier(role, 'role'))))
175 | self.changed = exec_sql(self, query, ddl=True)
176 |
177 | if self.changed:
178 | self.granted[group].append(role)
179 |
180 | return self.changed
181 |
182 | def revoke(self):
183 | for group in self.groups:
184 | self.revoked[group] = []
185 |
186 | for role in self.target_roles:
187 | # If role is not in a group now, pass:
188 | if not self.__check_membership(group, role):
189 | continue
190 |
191 | query = "REVOKE %s FROM %s" % ((pg_quote_identifier(group, 'role'),
192 | (pg_quote_identifier(role, 'role'))))
193 | self.changed = exec_sql(self, query, ddl=True)
194 |
195 | if self.changed:
196 | self.revoked[group].append(role)
197 |
198 | return self.changed
199 |
200 | def __check_membership(self, src_role, dst_role):
201 | query = ("SELECT ARRAY(SELECT b.rolname FROM "
202 | "pg_catalog.pg_auth_members m "
203 | "JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) "
204 | "WHERE m.member = r.oid) "
205 | "FROM pg_catalog.pg_roles r "
206 | "WHERE r.rolname = '%s'" % dst_role)
207 |
208 | res = exec_sql(self, query, add_to_executed=False)
209 | membership = []
210 | if res:
211 | membership = res[0][0]
212 |
213 | if not membership:
214 | return False
215 |
216 | if src_role in membership:
217 | return True
218 |
219 | return False
220 |
221 | def __check_roles_exist(self):
222 | for group in self.groups:
223 | if not self.__role_exists(group):
224 | if self.fail_on_role:
225 | self.module.fail_json(msg="Role %s does not exist" % group)
226 | else:
227 | self.module.warn("Role %s does not exist, pass" % group)
228 | self.non_existent_roles.append(group)
229 |
230 | for role in self.target_roles:
231 | if not self.__role_exists(role):
232 | if self.fail_on_role:
233 | self.module.fail_json(msg="Role %s does not exist" % role)
234 | else:
235 | self.module.warn("Role %s does not exist, pass" % role)
236 |
237 | if role not in self.groups:
238 | self.non_existent_roles.append(role)
239 |
240 | else:
241 | if self.fail_on_role:
242 | self.module.exit_json(msg="Role role '%s' is a member of role '%s'" % (role, role))
243 | else:
244 | self.module.warn("Role role '%s' is a member of role '%s', pass" % (role, role))
245 |
246 | # Update role lists, excluding non existent roles:
247 | self.groups = [g for g in self.groups if g not in self.non_existent_roles]
248 |
249 | self.target_roles = [r for r in self.target_roles if r not in self.non_existent_roles]
250 |
251 | def __role_exists(self, role):
252 | return exec_sql(self, "SELECT 1 FROM pg_roles WHERE rolname = '%s'" % role, add_to_executed=False)
253 |
254 |
255 | # ===========================================
256 | # Module execution.
257 | #
258 |
259 |
260 | def main():
261 | argument_spec = postgres_common_argument_spec()
262 | argument_spec.update(
263 | groups=dict(type='list', aliases=['group', 'source_role', 'source_roles']),
264 | target_roles=dict(type='list', aliases=['target_role', 'user', 'users']),
265 | fail_on_role=dict(type='bool', default=True),
266 | state=dict(type='str', default='present', choices=['absent', 'present']),
267 | db=dict(type='str', aliases=['login_db']),
268 | session_role=dict(type='str'),
269 | )
270 |
271 | module = AnsibleModule(
272 | argument_spec=argument_spec,
273 | supports_check_mode=True,
274 | )
275 |
276 | groups = module.params['groups']
277 | target_roles = module.params['target_roles']
278 | fail_on_role = module.params['fail_on_role']
279 | state = module.params['state']
280 |
281 | conn_params = get_conn_params(module, module.params, warn_db_default=False)
282 | db_connection = connect_to_db(module, conn_params, autocommit=False)
283 | cursor = db_connection.cursor()
284 |
285 | ##############
286 | # Create the object and do main job:
287 |
288 | pg_membership = PgMembership(module, cursor, groups, target_roles, fail_on_role)
289 |
290 | if state == 'present':
291 | pg_membership.grant()
292 |
293 | elif state == 'absent':
294 | pg_membership.revoke()
295 |
296 | # Rollback if it's possible and check_mode:
297 | if module.check_mode:
298 | db_connection.rollback()
299 | else:
300 | db_connection.commit()
301 |
302 | cursor.close()
303 | db_connection.close()
304 |
305 | # Make return values:
306 | return_dict = dict(
307 | changed=pg_membership.changed,
308 | state=state,
309 | groups=pg_membership.groups,
310 | target_roles=pg_membership.target_roles,
311 | queries=pg_membership.executed_queries,
312 | )
313 |
314 | if state == 'present':
315 | return_dict['granted'] = pg_membership.granted
316 | elif state == 'absent':
317 | return_dict['revoked'] = pg_membership.revoked
318 |
319 | module.exit_json(**return_dict)
320 |
321 |
322 | if __name__ == '__main__':
323 | main()
324 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_lang.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # (c) 2014, Jens Depuydt
5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 |
7 | # Contribution:
8 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
9 | # Welcome to https://t.me/pro_ansible for discussion and support
10 | # License: please see above
11 |
12 | from __future__ import absolute_import, division, print_function
13 | __metaclass__ = type
14 |
15 |
16 | ANSIBLE_METADATA = {'metadata_version': '1.1',
17 | 'status': ['preview'],
18 | 'supported_by': 'community'}
19 |
20 | DOCUMENTATION = r'''
21 | ---
22 | module: postgresql_lang
23 | short_description: Adds, removes or changes procedural languages with a PostgreSQL database
24 | description:
25 | - Adds, removes or changes procedural languages with a PostgreSQL database.
26 | - This module allows you to add a language, remote a language or change the trust
27 | relationship with a PostgreSQL database.
28 | - The module can be used on the machine where executed or on a remote host.
29 | - When removing a language from a database, it is possible that dependencies prevent
30 | the database from being removed. In that case, you can specify I(cascade=yes) to
31 | automatically drop objects that depend on the language (such as functions in the
32 | language).
33 | - In case the language can't be deleted because it is required by the
34 | database system, you can specify I(fail_on_drop=no) to ignore the error.
35 | - Be careful when marking a language as trusted since this could be a potential
36 | security breach. Untrusted languages allow only users with the PostgreSQL superuser
37 | privilege to use this language to create new functions.
38 | version_added: '1.7'
39 | options:
40 | lang:
41 | description:
42 | - Name of the procedural language to add, remove or change.
43 | required: true
44 | type: str
45 | aliases:
46 | - name
47 | trust:
48 | description:
49 | - Make this language trusted for the selected db.
50 | type: bool
51 | default: 'no'
52 | db:
53 | description:
54 | - Name of database to connect to and where the language will be added, removed or changed.
55 | type: str
56 | aliases:
57 | - login_db
58 | force_trust:
59 | description:
60 | - Marks the language as trusted, even if it's marked as untrusted in pg_pltemplate.
61 | - Use with care!
62 | type: bool
63 | default: 'no'
64 | fail_on_drop:
65 | description:
66 | - If C(yes), fail when removing a language. Otherwise just log and continue.
67 | - In some cases, it is not possible to remove a language (used by the db-system).
68 | - When dependencies block the removal, consider using I(cascade).
69 | type: bool
70 | default: 'yes'
71 | cascade:
72 | description:
73 | - When dropping a language, also delete object that depend on this language.
74 | - Only used when I(state=absent).
75 | type: bool
76 | default: 'no'
77 | session_role:
78 | version_added: '2.8'
79 | description:
80 | - Switch to session_role after connecting.
81 | - The specified I(session_role) must be a role that the current I(login_user) is a member of.
82 | - Permissions checking for SQL commands is carried out as though the I(session_role) were the one that had logged in originally.
83 | type: str
84 | state:
85 | description:
86 | - The state of the language for the selected database.
87 | type: str
88 | default: present
89 | choices: [ absent, present ]
90 | login_unix_socket:
91 | description:
92 | - Path to a Unix domain socket for local connections.
93 | type: str
94 | version_added: '2.8'
95 | ssl_mode:
96 | description:
97 | - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
98 | - See U(https://www.postgresql.org/docs/current/static/libpq-ssl.html) for more information on the modes.
99 | - Default of C(prefer) matches libpq default.
100 | type: str
101 | default: prefer
102 | choices: [ allow, disable, prefer, require, verify-ca, verify-full ]
103 | version_added: '2.8'
104 | ca_cert:
105 | description:
106 | - Specifies the name of a file containing SSL certificate authority (CA) certificate(s).
107 | - If the file exists, the server's certificate will be verified to be signed by one of these authorities.
108 | type: str
109 | aliases: [ ssl_rootcert ]
110 | version_added: '2.8'
111 | seealso:
112 | - name: PostgreSQL languages
113 | description: General information about PostgreSQL languages.
114 | link: https://www.postgresql.org/docs/current/xplang.html
115 | - name: CREATE LANGUAGE reference
116 | description: Complete reference of the CREATE LANGUAGE command documentation.
117 | link: https://www.postgresql.org/docs/current/sql-createlanguage.html
118 | - name: ALTER LANGUAGE reference
119 | description: Complete reference of the ALTER LANGUAGE command documentation.
120 | link: https://www.postgresql.org/docs/current/sql-alterlanguage.html
121 | - name: DROP LANGUAGE reference
122 | description: Complete reference of the DROP LANGUAGE command documentation.
123 | link: https://www.postgresql.org/docs/current/sql-droplanguage.html
124 | author:
125 | - Jens Depuydt (@jensdepuydt)
126 | - Thomas O'Donnell (@andytom)
127 | extends_documentation_fragment: postgres
128 | '''
129 |
130 | EXAMPLES = r'''
131 | - name: Add language pltclu to database testdb if it doesn't exist
132 | postgresql_lang: db=testdb lang=pltclu state=present
133 |
134 | # Add language pltclu to database testdb if it doesn't exist and mark it as trusted.
135 | # Marks the language as trusted if it exists but isn't trusted yet.
136 | # force_trust makes sure that the language will be marked as trusted
137 | - name: Add language pltclu to database testdb if it doesn't exist and mark it as trusted
138 | postgresql_lang:
139 | db: testdb
140 | lang: pltclu
141 | state: present
142 | trust: yes
143 | force_trust: yes
144 |
145 | - name: Remove language pltclu from database testdb
146 | postgresql_lang:
147 | db: testdb
148 | lang: pltclu
149 | state: absent
150 |
151 | - name: Remove language pltclu from database testdb and remove all dependencies
152 | postgresql_lang:
153 | db: testdb
154 | lang: pltclu
155 | state: absent
156 | cascade: yes
157 |
158 | - name: Remove language c from database testdb but ignore errors if something prevents the removal
159 | postgresql_lang:
160 | db: testdb
161 | lang: pltclu
162 | state: absent
163 | fail_on_drop: no
164 | '''
165 |
166 | RETURN = r'''
167 | queries:
168 | description: List of executed queries.
169 | returned: always
170 | type: list
171 | sample: ['CREATE LANGUAGE "acme"']
172 | version_added: '2.8'
173 | '''
174 |
175 | from ansible.module_utils.basic import AnsibleModule
176 | from ansible.module_utils.postgres import (
177 | connect_to_db,
178 | get_conn_params,
179 | postgres_common_argument_spec,
180 | )
181 |
182 | executed_queries = []
183 |
184 |
185 | def lang_exists(cursor, lang):
186 | """Checks if language exists for db"""
187 | query = "SELECT lanname FROM pg_language WHERE lanname = %s"
188 | cursor.execute(query, [lang])
189 | return cursor.rowcount > 0
190 |
191 |
192 | def lang_istrusted(cursor, lang):
193 | """Checks if language is trusted for db"""
194 | query = "SELECT lanpltrusted FROM pg_language WHERE lanname = %s"
195 | cursor.execute(query, [lang])
196 | return cursor.fetchone()[0]
197 |
198 |
199 | def lang_altertrust(cursor, lang, trust):
200 | """Changes if language is trusted for db"""
201 | query = "UPDATE pg_language SET lanpltrusted = %s WHERE lanname = %s"
202 | cursor.execute(query, (trust, lang))
203 | executed_queries.append(query % (trust, lang))
204 | return True
205 |
206 |
207 | def lang_add(cursor, lang, trust):
208 | """Adds language for db"""
209 | if trust:
210 | query = 'CREATE TRUSTED LANGUAGE "%s"' % lang
211 | else:
212 | query = 'CREATE LANGUAGE "%s"' % lang
213 | executed_queries.append(query)
214 | cursor.execute(query)
215 | return True
216 |
217 |
218 | def lang_drop(cursor, lang, cascade):
219 | """Drops language for db"""
220 | cursor.execute("SAVEPOINT ansible_pgsql_lang_drop")
221 | try:
222 | if cascade:
223 | query = "DROP LANGUAGE \"%s\" CASCADE" % lang
224 | else:
225 | query = "DROP LANGUAGE \"%s\"" % lang
226 | executed_queries.append(query)
227 | cursor.execute(query)
228 | except Exception:
229 | cursor.execute("ROLLBACK TO SAVEPOINT ansible_pgsql_lang_drop")
230 | cursor.execute("RELEASE SAVEPOINT ansible_pgsql_lang_drop")
231 | return False
232 | cursor.execute("RELEASE SAVEPOINT ansible_pgsql_lang_drop")
233 | return True
234 |
235 |
236 | def main():
237 | argument_spec = postgres_common_argument_spec()
238 | argument_spec.update(
239 | db=dict(type="str", required=True, aliases=["login_db"]),
240 | lang=dict(type="str", required=True, aliases=["name"]),
241 | state=dict(type="str", default="present", choices=["absent", "present"]),
242 | trust=dict(type="bool", default="no"),
243 | force_trust=dict(type="bool", default="no"),
244 | cascade=dict(type="bool", default="no"),
245 | fail_on_drop=dict(type="bool", default="yes"),
246 | session_role=dict(type="str"),
247 | )
248 |
249 | module = AnsibleModule(
250 | argument_spec=argument_spec,
251 | supports_check_mode=True,
252 | )
253 |
254 | db = module.params["db"]
255 | lang = module.params["lang"]
256 | state = module.params["state"]
257 | trust = module.params["trust"]
258 | force_trust = module.params["force_trust"]
259 | cascade = module.params["cascade"]
260 | fail_on_drop = module.params["fail_on_drop"]
261 |
262 | conn_params = get_conn_params(module, module.params)
263 | db_connection = connect_to_db(module, conn_params, autocommit=False)
264 | cursor = db_connection.cursor()
265 |
266 | changed = False
267 | kw = {'db': db, 'lang': lang, 'trust': trust}
268 |
269 | if state == "present":
270 | if lang_exists(cursor, lang):
271 | lang_trusted = lang_istrusted(cursor, lang)
272 | if (lang_trusted and not trust) or (not lang_trusted and trust):
273 | if module.check_mode:
274 | changed = True
275 | else:
276 | changed = lang_altertrust(cursor, lang, trust)
277 | else:
278 | if module.check_mode:
279 | changed = True
280 | else:
281 | changed = lang_add(cursor, lang, trust)
282 | if force_trust:
283 | changed = lang_altertrust(cursor, lang, trust)
284 |
285 | else:
286 | if lang_exists(cursor, lang):
287 | if module.check_mode:
288 | changed = True
289 | kw['lang_dropped'] = True
290 | else:
291 | changed = lang_drop(cursor, lang, cascade)
292 | if fail_on_drop and not changed:
293 | msg = "unable to drop language, use cascade to delete dependencies or fail_on_drop=no to ignore"
294 | module.fail_json(msg=msg)
295 | kw['lang_dropped'] = changed
296 |
297 | if changed:
298 | if module.check_mode:
299 | db_connection.rollback()
300 | else:
301 | db_connection.commit()
302 |
303 | kw['changed'] = changed
304 | kw['queries'] = executed_queries
305 | db_connection.close()
306 | module.exit_json(**kw)
307 |
308 |
309 | if __name__ == '__main__':
310 | main()
311 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_copy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2019, Andrew Klychkov (@Andersson007)
5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 |
7 | # Contribution:
8 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
9 | # Welcome to https://t.me/pro_ansible for discussion and support
10 | # License: please see above
11 |
12 | from __future__ import (absolute_import, division, print_function)
13 | __metaclass__ = type
14 |
15 | ANSIBLE_METADATA = {
16 | 'metadata_version': '1.1',
17 | 'supported_by': 'community',
18 | 'status': ['preview']
19 | }
20 |
21 | DOCUMENTATION = r'''
22 | ---
23 | module: postgresql_copy
24 | short_description: Copy data between a file/program and a PostgreSQL table
25 | description:
26 | - Copy data between a file/program and a PostgreSQL table.
27 | version_added: '2.9'
28 |
29 | options:
30 | copy_to:
31 | description:
32 | - Copy the contents of a table to a file.
33 | - Can also copy the results of a SELECT query.
34 | - Mutually exclusive with I(copy_from) and I(dst).
35 | type: path
36 | aliases: [ to ]
37 | copy_from:
38 | description:
39 | - Copy data from a file to a table (appending the data to whatever is in the table already).
40 | - Mutually exclusive with I(copy_to) and I(src).
41 | type: path
42 | aliases: [ from ]
43 | src:
44 | description:
45 | - Copy data from I(copy_from) to I(src=tablename).
46 | - Used with I(copy_to) only.
47 | type: str
48 | aliases: [ source ]
49 | dst:
50 | description:
51 | - Copy data to I(dst=tablename) from I(copy_from=/path/to/data.file).
52 | - Used with I(copy_from) only.
53 | type: str
54 | aliases: [ destination ]
55 | columns:
56 | description:
57 | - List of column names for the src/dst table to COPY FROM/TO.
58 | type: list
59 | elements: str
60 | aliases: [ column ]
61 | program:
62 | description:
63 | - Mark I(src)/I(dst) as a program. Data will be copied to/from a program.
64 | - See block Examples and PROGRAM arg description U(https://www.postgresql.org/docs/current/sql-copy.html).
65 | type: bool
66 | default: no
67 | options:
68 | description:
69 | - Options of COPY command.
70 | - See the full list of available options U(https://www.postgresql.org/docs/current/sql-copy.html).
71 | type: dict
72 | db:
73 | description:
74 | - Name of database to connect to.
75 | type: str
76 | aliases: [ login_db ]
77 | session_role:
78 | description:
79 | - Switch to session_role after connecting.
80 | The specified session_role must be a role that the current login_user is a member of.
81 | - Permissions checking for SQL commands is carried out as though
82 | the session_role were the one that had logged in originally.
83 | type: str
84 |
85 | notes:
86 | - Supports PostgreSQL version 9.4+.
87 | - COPY command is only allowed to database superusers.
88 | - if I(check_mode=yes), we just check the src/dst table availability
89 | and return the COPY query that actually has not been executed.
90 | - If i(check_mode=yes) and the source has been passed as SQL, the module
91 | will execute it and rolled the transaction back but pay attention
92 | it can affect database performance (e.g., if SQL collects a lot of data).
93 |
94 | seealso:
95 | - name: COPY command reference
96 | description: Complete reference of the COPY command documentation.
97 | link: https://www.postgresql.org/docs/current/sql-copy.html
98 |
99 | author:
100 | - Andrew Klychkov (@Andersson007)
101 |
102 | extends_documentation_fragment: postgres
103 | '''
104 |
105 | EXAMPLES = r'''
106 | - name: Copy text TAB-separated data from file /tmp/data.txt to acme table
107 | postgresql_copy:
108 | copy_from: /tmp/data.txt
109 | dst: acme
110 |
111 | - name: Copy CSV (comma-separated) data from file /tmp/data.csv to columns id, name of table acme
112 | postgresql_copy:
113 | copy_from: /tmp/data.csv
114 | dst: acme
115 | columns: id,name
116 | options:
117 | format: csv
118 |
119 | - name: >
120 | Copy text vertical-bar-separated data from file /tmp/data.txt to bar table.
121 | The NULL values are specified as N
122 | postgresql_copy:
123 | copy_from: /tmp/data.csv
124 | dst: bar
125 | options:
126 | delimiter: '|'
127 | null: 'N'
128 |
129 | - name: Copy data from acme table to file /tmp/data.txt in text format, TAB-separated
130 | postgresql_copy:
131 | src: acme
132 | copy_to: /tmp/data.txt
133 |
134 | - name: Copy data from SELECT query to/tmp/data.csv in CSV format
135 | postgresql_copy:
136 | src: 'SELECT * FROM acme'
137 | copy_to: /tmp/data.csv
138 | options:
139 | format: csv
140 |
141 | - name: Copy CSV data from my_table to gzip
142 | postgresql_copy:
143 | src: my_table
144 | copy_to: 'gzip > /tmp/data.csv.gz'
145 | program: yes
146 | options:
147 | format: csv
148 |
149 | - name: >
150 | Copy data from columns id, name of table bar to /tmp/data.txt.
151 | Output format is text, vertical-bar-separated, NULL as N
152 | postgresql_copy:
153 | src: bar
154 | columns:
155 | - id
156 | - name
157 | copy_to: /tmp/data.csv
158 | options:
159 | delimiter: '|'
160 | null: 'N'
161 | '''
162 |
163 | RETURN = r'''
164 | queries:
165 | description: List of executed queries.
166 | returned: always
167 | type: str
168 | sample: [ "COPY test_table FROM '/tmp/data_file.txt' (FORMAT csv, DELIMITER ',', NULL 'NULL')" ]
169 | src:
170 | description: Data source.
171 | returned: always
172 | type: str
173 | sample: "mytable"
174 | dst:
175 | description: Data destination.
176 | returned: always
177 | type: str
178 | sample: "/tmp/data.csv"
179 | '''
180 |
181 |
182 | from ansible.module_utils.basic import AnsibleModule
183 | from ansible.module_utils.database import pg_quote_identifier
184 | from ansible.module_utils.postgres import (
185 | connect_to_db,
186 | exec_sql,
187 | get_conn_params,
188 | postgres_common_argument_spec,
189 | )
190 | from ansible.module_utils.six import iteritems
191 |
192 |
193 | class PgCopyData(object):
194 |
195 | """Implements behavior of COPY FROM, COPY TO PostgreSQL command.
196 |
197 | Arguments:
198 | module (AnsibleModule) -- object of AnsibleModule class
199 | cursor (cursor) -- cursor object of psycopg2 library
200 |
201 | Attributes:
202 | module (AnsibleModule) -- object of AnsibleModule class
203 | cursor (cursor) -- cursor object of psycopg2 library
204 | changed (bool) -- something was changed after execution or not
205 | executed_queries (list) -- executed queries
206 | dst (str) -- data destination table (when copy_from)
207 | src (str) -- data source table (when copy_to)
208 | opt_need_quotes (tuple) -- values of these options must be passed
209 | to SQL in quotes
210 | """
211 |
212 | def __init__(self, module, cursor):
213 | self.module = module
214 | self.cursor = cursor
215 | self.executed_queries = []
216 | self.changed = False
217 | self.dst = ''
218 | self.src = ''
219 | self.opt_need_quotes = (
220 | 'DELIMITER',
221 | 'NULL',
222 | 'QUOTE',
223 | 'ESCAPE',
224 | 'ENCODING',
225 | )
226 |
227 | def copy_from(self):
228 | """Implements COPY FROM command behavior."""
229 | self.src = self.module.params['copy_from']
230 | self.dst = self.module.params['dst']
231 |
232 | query_fragments = ['COPY %s' % pg_quote_identifier(self.dst, 'table')]
233 |
234 | if self.module.params.get('columns'):
235 | query_fragments.append('(%s)' % ','.join(self.module.params['columns']))
236 |
237 | query_fragments.append('FROM')
238 |
239 | if self.module.params.get('program'):
240 | query_fragments.append('PROGRAM')
241 |
242 | query_fragments.append("'%s'" % self.src)
243 |
244 | if self.module.params.get('options'):
245 | query_fragments.append(self.__transform_options())
246 |
247 | # Note: check mode is implemented here:
248 | if self.module.check_mode:
249 | self.changed = self.__check_table(self.dst)
250 |
251 | if self.changed:
252 | self.executed_queries.append(' '.join(query_fragments))
253 | else:
254 | if exec_sql(self, ' '.join(query_fragments), ddl=True):
255 | self.changed = True
256 |
257 | def copy_to(self):
258 | """Implements COPY TO command behavior."""
259 | self.src = self.module.params['src']
260 | self.dst = self.module.params['copy_to']
261 |
262 | if 'SELECT ' in self.src.upper():
263 | # If src is SQL SELECT statement:
264 | query_fragments = ['COPY (%s)' % self.src]
265 | else:
266 | # If src is a table:
267 | query_fragments = ['COPY %s' % pg_quote_identifier(self.src, 'table')]
268 |
269 | if self.module.params.get('columns'):
270 | query_fragments.append('(%s)' % ','.join(self.module.params['columns']))
271 |
272 | query_fragments.append('TO')
273 |
274 | if self.module.params.get('program'):
275 | query_fragments.append('PROGRAM')
276 |
277 | query_fragments.append("'%s'" % self.dst)
278 |
279 | if self.module.params.get('options'):
280 | query_fragments.append(self.__transform_options())
281 |
282 | # Note: check mode is implemented here:
283 | if self.module.check_mode:
284 | self.changed = self.__check_table(self.src)
285 |
286 | if self.changed:
287 | self.executed_queries.append(' '.join(query_fragments))
288 | else:
289 | if exec_sql(self, ' '.join(query_fragments), ddl=True):
290 | self.changed = True
291 |
292 | def __transform_options(self):
293 | """Transform options dict into a suitable string."""
294 | for (key, val) in iteritems(self.module.params['options']):
295 | if key.upper() in self.opt_need_quotes:
296 | self.module.params['options'][key] = "'%s'" % val
297 |
298 | opt = ['%s %s' % (key, val) for (key, val) in iteritems(self.module.params['options'])]
299 | return '(%s)' % ', '.join(opt)
300 |
301 | def __check_table(self, table):
302 | """Check table or SQL in transaction mode for check_mode.
303 |
304 | Return True if it is OK.
305 |
306 | Arguments:
307 | table (str) - Table name that needs to be checked.
308 | It can be SQL SELECT statement that was passed
309 | instead of the table name.
310 | """
311 | if 'SELECT ' in table.upper():
312 | # In this case table is actually SQL SELECT statement.
313 | # If SQL fails, it's handled by exec_sql():
314 | exec_sql(self, table, add_to_executed=False)
315 | # If exec_sql was passed, it means all is OK:
316 | return True
317 |
318 | exec_sql(self, 'SELECT 1 FROM %s' % pg_quote_identifier(table, 'table'),
319 | add_to_executed=False)
320 | # If SQL was executed successfully:
321 | return True
322 |
323 |
324 | # ===========================================
325 | # Module execution.
326 | #
327 |
328 |
329 | def main():
330 | argument_spec = postgres_common_argument_spec()
331 | argument_spec.update(
332 | copy_to=dict(type='path', aliases=['to']),
333 | copy_from=dict(type='path', aliases=['from']),
334 | src=dict(type='str', aliases=['source']),
335 | dst=dict(type='str', aliases=['destination']),
336 | columns=dict(type='list', aliases=['column']),
337 | options=dict(type='dict'),
338 | program=dict(type='bool', default=False),
339 | db=dict(type='str', aliases=['login_db']),
340 | session_role=dict(type='str'),
341 | )
342 | module = AnsibleModule(
343 | argument_spec=argument_spec,
344 | supports_check_mode=True,
345 | mutually_exclusive=[
346 | ['copy_from', 'copy_to'],
347 | ['copy_from', 'src'],
348 | ['copy_to', 'dst'],
349 | ]
350 | )
351 |
352 | # Note: we don't need to check mutually exclusive params here, because they are
353 | # checked automatically by AnsibleModule (mutually_exclusive=[] list above).
354 | if module.params.get('copy_from') and not module.params.get('dst'):
355 | module.fail_json(msg='dst param is necessary with copy_from')
356 |
357 | elif module.params.get('copy_to') and not module.params.get('src'):
358 | module.fail_json(msg='src param is necessary with copy_to')
359 |
360 | # Connect to DB and make cursor object:
361 | conn_params = get_conn_params(module, module.params)
362 | db_connection = connect_to_db(module, conn_params, autocommit=False)
363 | cursor = db_connection.cursor()
364 |
365 | ##############
366 | # Create the object and do main job:
367 | data = PgCopyData(module, cursor)
368 |
369 | # Note: parameters like dst, src, etc. are got
370 | # from module object into data object of PgCopyData class.
371 | # Therefore not need to pass args to the methods below.
372 | # Note: check mode is implemented inside the methods below
373 | # by checking passed module.check_mode arg.
374 | if module.params.get('copy_to'):
375 | data.copy_to()
376 |
377 | elif module.params.get('copy_from'):
378 | data.copy_from()
379 |
380 | # Finish:
381 | if module.check_mode:
382 | db_connection.rollback()
383 | else:
384 | db_connection.commit()
385 |
386 | cursor.close()
387 | db_connection.close()
388 |
389 | # Return some values:
390 | module.exit_json(
391 | changed=data.changed,
392 | queries=data.executed_queries,
393 | src=data.src,
394 | dst=data.dst,
395 | )
396 |
397 |
398 | if __name__ == '__main__':
399 | main()
400 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_ext.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: Ansible Project
5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 |
7 | # Contribution:
8 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
9 | # Welcome to https://t.me/pro_ansible for discussion and support
10 | # License: please see above
11 |
12 | from __future__ import absolute_import, division, print_function
13 | __metaclass__ = type
14 |
15 | ANSIBLE_METADATA = {'metadata_version': '1.1',
16 | 'status': ['preview'],
17 | 'supported_by': 'community'}
18 |
19 | DOCUMENTATION = r'''
20 | ---
21 | module: postgresql_ext
22 | short_description: Add or remove PostgreSQL extensions from a database
23 | description:
24 | - Add or remove PostgreSQL extensions from a database.
25 | version_added: '1.9'
26 | options:
27 | name:
28 | description:
29 | - Name of the extension to add or remove.
30 | required: true
31 | type: str
32 | aliases:
33 | - ext
34 | db:
35 | description:
36 | - Name of the database to add or remove the extension to/from.
37 | required: true
38 | type: str
39 | aliases:
40 | - login_db
41 | schema:
42 | description:
43 | - Name of the schema to add the extension to.
44 | version_added: '2.8'
45 | type: str
46 | session_role:
47 | description:
48 | - Switch to session_role after connecting.
49 | - The specified session_role must be a role that the current login_user is a member of.
50 | - Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally.
51 | type: str
52 | version_added: '2.8'
53 | state:
54 | description:
55 | - The database extension state.
56 | default: present
57 | choices: [ absent, present ]
58 | type: str
59 | cascade:
60 | description:
61 | - Automatically install/remove any extensions that this extension depends on
62 | that are not already installed/removed (supported since PostgreSQL 9.6).
63 | type: bool
64 | default: no
65 | version_added: '2.8'
66 | login_unix_socket:
67 | description:
68 | - Path to a Unix domain socket for local connections.
69 | type: str
70 | version_added: '2.8'
71 | ssl_mode:
72 | description:
73 | - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
74 | - See https://www.postgresql.org/docs/current/static/libpq-ssl.html for more information on the modes.
75 | - Default of C(prefer) matches libpq default.
76 | type: str
77 | default: prefer
78 | choices: [ allow, disable, prefer, require, verify-ca, verify-full ]
79 | version_added: '2.8'
80 | ca_cert:
81 | description:
82 | - Specifies the name of a file containing SSL certificate authority (CA) certificate(s).
83 | - If the file exists, the server's certificate will be verified to be signed by one of these authorities.
84 | type: str
85 | aliases: [ ssl_rootcert ]
86 | version_added: '2.8'
87 | version:
88 | description:
89 | - Extension version to add or update to. Has effect with I(state=present) only.
90 | - If not specified, the latest extension version will be created.
91 | - It can't downgrade an extension version.
92 | When version downgrade is needed, remove the extension and create new one with appropriate version.
93 | - Set I(version=latest) to update the extension to the latest available version.
94 | type: str
95 | version_added: '2.9'
96 | seealso:
97 | - name: PostgreSQL extensions
98 | description: General information about PostgreSQL extensions.
99 | link: https://www.postgresql.org/docs/current/external-extensions.html
100 | - name: CREATE EXTENSION reference
101 | description: Complete reference of the CREATE EXTENSION command documentation.
102 | link: https://www.postgresql.org/docs/current/sql-createextension.html
103 | - name: ALTER EXTENSION reference
104 | description: Complete reference of the ALTER EXTENSION command documentation.
105 | link: https://www.postgresql.org/docs/current/sql-alterextension.html
106 | - name: DROP EXTENSION reference
107 | description: Complete reference of the DROP EXTENSION command documentation.
108 | link: https://www.postgresql.org/docs/current/sql-droppublication.html
109 | notes:
110 | - The default authentication assumes that you are either logging in as
111 | or sudo'ing to the C(postgres) account on the host.
112 | - This module uses I(psycopg2), a Python PostgreSQL database adapter.
113 | - You must ensure that psycopg2 is installed on the host before using this module.
114 | - If the remote host is the PostgreSQL server (which is the default case),
115 | then PostgreSQL must also be installed on the remote host.
116 | - For Ubuntu-based systems, install the C(postgresql), C(libpq-dev),
117 | and C(python-psycopg2) packages on the remote host before using this module.
118 | requirements: [ psycopg2 ]
119 | author:
120 | - Daniel Schep (@dschep)
121 | - Thomas O'Donnell (@andytom)
122 | - Sandro Santilli (@strk)
123 | - Andrew Klychkov (@Andersson007)
124 | extends_documentation_fragment: postgres
125 | '''
126 |
127 | EXAMPLES = r'''
128 | - name: Adds postgis extension to the database acme in the schema foo
129 | postgresql_ext:
130 | name: postgis
131 | db: acme
132 | schema: foo
133 |
134 | - name: Removes postgis extension to the database acme
135 | postgresql_ext:
136 | name: postgis
137 | db: acme
138 | state: absent
139 |
140 | - name: Adds earthdistance extension to the database template1 cascade
141 | postgresql_ext:
142 | name: earthdistance
143 | db: template1
144 | cascade: true
145 |
146 | # In the example below, if earthdistance extension is installed,
147 | # it will be removed too because it depends on cube:
148 | - name: Removes cube extension from the database acme cascade
149 | postgresql_ext:
150 | name: cube
151 | db: acme
152 | cascade: yes
153 | state: absent
154 |
155 | - name: Create extension foo of version 1.2 or update it if it's already created
156 | postgresql_ext:
157 | db: acme
158 | name: foo
159 | version: 1.2
160 |
161 | - name: Assuming extension foo is created, update it to the latest version
162 | postgresql_ext:
163 | db: acme
164 | name: foo
165 | version: latest
166 | '''
167 |
168 | RETURN = r'''
169 | query:
170 | description: List of executed queries.
171 | returned: always
172 | type: list
173 | sample: ["DROP EXTENSION \"acme\""]
174 |
175 | '''
176 |
177 | import traceback
178 |
179 | from distutils.version import LooseVersion
180 |
181 |
182 |
183 | from ansible.module_utils.basic import AnsibleModule
184 | from ansible.module_utils.postgres import (
185 | connect_to_db,
186 | get_conn_params,
187 | postgres_common_argument_spec,
188 | )
189 | from ansible.module_utils._text import to_native
190 |
191 | executed_queries = []
192 |
193 |
194 | class NotSupportedError(Exception):
195 | pass
196 |
197 |
198 | # ===========================================
199 | # PostgreSQL module specific support methods.
200 | #
201 |
202 | def ext_exists(cursor, ext):
203 | query = "SELECT * FROM pg_extension WHERE extname=%(ext)s"
204 | cursor.execute(query, {'ext': ext})
205 | return cursor.rowcount == 1
206 |
207 |
208 | def ext_delete(cursor, ext, cascade):
209 | if ext_exists(cursor, ext):
210 | query = "DROP EXTENSION \"%s\"" % ext
211 | if cascade:
212 | query += " CASCADE"
213 | cursor.execute(query)
214 | executed_queries.append(query)
215 | return True
216 | else:
217 | return False
218 |
219 |
220 | def ext_update_version(cursor, ext, version):
221 | """Update extension version.
222 |
223 | Return True if success.
224 |
225 | Args:
226 | cursor (cursor) -- cursor object of psycopg2 library
227 | ext (str) -- extension name
228 | version (str) -- extension version
229 | """
230 | if version != 'latest':
231 | query = ("ALTER EXTENSION \"%s\" UPDATE TO '%s'" % (ext, version))
232 | else:
233 | query = ("ALTER EXTENSION \"%s\" UPDATE" % ext)
234 | cursor.execute(query)
235 | executed_queries.append(query)
236 | return True
237 |
238 |
239 | def ext_create(cursor, ext, schema, cascade, version):
240 | query = "CREATE EXTENSION \"%s\"" % ext
241 | if schema:
242 | query += " WITH SCHEMA \"%s\"" % schema
243 | if version:
244 | query += " VERSION '%s'" % version
245 | if cascade:
246 | query += " CASCADE"
247 | cursor.execute(query)
248 | executed_queries.append(query)
249 | return True
250 |
251 |
252 | def ext_get_versions(cursor, ext):
253 | """
254 | Get the current created extension version and available versions.
255 |
256 | Return tuple (current_version, [list of available versions]).
257 |
258 | Note: the list of available versions contains only versions
259 | that higher than the current created version.
260 | If the extension is not created, this list will contain all
261 | available versions.
262 |
263 | Args:
264 | cursor (cursor) -- cursor object of psycopg2 library
265 | ext (str) -- extension name
266 | """
267 |
268 | # 1. Get the current extension version:
269 | query = ("SELECT extversion FROM pg_catalog.pg_extension "
270 | "WHERE extname = '%s'" % ext)
271 |
272 | current_version = '0'
273 | cursor.execute(query)
274 | res = cursor.fetchone()
275 | if res:
276 | current_version = res[0]
277 |
278 | # 2. Get available versions:
279 | query = ("SELECT version FROM pg_available_extension_versions "
280 | "WHERE name = '%s'" % ext)
281 | cursor.execute(query)
282 | res = cursor.fetchall()
283 |
284 | available_versions = []
285 | if res:
286 | # Make the list of available versions:
287 | for line in res:
288 | if LooseVersion(line[0]) > LooseVersion(current_version):
289 | available_versions.append(line[0])
290 |
291 | if current_version == '0':
292 | current_version = False
293 |
294 | return (current_version, available_versions)
295 |
296 | # ===========================================
297 | # Module execution.
298 | #
299 |
300 |
301 | def main():
302 | argument_spec = postgres_common_argument_spec()
303 | argument_spec.update(
304 | db=dict(type="str", required=True, aliases=["login_db"]),
305 | ext=dict(type="str", required=True, aliases=["name"]),
306 | schema=dict(type="str"),
307 | state=dict(type="str", default="present", choices=["absent", "present"]),
308 | cascade=dict(type="bool", default=False),
309 | session_role=dict(type="str"),
310 | version=dict(type="str"),
311 | )
312 |
313 | module = AnsibleModule(
314 | argument_spec=argument_spec,
315 | supports_check_mode=True,
316 | )
317 |
318 | ext = module.params["ext"]
319 | schema = module.params["schema"]
320 | state = module.params["state"]
321 | cascade = module.params["cascade"]
322 | version = module.params["version"]
323 | changed = False
324 |
325 | if version and state == 'absent':
326 | module.warn("Parameter version is ignored when state=absent")
327 |
328 | conn_params = get_conn_params(module, module.params)
329 | db_connection = connect_to_db(module, conn_params, autocommit=True)
330 | cursor = db_connection.cursor()
331 |
332 | try:
333 | # Get extension info and available versions:
334 | curr_version, available_versions = ext_get_versions(cursor, ext)
335 |
336 | if state == "present":
337 | if version == 'latest':
338 | if available_versions:
339 | version = available_versions[-1]
340 | else:
341 | version = ''
342 |
343 | if version:
344 | # If the specific version is passed and it is not available for update:
345 | if version not in available_versions:
346 | if not curr_version:
347 | module.fail_json(msg="Passed version '%s' is not available" % version)
348 |
349 | elif LooseVersion(curr_version) == LooseVersion(version):
350 | changed = False
351 |
352 | else:
353 | module.fail_json(msg="Passed version '%s' is lower than "
354 | "the current created version '%s' or "
355 | "the passed version is not available" % (version, curr_version))
356 |
357 | # If the specific version is passed and it is higher that the current version:
358 | if curr_version and version:
359 | if LooseVersion(curr_version) < LooseVersion(version):
360 | if module.check_mode:
361 | changed = True
362 | else:
363 | changed = ext_update_version(cursor, ext, version)
364 |
365 | # If the specific version is passed and it is created now:
366 | if curr_version == version:
367 | changed = False
368 |
369 | # If the ext doesn't exist and installed:
370 | elif not curr_version and available_versions:
371 | if module.check_mode:
372 | changed = True
373 | else:
374 | changed = ext_create(cursor, ext, schema, cascade, version)
375 |
376 | # If version is not passed:
377 | else:
378 | if not curr_version:
379 | # If the ext doesn't exist and it's installed:
380 | if available_versions:
381 | if module.check_mode:
382 | changed = True
383 | else:
384 | changed = ext_create(cursor, ext, schema, cascade, version)
385 |
386 | # If the ext doesn't exist and not installed:
387 | else:
388 | module.fail_json(msg="Extension %s is not installed" % ext)
389 |
390 | elif state == "absent":
391 | if curr_version:
392 | if module.check_mode:
393 | changed = True
394 | else:
395 | changed = ext_delete(cursor, ext, cascade)
396 | else:
397 | changed = False
398 |
399 | except Exception as e:
400 | db_connection.close()
401 | module.fail_json(msg="Database query failed: %s" % to_native(e), exception=traceback.format_exc())
402 |
403 | db_connection.close()
404 | module.exit_json(changed=changed, db=module.params["db"], ext=ext, queries=executed_queries)
405 |
406 |
407 | if __name__ == '__main__':
408 | main()
409 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_set.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2018, Andrew Klychkov (@Andersson007)
5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 |
7 | # Contribution:
8 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
9 | # Welcome to https://t.me/pro_ansible for discussion and support
10 | # License: please see above
11 |
12 | from __future__ import absolute_import, division, print_function
13 | __metaclass__ = type
14 |
15 | ANSIBLE_METADATA = {
16 | 'metadata_version': '1.1',
17 | 'status': ['preview'],
18 | 'supported_by': 'community'
19 | }
20 |
21 | DOCUMENTATION = r'''
22 | ---
23 | module: postgresql_set
24 | short_description: Change a PostgreSQL server configuration parameter
25 | description:
26 | - Allows to change a PostgreSQL server configuration parameter.
27 | - The module uses ALTER SYSTEM command and applies changes by reload server configuration.
28 | - ALTER SYSTEM is used for changing server configuration parameters across the entire database cluster.
29 | - It can be more convenient and safe than the traditional method of manually editing the postgresql.conf file.
30 | - ALTER SYSTEM writes the given parameter setting to the $PGDATA/postgresql.auto.conf file,
31 | which is read in addition to postgresql.conf.
32 | - The module allows to reset parameter to boot_val (cluster initial value) by I(reset=yes) or remove parameter
33 | string from postgresql.auto.conf and reload I(value=default) (for settings with postmaster context restart is required).
34 | - After change you can see in the ansible output the previous and
35 | the new parameter value and other information using returned values and M(debug) module.
36 | version_added: '2.8'
37 | options:
38 | name:
39 | description:
40 | - Name of PostgreSQL server parameter.
41 | type: str
42 | required: true
43 | value:
44 | description:
45 | - Parameter value to set.
46 | - To remove parameter string from postgresql.auto.conf and
47 | reload the server configuration you must pass I(value=default).
48 | With I(value=default) the playbook always returns changed is true.
49 | type: str
50 | required: true
51 | reset:
52 | description:
53 | - Restore parameter to initial state (boot_val). Mutually exclusive with I(value).
54 | type: bool
55 | default: false
56 | session_role:
57 | description:
58 | - Switch to session_role after connecting. The specified session_role must
59 | be a role that the current login_user is a member of.
60 | - Permissions checking for SQL commands is carried out as though
61 | the session_role were the one that had logged in originally.
62 | type: str
63 | db:
64 | description:
65 | - Name of database to connect.
66 | type: str
67 | aliases:
68 | - login_db
69 | notes:
70 | - Supported version of PostgreSQL is 9.4 and later.
71 | - Pay attention, change setting with 'postmaster' context can return changed is true
72 | when actually nothing changes because the same value may be presented in
73 | several different form, for example, 1024MB, 1GB, etc. However in pg_settings
74 | system view it can be defined like 131072 number of 8kB pages.
75 | The final check of the parameter value cannot compare it because the server was
76 | not restarted and the value in pg_settings is not updated yet.
77 | - For some parameters restart of PostgreSQL server is required.
78 | See official documentation U(https://www.postgresql.org/docs/current/view-pg-settings.html).
79 | seealso:
80 | - module: postgresql_info
81 | - name: PostgreSQL server configuration
82 | description: General information about PostgreSQL server configuration.
83 | link: https://www.postgresql.org/docs/current/runtime-config.html
84 | - name: PostgreSQL view pg_settings reference
85 | description: Complete reference of the pg_settings view documentation.
86 | link: https://www.postgresql.org/docs/current/view-pg-settings.html
87 | - name: PostgreSQL ALTER SYSTEM command reference
88 | description: Complete reference of the ALTER SYSTEM command documentation.
89 | link: https://www.postgresql.org/docs/current/sql-altersystem.html
90 | author:
91 | - Andrew Klychkov (@Andersson007)
92 | extends_documentation_fragment: postgres
93 | '''
94 |
95 | EXAMPLES = r'''
96 | - name: Restore wal_keep_segments parameter to initial state
97 | postgresql_set:
98 | name: wal_keep_segments
99 | reset: yes
100 |
101 | # Set work_mem parameter to 32MB and show what's been changed and restart is required or not
102 | # (output example: "msg": "work_mem 4MB >> 64MB restart_req: False")
103 | - name: Set work mem parameter
104 | postgresql_set:
105 | name: work_mem
106 | value: 32mb
107 | register: set
108 |
109 | - debug:
110 | msg: "{{ set.name }} {{ set.prev_val_pretty }} >> {{ set.value_pretty }} restart_req: {{ set.restart_required }}"
111 | when: set.changed
112 | # Ensure that the restart of PostgreSQL server must be required for some parameters.
113 | # In this situation you see the same parameter in prev_val and value_prettyue, but 'changed=True'
114 | # (If you passed the value that was different from the current server setting).
115 |
116 | - name: Set log_min_duration_statement parameter to 1 second
117 | postgresql_set:
118 | name: log_min_duration_statement
119 | value: 1s
120 |
121 | - name: Set wal_log_hints parameter to default value (remove parameter from postgresql.auto.conf)
122 | postgresql_set:
123 | name: wal_log_hints
124 | value: default
125 | '''
126 |
127 | RETURN = r'''
128 | name:
129 | description: Name of PostgreSQL server parameter.
130 | returned: always
131 | type: str
132 | sample: 'shared_buffers'
133 | restart_required:
134 | description: Information about parameter current state.
135 | returned: always
136 | type: bool
137 | sample: true
138 | prev_val_pretty:
139 | description: Information about previous state of the parameter.
140 | returned: always
141 | type: str
142 | sample: '4MB'
143 | value_pretty:
144 | description: Information about current state of the parameter.
145 | returned: always
146 | type: str
147 | sample: '64MB'
148 | value:
149 | description:
150 | - Dictionary that contains the current parameter value (at the time of playbook finish).
151 | - Pay attention that for real change some parameters restart of PostgreSQL server is required.
152 | - Returns the current value in the check mode.
153 | returned: always
154 | type: dict
155 | sample: { "value": 67108864, "unit": "b" }
156 | context:
157 | description:
158 | - PostgreSQL setting context.
159 | returned: always
160 | type: str
161 | sample: user
162 | '''
163 |
164 | from copy import deepcopy
165 |
166 | from ansible.module_utils.basic import AnsibleModule
167 | from ansible.module_utils.postgres import (
168 | connect_to_db,
169 | get_conn_params,
170 | postgres_common_argument_spec,
171 | )
172 | from ansible.module_utils._text import to_native
173 |
174 | PG_REQ_VER = 90400
175 |
176 | # To allow to set value like 1mb instead of 1MB, etc:
177 | POSSIBLE_SIZE_UNITS = ("mb", "gb", "tb")
178 |
179 | # ===========================================
180 | # PostgreSQL module specific support methods.
181 | #
182 |
183 |
184 | def param_get(cursor, module, name):
185 | query = ("SELECT name, setting, unit, context, boot_val "
186 | "FROM pg_settings WHERE name = %s")
187 | try:
188 | cursor.execute(query, [name])
189 | info = cursor.fetchall()
190 | cursor.execute("SHOW %s" % name)
191 | val = cursor.fetchone()
192 |
193 | except Exception as e:
194 | module.fail_json(msg="Unable to get %s value due to : %s" % (name, to_native(e)))
195 |
196 | raw_val = info[0][1]
197 | unit = info[0][2]
198 | context = info[0][3]
199 | boot_val = info[0][4]
200 |
201 | if val[0] == 'True':
202 | val[0] = 'on'
203 | elif val[0] == 'False':
204 | val[0] = 'off'
205 |
206 | if unit == 'kB':
207 | if int(raw_val) > 0:
208 | raw_val = int(raw_val) * 1024
209 | if int(boot_val) > 0:
210 | boot_val = int(boot_val) * 1024
211 |
212 | unit = 'b'
213 |
214 | elif unit == 'MB':
215 | if int(raw_val) > 0:
216 | raw_val = int(raw_val) * 1024 * 1024
217 | if int(boot_val) > 0:
218 | boot_val = int(boot_val) * 1024 * 1024
219 |
220 | unit = 'b'
221 |
222 | return (val[0], raw_val, unit, boot_val, context)
223 |
224 |
225 | def pretty_to_bytes(pretty_val):
226 | # The function returns a value in bytes
227 | # if the value contains 'B', 'kB', 'MB', 'GB', 'TB'.
228 | # Otherwise it returns the passed argument.
229 |
230 | val_in_bytes = None
231 |
232 | if 'kB' in pretty_val:
233 | num_part = int(''.join(d for d in pretty_val if d.isdigit()))
234 | val_in_bytes = num_part * 1024
235 |
236 | elif 'MB' in pretty_val.upper():
237 | num_part = int(''.join(d for d in pretty_val if d.isdigit()))
238 | val_in_bytes = num_part * 1024 * 1024
239 |
240 | elif 'GB' in pretty_val.upper():
241 | num_part = int(''.join(d for d in pretty_val if d.isdigit()))
242 | val_in_bytes = num_part * 1024 * 1024 * 1024
243 |
244 | elif 'TB' in pretty_val.upper():
245 | num_part = int(''.join(d for d in pretty_val if d.isdigit()))
246 | val_in_bytes = num_part * 1024 * 1024 * 1024 * 1024
247 |
248 | elif 'B' in pretty_val.upper():
249 | num_part = int(''.join(d for d in pretty_val if d.isdigit()))
250 | val_in_bytes = num_part
251 |
252 | else:
253 | return pretty_val
254 |
255 | return val_in_bytes
256 |
257 |
258 | def param_set(cursor, module, name, value, context):
259 | try:
260 | if str(value).lower() == 'default':
261 | query = "ALTER SYSTEM SET %s = DEFAULT" % name
262 | else:
263 | query = "ALTER SYSTEM SET %s = '%s'" % (name, value)
264 | cursor.execute(query)
265 |
266 | if context != 'postmaster':
267 | cursor.execute("SELECT pg_reload_conf()")
268 |
269 | except Exception as e:
270 | module.fail_json(msg="Unable to get %s value due to : %s" % (name, to_native(e)))
271 |
272 | return True
273 |
274 |
275 | # ===========================================
276 | # Module execution.
277 | #
278 |
279 |
280 | def main():
281 | argument_spec = postgres_common_argument_spec()
282 | argument_spec.update(
283 | name=dict(type='str', required=True),
284 | db=dict(type='str', aliases=['login_db']),
285 | value=dict(type='str'),
286 | reset=dict(type='bool'),
287 | session_role=dict(type='str'),
288 | )
289 | module = AnsibleModule(
290 | argument_spec=argument_spec,
291 | supports_check_mode=True,
292 | )
293 |
294 | name = module.params["name"]
295 | value = module.params["value"]
296 | reset = module.params["reset"]
297 |
298 | # Allow to pass values like 1mb instead of 1MB, etc:
299 | if value:
300 | for unit in POSSIBLE_SIZE_UNITS:
301 | if value[:-2].isdigit() and unit in value[-2:]:
302 | value = value.upper()
303 |
304 | if value and reset:
305 | module.fail_json(msg="%s: value and reset params are mutually exclusive" % name)
306 |
307 | if not value and not reset:
308 | module.fail_json(msg="%s: at least one of value or reset param must be specified" % name)
309 |
310 | conn_params = get_conn_params(module, module.params, warn_db_default=False)
311 | db_connection = connect_to_db(module, conn_params, autocommit=True)
312 | cursor = db_connection.cursor()
313 |
314 | kw = {}
315 | # Check server version (needs 9.4 or later):
316 |
317 | ver = db_connection.server_version
318 | if ver < PG_REQ_VER:
319 | module.warn("PostgreSQL is %s version but %s or later is required" % (ver, PG_REQ_VER))
320 | kw = dict(
321 | changed=False,
322 | restart_required=False,
323 | value_pretty="",
324 | prev_val_pretty="",
325 | value={"value": "", "unit": ""},
326 | )
327 | kw['name'] = name
328 | db_connection.close()
329 | module.exit_json(**kw)
330 |
331 | # Set default returned values:
332 | restart_required = False
333 | changed = False
334 | kw['name'] = name
335 | kw['restart_required'] = False
336 |
337 | # Get info about param state:
338 | res = param_get(cursor, module, name)
339 | current_value = res[0]
340 | raw_val = res[1]
341 | unit = res[2]
342 | boot_val = res[3]
343 | context = res[4]
344 |
345 | if value == 'True':
346 | value = 'on'
347 | elif value == 'False':
348 | value = 'off'
349 |
350 | kw['prev_val_pretty'] = current_value
351 | kw['value_pretty'] = deepcopy(kw['prev_val_pretty'])
352 | kw['context'] = context
353 |
354 | # Do job
355 | if context == "internal":
356 | module.fail_json(msg="%s: cannot be changed (internal context). See "
357 | "https://www.postgresql.org/docs/current/runtime-config-preset.html" % name)
358 |
359 | if context == "postmaster":
360 | restart_required = True
361 |
362 | # If check_mode, just compare and exit:
363 | if module.check_mode:
364 | if pretty_to_bytes(value) == pretty_to_bytes(current_value):
365 | kw['changed'] = False
366 |
367 | else:
368 | kw['value_pretty'] = value
369 | kw['changed'] = True
370 |
371 | # Anyway returns current raw value in the check_mode:
372 | kw['value'] = dict(
373 | value=raw_val,
374 | unit=unit,
375 | )
376 | kw['restart_required'] = restart_required
377 | module.exit_json(**kw)
378 |
379 | # Set param:
380 | if value and value != current_value:
381 | changed = param_set(cursor, module, name, value, context)
382 |
383 | kw['value_pretty'] = value
384 |
385 | # Reset param:
386 | elif reset:
387 | if raw_val == boot_val:
388 | # nothing to change, exit:
389 | kw['value'] = dict(
390 | value=raw_val,
391 | unit=unit,
392 | )
393 | module.exit_json(**kw)
394 |
395 | changed = param_set(cursor, module, name, boot_val, context)
396 |
397 | if restart_required:
398 | module.warn("Restart of PostgreSQL is required for setting %s" % name)
399 |
400 | cursor.close()
401 | db_connection.close()
402 |
403 | # Reconnect and recheck current value:
404 | if context in ('sighup', 'superuser-backend', 'backend', 'superuser', 'user'):
405 | db_connection = connect_to_db(module, conn_params, autocommit=True)
406 | cursor = db_connection.cursor()
407 |
408 | res = param_get(cursor, module, name)
409 | # f_ means 'final'
410 | f_value = res[0]
411 | f_raw_val = res[1]
412 |
413 | if raw_val == f_raw_val:
414 | changed = False
415 |
416 | else:
417 | changed = True
418 |
419 | kw['value_pretty'] = f_value
420 | kw['value'] = dict(
421 | value=f_raw_val,
422 | unit=unit,
423 | )
424 |
425 | cursor.close()
426 | db_connection.close()
427 |
428 | kw['changed'] = changed
429 | kw['restart_required'] = restart_required
430 | module.exit_json(**kw)
431 |
432 |
433 | if __name__ == '__main__':
434 | main()
435 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_owner.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2019, Andrew Klychkov (@Andersson007)
5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 |
7 | # Contribution:
8 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
9 | # Welcome to https://t.me/pro_ansible for discussion and support
10 | # License: please see above
11 |
12 | from __future__ import (absolute_import, division, print_function)
13 | __metaclass__ = type
14 |
15 | ANSIBLE_METADATA = {
16 | 'metadata_version': '1.1',
17 | 'supported_by': 'community',
18 | 'status': ['preview']
19 | }
20 |
21 | DOCUMENTATION = r'''
22 | ---
23 | module: postgresql_owner
24 | short_description: Change an owner of PostgreSQL database object
25 | description:
26 | - Change an owner of PostgreSQL database object.
27 | - Also allows to reassign the ownership of database objects owned by a database role to another role.
28 | version_added: '2.8'
29 |
30 | options:
31 | new_owner:
32 | description:
33 | - Role (user/group) to set as an I(obj_name) owner.
34 | type: str
35 | required: yes
36 | obj_name:
37 | description:
38 | - Name of a database object to change ownership.
39 | - Mutually exclusive with I(reassign_owned_by).
40 | type: str
41 | obj_type:
42 | description:
43 | - Type of a database object.
44 | - Mutually exclusive with I(reassign_owned_by).
45 | type: str
46 | required: yes
47 | choices: [ database, function, matview, sequence, schema, table, tablespace, view ]
48 | aliases:
49 | - type
50 | reassign_owned_by:
51 | description:
52 | - The list of role names. The ownership of all the objects within the current database,
53 | and of all shared objects (databases, tablespaces), owned by this role(s) will be reassigned to I(owner).
54 | - Pay attention - it reassigns all objects owned by this role(s) in the I(db)!
55 | - If role(s) exists, always returns changed True.
56 | - Cannot reassign ownership of objects that are required by the database system.
57 | - Mutually exclusive with C(obj_type).
58 | type: list
59 | elements: str
60 | fail_on_role:
61 | description:
62 | - If C(yes), fail when I(reassign_owned_by) role does not exist.
63 | Otherwise just warn and continue.
64 | - Mutually exclusive with I(obj_name) and I(obj_type).
65 | default: yes
66 | type: bool
67 | db:
68 | description:
69 | - Name of database to connect to.
70 | type: str
71 | aliases:
72 | - login_db
73 | session_role:
74 | description:
75 | - Switch to session_role after connecting.
76 | The specified session_role must be a role that the current login_user is a member of.
77 | - Permissions checking for SQL commands is carried out as though
78 | the session_role were the one that had logged in originally.
79 | type: str
80 | seealso:
81 | - module: postgresql_user
82 | - module: postgresql_privs
83 | - module: postgresql_membership
84 | - name: PostgreSQL REASSIGN OWNED command reference
85 | description: Complete reference of the PostgreSQL REASSIGN OWNED command documentation.
86 | link: https://www.postgresql.org/docs/current/sql-reassign-owned.html
87 | author:
88 | - Andrew Klychkov (@Andersson007)
89 | extends_documentation_fragment: postgres
90 | '''
91 |
92 | EXAMPLES = r'''
93 | # Set owner as alice for function myfunc in database bar by ansible ad-hoc command:
94 | # ansible -m postgresql_owner -a "db=bar new_owner=alice obj_name=myfunc obj_type=function"
95 |
96 | - name: The same as above by playbook
97 | postgresql_owner:
98 | db: bar
99 | new_owner: alice
100 | obj_name: myfunc
101 | obj_type: function
102 |
103 | - name: Set owner as bob for table acme in database bar
104 | postgresql_owner:
105 | db: bar
106 | new_owner: bob
107 | obj_name: acme
108 | obj_type: table
109 |
110 | - name: Set owner as alice for view test_view in database bar
111 | postgresql_owner:
112 | db: bar
113 | new_owner: alice
114 | obj_name: test_view
115 | obj_type: view
116 |
117 | - name: Set owner as bob for tablespace ssd in database foo
118 | postgresql_owner:
119 | db: foo
120 | new_owner: bob
121 | obj_name: ssd
122 | obj_type: tablespace
123 |
124 | - name: Reassign all object in database bar owned by bob to alice
125 | postgresql_owner:
126 | db: bar
127 | new_owner: alice
128 | reassign_owned_by: bob
129 |
130 | - name: Reassign all object in database bar owned by bob and bill to alice
131 | postgresql_owner:
132 | db: bar
133 | new_owner: alice
134 | reassign_owned_by:
135 | - bob
136 | - bill
137 | '''
138 |
139 | RETURN = r'''
140 | queries:
141 | description: List of executed queries.
142 | returned: always
143 | type: str
144 | sample: [ 'REASSIGN OWNED BY "bob" TO "alice"' ]
145 | '''
146 |
147 | from ansible.module_utils.basic import AnsibleModule
148 | from ansible.module_utils.database import pg_quote_identifier
149 | from ansible.module_utils.postgres import (
150 | connect_to_db,
151 | exec_sql,
152 | get_conn_params,
153 | postgres_common_argument_spec,
154 | )
155 |
156 |
157 | class PgOwnership(object):
158 |
159 | """Class for changing ownership of PostgreSQL objects.
160 |
161 | Arguments:
162 | module (AnsibleModule): Object of Ansible module class.
163 | cursor (psycopg2.connect.cursor): Cursor object for interaction with the database.
164 | role (str): Role name to set as a new owner of objects.
165 |
166 | Important:
167 | If you want to add handling of a new type of database objects:
168 | 1. Add a specific method for this like self.__set_db_owner(), etc.
169 | 2. Add a condition with a check of ownership for new type objects to self.__is_owner()
170 | 3. Add a condition with invocation of the specific method to self.set_owner()
171 | 4. Add the information to the module documentation
172 | That's all.
173 | """
174 |
175 | def __init__(self, module, cursor, role):
176 | self.module = module
177 | self.cursor = cursor
178 | self.check_role_exists(role)
179 | self.role = role
180 | self.changed = False
181 | self.executed_queries = []
182 | self.obj_name = ''
183 | self.obj_type = ''
184 |
185 | def check_role_exists(self, role, fail_on_role=True):
186 | """Check the role exists or not.
187 |
188 | Arguments:
189 | role (str): Role name.
190 | fail_on_role (bool): If True, fail when the role does not exist.
191 | Otherwise just warn and continue.
192 | """
193 | if not self.__role_exists(role):
194 | if fail_on_role:
195 | self.module.fail_json(msg="Role '%s' does not exist" % role)
196 | else:
197 | self.module.warn("Role '%s' does not exist, pass" % role)
198 |
199 | return False
200 |
201 | else:
202 | return True
203 |
204 | def reassign(self, old_owners, fail_on_role):
205 | """Implements REASSIGN OWNED BY command.
206 |
207 | If success, set self.changed as True.
208 |
209 | Arguments:
210 | old_owners (list): The ownership of all the objects within
211 | the current database, and of all shared objects (databases, tablespaces),
212 | owned by these roles will be reassigned to self.role.
213 | fail_on_role (bool): If True, fail when a role from old_owners does not exist.
214 | Otherwise just warn and continue.
215 | """
216 | roles = []
217 | for r in old_owners:
218 | if self.check_role_exists(r, fail_on_role):
219 | roles.append(pg_quote_identifier(r, 'role'))
220 |
221 | # Roles do not exist, nothing to do, exit:
222 | if not roles:
223 | return False
224 |
225 | old_owners = ','.join(roles)
226 |
227 | query = ['REASSIGN OWNED BY']
228 | query.append(old_owners)
229 | query.append('TO %s' % pg_quote_identifier(self.role, 'role'))
230 | query = ' '.join(query)
231 |
232 | self.changed = exec_sql(self, query, ddl=True)
233 |
234 | def set_owner(self, obj_type, obj_name):
235 | """Change owner of a database object.
236 |
237 | Arguments:
238 | obj_type (str): Type of object (like database, table, view, etc.).
239 | obj_name (str): Object name.
240 | """
241 | self.obj_name = obj_name
242 | self.obj_type = obj_type
243 |
244 | # if a new_owner is the object owner now,
245 | # nothing to do:
246 | if self.__is_owner():
247 | return False
248 |
249 | if obj_type == 'database':
250 | self.__set_db_owner()
251 |
252 | elif obj_type == 'function':
253 | self.__set_func_owner()
254 |
255 | elif obj_type == 'sequence':
256 | self.__set_seq_owner()
257 |
258 | elif obj_type == 'schema':
259 | self.__set_schema_owner()
260 |
261 | elif obj_type == 'table':
262 | self.__set_table_owner()
263 |
264 | elif obj_type == 'tablespace':
265 | self.__set_tablespace_owner()
266 |
267 | elif obj_type == 'view':
268 | self.__set_view_owner()
269 |
270 | elif obj_type == 'matview':
271 | self.__set_mat_view_owner()
272 |
273 | def __is_owner(self):
274 | """Return True if self.role is the current object owner."""
275 | if self.obj_type == 'table':
276 | query = ("SELECT 1 FROM pg_tables "
277 | "WHERE tablename = %s "
278 | "AND tableowner = %s")
279 |
280 | elif self.obj_type == 'database':
281 | query = ("SELECT 1 FROM pg_database AS d "
282 | "JOIN pg_roles AS r ON d.datdba = r.oid "
283 | "WHERE d.datname = %s "
284 | "AND r.rolname = %s")
285 |
286 | elif self.obj_type == 'function':
287 | query = ("SELECT 1 FROM pg_proc AS f "
288 | "JOIN pg_roles AS r ON f.proowner = r.oid "
289 | "WHERE f.proname = %s "
290 | "AND r.rolname = %s")
291 |
292 | elif self.obj_type == 'sequence':
293 | query = ("SELECT 1 FROM pg_class AS c "
294 | "JOIN pg_roles AS r ON c.relowner = r.oid "
295 | "WHERE c.relkind = 'S' AND c.relname = %s "
296 | "AND r.rolname = %s")
297 |
298 | elif self.obj_type == 'schema':
299 | query = ("SELECT 1 FROM information_schema.schemata "
300 | "WHERE schema_name = %s "
301 | "AND schema_owner = %s")
302 |
303 | elif self.obj_type == 'tablespace':
304 | query = ("SELECT 1 FROM pg_tablespace AS t "
305 | "JOIN pg_roles AS r ON t.spcowner = r.oid "
306 | "WHERE t.spcname = %s "
307 | "AND r.rolname = %s")
308 |
309 | elif self.obj_type == 'view':
310 | query = ("SELECT 1 FROM pg_views "
311 | "WHERE viewname = %s "
312 | "AND viewowner = %s")
313 |
314 | elif self.obj_type == 'matview':
315 | query = ("SELECT 1 FROM pg_matviews "
316 | "WHERE matviewname = %s "
317 | "AND matviewowner = %s")
318 |
319 | return exec_sql(self, query, (self.obj_name,self.role,), add_to_executed=False)
320 |
321 | def __set_db_owner(self):
322 | """Set the database owner."""
323 | query = "ALTER DATABASE %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'database'),
324 | pg_quote_identifier(self.role, 'role'))
325 | self.changed = exec_sql(self, query, ddl=True)
326 |
327 | def __set_func_owner(self):
328 | """Set the function owner."""
329 | query = "ALTER FUNCTION %s OWNER TO %s" % (self.obj_name,
330 | pg_quote_identifier(self.role, 'role'))
331 | self.changed = exec_sql(self, query, ddl=True)
332 |
333 | def __set_seq_owner(self):
334 | """Set the sequence owner."""
335 | query = "ALTER SEQUENCE %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'table'),
336 | pg_quote_identifier(self.role, 'role'))
337 | self.changed = exec_sql(self, query, ddl=True)
338 |
339 | def __set_schema_owner(self):
340 | """Set the schema owner."""
341 | query = "ALTER SCHEMA %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'schema'),
342 | pg_quote_identifier(self.role, 'role'))
343 | self.changed = exec_sql(self, query, ddl=True)
344 |
345 | def __set_table_owner(self):
346 | """Set the table owner."""
347 | query = "ALTER TABLE %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'table'),
348 | pg_quote_identifier(self.role, 'role'))
349 | self.changed = exec_sql(self, query, ddl=True)
350 |
351 | def __set_tablespace_owner(self):
352 | """Set the tablespace owner."""
353 | query = "ALTER TABLESPACE %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'database'),
354 | pg_quote_identifier(self.role, 'role'))
355 | self.changed = exec_sql(self, query, ddl=True)
356 |
357 | def __set_view_owner(self):
358 | """Set the view owner."""
359 | query = "ALTER VIEW %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'table'),
360 | pg_quote_identifier(self.role, 'role'))
361 | self.changed = exec_sql(self, query, ddl=True)
362 |
363 | def __set_mat_view_owner(self):
364 | """Set the materialized view owner."""
365 | query = "ALTER MATERIALIZED VIEW %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'table'),
366 | pg_quote_identifier(self.role, 'role'))
367 | self.changed = exec_sql(self, query, ddl=True)
368 |
369 | def __role_exists(self, role):
370 | """Return True if role exists, otherwise return False."""
371 | query_params = (role,)
372 | query = "SELECT 1 FROM pg_roles WHERE rolname = %s"
373 | return exec_sql(self, query, query_params, add_to_executed=False)
374 |
375 |
376 | # ===========================================
377 | # Module execution.
378 | #
379 |
380 |
381 | def main():
382 | argument_spec = postgres_common_argument_spec()
383 | argument_spec.update(
384 | new_owner=dict(type='str', required=True),
385 | obj_name=dict(type='str'),
386 | obj_type=dict(type='str', aliases=['type'], choices=[
387 | 'database', 'function', 'matview', 'sequence', 'schema', 'table', 'tablespace', 'view']),
388 | reassign_owned_by=dict(type='list'),
389 | fail_on_role=dict(type='bool', default=True),
390 | db=dict(type='str', aliases=['login_db']),
391 | session_role=dict(type='str'),
392 | )
393 | module = AnsibleModule(
394 | argument_spec=argument_spec,
395 | mutually_exclusive=[
396 | ['obj_name', 'reassign_owned_by'],
397 | ['obj_type', 'reassign_owned_by'],
398 | ['obj_name', 'fail_on_role'],
399 | ['obj_type', 'fail_on_role'],
400 | ],
401 | supports_check_mode=True,
402 | )
403 |
404 | new_owner = module.params['new_owner']
405 | obj_name = module.params['obj_name']
406 | obj_type = module.params['obj_type']
407 | reassign_owned_by = module.params['reassign_owned_by']
408 | fail_on_role = module.params['fail_on_role']
409 |
410 | conn_params = get_conn_params(module, module.params)
411 | db_connection = connect_to_db(module, conn_params, autocommit=False)
412 | cursor = db_connection.cursor()
413 |
414 | ##############
415 | # Create the object and do main job:
416 | pg_ownership = PgOwnership(module, cursor, new_owner)
417 |
418 | # if we want to change ownership:
419 | if obj_name:
420 | pg_ownership.set_owner(obj_type, obj_name)
421 |
422 | # if we want to reassign objects owned by roles:
423 | elif reassign_owned_by:
424 | pg_ownership.reassign(reassign_owned_by, fail_on_role)
425 |
426 | # Rollback if it's possible and check_mode:
427 | if module.check_mode:
428 | db_connection.rollback()
429 | else:
430 | db_connection.commit()
431 |
432 | cursor.close()
433 | db_connection.close()
434 |
435 | module.exit_json(
436 | changed=pg_ownership.changed,
437 | queries=pg_ownership.executed_queries,
438 | )
439 |
440 |
441 | if __name__ == '__main__':
442 | main()
443 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_tablespace.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2017, Flavien Chantelot (@Dorn-)
5 | # Copyright: (c) 2018, Antoine Levy-Lambert (@antoinell)
6 | # Copyright: (c) 2019, Andrew Klychkov (@Andersson007)
7 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
8 |
9 | # Contribution:
10 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
11 | # Welcome to https://t.me/pro_ansible for discussion and support
12 | # License: please see above
13 |
14 | from __future__ import (absolute_import, division, print_function)
15 | __metaclass__ = type
16 |
17 | ANSIBLE_METADATA = {
18 | 'metadata_version': '1.1',
19 | 'supported_by': 'community',
20 | 'status': ['preview']
21 | }
22 |
23 | DOCUMENTATION = r'''
24 | ---
25 | module: postgresql_tablespace
26 | short_description: Add or remove PostgreSQL tablespaces from remote hosts
27 | description:
28 | - Adds or removes PostgreSQL tablespaces from remote hosts.
29 | version_added: '2.8'
30 | options:
31 | tablespace:
32 | description:
33 | - Name of the tablespace to add or remove.
34 | required: true
35 | type: str
36 | aliases:
37 | - name
38 | location:
39 | description:
40 | - Path to the tablespace directory in the file system.
41 | - Ensure that the location exists and has right privileges.
42 | type: path
43 | aliases:
44 | - path
45 | state:
46 | description:
47 | - Tablespace state.
48 | - I(state=present) implies the tablespace must be created if it doesn't exist.
49 | - I(state=absent) implies the tablespace must be removed if present.
50 | I(state=absent) is mutually exclusive with I(location), I(owner), i(set).
51 | - See the Notes section for information about check mode restrictions.
52 | type: str
53 | default: present
54 | choices: [ absent, present ]
55 | owner:
56 | description:
57 | - Name of the role to set as an owner of the tablespace.
58 | - If this option is not specified, the tablespace owner is a role that creates the tablespace.
59 | type: str
60 | set:
61 | description:
62 | - Dict of tablespace options to set. Supported from PostgreSQL 9.0.
63 | - For more information see U(https://www.postgresql.org/docs/current/sql-createtablespace.html).
64 | - When reset is passed as an option's value, if the option was set previously, it will be removed.
65 | type: dict
66 | rename_to:
67 | description:
68 | - New name of the tablespace.
69 | - The new name cannot begin with pg_, as such names are reserved for system tablespaces.
70 | session_role:
71 | description:
72 | - Switch to session_role after connecting. The specified session_role must
73 | be a role that the current login_user is a member of.
74 | - Permissions checking for SQL commands is carried out as though
75 | the session_role were the one that had logged in originally.
76 | type: str
77 | db:
78 | description:
79 | - Name of database to connect to and run queries against.
80 | type: str
81 | aliases:
82 | - login_db
83 |
84 | notes:
85 | - I(state=absent) and I(state=present) (the second one if the tablespace doesn't exist) do not
86 | support check mode because the corresponding PostgreSQL DROP and CREATE TABLESPACE commands
87 | can not be run inside the transaction block.
88 |
89 | seealso:
90 | - name: PostgreSQL tablespaces
91 | description: General information about PostgreSQL tablespaces.
92 | link: https://www.postgresql.org/docs/current/manage-ag-tablespaces.html
93 | - name: CREATE TABLESPACE reference
94 | description: Complete reference of the CREATE TABLESPACE command documentation.
95 | link: https://www.postgresql.org/docs/current/sql-createtablespace.html
96 | - name: ALTER TABLESPACE reference
97 | description: Complete reference of the ALTER TABLESPACE command documentation.
98 | link: https://www.postgresql.org/docs/current/sql-altertablespace.html
99 | - name: DROP TABLESPACE reference
100 | description: Complete reference of the DROP TABLESPACE command documentation.
101 | link: https://www.postgresql.org/docs/current/sql-droptablespace.html
102 |
103 | author:
104 | - Flavien Chantelot (@Dorn-)
105 | - Antoine Levy-Lambert (@antoinell)
106 | - Andrew Klychkov (@Andersson007)
107 |
108 | extends_documentation_fragment: postgres
109 | '''
110 |
111 | EXAMPLES = r'''
112 | - name: Create a new tablespace called acme and set bob as an its owner
113 | postgresql_tablespace:
114 | name: acme
115 | owner: bob
116 | location: /data/foo
117 |
118 | - name: Create a new tablespace called bar with tablespace options
119 | postgresql_tablespace:
120 | name: bar
121 | set:
122 | random_page_cost: 1
123 | seq_page_cost: 1
124 |
125 | - name: Reset random_page_cost option
126 | postgresql_tablespace:
127 | name: bar
128 | set:
129 | random_page_cost: reset
130 |
131 | - name: Rename the tablespace from bar to pcie_ssd
132 | postgresql_tablespace:
133 | name: bar
134 | rename_to: pcie_ssd
135 |
136 | - name: Drop tablespace called bloat
137 | postgresql_tablespace:
138 | name: bloat
139 | state: absent
140 | '''
141 |
142 | RETURN = r'''
143 | queries:
144 | description: List of queries that was tried to be executed.
145 | returned: always
146 | type: str
147 | sample: [ "CREATE TABLESPACE bar LOCATION '/incredible/ssd'" ]
148 | tablespace:
149 | description: Tablespace name.
150 | returned: always
151 | type: str
152 | sample: 'ssd'
153 | owner:
154 | description: Tablespace owner.
155 | returned: always
156 | type: str
157 | sample: 'Bob'
158 | options:
159 | description: Tablespace options.
160 | returned: always
161 | type: dict
162 | sample: { 'random_page_cost': 1, 'seq_page_cost': 1 }
163 | location:
164 | description: Path to the tablespace in the file system.
165 | returned: always
166 | type: str
167 | sample: '/incredible/fast/ssd'
168 | newname:
169 | description: New tablespace name
170 | returned: if existent
171 | type: str
172 | sample: new_ssd
173 | state:
174 | description: Tablespace state at the end of execution.
175 | returned: always
176 | type: str
177 | sample: 'present'
178 | '''
179 |
180 | from ansible.module_utils.basic import AnsibleModule
181 | from ansible.module_utils.database import pg_quote_identifier
182 | from ansible.module_utils.postgres import (
183 | connect_to_db,
184 | exec_sql,
185 | get_conn_params,
186 | postgres_common_argument_spec,
187 | )
188 |
189 |
190 | class PgTablespace(object):
191 |
192 | """Class for working with PostgreSQL tablespaces.
193 |
194 | Args:
195 | module (AnsibleModule) -- object of AnsibleModule class
196 | cursor (cursor) -- cursor object of psycopg2 library
197 | name (str) -- name of the tablespace
198 |
199 | Attrs:
200 | module (AnsibleModule) -- object of AnsibleModule class
201 | cursor (cursor) -- cursor object of psycopg2 library
202 | name (str) -- name of the tablespace
203 | exists (bool) -- flag the tablespace exists in the DB or not
204 | owner (str) -- tablespace owner
205 | location (str) -- path to the tablespace directory in the file system
206 | executed_queries (list) -- list of executed queries
207 | new_name (str) -- new name for the tablespace
208 | opt_not_supported (bool) -- flag indicates a tablespace option is supported or not
209 | """
210 |
211 | def __init__(self, module, cursor, name):
212 | self.module = module
213 | self.cursor = cursor
214 | self.name = name
215 | self.exists = False
216 | self.owner = ''
217 | self.settings = {}
218 | self.location = ''
219 | self.executed_queries = []
220 | self.new_name = ''
221 | self.opt_not_supported = False
222 | # Collect info:
223 | self.get_info()
224 |
225 | def get_info(self):
226 | """Get tablespace information."""
227 | # Check that spcoptions exists:
228 | opt = exec_sql(self, "SELECT 1 FROM information_schema.columns "
229 | "WHERE table_name = 'pg_tablespace' "
230 | "AND column_name = 'spcoptions'", add_to_executed=False)
231 |
232 | # For 9.1 version and earlier:
233 | location = exec_sql(self, "SELECT 1 FROM information_schema.columns "
234 | "WHERE table_name = 'pg_tablespace' "
235 | "AND column_name = 'spclocation'", add_to_executed=False)
236 | if location:
237 | location = 'spclocation'
238 | else:
239 | location = 'pg_tablespace_location(t.oid)'
240 |
241 | if not opt:
242 | self.opt_not_supported = True
243 | query = ("SELECT r.rolname, (SELECT Null), %s "
244 | "FROM pg_catalog.pg_tablespace AS t "
245 | "JOIN pg_catalog.pg_roles AS r "
246 | "ON t.spcowner = r.oid "
247 | "WHERE t.spcname = '%s'" % (location, self.name))
248 | else:
249 | query = ("SELECT r.rolname, t.spcoptions, %s "
250 | "FROM pg_catalog.pg_tablespace AS t "
251 | "JOIN pg_catalog.pg_roles AS r "
252 | "ON t.spcowner = r.oid "
253 | "WHERE t.spcname = '%s'" % (location, self.name))
254 |
255 | res = exec_sql(self, query, add_to_executed=False)
256 |
257 | if not res:
258 | self.exists = False
259 | return False
260 |
261 | if res[0][0]:
262 | self.exists = True
263 | self.owner = res[0][0]
264 |
265 | if res[0][1]:
266 | # Options exist:
267 | for i in res[0][1]:
268 | i = i.split('=')
269 | self.settings[i[0]] = i[1]
270 |
271 | if res[0][2]:
272 | # Location exists:
273 | self.location = res[0][2]
274 |
275 | def create(self, location):
276 | """Create tablespace.
277 |
278 | Return True if success, otherwise, return False.
279 |
280 | args:
281 | location (str) -- tablespace directory path in the FS
282 | """
283 | query = ("CREATE TABLESPACE %s LOCATION '%s'" % (pg_quote_identifier(self.name, 'database'), location))
284 | return exec_sql(self, query, ddl=True)
285 |
286 | def drop(self):
287 | """Drop tablespace.
288 |
289 | Return True if success, otherwise, return False.
290 | """
291 | return exec_sql(self, "DROP TABLESPACE %s" % pg_quote_identifier(self.name, 'database'), ddl=True)
292 |
293 | def set_owner(self, new_owner):
294 | """Set tablespace owner.
295 |
296 | Return True if success, otherwise, return False.
297 |
298 | args:
299 | new_owner (str) -- name of a new owner for the tablespace"
300 | """
301 | if new_owner == self.owner:
302 | return False
303 |
304 | query = "ALTER TABLESPACE %s OWNER TO %s" % (pg_quote_identifier(self.name, 'database'), new_owner)
305 | return exec_sql(self, query, ddl=True)
306 |
307 | def rename(self, newname):
308 | """Rename tablespace.
309 |
310 | Return True if success, otherwise, return False.
311 |
312 | args:
313 | newname (str) -- new name for the tablespace"
314 | """
315 | query = "ALTER TABLESPACE %s RENAME TO %s" % (pg_quote_identifier(self.name, 'database'), newname)
316 | self.new_name = newname
317 | return exec_sql(self, query, ddl=True)
318 |
319 | def set_settings(self, new_settings):
320 | """Set tablespace settings (options).
321 |
322 | If some setting has been changed, set changed = True.
323 | After all settings list is handling, return changed.
324 |
325 | args:
326 | new_settings (list) -- list of new settings
327 | """
328 | # settings must be a dict {'key': 'value'}
329 | if self.opt_not_supported:
330 | return False
331 |
332 | changed = False
333 |
334 | # Apply new settings:
335 | for i in new_settings:
336 | if new_settings[i] == 'reset':
337 | if i in self.settings:
338 | changed = self.__reset_setting(i)
339 | self.settings[i] = None
340 |
341 | elif (i not in self.settings) or (str(new_settings[i]) != self.settings[i]):
342 | changed = self.__set_setting("%s = '%s'" % (i, new_settings[i]))
343 |
344 | return changed
345 |
346 | def __reset_setting(self, setting):
347 | """Reset tablespace setting.
348 |
349 | Return True if success, otherwise, return False.
350 |
351 | args:
352 | setting (str) -- string in format "setting_name = 'setting_value'"
353 | """
354 | query = "ALTER TABLESPACE %s RESET (%s)" % (pg_quote_identifier(self.name, 'database'), setting)
355 | return exec_sql(self, query, ddl=True)
356 |
357 | def __set_setting(self, setting):
358 | """Set tablespace setting.
359 |
360 | Return True if success, otherwise, return False.
361 |
362 | args:
363 | setting (str) -- string in format "setting_name = 'setting_value'"
364 | """
365 | query = "ALTER TABLESPACE %s SET (%s)" % (pg_quote_identifier(self.name, 'database'), setting)
366 | return exec_sql(self, query, ddl=True)
367 |
368 |
369 | # ===========================================
370 | # Module execution.
371 | #
372 |
373 |
374 | def main():
375 | argument_spec = postgres_common_argument_spec()
376 | argument_spec.update(
377 | tablespace=dict(type='str', aliases=['name']),
378 | state=dict(type='str', default="present", choices=["absent", "present"]),
379 | location=dict(type='path', aliases=['path']),
380 | owner=dict(type='str'),
381 | set=dict(type='dict'),
382 | rename_to=dict(type='str'),
383 | db=dict(type='str', aliases=['login_db']),
384 | session_role=dict(type='str'),
385 | )
386 |
387 | module = AnsibleModule(
388 | argument_spec=argument_spec,
389 | mutually_exclusive=(('positional_args', 'named_args'),),
390 | supports_check_mode=True,
391 | )
392 |
393 | tablespace = module.params["tablespace"]
394 | state = module.params["state"]
395 | location = module.params["location"]
396 | owner = module.params["owner"]
397 | rename_to = module.params["rename_to"]
398 | settings = module.params["set"]
399 |
400 | if state == 'absent' and (location or owner or rename_to or settings):
401 | module.fail_json(msg="state=absent is mutually exclusive location, "
402 | "owner, rename_to, and set")
403 |
404 | conn_params = get_conn_params(module, module.params, warn_db_default=False)
405 | db_connection = connect_to_db(module, conn_params, autocommit=True)
406 | cursor = db_connection.cursor()
407 |
408 | # Change autocommit to False if check_mode:
409 | if module.check_mode:
410 | db_connection.autocommit=False
411 |
412 | # Set defaults:
413 | autocommit = False
414 | changed = False
415 |
416 | ##############
417 | # Create PgTablespace object and do main job:
418 | tblspace = PgTablespace(module, cursor, tablespace)
419 |
420 | # If tablespace exists with different location, exit:
421 | if tblspace.exists and location and location != tblspace.location:
422 | module.fail_json(msg="Tablespace '%s' exists with different location '%s'" % (tblspace.name, tblspace.location))
423 |
424 | # Create new tablespace:
425 | if not tblspace.exists and state == 'present':
426 | if rename_to:
427 | module.fail_json(msg="Tablespace %s does not exist, nothing to rename" % tablespace)
428 |
429 | if not location:
430 | module.fail_json(msg="'location' parameter must be passed with "
431 | "state=present if the tablespace doesn't exist")
432 |
433 | # Because CREATE TABLESPACE can not be run inside the transaction block:
434 | autocommit = True
435 | db_connection.autocommit=True
436 |
437 | changed = tblspace.create(location)
438 |
439 | # Drop non-existing tablespace:
440 | elif not tblspace.exists and state == 'absent':
441 | # Nothing to do:
442 | #???? module.fail_json(msg="Tries to drop nonexistent tablespace '%s'" % tblspace.name)
443 | changed = False
444 |
445 | # Drop existing tablespace:
446 | elif tblspace.exists and state == 'absent':
447 | # Because DROP TABLESPACE can not be run inside the transaction block:
448 | autocommit = True
449 | db_connection.autocommit=True
450 | changed = tblspace.drop()
451 |
452 | # Rename tablespace:
453 | elif tblspace.exists and rename_to:
454 | if tblspace.name != rename_to:
455 | changed = tblspace.rename(rename_to)
456 |
457 | if state == 'present':
458 | # Refresh information:
459 | tblspace.get_info()
460 |
461 | # Change owner and settings:
462 | if state == 'present' and tblspace.exists:
463 | if owner:
464 | changed = tblspace.set_owner(owner)
465 |
466 | if settings:
467 | changed = tblspace.set_settings(settings)
468 |
469 | tblspace.get_info()
470 |
471 | # Rollback if it's possible and check_mode:
472 | if not autocommit:
473 | if module.check_mode:
474 | db_connection.rollback()
475 | else:
476 | db_connection.commit()
477 |
478 | cursor.close()
479 | db_connection.close()
480 |
481 | # Make return values:
482 | kw = dict(
483 | changed=changed,
484 | state=state,
485 | tablespace=tblspace.name,
486 | owner=tblspace.owner,
487 | queries=tblspace.executed_queries,
488 | options=tblspace.settings,
489 | location=tblspace.location,
490 | )
491 |
492 | if state == 'present' and tblspace.new_name:
493 | kw['newname'] = tblspace.new_name
494 |
495 | module.exit_json(**kw)
496 |
497 |
498 | if __name__ == '__main__':
499 | main()
500 |
--------------------------------------------------------------------------------
/playbooks/library/postgresql_idx.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2018-2019, Andrey Klychkov (@Andersson007)
5 | # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6 |
7 | # Contribution:
8 | # Adaptation to pg8000 driver (C) Sergey Pechenko <10977752+tnt4brain@users.noreply.github.com>, 2021
9 | # Welcome to https://t.me/pro_ansible for discussion and support
10 | # License: please see above
11 |
12 | from __future__ import absolute_import, division, print_function
13 | __metaclass__ = type
14 |
15 | ANSIBLE_METADATA = {
16 | 'metadata_version': '1.1',
17 | 'status': ['preview'],
18 | 'supported_by': 'community'
19 | }
20 |
21 | DOCUMENTATION = r'''
22 | ---
23 | module: postgresql_idx
24 | short_description: Create or drop indexes from a PostgreSQL database
25 | description:
26 | - Create or drop indexes from a PostgreSQL database.
27 | version_added: '2.8'
28 |
29 | options:
30 | idxname:
31 | description:
32 | - Name of the index to create or drop.
33 | type: str
34 | required: true
35 | aliases:
36 | - name
37 | db:
38 | description:
39 | - Name of database to connect to and where the index will be created/dropped.
40 | type: str
41 | aliases:
42 | - login_db
43 | session_role:
44 | description:
45 | - Switch to session_role after connecting.
46 | The specified session_role must be a role that the current login_user is a member of.
47 | - Permissions checking for SQL commands is carried out as though
48 | the session_role were the one that had logged in originally.
49 | type: str
50 | schema:
51 | description:
52 | - Name of a database schema where the index will be created.
53 | type: str
54 | state:
55 | description:
56 | - Index state.
57 | - I(state=present) implies the index will be created if it does not exist.
58 | - I(state=absent) implies the index will be dropped if it exists.
59 | type: str
60 | default: present
61 | choices: [ absent, present ]
62 | table:
63 | description:
64 | - Table to create index on it.
65 | - Mutually exclusive with I(state=absent).
66 | type: str
67 | required: true
68 | columns:
69 | description:
70 | - List of index columns that need to be covered by index.
71 | - Mutually exclusive with I(state=absent).
72 | type: list
73 | elements: str
74 | aliases:
75 | - column
76 | cond:
77 | description:
78 | - Index conditions.
79 | - Mutually exclusive with I(state=absent).
80 | type: str
81 | idxtype:
82 | description:
83 | - Index type (like btree, gist, gin, etc.).
84 | - Mutually exclusive with I(state=absent).
85 | type: str
86 | aliases:
87 | - type
88 | concurrent:
89 | description:
90 | - Enable or disable concurrent mode (CREATE / DROP INDEX CONCURRENTLY).
91 | - Pay attention, if I(concurrent=no), the table will be locked (ACCESS EXCLUSIVE) during the building process.
92 | For more information about the lock levels see U(https://www.postgresql.org/docs/current/explicit-locking.html).
93 | - If the building process was interrupted for any reason when I(cuncurrent=yes), the index becomes invalid.
94 | In this case it should be dropped and created again.
95 | - Mutually exclusive with I(cascade=yes).
96 | type: bool
97 | default: yes
98 | tablespace:
99 | description:
100 | - Set a tablespace for the index.
101 | - Mutually exclusive with I(state=absent).
102 | required: false
103 | type: str
104 | storage_params:
105 | description:
106 | - Storage parameters like fillfactor, vacuum_cleanup_index_scale_factor, etc.
107 | - Mutually exclusive with I(state=absent).
108 | type: list
109 | elements: str
110 | cascade:
111 | description:
112 | - Automatically drop objects that depend on the index,
113 | and in turn all objects that depend on those objects.
114 | - It used only with I(state=absent).
115 | - Mutually exclusive with I(concurrent=yes)
116 | type: bool
117 | default: no
118 |
119 | seealso:
120 | - module: postgresql_table
121 | - module: postgresql_tablespace
122 | - name: PostgreSQL indexes reference
123 | description: General information about PostgreSQL indexes.
124 | link: https://www.postgresql.org/docs/current/indexes.html
125 | - name: CREATE INDEX reference
126 | description: Complete reference of the CREATE INDEX command documentation.
127 | link: https://www.postgresql.org/docs/current/sql-createindex.html
128 | - name: ALTER INDEX reference
129 | description: Complete reference of the ALTER INDEX command documentation.
130 | link: https://www.postgresql.org/docs/current/sql-alterindex.html
131 | - name: DROP INDEX reference
132 | description: Complete reference of the DROP INDEX command documentation.
133 | link: https://www.postgresql.org/docs/current/sql-dropindex.html
134 |
135 | notes:
136 | - The index building process can affect database performance.
137 | - To avoid table locks on production databases, use I(concurrent=yes) (default behavior).
138 |
139 | author:
140 | - Andrew Klychkov (@Andersson007)
141 |
142 | extends_documentation_fragment: postgres
143 | '''
144 |
145 | EXAMPLES = r'''
146 | - name: Create btree index if not exists test_idx concurrently covering columns id and name of table products
147 | postgresql_idx:
148 | db: acme
149 | table: products
150 | columns: id,name
151 | name: test_idx
152 |
153 | - name: Create btree index test_idx concurrently with tablespace called ssd and storage parameter
154 | postgresql_idx:
155 | db: acme
156 | table: products
157 | columns:
158 | - id
159 | - name
160 | idxname: test_idx
161 | tablespace: ssd
162 | storage_params:
163 | - fillfactor=90
164 |
165 | - name: Create gist index test_gist_idx concurrently on column geo_data of table map
166 | postgresql_idx:
167 | db: somedb
168 | table: map
169 | idxtype: gist
170 | columns: geo_data
171 | idxname: test_gist_idx
172 |
173 | # Note: for the example below pg_trgm extension must be installed for gin_trgm_ops
174 | - name: Create gin index gin0_idx not concurrently on column comment of table test
175 | postgresql_idx:
176 | idxname: gin0_idx
177 | table: test
178 | columns: comment gin_trgm_ops
179 | concurrent: no
180 | idxtype: gin
181 |
182 | - name: Drop btree test_idx concurrently
183 | postgresql_idx:
184 | db: mydb
185 | idxname: test_idx
186 | state: absent
187 |
188 | - name: Drop test_idx cascade
189 | postgresql_idx:
190 | db: mydb
191 | idxname: test_idx
192 | state: absent
193 | cascade: yes
194 | concurrent: no
195 |
196 | - name: Create btree index test_idx concurrently on columns id,comment where column id > 1
197 | postgresql_idx:
198 | db: mydb
199 | table: test
200 | columns: id,comment
201 | idxname: test_idx
202 | cond: id > 1
203 | '''
204 |
205 | RETURN = r'''
206 | name:
207 | description: Index name.
208 | returned: always
209 | type: str
210 | sample: 'foo_idx'
211 | state:
212 | description: Index state.
213 | returned: always
214 | type: str
215 | sample: 'present'
216 | schema:
217 | description: Schema where index exists.
218 | returned: always
219 | type: str
220 | sample: 'public'
221 | tablespace:
222 | description: Tablespace where index exists.
223 | returned: always
224 | type: str
225 | sample: 'ssd'
226 | query:
227 | description: Query that was tried to be executed.
228 | returned: always
229 | type: str
230 | sample: 'CREATE INDEX CONCURRENTLY foo_idx ON test_table USING BTREE (id)'
231 | storage_params:
232 | description: Index storage parameters.
233 | returned: always
234 | type: list
235 | sample: [ "fillfactor=90" ]
236 | valid:
237 | description: Index validity.
238 | returned: always
239 | type: bool
240 | sample: true
241 | '''
242 |
243 |
244 | from ansible.module_utils.basic import AnsibleModule
245 | from ansible.module_utils.postgres import (
246 | connect_to_db,
247 | exec_sql,
248 | get_conn_params,
249 | postgres_common_argument_spec,
250 | )
251 |
252 |
253 | VALID_IDX_TYPES = ('BTREE', 'HASH', 'GIST', 'SPGIST', 'GIN', 'BRIN')
254 |
255 |
256 | # ===========================================
257 | # PostgreSQL module specific support methods.
258 | #
259 |
260 | class Index(object):
261 |
262 | """Class for working with PostgreSQL indexes.
263 |
264 | TODO:
265 | 1. Add possibility to change ownership
266 | 2. Add possibility to change tablespace
267 | 3. Add list called executed_queries (executed_query should be left too)
268 | 4. Use self.module instead of passing arguments to the methods whenever possible
269 |
270 | Args:
271 | module (AnsibleModule) -- object of AnsibleModule class
272 | cursor (cursor) -- cursor object of psycopg2 library
273 | schema (str) -- name of the index schema
274 | name (str) -- name of the index
275 |
276 | Attrs:
277 | module (AnsibleModule) -- object of AnsibleModule class
278 | cursor (cursor) -- cursor object of psycopg2 library
279 | schema (str) -- name of the index schema
280 | name (str) -- name of the index
281 | exists (bool) -- flag the index exists in the DB or not
282 | info (dict) -- dict that contents information about the index
283 | executed_query (str) -- executed query
284 | """
285 |
286 | def __init__(self, module, cursor, schema, name):
287 | self.name = name
288 | if schema:
289 | self.schema = schema
290 | else:
291 | self.schema = 'public'
292 | self.module = module
293 | self.cursor = cursor
294 | self.info = {
295 | 'name': self.name,
296 | 'state': 'absent',
297 | 'schema': '',
298 | 'tblname': '',
299 | 'tblspace': '',
300 | 'valid': True,
301 | 'storage_params': [],
302 | }
303 | self.exists = False
304 | self.__exists_in_db()
305 | self.executed_query = ''
306 |
307 | def get_info(self):
308 | """Refresh index info.
309 |
310 | Return self.info dict.
311 | """
312 | self.__exists_in_db()
313 | return self.info
314 |
315 | def __exists_in_db(self):
316 | """Check index existence, collect info, add it to self.info dict.
317 |
318 | Return True if the index exists, otherwise, return False.
319 | """
320 | query = ("SELECT i.schemaname, i.tablename, i.tablespace, "
321 | "pi.indisvalid, c.reloptions "
322 | "FROM pg_catalog.pg_indexes AS i "
323 | "JOIN pg_catalog.pg_class AS c "
324 | "ON i.indexname = c.relname "
325 | "JOIN pg_catalog.pg_index AS pi "
326 | "ON c.oid = pi.indexrelid "
327 | "WHERE i.indexname = %s")
328 |
329 | res = exec_sql(self, query, query_params=[self.name], add_to_executed=False)
330 | if res:
331 | self.exists = True
332 | self.info = dict(
333 | name=self.name,
334 | state='present',
335 | schema=res[0][0],
336 | tblname=res[0][1],
337 | tblspace=res[0][2] if res[0][2] else '',
338 | valid=res[0][3],
339 | storage_params=res[0][4] if res[0][4] else [],
340 | )
341 | return True
342 |
343 | else:
344 | self.exists = False
345 | return False
346 |
347 | def create(self, tblname, idxtype, columns, cond, tblspace, storage_params, concurrent=True):
348 | """Create PostgreSQL index.
349 |
350 | Return True if success, otherwise, return False.
351 |
352 | Args:
353 | tblname (str) -- name of a table for the index
354 | idxtype (str) -- type of the index like BTREE, BRIN, etc
355 | columns (str) -- string of comma-separated columns that need to be covered by index
356 | tblspace (str) -- tablespace for storing the index
357 | storage_params (str) -- string of comma-separated storage parameters
358 |
359 | Kwargs:
360 | concurrent (bool) -- build index in concurrent mode, default True
361 | """
362 | if self.exists:
363 | return False
364 |
365 | changed = False
366 | if idxtype is None:
367 | idxtype = "BTREE"
368 |
369 | query = 'CREATE INDEX'
370 |
371 | if concurrent:
372 | query += ' CONCURRENTLY'
373 |
374 | query += ' %s' % self.name
375 |
376 | if self.schema:
377 | query += ' ON %s.%s ' % (self.schema, tblname)
378 | else:
379 | query += 'public.%s ' % tblname
380 |
381 | query += 'USING %s (%s)' % (idxtype, columns)
382 |
383 | if storage_params:
384 | query += ' WITH (%s)' % storage_params
385 |
386 | if tblspace:
387 | query += ' TABLESPACE %s' % tblspace
388 |
389 | if cond:
390 | query += ' WHERE %s' % cond
391 |
392 | self.executed_query = query
393 |
394 | if exec_sql(self, query, ddl=True, add_to_executed=False):
395 | return True
396 |
397 | return False
398 |
399 | def drop(self, schema, cascade=False, concurrent=True):
400 | """Drop PostgreSQL index.
401 |
402 | Return True if success, otherwise, return False.
403 |
404 | Args:
405 | schema (str) -- name of the index schema
406 |
407 | Kwargs:
408 | cascade (bool) -- automatically drop objects that depend on the index,
409 | default False
410 | concurrent (bool) -- build index in concurrent mode, default True
411 | """
412 | changed = False
413 | if not self.exists:
414 | return False
415 |
416 | query = 'DROP INDEX'
417 |
418 | if concurrent:
419 | query += ' CONCURRENTLY'
420 |
421 | if not schema:
422 | query += ' public.%s' % self.name
423 | else:
424 | query += ' %s.%s' % (schema, self.name)
425 |
426 | if cascade:
427 | query += ' CASCADE'
428 |
429 | self.executed_query = query
430 |
431 | if exec_sql(self, query, ddl=True, add_to_executed=False):
432 | return True
433 |
434 | return False
435 |
436 |
437 | # ===========================================
438 | # Module execution.
439 | #
440 |
441 |
442 | def main():
443 | argument_spec = postgres_common_argument_spec()
444 | argument_spec.update(
445 | idxname=dict(type='str', required=True, aliases=['name']),
446 | db=dict(type='str', aliases=['login_db']),
447 | state=dict(type='str', default='present', choices=['absent', 'present']),
448 | concurrent=dict(type='bool', default=True),
449 | table=dict(type='str'),
450 | idxtype=dict(type='str', aliases=['type']),
451 | columns=dict(type='list', aliases=['column']),
452 | cond=dict(type='str'),
453 | session_role=dict(type='str'),
454 | tablespace=dict(type='str'),
455 | storage_params=dict(type='list'),
456 | cascade=dict(type='bool', default=False),
457 | schema=dict(type='str'),
458 | )
459 | module = AnsibleModule(
460 | argument_spec=argument_spec,
461 | supports_check_mode=True,
462 | )
463 |
464 | idxname = module.params["idxname"]
465 | state = module.params["state"]
466 | concurrent = module.params["concurrent"]
467 | table = module.params["table"]
468 | idxtype = module.params["idxtype"]
469 | columns = module.params["columns"]
470 | cond = module.params["cond"]
471 | tablespace = module.params["tablespace"]
472 | storage_params = module.params["storage_params"]
473 | cascade = module.params["cascade"]
474 | schema = module.params["schema"]
475 |
476 | if concurrent and cascade:
477 | module.fail_json(msg="Cuncurrent mode and cascade parameters are mutually exclusive")
478 |
479 | if state == 'present':
480 | if not table:
481 | module.fail_json(msg="Table must be specified")
482 | if not columns:
483 | module.fail_json(msg="At least one column must be specified")
484 | else:
485 | if table or columns or cond or idxtype or tablespace:
486 | module.fail_json(msg="Index %s is going to be removed, so it does not "
487 | "make sense to pass a table name, columns, conditions, "
488 | "index type, or tablespace" % idxname)
489 |
490 | if cascade and state != 'absent':
491 | module.fail_json(msg="cascade parameter used only with state=absent")
492 |
493 | conn_params = get_conn_params(module, module.params)
494 | db_connection = connect_to_db(module, conn_params, autocommit=True)
495 | cursor = db_connection.cursor()
496 |
497 | # Set defaults:
498 | changed = False
499 |
500 | # Do job:
501 | index = Index(module, cursor, schema, idxname)
502 | kw = index.get_info()
503 | kw['query'] = ''
504 |
505 | #
506 | # check_mode start
507 | if module.check_mode:
508 | if state == 'present' and index.exists:
509 | kw['changed'] = False
510 | module.exit_json(**kw)
511 |
512 | elif state == 'present' and not index.exists:
513 | kw['changed'] = True
514 | module.exit_json(**kw)
515 |
516 | elif state == 'absent' and not index.exists:
517 | kw['changed'] = False
518 | module.exit_json(**kw)
519 |
520 | elif state == 'absent' and index.exists:
521 | kw['changed'] = True
522 | module.exit_json(**kw)
523 | # check_mode end
524 | #
525 |
526 | if state == "present":
527 | if idxtype and idxtype.upper() not in VALID_IDX_TYPES:
528 | module.fail_json(msg="Index type '%s' of %s is not in valid types" % (idxtype, idxname))
529 |
530 | columns = ','.join(columns)
531 |
532 | if storage_params:
533 | storage_params = ','.join(storage_params)
534 |
535 | changed = index.create(table, idxtype, columns, cond, tablespace, storage_params, concurrent)
536 |
537 | if changed:
538 | kw = index.get_info()
539 | kw['state'] = 'present'
540 | kw['query'] = index.executed_query
541 |
542 | else:
543 | changed = index.drop(schema, cascade, concurrent)
544 |
545 | if changed:
546 | kw['state'] = 'absent'
547 | kw['query'] = index.executed_query
548 |
549 | if not kw['valid']:
550 | db_connection.rollback()
551 | module.warn("Index %s is invalid! ROLLBACK" % idxname)
552 |
553 | if not concurrent:
554 | db_connection.commit()
555 |
556 | kw['changed'] = changed
557 | db_connection.close()
558 | module.exit_json(**kw)
559 |
560 |
561 | if __name__ == '__main__':
562 | main()
563 |
--------------------------------------------------------------------------------
/playbooks/module_utils/scramp/_version.py:
--------------------------------------------------------------------------------
1 |
2 | # This file helps to compute a version number in source trees obtained from
3 | # git-archive tarball (such as those provided by githubs download-from-tag
4 | # feature). Distribution tarballs (built by setup.py sdist) and build
5 | # directories (produced by setup.py build) will contain a much shorter file
6 | # that just contains the computed version number.
7 |
8 | # This file is released into the public domain. Generated by
9 | # versioneer-0.18 (https://github.com/warner/python-versioneer)
10 |
11 | """Git implementation of _version.py."""
12 |
13 | import errno
14 | import os
15 | import re
16 | import subprocess
17 | import sys
18 |
19 |
20 | def get_keywords():
21 | """Get the keywords needed to look up the version information."""
22 | # these strings will be replaced by git during git-archive.
23 | # setup.py/versioneer.py will grep for the variable names, so they must
24 | # each be defined on a line of their own. _version.py will just call
25 | # get_keywords().
26 | git_refnames = " (tag: 1.4.0)"
27 | git_full = "6fd6ae54b6ca911b6d2742149227c2fa13f9f67a"
28 | git_date = "2021-03-28 19:08:43 +0100"
29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
30 | return keywords
31 |
32 |
33 | class VersioneerConfig:
34 | """Container for Versioneer configuration parameters."""
35 |
36 |
37 | def get_config():
38 | """Create, populate and return the VersioneerConfig() object."""
39 | # these strings are filled in when 'setup.py versioneer' creates
40 | # _version.py
41 | cfg = VersioneerConfig()
42 | cfg.VCS = "git"
43 | cfg.style = "pep440"
44 | cfg.tag_prefix = ""
45 | cfg.parentdir_prefix = "scramp-"
46 | cfg.versionfile_source = "scramp/_version.py"
47 | cfg.verbose = False
48 | return cfg
49 |
50 |
51 | class NotThisMethod(Exception):
52 | """Exception raised if a method is not valid for the current scenario."""
53 |
54 |
55 | LONG_VERSION_PY = {}
56 | HANDLERS = {}
57 |
58 |
59 | def register_vcs_handler(vcs, method): # decorator
60 | """Decorator to mark a method as the handler for a particular VCS."""
61 | def decorate(f):
62 | """Store f in HANDLERS[vcs][method]."""
63 | if vcs not in HANDLERS:
64 | HANDLERS[vcs] = {}
65 | HANDLERS[vcs][method] = f
66 | return f
67 | return decorate
68 |
69 |
70 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
71 | env=None):
72 | """Call the given command(s)."""
73 | assert isinstance(commands, list)
74 | p = None
75 | for c in commands:
76 | try:
77 | dispcmd = str([c] + args)
78 | # remember shell=False, so use git.cmd on windows, not just git
79 | p = subprocess.Popen([c] + args, cwd=cwd, env=env,
80 | stdout=subprocess.PIPE,
81 | stderr=(subprocess.PIPE if hide_stderr
82 | else None))
83 | break
84 | except EnvironmentError:
85 | e = sys.exc_info()[1]
86 | if e.errno == errno.ENOENT:
87 | continue
88 | if verbose:
89 | print("unable to run %s" % dispcmd)
90 | print(e)
91 | return None, None
92 | else:
93 | if verbose:
94 | print("unable to find command, tried %s" % (commands,))
95 | return None, None
96 | stdout = p.communicate()[0].strip()
97 | if sys.version_info[0] >= 3:
98 | stdout = stdout.decode()
99 | if p.returncode != 0:
100 | if verbose:
101 | print("unable to run %s (error)" % dispcmd)
102 | print("stdout was %s" % stdout)
103 | return None, p.returncode
104 | return stdout, p.returncode
105 |
106 |
107 | def versions_from_parentdir(parentdir_prefix, root, verbose):
108 | """Try to determine the version from the parent directory name.
109 |
110 | Source tarballs conventionally unpack into a directory that includes both
111 | the project name and a version string. We will also support searching up
112 | two directory levels for an appropriately named parent directory
113 | """
114 | rootdirs = []
115 |
116 | for i in range(3):
117 | dirname = os.path.basename(root)
118 | if dirname.startswith(parentdir_prefix):
119 | return {"version": dirname[len(parentdir_prefix):],
120 | "full-revisionid": None,
121 | "dirty": False, "error": None, "date": None}
122 | else:
123 | rootdirs.append(root)
124 | root = os.path.dirname(root) # up a level
125 |
126 | if verbose:
127 | print("Tried directories %s but none started with prefix %s" %
128 | (str(rootdirs), parentdir_prefix))
129 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
130 |
131 |
132 | @register_vcs_handler("git", "get_keywords")
133 | def git_get_keywords(versionfile_abs):
134 | """Extract version information from the given file."""
135 | # the code embedded in _version.py can just fetch the value of these
136 | # keywords. When used from setup.py, we don't want to import _version.py,
137 | # so we do it with a regexp instead. This function is not used from
138 | # _version.py.
139 | keywords = {}
140 | try:
141 | f = open(versionfile_abs, "r")
142 | for line in f.readlines():
143 | if line.strip().startswith("git_refnames ="):
144 | mo = re.search(r'=\s*"(.*)"', line)
145 | if mo:
146 | keywords["refnames"] = mo.group(1)
147 | if line.strip().startswith("git_full ="):
148 | mo = re.search(r'=\s*"(.*)"', line)
149 | if mo:
150 | keywords["full"] = mo.group(1)
151 | if line.strip().startswith("git_date ="):
152 | mo = re.search(r'=\s*"(.*)"', line)
153 | if mo:
154 | keywords["date"] = mo.group(1)
155 | f.close()
156 | except EnvironmentError:
157 | pass
158 | return keywords
159 |
160 |
161 | @register_vcs_handler("git", "keywords")
162 | def git_versions_from_keywords(keywords, tag_prefix, verbose):
163 | """Get version information from git keywords."""
164 | if not keywords:
165 | raise NotThisMethod("no keywords at all, weird")
166 | date = keywords.get("date")
167 | if date is not None:
168 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
169 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
170 | # -like" string, which we must then edit to make compliant), because
171 | # it's been around since git-1.5.3, and it's too difficult to
172 | # discover which version we're using, or to work around using an
173 | # older one.
174 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
175 | refnames = keywords["refnames"].strip()
176 | if refnames.startswith("$Format"):
177 | if verbose:
178 | print("keywords are unexpanded, not using")
179 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
180 | refs = set([r.strip() for r in refnames.strip("()").split(",")])
181 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
182 | # just "foo-1.0". If we see a "tag: " prefix, prefer those.
183 | TAG = "tag: "
184 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
185 | if not tags:
186 | # Either we're using git < 1.8.3, or there really are no tags. We use
187 | # a heuristic: assume all version tags have a digit. The old git %d
188 | # expansion behaves like git log --decorate=short and strips out the
189 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish
190 | # between branches and tags. By ignoring refnames without digits, we
191 | # filter out many common branch names like "release" and
192 | # "stabilization", as well as "HEAD" and "master".
193 | tags = set([r for r in refs if re.search(r'\d', r)])
194 | if verbose:
195 | print("discarding '%s', no digits" % ",".join(refs - tags))
196 | if verbose:
197 | print("likely tags: %s" % ",".join(sorted(tags)))
198 | for ref in sorted(tags):
199 | # sorting will prefer e.g. "2.0" over "2.0rc1"
200 | if ref.startswith(tag_prefix):
201 | r = ref[len(tag_prefix):]
202 | if verbose:
203 | print("picking %s" % r)
204 | return {"version": r,
205 | "full-revisionid": keywords["full"].strip(),
206 | "dirty": False, "error": None,
207 | "date": date}
208 | # no suitable tags, so version is "0+unknown", but full hex is still there
209 | if verbose:
210 | print("no suitable tags, using unknown + full revision id")
211 | return {"version": "0+unknown",
212 | "full-revisionid": keywords["full"].strip(),
213 | "dirty": False, "error": "no suitable tags", "date": None}
214 |
215 |
216 | @register_vcs_handler("git", "pieces_from_vcs")
217 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
218 | """Get version from 'git describe' in the root of the source tree.
219 |
220 | This only gets called if the git-archive 'subst' keywords were *not*
221 | expanded, and _version.py hasn't already been rewritten with a short
222 | version string, meaning we're inside a checked out source tree.
223 | """
224 | GITS = ["git"]
225 | if sys.platform == "win32":
226 | GITS = ["git.cmd", "git.exe"]
227 |
228 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
229 | hide_stderr=True)
230 | if rc != 0:
231 | if verbose:
232 | print("Directory %s not under git control" % root)
233 | raise NotThisMethod("'git rev-parse --git-dir' returned error")
234 |
235 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
236 | # if there isn't one, this yields HEX[-dirty] (no NUM)
237 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
238 | "--always", "--long",
239 | "--match", "%s*" % tag_prefix],
240 | cwd=root)
241 | # --long was added in git-1.5.5
242 | if describe_out is None:
243 | raise NotThisMethod("'git describe' failed")
244 | describe_out = describe_out.strip()
245 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
246 | if full_out is None:
247 | raise NotThisMethod("'git rev-parse' failed")
248 | full_out = full_out.strip()
249 |
250 | pieces = {}
251 | pieces["long"] = full_out
252 | pieces["short"] = full_out[:7] # maybe improved later
253 | pieces["error"] = None
254 |
255 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
256 | # TAG might have hyphens.
257 | git_describe = describe_out
258 |
259 | # look for -dirty suffix
260 | dirty = git_describe.endswith("-dirty")
261 | pieces["dirty"] = dirty
262 | if dirty:
263 | git_describe = git_describe[:git_describe.rindex("-dirty")]
264 |
265 | # now we have TAG-NUM-gHEX or HEX
266 |
267 | if "-" in git_describe:
268 | # TAG-NUM-gHEX
269 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
270 | if not mo:
271 | # unparseable. Maybe git-describe is misbehaving?
272 | pieces["error"] = ("unable to parse git-describe output: '%s'"
273 | % describe_out)
274 | return pieces
275 |
276 | # tag
277 | full_tag = mo.group(1)
278 | if not full_tag.startswith(tag_prefix):
279 | if verbose:
280 | fmt = "tag '%s' doesn't start with prefix '%s'"
281 | print(fmt % (full_tag, tag_prefix))
282 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
283 | % (full_tag, tag_prefix))
284 | return pieces
285 | pieces["closest-tag"] = full_tag[len(tag_prefix):]
286 |
287 | # distance: number of commits since tag
288 | pieces["distance"] = int(mo.group(2))
289 |
290 | # commit: short hex revision ID
291 | pieces["short"] = mo.group(3)
292 |
293 | else:
294 | # HEX: no tags
295 | pieces["closest-tag"] = None
296 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
297 | cwd=root)
298 | pieces["distance"] = int(count_out) # total number of commits
299 |
300 | # commit date: see ISO-8601 comment in git_versions_from_keywords()
301 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
302 | cwd=root)[0].strip()
303 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
304 |
305 | return pieces
306 |
307 |
308 | def plus_or_dot(pieces):
309 | """Return a + if we don't already have one, else return a ."""
310 | if "+" in pieces.get("closest-tag", ""):
311 | return "."
312 | return "+"
313 |
314 |
315 | def render_pep440(pieces):
316 | """Build up version string, with post-release "local version identifier".
317 |
318 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
319 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
320 |
321 | Exceptions:
322 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
323 | """
324 | if pieces["closest-tag"]:
325 | rendered = pieces["closest-tag"]
326 | if pieces["distance"] or pieces["dirty"]:
327 | rendered += plus_or_dot(pieces)
328 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
329 | if pieces["dirty"]:
330 | rendered += ".dirty"
331 | else:
332 | # exception #1
333 | rendered = "0+untagged.%d.g%s" % (pieces["distance"],
334 | pieces["short"])
335 | if pieces["dirty"]:
336 | rendered += ".dirty"
337 | return rendered
338 |
339 |
340 | def render_pep440_pre(pieces):
341 | """TAG[.post.devDISTANCE] -- No -dirty.
342 |
343 | Exceptions:
344 | 1: no tags. 0.post.devDISTANCE
345 | """
346 | if pieces["closest-tag"]:
347 | rendered = pieces["closest-tag"]
348 | if pieces["distance"]:
349 | rendered += ".post.dev%d" % pieces["distance"]
350 | else:
351 | # exception #1
352 | rendered = "0.post.dev%d" % pieces["distance"]
353 | return rendered
354 |
355 |
356 | def render_pep440_post(pieces):
357 | """TAG[.postDISTANCE[.dev0]+gHEX] .
358 |
359 | The ".dev0" means dirty. Note that .dev0 sorts backwards
360 | (a dirty tree will appear "older" than the corresponding clean one),
361 | but you shouldn't be releasing software with -dirty anyways.
362 |
363 | Exceptions:
364 | 1: no tags. 0.postDISTANCE[.dev0]
365 | """
366 | if pieces["closest-tag"]:
367 | rendered = pieces["closest-tag"]
368 | if pieces["distance"] or pieces["dirty"]:
369 | rendered += ".post%d" % pieces["distance"]
370 | if pieces["dirty"]:
371 | rendered += ".dev0"
372 | rendered += plus_or_dot(pieces)
373 | rendered += "g%s" % pieces["short"]
374 | else:
375 | # exception #1
376 | rendered = "0.post%d" % pieces["distance"]
377 | if pieces["dirty"]:
378 | rendered += ".dev0"
379 | rendered += "+g%s" % pieces["short"]
380 | return rendered
381 |
382 |
383 | def render_pep440_old(pieces):
384 | """TAG[.postDISTANCE[.dev0]] .
385 |
386 | The ".dev0" means dirty.
387 |
388 | Eexceptions:
389 | 1: no tags. 0.postDISTANCE[.dev0]
390 | """
391 | if pieces["closest-tag"]:
392 | rendered = pieces["closest-tag"]
393 | if pieces["distance"] or pieces["dirty"]:
394 | rendered += ".post%d" % pieces["distance"]
395 | if pieces["dirty"]:
396 | rendered += ".dev0"
397 | else:
398 | # exception #1
399 | rendered = "0.post%d" % pieces["distance"]
400 | if pieces["dirty"]:
401 | rendered += ".dev0"
402 | return rendered
403 |
404 |
405 | def render_git_describe(pieces):
406 | """TAG[-DISTANCE-gHEX][-dirty].
407 |
408 | Like 'git describe --tags --dirty --always'.
409 |
410 | Exceptions:
411 | 1: no tags. HEX[-dirty] (note: no 'g' prefix)
412 | """
413 | if pieces["closest-tag"]:
414 | rendered = pieces["closest-tag"]
415 | if pieces["distance"]:
416 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
417 | else:
418 | # exception #1
419 | rendered = pieces["short"]
420 | if pieces["dirty"]:
421 | rendered += "-dirty"
422 | return rendered
423 |
424 |
425 | def render_git_describe_long(pieces):
426 | """TAG-DISTANCE-gHEX[-dirty].
427 |
428 | Like 'git describe --tags --dirty --always -long'.
429 | The distance/hash is unconditional.
430 |
431 | Exceptions:
432 | 1: no tags. HEX[-dirty] (note: no 'g' prefix)
433 | """
434 | if pieces["closest-tag"]:
435 | rendered = pieces["closest-tag"]
436 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
437 | else:
438 | # exception #1
439 | rendered = pieces["short"]
440 | if pieces["dirty"]:
441 | rendered += "-dirty"
442 | return rendered
443 |
444 |
445 | def render(pieces, style):
446 | """Render the given version pieces into the requested style."""
447 | if pieces["error"]:
448 | return {"version": "unknown",
449 | "full-revisionid": pieces.get("long"),
450 | "dirty": None,
451 | "error": pieces["error"],
452 | "date": None}
453 |
454 | if not style or style == "default":
455 | style = "pep440" # the default
456 |
457 | if style == "pep440":
458 | rendered = render_pep440(pieces)
459 | elif style == "pep440-pre":
460 | rendered = render_pep440_pre(pieces)
461 | elif style == "pep440-post":
462 | rendered = render_pep440_post(pieces)
463 | elif style == "pep440-old":
464 | rendered = render_pep440_old(pieces)
465 | elif style == "git-describe":
466 | rendered = render_git_describe(pieces)
467 | elif style == "git-describe-long":
468 | rendered = render_git_describe_long(pieces)
469 | else:
470 | raise ValueError("unknown style '%s'" % style)
471 |
472 | return {"version": rendered, "full-revisionid": pieces["long"],
473 | "dirty": pieces["dirty"], "error": None,
474 | "date": pieces.get("date")}
475 |
476 |
477 | def get_versions():
478 | """Get version information or return default if unable to do so."""
479 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
480 | # __file__, we can work backwards from there to the root. Some
481 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
482 | # case we can only use expanded keywords.
483 |
484 | cfg = get_config()
485 | verbose = cfg.verbose
486 |
487 | try:
488 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
489 | verbose)
490 | except NotThisMethod:
491 | pass
492 |
493 | try:
494 | root = os.path.realpath(__file__)
495 | # versionfile_source is the relative path from the top of the source
496 | # tree (where the .git directory might live) to this file. Invert
497 | # this to find the root from __file__.
498 | for i in cfg.versionfile_source.split('/'):
499 | root = os.path.dirname(root)
500 | except NameError:
501 | return {"version": "0+unknown", "full-revisionid": None,
502 | "dirty": None,
503 | "error": "unable to find root of source tree",
504 | "date": None}
505 |
506 | try:
507 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
508 | return render(pieces, cfg.style)
509 | except NotThisMethod:
510 | pass
511 |
512 | try:
513 | if cfg.parentdir_prefix:
514 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
515 | except NotThisMethod:
516 | pass
517 |
518 | return {"version": "0+unknown", "full-revisionid": None,
519 | "dirty": None,
520 | "error": "unable to compute version", "date": None}
521 |
--------------------------------------------------------------------------------