diff options
Diffstat (limited to 'lib/ansible/modules/database')
52 files changed, 0 insertions, 21830 deletions
diff --git a/lib/ansible/modules/database/aerospike/aerospike_migrations.py b/lib/ansible/modules/database/aerospike/aerospike_migrations.py deleted file mode 100644 index 1e368215be..0000000000 --- a/lib/ansible/modules/database/aerospike/aerospike_migrations.py +++ /dev/null @@ -1,523 +0,0 @@ -#!/usr/bin/python -"""short_description: Check or wait for migrations between nodes""" - -# Copyright: (c) 2018, Albert Autin -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import (absolute_import, division, print_function) - -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: aerospike_migrations -short_description: Check or wait for migrations between nodes -description: - - This can be used to check for migrations in a cluster. - This makes it easy to do a rolling upgrade/update on Aerospike nodes. - - If waiting for migrations is not desired, simply just poll until - port 3000 if available or asinfo -v status returns ok -version_added: 2.8 -author: "Albert Autin (@Alb0t)" -options: - host: - description: - - Which host do we use as seed for info connection - required: False - type: str - default: localhost - port: - description: - - Which port to connect to Aerospike on (service port) - required: False - type: int - default: 3000 - connect_timeout: - description: - - How long to try to connect before giving up (milliseconds) - required: False - type: int - default: 1000 - consecutive_good_checks: - description: - - How many times should the cluster report "no migrations" - consecutively before returning OK back to ansible? - required: False - type: int - default: 3 - sleep_between_checks: - description: - - How long to sleep between each check (seconds). - required: False - type: int - default: 60 - tries_limit: - description: - - How many times do we poll before giving up and failing? - default: 300 - required: False - type: int - local_only: - description: - - Do you wish to only check for migrations on the local node - before returning, or do you want all nodes in the cluster - to finish before returning? - required: True - type: bool - min_cluster_size: - description: - - Check will return bad until cluster size is met - or until tries is exhausted - required: False - type: int - default: 1 - fail_on_cluster_change: - description: - - Fail if the cluster key changes - if something else is changing the cluster, we may want to fail - required: False - type: bool - default: True - migrate_tx_key: - description: - - The metric key used to determine if we have tx migrations - remaining. Changeable due to backwards compatibility. - required: False - type: str - default: migrate_tx_partitions_remaining - migrate_rx_key: - description: - - The metric key used to determine if we have rx migrations - remaining. Changeable due to backwards compatibility. - required: False - type: str - default: migrate_rx_partitions_remaining - target_cluster_size: - description: - - When all aerospike builds in the cluster are greater than - version 4.3, then the C(cluster-stable) info command will be used. - Inside this command, you can optionally specify what the target - cluster size is - but it is not necessary. You can still rely on - min_cluster_size if you don't want to use this option. - - If this option is specified on a cluster that has at least 1 - host <4.3 then it will be ignored until the min version reaches - 4.3. - required: False - type: int -''' -EXAMPLES = ''' -# check for migrations on local node -- name: wait for migrations on local node before proceeding - aerospike_migrations: - host: "localhost" - connect_timeout: 2000 - consecutive_good_checks: 5 - sleep_between_checks: 15 - tries_limit: 600 - local_only: False - -# example playbook: ---- -- name: upgrade aerospike - hosts: all - become: true - serial: 1 - tasks: - - name: Install dependencies - apt: - name: - - python - - python-pip - - python-setuptools - state: latest - - name: setup aerospike - pip: - name: aerospike -# check for migrations every (sleep_between_checks) -# If at least (consecutive_good_checks) checks come back OK in a row, then return OK. -# Will exit if any exception, which can be caused by bad nodes, -# nodes not returning data, or other reasons. -# Maximum runtime before giving up in this case will be: -# Tries Limit * Sleep Between Checks * delay * retries - - name: wait for aerospike migrations - aerospike_migrations: - local_only: True - sleep_between_checks: 1 - tries_limit: 5 - consecutive_good_checks: 3 - fail_on_cluster_change: true - min_cluster_size: 3 - target_cluster_size: 4 - register: migrations_check - until: migrations_check is succeeded - changed_when: false - delay: 60 - retries: 120 - - name: another thing - shell: | - echo foo - - name: reboot - reboot: -''' - -RETURN = ''' -# Returns only a success/failure result. Changed is always false. -''' - -import traceback - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib - -LIB_FOUND_ERR = None -try: - import aerospike - from time import sleep - import re -except ImportError as ie: - LIB_FOUND = False - LIB_FOUND_ERR = traceback.format_exc() -else: - LIB_FOUND = True - - -def run_module(): - """run ansible module""" - module_args = dict( - host=dict(type='str', required=False, default='localhost'), - port=dict(type='int', required=False, default=3000), - connect_timeout=dict(type='int', required=False, default=1000), - consecutive_good_checks=dict(type='int', required=False, default=3), - sleep_between_checks=dict(type='int', required=False, default=60), - tries_limit=dict(type='int', required=False, default=300), - local_only=dict(type='bool', required=True), - min_cluster_size=dict(type='int', required=False, default=1), - target_cluster_size=dict(type='int', required=False, default=None), - fail_on_cluster_change=dict(type='bool', required=False, default=True), - migrate_tx_key=dict(type='str', required=False, - default="migrate_tx_partitions_remaining"), - migrate_rx_key=dict(type='str', required=False, - default="migrate_rx_partitions_remaining") - ) - - result = dict( - changed=False, - ) - - module = AnsibleModule( - argument_spec=module_args, - supports_check_mode=True - ) - if not LIB_FOUND: - module.fail_json(msg=missing_required_lib('aerospike'), - exception=LIB_FOUND_ERR) - - try: - if module.check_mode: - has_migrations, skip_reason = False, None - else: - migrations = Migrations(module) - has_migrations, skip_reason = migrations.has_migs( - module.params['local_only'] - ) - - if has_migrations: - module.fail_json(msg="Failed.", skip_reason=skip_reason) - except Exception as e: - module.fail_json(msg="Error: {0}".format(e)) - - module.exit_json(**result) - - -class Migrations: - """ Check or wait for migrations between nodes """ - - def __init__(self, module): - self.module = module - self._client = self._create_client().connect() - self._nodes = {} - self._update_nodes_list() - self._cluster_statistics = {} - self._update_cluster_statistics() - self._namespaces = set() - self._update_cluster_namespace_list() - self._build_list = set() - self._update_build_list() - self._start_cluster_key = \ - self._cluster_statistics[self._nodes[0]]['cluster_key'] - - def _create_client(self): - """ TODO: add support for auth, tls, and other special features - I won't use those features, so I'll wait until somebody complains - or does it for me (Cross fingers) - create the client object""" - config = { - 'hosts': [ - (self.module.params['host'], self.module.params['port']) - ], - 'policies': { - 'timeout': self.module.params['connect_timeout'] - } - } - return aerospike.client(config) - - def _info_cmd_helper(self, cmd, node=None, delimiter=';'): - """delimiter is for separate stats that come back, NOT for kv - separation which is =""" - if node is None: # If no node passed, use the first one (local) - node = self._nodes[0] - data = self._client.info_node(cmd, node) - data = data.split("\t") - if len(data) != 1 and len(data) != 2: - self.module.fail_json( - msg="Unexpected number of values returned in info command: " + - str(len(data)) - ) - # data will be in format 'command\touput' - data = data[-1] - data = data.rstrip("\n\r") - data_arr = data.split(delimiter) - - # some commands don't return in kv format - # so we dont want a dict from those. - if '=' in data: - retval = dict( - metric.split("=", 1) for metric in data_arr - ) - else: - # if only 1 element found, and not kv, return just the value. - if len(data_arr) == 1: - retval = data_arr[0] - else: - retval = data_arr - return retval - - def _update_build_list(self): - """creates self._build_list which is a unique list - of build versions.""" - self._build_list = set() - for node in self._nodes: - build = self._info_cmd_helper('build', node) - self._build_list.add(build) - - # just checks to see if the version is 4.3 or greater - def _can_use_cluster_stable(self): - # if version <4.3 we can't use cluster-stable info cmd - # regex hack to check for versions beginning with 0-3 or - # beginning with 4.0,4.1,4.2 - if re.search(R'^([0-3]\.|4\.[0-2])', min(self._build_list)): - return False - return True - - def _update_cluster_namespace_list(self): - """ make a unique list of namespaces - TODO: does this work on a rolling namespace add/deletion? - thankfully if it doesn't, we dont need this on builds >=4.3""" - self._namespaces = set() - for node in self._nodes: - namespaces = self._info_cmd_helper('namespaces', node) - for namespace in namespaces: - self._namespaces.add(namespace) - - def _update_cluster_statistics(self): - """create a dict of nodes with their related stats """ - self._cluster_statistics = {} - for node in self._nodes: - self._cluster_statistics[node] = \ - self._info_cmd_helper('statistics', node) - - def _update_nodes_list(self): - """get a fresh list of all the nodes""" - self._nodes = self._client.get_nodes() - if not self._nodes: - self.module.fail_json("Failed to retrieve at least 1 node.") - - def _namespace_has_migs(self, namespace, node=None): - """returns a True or False. - Does the namespace have migrations for the node passed? - If no node passed, uses the local node or the first one in the list""" - namespace_stats = self._info_cmd_helper("namespace/" + namespace, node) - try: - namespace_tx = \ - int(namespace_stats[self.module.params['migrate_tx_key']]) - namespace_rx = \ - int(namespace_stats[self.module.params['migrate_tx_key']]) - except KeyError: - self.module.fail_json( - msg="Did not find partition remaining key:" + - self.module.params['migrate_tx_key'] + - " or key:" + - self.module.params['migrate_rx_key'] + - " in 'namespace/" + - namespace + - "' output." - ) - except TypeError: - self.module.fail_json( - msg="namespace stat returned was not numerical" - ) - return namespace_tx != 0 or namespace_rx != 0 - - def _node_has_migs(self, node=None): - """just calls namespace_has_migs and - if any namespace has migs returns true""" - migs = 0 - self._update_cluster_namespace_list() - for namespace in self._namespaces: - if self._namespace_has_migs(namespace, node): - migs += 1 - return migs != 0 - - def _cluster_key_consistent(self): - """create a dictionary to store what each node - returns the cluster key as. we should end up with only 1 dict key, - with the key being the cluster key.""" - cluster_keys = {} - for node in self._nodes: - cluster_key = self._cluster_statistics[node][ - 'cluster_key'] - if cluster_key not in cluster_keys: - cluster_keys[cluster_key] = 1 - else: - cluster_keys[cluster_key] += 1 - if len(cluster_keys.keys()) == 1 and \ - self._start_cluster_key in cluster_keys: - return True - return False - - def _cluster_migrates_allowed(self): - """ensure all nodes have 'migrate_allowed' in their stats output""" - for node in self._nodes: - node_stats = self._info_cmd_helper('statistics', node) - allowed = node_stats['migrate_allowed'] - if allowed == "false": - return False - return True - - def _cluster_has_migs(self): - """calls node_has_migs for each node""" - migs = 0 - for node in self._nodes: - if self._node_has_migs(node): - migs += 1 - if migs == 0: - return False - return True - - def _has_migs(self, local): - if local: - return self._local_node_has_migs() - return self._cluster_has_migs() - - def _local_node_has_migs(self): - return self._node_has_migs(None) - - def _is_min_cluster_size(self): - """checks that all nodes in the cluster are returning the - minimum cluster size specified in their statistics output""" - sizes = set() - for node in self._cluster_statistics: - sizes.add(int(self._cluster_statistics[node]['cluster_size'])) - - if (len(sizes)) > 1: # if we are getting more than 1 size, lets say no - return False - if (min(sizes)) >= self.module.params['min_cluster_size']: - return True - return False - - def _cluster_stable(self): - """Added 4.3: - cluster-stable:size=<target-cluster-size>;ignore-migrations=<yes/no>;namespace=<namespace-name> - Returns the current 'cluster_key' when the following are satisfied: - - If 'size' is specified then the target node's 'cluster-size' - must match size. - If 'ignore-migrations' is either unspecified or 'false' then - the target node's migrations counts must be zero for the provided - 'namespace' or all namespaces if 'namespace' is not provided.""" - cluster_key = set() - cluster_key.add(self._info_cmd_helper('statistics')['cluster_key']) - cmd = "cluster-stable:" - target_cluster_size = self.module.params['target_cluster_size'] - if target_cluster_size is not None: - cmd = cmd + "size=" + str(target_cluster_size) + ";" - for node in self._nodes: - cluster_key.add(self._info_cmd_helper(cmd, node)) - if len(cluster_key) == 1: - return True - return False - - def _cluster_good_state(self): - """checks a few things to make sure we're OK to say the cluster - has no migs. It could be in a unhealthy condition that does not allow - migs, or a split brain""" - if self._cluster_key_consistent() is not True: - return False, "Cluster key inconsistent." - if self._is_min_cluster_size() is not True: - return False, "Cluster min size not reached." - if self._cluster_migrates_allowed() is not True: - return False, "migrate_allowed is false somewhere." - return True, "OK." - - def has_migs(self, local=True): - """returns a boolean, False if no migrations otherwise True""" - consecutive_good = 0 - try_num = 0 - skip_reason = list() - while \ - try_num < int(self.module.params['tries_limit']) and \ - consecutive_good < \ - int(self.module.params['consecutive_good_checks']): - - self._update_nodes_list() - self._update_cluster_statistics() - - # These checks are outside of the while loop because - # we probably want to skip & sleep instead of failing entirely - stable, reason = self._cluster_good_state() - if stable is not True: - skip_reason.append( - "Skipping on try#" + str(try_num) + - " for reason:" + reason - ) - else: - if self._can_use_cluster_stable(): - if self._cluster_stable(): - consecutive_good += 1 - else: - consecutive_good = 0 - skip_reason.append( - "Skipping on try#" + str(try_num) + - " for reason:" + " cluster_stable" - ) - elif self._has_migs(local): - # print("_has_migs") - skip_reason.append( - "Skipping on try#" + str(try_num) + - " for reason:" + " migrations" - ) - consecutive_good = 0 - else: - consecutive_good += 1 - if consecutive_good == self.module.params[ - 'consecutive_good_checks']: - break - try_num += 1 - sleep(self.module.params['sleep_between_checks']) - # print(skip_reason) - if consecutive_good == self.module.params['consecutive_good_checks']: - return False, None - return True, skip_reason - - -def main(): - """main method for ansible module""" - run_module() - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/influxdb/influxdb_database.py b/lib/ansible/modules/database/influxdb/influxdb_database.py deleted file mode 100644 index ae6c44b232..0000000000 --- a/lib/ansible/modules/database/influxdb/influxdb_database.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2016, Kamil Szczygiel <kamil.szczygiel () intel.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: influxdb_database -short_description: Manage InfluxDB databases -description: - - Manage InfluxDB databases. -version_added: 2.1 -author: "Kamil Szczygiel (@kamsz)" -requirements: - - "python >= 2.6" - - "influxdb >= 0.9 & <= 1.2.4" - - requests -options: - database_name: - description: - - Name of the database. - required: true - type: str - state: - description: - - Determines if the database should be created or destroyed. - choices: [ absent, present ] - default: present - type: str -extends_documentation_fragment: influxdb -''' - -EXAMPLES = r''' -# Example influxdb_database command from Ansible Playbooks -- name: Create database - influxdb_database: - hostname: "{{influxdb_ip_address}}" - database_name: "{{influxdb_database_name}}" - -- name: Destroy database - influxdb_database: - hostname: "{{influxdb_ip_address}}" - database_name: "{{influxdb_database_name}}" - state: absent - -- name: Create database using custom credentials - influxdb_database: - hostname: "{{influxdb_ip_address}}" - username: "{{influxdb_username}}" - password: "{{influxdb_password}}" - database_name: "{{influxdb_database_name}}" - ssl: yes - validate_certs: yes -''' - -RETURN = r''' -# only defaults -''' - -try: - import requests.exceptions - from influxdb import exceptions -except ImportError: - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.influxdb import InfluxDb - - -def find_database(module, client, database_name): - database = None - - try: - databases = client.get_list_database() - for db in databases: - if db['name'] == database_name: - database = db - break - except requests.exceptions.ConnectionError as e: - module.fail_json(msg=str(e)) - return database - - -def create_database(module, client, database_name): - if not module.check_mode: - try: - client.create_database(database_name) - except requests.exceptions.ConnectionError as e: - module.fail_json(msg=str(e)) - - module.exit_json(changed=True) - - -def drop_database(module, client, database_name): - if not module.check_mode: - try: - client.drop_database(database_name) - except exceptions.InfluxDBClientError as e: - module.fail_json(msg=e.content) - - module.exit_json(changed=True) - - -def main(): - argument_spec = InfluxDb.influxdb_argument_spec() - argument_spec.update( - database_name=dict(required=True, type='str'), - state=dict(default='present', type='str', choices=['present', 'absent']) - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - state = module.params['state'] - - influxdb = InfluxDb(module) - client = influxdb.connect_to_influxdb() - database_name = influxdb.database_name - database = find_database(module, client, database_name) - - if state == 'present': - if database: - module.exit_json(changed=False) - else: - create_database(module, client, database_name) - - if state == 'absent': - if database: - drop_database(module, client, database_name) - else: - module.exit_json(changed=False) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/influxdb/influxdb_query.py b/lib/ansible/modules/database/influxdb/influxdb_query.py deleted file mode 100644 index 4ecc178569..0000000000 --- a/lib/ansible/modules/database/influxdb/influxdb_query.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright (c) 2017, René Moser <mail@renemoser.net> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: influxdb_query -short_description: Query data points from InfluxDB -description: - - Query data points from InfluxDB. -version_added: 2.5 -author: "René Moser (@resmo)" -requirements: - - "python >= 2.6" - - "influxdb >= 0.9" -options: - query: - description: - - Query to be executed. - required: true - type: str - database_name: - description: - - Name of the database. - required: true - type: str -extends_documentation_fragment: influxdb -''' - -EXAMPLES = r''' -- name: Query connections - influxdb_query: - hostname: "{{ influxdb_ip_address }}" - database_name: "{{ influxdb_database_name }}" - query: "select mean(value) from connections" - register: connection - -- name: Query connections with tags filters - influxdb_query: - hostname: "{{ influxdb_ip_address }}" - database_name: "{{ influxdb_database_name }}" - query: "select mean(value) from connections where region='zue01' and host='server01'" - register: connection - -- name: Print results from the query - debug: - var: connection.query_results -''' - -RETURN = r''' -query_results: - description: Result from the query - returned: success - type: list - sample: - - mean: 1245.5333333333333 - time: "1970-01-01T00:00:00Z" -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible.module_utils.influxdb import InfluxDb - - -class AnsibleInfluxDBRead(InfluxDb): - - def read_by_query(self, query): - client = self.connect_to_influxdb() - try: - rs = client.query(query) - if rs: - return list(rs.get_points()) - except Exception as e: - self.module.fail_json(msg=to_native(e)) - - -def main(): - argument_spec = InfluxDb.influxdb_argument_spec() - argument_spec.update( - query=dict(type='str', required=True), - database_name=dict(required=True, type='str'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - influx = AnsibleInfluxDBRead(module) - query = module.params.get('query') - results = influx.read_by_query(query) - module.exit_json(changed=True, query_results=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/influxdb/influxdb_retention_policy.py b/lib/ansible/modules/database/influxdb/influxdb_retention_policy.py deleted file mode 100644 index 6a2eb697fb..0000000000 --- a/lib/ansible/modules/database/influxdb/influxdb_retention_policy.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2016, Kamil Szczygiel <kamil.szczygiel () intel.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: influxdb_retention_policy -short_description: Manage InfluxDB retention policies -description: - - Manage InfluxDB retention policies. -version_added: 2.1 -author: "Kamil Szczygiel (@kamsz)" -requirements: - - "python >= 2.6" - - "influxdb >= 0.9" - - requests -options: - database_name: - description: - - Name of the database. - required: true - type: str - policy_name: - description: - - Name of the retention policy. - required: true - type: str - duration: - description: - - Determines how long InfluxDB should keep the data. - required: true - type: str - replication: - description: - - Determines how many independent copies of each point are stored in the cluster. - required: true - type: int - default: - description: - - Sets the retention policy as default retention policy. - type: bool -extends_documentation_fragment: influxdb -''' - -EXAMPLES = r''' -# Example influxdb_retention_policy command from Ansible Playbooks -- name: create 1 hour retention policy - influxdb_retention_policy: - hostname: "{{influxdb_ip_address}}" - database_name: "{{influxdb_database_name}}" - policy_name: test - duration: 1h - replication: 1 - ssl: yes - validate_certs: yes - -- name: create 1 day retention policy - influxdb_retention_policy: - hostname: "{{influxdb_ip_address}}" - database_name: "{{influxdb_database_name}}" - policy_name: test - duration: 1d - replication: 1 - -- name: create 1 week retention policy - influxdb_retention_policy: - hostname: "{{influxdb_ip_address}}" - database_name: "{{influxdb_database_name}}" - policy_name: test - duration: 1w - replication: 1 - -- name: create infinite retention policy - influxdb_retention_policy: - hostname: "{{influxdb_ip_address}}" - database_name: "{{influxdb_database_name}}" - policy_name: test - duration: INF - replication: 1 - ssl: no - validate_certs: no -''' - -RETURN = r''' -# only defaults -''' - -import re - -try: - import requests.exceptions - from influxdb import exceptions -except ImportError: - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.influxdb import InfluxDb -from ansible.module_utils._text import to_native - - -def find_retention_policy(module, client): - database_name = module.params['database_name'] - policy_name = module.params['policy_name'] - hostname = module.params['hostname'] - retention_policy = None - - try: - retention_policies = client.get_list_retention_policies(database=database_name) - for policy in retention_policies: - if policy['name'] == policy_name: - retention_policy = policy - break - except requests.exceptions.ConnectionError as e: - module.fail_json(msg="Cannot connect to database %s on %s : %s" % (database_name, hostname, to_native(e))) - return retention_policy - - -def create_retention_policy(module, client): - database_name = module.params['database_name'] - policy_name = module.params['policy_name'] - duration = module.params['duration'] - replication = module.params['replication'] - default = module.params['default'] - - if not module.check_mode: - try: - client.create_retention_policy(policy_name, duration, replication, database_name, default) - except exceptions.InfluxDBClientError as e: - module.fail_json(msg=e.content) - module.exit_json(changed=True) - - -def alter_retention_policy(module, client, retention_policy): - database_name = module.params['database_name'] - policy_name = module.params['policy_name'] - duration = module.params['duration'] - replication = module.params['replication'] - default = module.params['default'] - duration_regexp = re.compile(r'(\d+)([hdw]{1})|(^INF$){1}') - changed = False - - duration_lookup = duration_regexp.search(duration) - - if duration_lookup.group(2) == 'h': - influxdb_duration_format = '%s0m0s' % duration - elif duration_lookup.group(2) == 'd': - influxdb_duration_format = '%sh0m0s' % (int(duration_lookup.group(1)) * 24) - elif duration_lookup.group(2) == 'w': - influxdb_duration_format = '%sh0m0s' % (int(duration_lookup.group(1)) * 24 * 7) - elif duration == 'INF': - influxdb_duration_format = '0' - - if (not retention_policy['duration'] == influxdb_duration_format or - not retention_policy['replicaN'] == int(replication) or - not retention_policy['default'] == default): - if not module.check_mode: - try: - client.alter_retention_policy(policy_name, database_name, duration, replication, default) - except exceptions.InfluxDBClientError as e: - module.fail_json(msg=e.content) - changed = True - module.exit_json(changed=changed) - - -def main(): - argument_spec = InfluxDb.influxdb_argument_spec() - argument_spec.update( - database_name=dict(required=True, type='str'), - policy_name=dict(required=True, type='str'), - duration=dict(required=True, type='str'), - replication=dict(required=True, type='int'), - default=dict(default=False, type='bool') - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - influxdb = InfluxDb(module) - client = influxdb.connect_to_influxdb() - - retention_policy = find_retention_policy(module, client) - - if retention_policy: - alter_retention_policy(module, client, retention_policy) - else: - create_retention_policy(module, client) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/influxdb/influxdb_user.py b/lib/ansible/modules/database/influxdb/influxdb_user.py deleted file mode 100644 index 0651942e7a..0000000000 --- a/lib/ansible/modules/database/influxdb/influxdb_user.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2017, Vitaliy Zhhuta <zhhuta () gmail.com> -# insipred by Kamil Szczygiel <kamil.szczygiel () intel.com> influxdb_database module -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: influxdb_user -short_description: Manage InfluxDB users -description: - - Manage InfluxDB users. -version_added: 2.5 -author: "Vitaliy Zhhuta (@zhhuta)" -requirements: - - "python >= 2.6" - - "influxdb >= 0.9" -options: - user_name: - description: - - Name of the user. - required: True - type: str - user_password: - description: - - Password to be set for the user. - required: false - type: str - admin: - description: - - Whether the user should be in the admin role or not. - - Since version 2.8, the role will also be updated. - default: no - type: bool - state: - description: - - State of the user. - choices: [ absent, present ] - default: present - type: str - grants: - description: - - Privileges to grant to this user. - - Takes a list of dicts containing the "database" and "privilege" keys. - - If this argument is not provided, the current grants will be left alone. - - If an empty list is provided, all grants for the user will be removed. - version_added: 2.8 - type: list - elements: dict -extends_documentation_fragment: influxdb -''' - -EXAMPLES = r''' -- name: Create a user on localhost using default login credentials - influxdb_user: - user_name: john - user_password: s3cr3t - -- name: Create a user on localhost using custom login credentials - influxdb_user: - user_name: john - user_password: s3cr3t - login_username: "{{ influxdb_username }}" - login_password: "{{ influxdb_password }}" - -- name: Create an admin user on a remote host using custom login credentials - influxdb_user: - user_name: john - user_password: s3cr3t - admin: yes - hostname: "{{ influxdb_hostname }}" - login_username: "{{ influxdb_username }}" - login_password: "{{ influxdb_password }}" - -- name: Create a user on localhost with privileges - influxdb_user: - user_name: john - user_password: s3cr3t - login_username: "{{ influxdb_username }}" - login_password: "{{ influxdb_password }}" - grants: - - database: 'collectd' - privilege: 'WRITE' - - database: 'graphite' - privilege: 'READ' - -- name: Destroy a user using custom login credentials - influxdb_user: - user_name: john - login_username: "{{ influxdb_username }}" - login_password: "{{ influxdb_password }}" - state: absent -''' - -RETURN = r''' -#only defaults -''' - -from ansible.module_utils.urls import ConnectionError -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -import ansible.module_utils.influxdb as influx - - -def find_user(module, client, user_name): - user_result = None - - try: - users = client.get_list_users() - for user in users: - if user['user'] == user_name: - user_result = user - break - except (ConnectionError, influx.exceptions.InfluxDBClientError) as e: - module.fail_json(msg=to_native(e)) - return user_result - - -def check_user_password(module, client, user_name, user_password): - try: - client.switch_user(user_name, user_password) - client.get_list_users() - except influx.exceptions.InfluxDBClientError as e: - if e.code == 401: - return False - except ConnectionError as e: - module.fail_json(msg=to_native(e)) - finally: - # restore previous user - client.switch_user(module.params['username'], module.params['password']) - return True - - -def set_user_password(module, client, user_name, user_password): - if not module.check_mode: - try: - client.set_user_password(user_name, user_password) - except ConnectionError as e: - module.fail_json(msg=to_native(e)) - - -def create_user(module, client, user_name, user_password, admin): - if not module.check_mode: - try: - client.create_user(user_name, user_password, admin) - except ConnectionError as e: - module.fail_json(msg=to_native(e)) - - -def drop_user(module, client, user_name): - if not module.check_mode: - try: - client.drop_user(user_name) - except influx.exceptions.InfluxDBClientError as e: - module.fail_json(msg=e.content) - - module.exit_json(changed=True) - - -def set_user_grants(module, client, user_name, grants): - changed = False - - try: - current_grants = client.get_list_privileges(user_name) - # Fix privileges wording - for i, v in enumerate(current_grants): - if v['privilege'] == 'ALL PRIVILEGES': - v['privilege'] = 'ALL' - current_grants[i] = v - elif v['privilege'] == 'NO PRIVILEGES': - del(current_grants[i]) - - # check if the current grants are included in the desired ones - for current_grant in current_grants: - if current_grant not in grants: - if not module.check_mode: - client.revoke_privilege(current_grant['privilege'], - current_grant['database'], - user_name) - changed = True - - # check if the desired grants are included in the current ones - for grant in grants: - if grant not in current_grants: - if not module.check_mode: - client.grant_privilege(grant['privilege'], - grant['database'], - user_name) - changed = True - - except influx.exceptions.InfluxDBClientError as e: - module.fail_json(msg=e.content) - - return changed - - -def main(): - argument_spec = influx.InfluxDb.influxdb_argument_spec() - argument_spec.update( - state=dict(default='present', type='str', choices=['present', 'absent']), - user_name=dict(required=True, type='str'), - user_password=dict(required=False, type='str', no_log=True), - admin=dict(default='False', type='bool'), - grants=dict(type='list', elements='dict'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - state = module.params['state'] - user_name = module.params['user_name'] - user_password = module.params['user_password'] - admin = module.params['admin'] - grants = module.params['grants'] - influxdb = influx.InfluxDb(module) - client = influxdb.connect_to_influxdb() - user = find_user(module, client, user_name) - - changed = False - - if state == 'present': - if user: - if not check_user_password(module, client, user_name, user_password) and user_password is not None: - set_user_password(module, client, user_name, user_password) - changed = True - - try: - if admin and not user['admin']: - client.grant_admin_privileges(user_name) - changed = True - elif not admin and user['admin']: - client.revoke_admin_privileges(user_name) - changed = True - except influx.exceptions.InfluxDBClientError as e: - module.fail_json(msg=to_native(e)) - - else: - user_password = user_password or '' - create_user(module, client, user_name, user_password, admin) - changed = True - - if grants is not None: - if set_user_grants(module, client, user_name, grants): - changed = True - - module.exit_json(changed=changed) - - if state == 'absent': - if user: - drop_user(module, client, user_name) - else: - module.exit_json(changed=False) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/influxdb/influxdb_write.py b/lib/ansible/modules/database/influxdb/influxdb_write.py deleted file mode 100644 index ccd462c19c..0000000000 --- a/lib/ansible/modules/database/influxdb/influxdb_write.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright (c) 2017, René Moser <mail@renemoser.net> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: influxdb_write -short_description: Write data points into InfluxDB -description: - - Write data points into InfluxDB. -version_added: 2.5 -author: "René Moser (@resmo)" -requirements: - - "python >= 2.6" - - "influxdb >= 0.9" -options: - data_points: - description: - - Data points as dict to write into the database. - required: true - type: list - elements: dict - database_name: - description: - - Name of the database. - required: true - type: str -extends_documentation_fragment: influxdb -''' - -EXAMPLES = r''' -- name: Write points into database - influxdb_write: - hostname: "{{influxdb_ip_address}}" - database_name: "{{influxdb_database_name}}" - data_points: - - measurement: connections - tags: - host: server01 - region: us-west - time: "{{ ansible_date_time.iso8601 }}" - fields: - value: 2000 - - measurement: connections - tags: - host: server02 - region: us-east - time: "{{ ansible_date_time.iso8601 }}" - fields: - value: 3000 -''' - -RETURN = r''' -# only defaults -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native -from ansible.module_utils.influxdb import InfluxDb - - -class AnsibleInfluxDBWrite(InfluxDb): - - def write_data_point(self, data_points): - client = self.connect_to_influxdb() - - try: - client.write_points(data_points) - except Exception as e: - self.module.fail_json(msg=to_native(e)) - - -def main(): - argument_spec = InfluxDb.influxdb_argument_spec() - argument_spec.update( - data_points=dict(required=True, type='list', elements='dict'), - database_name=dict(required=True, type='str'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - ) - - influx = AnsibleInfluxDBWrite(module) - data_points = module.params.get('data_points') - influx.write_data_point(data_points) - module.exit_json(changed=True) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/misc/elasticsearch_plugin.py b/lib/ansible/modules/database/misc/elasticsearch_plugin.py deleted file mode 100644 index bcf19fcf15..0000000000 --- a/lib/ansible/modules/database/misc/elasticsearch_plugin.py +++ /dev/null @@ -1,303 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# (c) 2015, Mathew Davies <thepixeldeveloper@googlemail.com> -# (c) 2017, Sam Doran <sdoran@redhat.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - - -DOCUMENTATION = ''' ---- -module: elasticsearch_plugin -short_description: Manage Elasticsearch plugins -description: - - Manages Elasticsearch plugins. -version_added: "2.0" -author: - - Mathew Davies (@ThePixelDeveloper) - - Sam Doran (@samdoran) -options: - name: - description: - - Name of the plugin to install. - required: True - state: - description: - - Desired state of a plugin. - choices: ["present", "absent"] - default: present - src: - description: - - Optionally set the source location to retrieve the plugin from. This can be a file:// - URL to install from a local file, or a remote URL. If this is not set, the plugin - location is just based on the name. - - The name parameter must match the descriptor in the plugin ZIP specified. - - Is only used if the state would change, which is solely checked based on the name - parameter. If, for example, the plugin is already installed, changing this has no - effect. - - For ES 1.x use url. - required: False - version_added: "2.7" - url: - description: - - Set exact URL to download the plugin from (Only works for ES 1.x). - - For ES 2.x and higher, use src. - required: False - timeout: - description: - - "Timeout setting: 30s, 1m, 1h..." - - Only valid for Elasticsearch < 5.0. This option is ignored for Elasticsearch > 5.0. - default: 1m - force: - description: - - "Force batch mode when installing plugins. This is only necessary if a plugin requires additional permissions and console detection fails." - default: False - type: bool - version_added: "2.7" - plugin_bin: - description: - - Location of the plugin binary. If this file is not found, the default plugin binaries will be used. - - The default changed in Ansible 2.4 to None. - plugin_dir: - description: - - Your configured plugin directory specified in Elasticsearch - default: /usr/share/elasticsearch/plugins/ - proxy_host: - description: - - Proxy host to use during plugin installation - version_added: "2.1" - proxy_port: - description: - - Proxy port to use during plugin installation - version_added: "2.1" - version: - description: - - Version of the plugin to be installed. - If plugin exists with previous version, it will NOT be updated -''' - -EXAMPLES = ''' -# Install Elasticsearch Head plugin in Elasticsearch 2.x -- elasticsearch_plugin: - name: mobz/elasticsearch-head - state: present - -# Install a specific version of Elasticsearch Head in Elasticsearch 2.x -- elasticsearch_plugin: - name: mobz/elasticsearch-head - version: 2.0.0 - -# Uninstall Elasticsearch head plugin in Elasticsearch 2.x -- elasticsearch_plugin: - name: mobz/elasticsearch-head - state: absent - -# Install a specific plugin in Elasticsearch >= 5.0 -- elasticsearch_plugin: - name: analysis-icu - state: present - -# Install the ingest-geoip plugin with a forced installation -- elasticsearch_plugin: - name: ingest-geoip - state: present - force: yes -''' - -import os - -from ansible.module_utils.basic import AnsibleModule - - -PACKAGE_STATE_MAP = dict( - present="install", - absent="remove" -) - -PLUGIN_BIN_PATHS = tuple([ - '/usr/share/elasticsearch/bin/elasticsearch-plugin', - '/usr/share/elasticsearch/bin/plugin' -]) - - -def parse_plugin_repo(string): - elements = string.split("/") - - # We first consider the simplest form: pluginname - repo = elements[0] - - # We consider the form: username/pluginname - if len(elements) > 1: - repo = elements[1] - - # remove elasticsearch- prefix - # remove es- prefix - for string in ("elasticsearch-", "es-"): - if repo.startswith(string): - return repo[len(string):] - - return repo - - -def is_plugin_present(plugin_name, plugin_dir): - return os.path.isdir(os.path.join(plugin_dir, plugin_name)) - - -def parse_error(string): - reason = "ERROR: " - try: - return string[string.index(reason) + len(reason):].strip() - except ValueError: - return string - - -def install_plugin(module, plugin_bin, plugin_name, version, src, url, proxy_host, proxy_port, timeout, force): - cmd_args = [plugin_bin, PACKAGE_STATE_MAP["present"]] - is_old_command = (os.path.basename(plugin_bin) == 'plugin') - - # Timeout and version are only valid for plugin, not elasticsearch-plugin - if is_old_command: - if timeout: - cmd_args.append("--timeout %s" % timeout) - - if version: - plugin_name = plugin_name + '/' + version - cmd_args[2] = plugin_name - - if proxy_host and proxy_port: - cmd_args.append("-DproxyHost=%s -DproxyPort=%s" % (proxy_host, proxy_port)) - - # Legacy ES 1.x - if url: - cmd_args.append("--url %s" % url) - - if force: - cmd_args.append("--batch") - if src: - cmd_args.append(src) - else: - cmd_args.append(plugin_name) - - cmd = " ".join(cmd_args) - - if module.check_mode: - rc, out, err = 0, "check mode", "" - else: - rc, out, err = module.run_command(cmd) - - if rc != 0: - reason = parse_error(out) - module.fail_json(msg="Installing plugin '%s' failed: %s" % (plugin_name, reason), err=err) - - return True, cmd, out, err - - -def remove_plugin(module, plugin_bin, plugin_name): - cmd_args = [plugin_bin, PACKAGE_STATE_MAP["absent"], parse_plugin_repo(plugin_name)] - - cmd = " ".join(cmd_args) - - if module.check_mode: - rc, out, err = 0, "check mode", "" - else: - rc, out, err = module.run_command(cmd) - - if rc != 0: - reason = parse_error(out) - module.fail_json(msg="Removing plugin '%s' failed: %s" % (plugin_name, reason), err=err) - - return True, cmd, out, err - - -def get_plugin_bin(module, plugin_bin=None): - # Use the plugin_bin that was supplied first before trying other options - valid_plugin_bin = None - if plugin_bin and os.path.isfile(plugin_bin): - valid_plugin_bin = plugin_bin - - else: - # Add the plugin_bin passed into the module to the top of the list of paths to test, - # testing for that binary name first before falling back to the default paths. - bin_paths = list(PLUGIN_BIN_PATHS) - if plugin_bin and plugin_bin not in bin_paths: - bin_paths.insert(0, plugin_bin) - - # Get separate lists of dirs and binary names from the full paths to the - # plugin binaries. - plugin_dirs = list(set([os.path.dirname(x) for x in bin_paths])) - plugin_bins = list(set([os.path.basename(x) for x in bin_paths])) - - # Check for the binary names in the default system paths as well as the path - # specified in the module arguments. - for bin_file in plugin_bins: - valid_plugin_bin = module.get_bin_path(bin_file, opt_dirs=plugin_dirs) - if valid_plugin_bin: - break - - if not valid_plugin_bin: - module.fail_json(msg='%s does not exist and no other valid plugin installers were found. Make sure Elasticsearch is installed.' % plugin_bin) - - return valid_plugin_bin - - -def main(): - module = AnsibleModule( - argument_spec=dict( - name=dict(required=True), - state=dict(default="present", choices=PACKAGE_STATE_MAP.keys()), - src=dict(default=None), - url=dict(default=None), - timeout=dict(default="1m"), - force=dict(type='bool', default=False), - plugin_bin=dict(type="path"), - plugin_dir=dict(default="/usr/share/elasticsearch/plugins/", type="path"), - proxy_host=dict(default=None), - proxy_port=dict(default=None), - version=dict(default=None) - ), - mutually_exclusive=[("src", "url")], - supports_check_mode=True - ) - - name = module.params["name"] - state = module.params["state"] - url = module.params["url"] - src = module.params["src"] - timeout = module.params["timeout"] - force = module.params["force"] - plugin_bin = module.params["plugin_bin"] - plugin_dir = module.params["plugin_dir"] - proxy_host = module.params["proxy_host"] - proxy_port = module.params["proxy_port"] - version = module.params["version"] - - # Search provided path and system paths for valid binary - plugin_bin = get_plugin_bin(module, plugin_bin) - - repo = parse_plugin_repo(name) - present = is_plugin_present(repo, plugin_dir) - - # skip if the state is correct - if (present and state == "present") or (state == "absent" and not present): - module.exit_json(changed=False, name=name, state=state) - - if state == "present": - changed, cmd, out, err = install_plugin(module, plugin_bin, name, version, src, url, proxy_host, proxy_port, timeout, force) - - elif state == "absent": - changed, cmd, out, err = remove_plugin(module, plugin_bin, name) - - module.exit_json(changed=changed, cmd=cmd, name=name, state=state, url=url, timeout=timeout, stdout=out, stderr=err) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/misc/kibana_plugin.py b/lib/ansible/modules/database/misc/kibana_plugin.py deleted file mode 100644 index 3db9f88bc3..0000000000 --- a/lib/ansible/modules/database/misc/kibana_plugin.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright (c) 2016, Thierno IB. BARRY @barryib -# Sponsored by Polyconseil http://polyconseil.fr. -# -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - - -DOCUMENTATION = ''' ---- -module: kibana_plugin -short_description: Manage Kibana plugins -description: - - This module can be used to manage Kibana plugins. -version_added: "2.2" -author: Thierno IB. BARRY (@barryib) -options: - name: - description: - - Name of the plugin to install. - required: True - state: - description: - - Desired state of a plugin. - choices: ["present", "absent"] - default: present - url: - description: - - Set exact URL to download the plugin from. - - For local file, prefix its absolute path with file:// - timeout: - description: - - "Timeout setting: 30s, 1m, 1h etc." - default: 1m - plugin_bin: - description: - - Location of the Kibana binary. - default: /opt/kibana/bin/kibana - plugin_dir: - description: - - Your configured plugin directory specified in Kibana. - default: /opt/kibana/installedPlugins/ - version: - description: - - Version of the plugin to be installed. - - If plugin exists with previous version, plugin will NOT be updated unless C(force) is set to yes. - force: - description: - - Delete and re-install the plugin. Can be useful for plugins update. - type: bool - default: 'no' -''' - -EXAMPLES = ''' -- name: Install Elasticsearch head plugin - kibana_plugin: - state: present - name: elasticsearch/marvel - -- name: Install specific version of a plugin - kibana_plugin: - state: present - name: elasticsearch/marvel - version: '2.3.3' - -- name: Uninstall Elasticsearch head plugin - kibana_plugin: - state: absent - name: elasticsearch/marvel -''' - -RETURN = ''' -cmd: - description: the launched command during plugin management (install / remove) - returned: success - type: str -name: - description: the plugin name to install or remove - returned: success - type: str -url: - description: the url from where the plugin is installed from - returned: success - type: str -timeout: - description: the timeout for plugin download - returned: success - type: str -stdout: - description: the command stdout - returned: success - type: str -stderr: - description: the command stderr - returned: success - type: str -state: - description: the state for the managed plugin - returned: success - type: str -''' - -import os -from distutils.version import LooseVersion -from ansible.module_utils.basic import AnsibleModule - - -PACKAGE_STATE_MAP = dict( - present="--install", - absent="--remove" -) - - -def parse_plugin_repo(string): - elements = string.split("/") - - # We first consider the simplest form: pluginname - repo = elements[0] - - # We consider the form: username/pluginname - if len(elements) > 1: - repo = elements[1] - - # remove elasticsearch- prefix - # remove es- prefix - for string in ("elasticsearch-", "es-"): - if repo.startswith(string): - return repo[len(string):] - - return repo - - -def is_plugin_present(plugin_dir, working_dir): - return os.path.isdir(os.path.join(working_dir, plugin_dir)) - - -def parse_error(string): - reason = "reason: " - try: - return string[string.index(reason) + len(reason):].strip() - except ValueError: - return string - - -def install_plugin(module, plugin_bin, plugin_name, url, timeout, kibana_version='4.6'): - if LooseVersion(kibana_version) > LooseVersion('4.6'): - kibana_plugin_bin = os.path.join(os.path.dirname(plugin_bin), 'kibana-plugin') - cmd_args = [kibana_plugin_bin, "install"] - if url: - cmd_args.append(url) - else: - cmd_args.append(plugin_name) - else: - cmd_args = [plugin_bin, "plugin", PACKAGE_STATE_MAP["present"], plugin_name] - - if url: - cmd_args.append("--url %s" % url) - - if timeout: - cmd_args.append("--timeout %s" % timeout) - - cmd = " ".join(cmd_args) - - if module.check_mode: - return True, cmd, "check mode", "" - - rc, out, err = module.run_command(cmd) - if rc != 0: - reason = parse_error(out) - module.fail_json(msg=reason) - - return True, cmd, out, err - - -def remove_plugin(module, plugin_bin, plugin_name, kibana_version='4.6'): - if LooseVersion(kibana_version) > LooseVersion('4.6'): - kibana_plugin_bin = os.path.join(os.path.dirname(plugin_bin), 'kibana-plugin') - cmd_args = [kibana_plugin_bin, "remove", plugin_name] - else: - cmd_args = [plugin_bin, "plugin", PACKAGE_STATE_MAP["absent"], plugin_name] - - cmd = " ".join(cmd_args) - - if module.check_mode: - return True, cmd, "check mode", "" - - rc, out, err = module.run_command(cmd) - if rc != 0: - reason = parse_error(out) - module.fail_json(msg=reason) - - return True, cmd, out, err - - -def get_kibana_version(module, plugin_bin): - cmd_args = [plugin_bin, '--version'] - cmd = " ".join(cmd_args) - rc, out, err = module.run_command(cmd) - if rc != 0: - module.fail_json(msg="Failed to get Kibana version : %s" % err) - - return out.strip() - - -def main(): - module = AnsibleModule( - argument_spec=dict( - name=dict(required=True), - state=dict(default="present", choices=PACKAGE_STATE_MAP.keys()), - url=dict(default=None), - timeout=dict(default="1m"), - plugin_bin=dict(default="/opt/kibana/bin/kibana", type="path"), - plugin_dir=dict(default="/opt/kibana/installedPlugins/", type="path"), - version=dict(default=None), - force=dict(default="no", type="bool") - ), - supports_check_mode=True, - ) - - name = module.params["name"] - state = module.params["state"] - url = module.params["url"] - timeout = module.params["timeout"] - plugin_bin = module.params["plugin_bin"] - plugin_dir = module.params["plugin_dir"] - version = module.params["version"] - force = module.params["force"] - - changed, cmd, out, err = False, '', '', '' - - kibana_version = get_kibana_version(module, plugin_bin) - - present = is_plugin_present(parse_plugin_repo(name), plugin_dir) - - # skip if the state is correct - if (present and state == "present" and not force) or (state == "absent" and not present and not force): - module.exit_json(changed=False, name=name, state=state) - - if version: - name = name + '/' + version - - if state == "present": - if force: - remove_plugin(module, plugin_bin, name) - changed, cmd, out, err = install_plugin(module, plugin_bin, name, url, timeout, kibana_version) - - elif state == "absent": - changed, cmd, out, err = remove_plugin(module, plugin_bin, name, kibana_version) - - module.exit_json(changed=changed, cmd=cmd, name=name, state=state, url=url, timeout=timeout, stdout=out, stderr=err) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/misc/redis.py b/lib/ansible/modules/database/misc/redis.py deleted file mode 100644 index 475f072892..0000000000 --- a/lib/ansible/modules/database/misc/redis.py +++ /dev/null @@ -1,315 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: redis -short_description: Various redis commands, slave and flush -description: - - Unified utility to interact with redis instances. -version_added: "1.3" -options: - command: - description: - - The selected redis command - - C(config) (new in 1.6), ensures a configuration setting on an instance. - - C(flush) flushes all the instance or a specified db. - - C(slave) sets a redis instance in slave or master mode. - required: true - choices: [ config, flush, slave ] - login_password: - description: - - The password used to authenticate with (usually not used) - login_host: - description: - - The host running the database - default: localhost - login_port: - description: - - The port to connect to - default: 6379 - master_host: - description: - - The host of the master instance [slave command] - master_port: - description: - - The port of the master instance [slave command] - slave_mode: - description: - - the mode of the redis instance [slave command] - default: slave - choices: [ master, slave ] - db: - description: - - The database to flush (used in db mode) [flush command] - flush_mode: - description: - - Type of flush (all the dbs in a redis instance or a specific one) - [flush command] - default: all - choices: [ all, db ] - name: - description: - - A redis config key. - version_added: 1.6 - value: - description: - - A redis config value. When memory size is needed, it is possible - to specify it in the usal form of 1KB, 2M, 400MB where the base is 1024. - Units are case insensitive i.e. 1m = 1mb = 1M = 1MB. - version_added: 1.6 - -notes: - - Requires the redis-py Python package on the remote host. You can - install it with pip (pip install redis) or with a package manager. - https://github.com/andymccurdy/redis-py - - If the redis master instance we are making slave of is password protected - this needs to be in the redis.conf in the masterauth variable - -requirements: [ redis ] -author: "Xabier Larrakoetxea (@slok)" -''' - -EXAMPLES = ''' -- name: Set local redis instance to be slave of melee.island on port 6377 - redis: - command: slave - master_host: melee.island - master_port: 6377 - -- name: Deactivate slave mode - redis: - command: slave - slave_mode: master - -- name: Flush all the redis db - redis: - command: flush - flush_mode: all - -- name: Flush only one db in a redis instance - redis: - command: flush - db: 1 - flush_mode: db - -- name: Configure local redis to have 10000 max clients - redis: - command: config - name: maxclients - value: 10000 - -- name: Configure local redis maxmemory to 4GB - redis: - command: config - name: maxmemory - value: 4GB - -- name: Configure local redis to have lua time limit of 100 ms - redis: - command: config - name: lua-time-limit - value: 100 -''' - -import traceback - -REDIS_IMP_ERR = None -try: - import redis -except ImportError: - REDIS_IMP_ERR = traceback.format_exc() - redis_found = False -else: - redis_found = True - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.common.text.formatters import human_to_bytes -from ansible.module_utils._text import to_native - - -# Redis module specific support methods. -def set_slave_mode(client, master_host, master_port): - try: - return client.slaveof(master_host, master_port) - except Exception: - return False - - -def set_master_mode(client): - try: - return client.slaveof() - except Exception: - return False - - -def flush(client, db=None): - try: - if not isinstance(db, int): - return client.flushall() - else: - # The passed client has been connected to the database already - return client.flushdb() - except Exception: - return False - - -# Module execution. -def main(): - module = AnsibleModule( - argument_spec=dict( - command=dict(type='str', choices=['config', 'flush', 'slave']), - login_password=dict(type='str', no_log=True), - login_host=dict(type='str', default='localhost'), - login_port=dict(type='int', default=6379), - master_host=dict(type='str'), - master_port=dict(type='int'), - slave_mode=dict(type='str', default='slave', choices=['master', 'slave']), - db=dict(type='int'), - flush_mode=dict(type='str', default='all', choices=['all', 'db']), - name=dict(type='str'), - value=dict(type='str') - ), - supports_check_mode=True, - ) - - if not redis_found: - module.fail_json(msg=missing_required_lib('redis'), exception=REDIS_IMP_ERR) - - login_password = module.params['login_password'] - login_host = module.params['login_host'] - login_port = module.params['login_port'] - command = module.params['command'] - - # Slave Command section ----------- - if command == "slave": - master_host = module.params['master_host'] - master_port = module.params['master_port'] - mode = module.params['slave_mode'] - - # Check if we have all the data - if mode == "slave": # Only need data if we want to be slave - if not master_host: - module.fail_json(msg='In slave mode master host must be provided') - - if not master_port: - module.fail_json(msg='In slave mode master port must be provided') - - # Connect and check - r = redis.StrictRedis(host=login_host, port=login_port, password=login_password) - try: - r.ping() - except Exception as e: - module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) - - # Check if we are already in the mode that we want - info = r.info() - if mode == "master" and info["role"] == "master": - module.exit_json(changed=False, mode=mode) - - elif mode == "slave" and info["role"] == "slave" and info["master_host"] == master_host and info["master_port"] == master_port: - status = dict( - status=mode, - master_host=master_host, - master_port=master_port, - ) - module.exit_json(changed=False, mode=status) - else: - # Do the stuff - # (Check Check_mode before commands so the commands aren't evaluated - # if not necessary) - if mode == "slave": - if module.check_mode or\ - set_slave_mode(r, master_host, master_port): - info = r.info() - status = { - 'status': mode, - 'master_host': master_host, - 'master_port': master_port, - } - module.exit_json(changed=True, mode=status) - else: - module.fail_json(msg='Unable to set slave mode') - - else: - if module.check_mode or set_master_mode(r): - module.exit_json(changed=True, mode=mode) - else: - module.fail_json(msg='Unable to set master mode') - - # flush Command section ----------- - elif command == "flush": - db = module.params['db'] - mode = module.params['flush_mode'] - - # Check if we have all the data - if mode == "db": - if db is None: - module.fail_json(msg="In db mode the db number must be provided") - - # Connect and check - r = redis.StrictRedis(host=login_host, port=login_port, password=login_password, db=db) - try: - r.ping() - except Exception as e: - module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) - - # Do the stuff - # (Check Check_mode before commands so the commands aren't evaluated - # if not necessary) - if mode == "all": - if module.check_mode or flush(r): - module.exit_json(changed=True, flushed=True) - else: # Flush never fails :) - module.fail_json(msg="Unable to flush all databases") - - else: - if module.check_mode or flush(r, db): - module.exit_json(changed=True, flushed=True, db=db) - else: # Flush never fails :) - module.fail_json(msg="Unable to flush '%d' database" % db) - elif command == 'config': - name = module.params['name'] - - try: # try to parse the value as if it were the memory size - value = str(human_to_bytes(module.params['value'].upper())) - except ValueError: - value = module.params['value'] - - r = redis.StrictRedis(host=login_host, port=login_port, password=login_password) - - try: - r.ping() - except Exception as e: - module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) - - try: - old_value = r.config_get(name)[name] - except Exception as e: - module.fail_json(msg="unable to read config: %s" % to_native(e), exception=traceback.format_exc()) - changed = old_value != value - - if module.check_mode or not changed: - module.exit_json(changed=changed, name=name, value=value) - else: - try: - r.config_set(name, value) - except Exception as e: - module.fail_json(msg="unable to write config: %s" % to_native(e), exception=traceback.format_exc()) - module.exit_json(changed=changed, name=name, value=value) - else: - module.fail_json(msg='A valid command must be provided') - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/misc/riak.py b/lib/ansible/modules/database/misc/riak.py deleted file mode 100644 index 46b048b834..0000000000 --- a/lib/ansible/modules/database/misc/riak.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2013, James Martin <jmartin@basho.com>, Drew Kerrigan <dkerrigan@basho.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: riak -short_description: This module handles some common Riak operations -description: - - This module can be used to join nodes to a cluster, check - the status of the cluster. -version_added: "1.2" -author: - - "James Martin (@jsmartin)" - - "Drew Kerrigan (@drewkerrigan)" -options: - command: - description: - - The command you would like to perform against the cluster. - choices: ['ping', 'kv_test', 'join', 'plan', 'commit'] - config_dir: - description: - - The path to the riak configuration directory - default: /etc/riak - http_conn: - description: - - The ip address and port that is listening for Riak HTTP queries - default: 127.0.0.1:8098 - target_node: - description: - - The target node for certain operations (join, ping) - default: riak@127.0.0.1 - wait_for_handoffs: - description: - - Number of seconds to wait for handoffs to complete. - wait_for_ring: - description: - - Number of seconds to wait for all nodes to agree on the ring. - wait_for_service: - description: - - Waits for a riak service to come online before continuing. - choices: ['kv'] - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used - on personally controlled sites using self-signed certificates. - type: bool - default: 'yes' - version_added: 1.5.1 -''' - -EXAMPLES = ''' -# Join's a Riak node to another node -- riak: - command: join - target_node: riak@10.1.1.1 - -# Wait for handoffs to finish. Use with async and poll. -- riak: - wait_for_handoffs: yes - -# Wait for riak_kv service to startup -- riak: - wait_for_service: kv -''' - -import json -import time - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.urls import fetch_url - - -def ring_check(module, riak_admin_bin): - cmd = '%s ringready' % riak_admin_bin - rc, out, err = module.run_command(cmd) - if rc == 0 and 'TRUE All nodes agree on the ring' in out: - return True - else: - return False - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - command=dict(required=False, default=None, choices=[ - 'ping', 'kv_test', 'join', 'plan', 'commit']), - config_dir=dict(default='/etc/riak', type='path'), - http_conn=dict(required=False, default='127.0.0.1:8098'), - target_node=dict(default='riak@127.0.0.1', required=False), - wait_for_handoffs=dict(default=False, type='int'), - wait_for_ring=dict(default=False, type='int'), - wait_for_service=dict( - required=False, default=None, choices=['kv']), - validate_certs=dict(default='yes', type='bool')) - ) - - command = module.params.get('command') - http_conn = module.params.get('http_conn') - target_node = module.params.get('target_node') - wait_for_handoffs = module.params.get('wait_for_handoffs') - wait_for_ring = module.params.get('wait_for_ring') - wait_for_service = module.params.get('wait_for_service') - - # make sure riak commands are on the path - riak_bin = module.get_bin_path('riak') - riak_admin_bin = module.get_bin_path('riak-admin') - - timeout = time.time() + 120 - while True: - if time.time() > timeout: - module.fail_json(msg='Timeout, could not fetch Riak stats.') - (response, info) = fetch_url(module, 'http://%s/stats' % (http_conn), force=True, timeout=5) - if info['status'] == 200: - stats_raw = response.read() - break - time.sleep(5) - - # here we attempt to load those stats, - try: - stats = json.loads(stats_raw) - except Exception: - module.fail_json(msg='Could not parse Riak stats.') - - node_name = stats['nodename'] - nodes = stats['ring_members'] - ring_size = stats['ring_creation_size'] - rc, out, err = module.run_command([riak_bin, 'version']) - version = out.strip() - - result = dict(node_name=node_name, - nodes=nodes, - ring_size=ring_size, - version=version) - - if command == 'ping': - cmd = '%s ping %s' % (riak_bin, target_node) - rc, out, err = module.run_command(cmd) - if rc == 0: - result['ping'] = out - else: - module.fail_json(msg=out) - - elif command == 'kv_test': - cmd = '%s test' % riak_admin_bin - rc, out, err = module.run_command(cmd) - if rc == 0: - result['kv_test'] = out - else: - module.fail_json(msg=out) - - elif command == 'join': - if nodes.count(node_name) == 1 and len(nodes) > 1: - result['join'] = 'Node is already in cluster or staged to be in cluster.' - else: - cmd = '%s cluster join %s' % (riak_admin_bin, target_node) - rc, out, err = module.run_command(cmd) - if rc == 0: - result['join'] = out - result['changed'] = True - else: - module.fail_json(msg=out) - - elif command == 'plan': - cmd = '%s cluster plan' % riak_admin_bin - rc, out, err = module.run_command(cmd) - if rc == 0: - result['plan'] = out - if 'Staged Changes' in out: - result['changed'] = True - else: - module.fail_json(msg=out) - - elif command == 'commit': - cmd = '%s cluster commit' % riak_admin_bin - rc, out, err = module.run_command(cmd) - if rc == 0: - result['commit'] = out - result['changed'] = True - else: - module.fail_json(msg=out) - -# this could take a while, recommend to run in async mode - if wait_for_handoffs: - timeout = time.time() + wait_for_handoffs - while True: - cmd = '%s transfers' % riak_admin_bin - rc, out, err = module.run_command(cmd) - if 'No transfers active' in out: - result['handoffs'] = 'No transfers active.' - break - time.sleep(10) - if time.time() > timeout: - module.fail_json(msg='Timeout waiting for handoffs.') - - if wait_for_service: - cmd = [riak_admin_bin, 'wait_for_service', 'riak_%s' % wait_for_service, node_name] - rc, out, err = module.run_command(cmd) - result['service'] = out - - if wait_for_ring: - timeout = time.time() + wait_for_ring - while True: - if ring_check(module, riak_admin_bin): - break - time.sleep(10) - if time.time() > timeout: - module.fail_json(msg='Timeout waiting for nodes to agree on ring.') - - result['ring_ready'] = ring_check(module, riak_admin_bin) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/mssql/mssql_db.py b/lib/ansible/modules/database/mssql/mssql_db.py deleted file mode 100644 index ac23e0be7e..0000000000 --- a/lib/ansible/modules/database/mssql/mssql_db.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2014, Vedit Firat Arig <firatarig@gmail.com> -# Outline and parts are reused from Mark Theunissen's mysql_db module -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: mssql_db -short_description: Add or remove MSSQL databases from a remote host. -description: - - Add or remove MSSQL databases from a remote host. -version_added: "2.2" -options: - name: - description: - - name of the database to add or remove - required: true - aliases: [ db ] - login_user: - description: - - The username used to authenticate with - login_password: - description: - - The password used to authenticate with - login_host: - description: - - Host running the database - login_port: - description: - - Port of the MSSQL server. Requires login_host be defined as other than localhost if login_port is used - default: 1433 - state: - description: - - The database state - default: present - choices: [ "present", "absent", "import" ] - target: - description: - - Location, on the remote host, of the dump file to read from or write to. Uncompressed SQL - files (C(.sql)) files are supported. - autocommit: - description: - - Automatically commit the change only if the import succeed. Sometimes it is necessary to use autocommit=true, since some content can't be changed - within a transaction. - type: bool - default: 'no' -notes: - - Requires the pymssql Python package on the remote host. For Ubuntu, this - is as easy as pip install pymssql (See M(pip).) -requirements: - - python >= 2.7 - - pymssql -author: Vedit Firat Arig (@vedit) -''' - -EXAMPLES = ''' -# Create a new database with name 'jackdata' -- mssql_db: - name: jackdata - state: present - -# Copy database dump file to remote host and restore it to database 'my_db' -- copy: - src: dump.sql - dest: /tmp - -- mssql_db: - name: my_db - state: import - target: /tmp/dump.sql -''' - -RETURN = ''' -# -''' - -import os -import traceback - -PYMSSQL_IMP_ERR = None -try: - import pymssql -except ImportError: - PYMSSQL_IMP_ERR = traceback.format_exc() - mssql_found = False -else: - mssql_found = True - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib - - -def db_exists(conn, cursor, db): - cursor.execute("SELECT name FROM master.sys.databases WHERE name = %s", db) - conn.commit() - return bool(cursor.rowcount) - - -def db_create(conn, cursor, db): - cursor.execute("CREATE DATABASE [%s]" % db) - return db_exists(conn, cursor, db) - - -def db_delete(conn, cursor, db): - try: - cursor.execute("ALTER DATABASE [%s] SET single_user WITH ROLLBACK IMMEDIATE" % db) - except Exception: - pass - cursor.execute("DROP DATABASE [%s]" % db) - return not db_exists(conn, cursor, db) - - -def db_import(conn, cursor, module, db, target): - if os.path.isfile(target): - with open(target, 'r') as backup: - sqlQuery = "USE [%s]\n" % db - for line in backup: - if line is None: - break - elif line.startswith('GO'): - cursor.execute(sqlQuery) - sqlQuery = "USE [%s]\n" % db - else: - sqlQuery += line - cursor.execute(sqlQuery) - conn.commit() - return 0, "import successful", "" - else: - return 1, "cannot find target file", "cannot find target file" - - -def main(): - module = AnsibleModule( - argument_spec=dict( - name=dict(required=True, aliases=['db']), - login_user=dict(default=''), - login_password=dict(default='', no_log=True), - login_host=dict(required=True), - login_port=dict(default='1433'), - target=dict(default=None), - autocommit=dict(type='bool', default=False), - state=dict( - default='present', choices=['present', 'absent', 'import']) - ) - ) - - if not mssql_found: - module.fail_json(msg=missing_required_lib('pymssql'), exception=PYMSSQL_IMP_ERR) - - db = module.params['name'] - state = module.params['state'] - autocommit = module.params['autocommit'] - target = module.params["target"] - - login_user = module.params['login_user'] - login_password = module.params['login_password'] - login_host = module.params['login_host'] - login_port = module.params['login_port'] - - login_querystring = login_host - if login_port != "1433": - login_querystring = "%s:%s" % (login_host, login_port) - - if login_user != "" and login_password == "": - module.fail_json(msg="when supplying login_user arguments login_password must be provided") - - try: - conn = pymssql.connect(user=login_user, password=login_password, host=login_querystring, database='master') - cursor = conn.cursor() - except Exception as e: - if "Unknown database" in str(e): - errno, errstr = e.args - module.fail_json(msg="ERROR: %s %s" % (errno, errstr)) - else: - module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check your " - "@sysconfdir@/freetds.conf / ${HOME}/.freetds.conf") - - conn.autocommit(True) - changed = False - - if db_exists(conn, cursor, db): - if state == "absent": - try: - changed = db_delete(conn, cursor, db) - except Exception as e: - module.fail_json(msg="error deleting database: " + str(e)) - elif state == "import": - conn.autocommit(autocommit) - rc, stdout, stderr = db_import(conn, cursor, module, db, target) - - if rc != 0: - module.fail_json(msg="%s" % stderr) - else: - module.exit_json(changed=True, db=db, msg=stdout) - else: - if state == "present": - try: - changed = db_create(conn, cursor, db) - except Exception as e: - module.fail_json(msg="error creating database: " + str(e)) - elif state == "import": - try: - changed = db_create(conn, cursor, db) - except Exception as e: - module.fail_json(msg="error creating database: " + str(e)) - - conn.autocommit(autocommit) - rc, stdout, stderr = db_import(conn, cursor, module, db, target) - - if rc != 0: - module.fail_json(msg="%s" % stderr) - else: - module.exit_json(changed=True, db=db, msg=stdout) - - module.exit_json(changed=changed, db=db) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/mysql/mysql_db.py b/lib/ansible/modules/database/mysql/mysql_db.py deleted file mode 100644 index 58bcca7832..0000000000 --- a/lib/ansible/modules/database/mysql/mysql_db.py +++ /dev/null @@ -1,617 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2012, Mark Theunissen <mark.theunissen@gmail.com> -# Sponsored by Four Kitchens http://fourkitchens.com. -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: mysql_db -short_description: Add or remove MySQL databases from a remote host -description: -- Add or remove MySQL databases from a remote host. -version_added: '0.6' -options: - name: - description: - - Name of the database to add or remove. - - I(name=all) may only be provided if I(state) is C(dump) or C(import). - - List of databases is provided with I(state=dump), I(state=present) and I(state=absent). - - If I(name=all) it works like --all-databases option for mysqldump (Added in 2.0). - required: true - type: list - elements: str - aliases: [db] - state: - description: - - The database state - type: str - default: present - choices: ['absent', 'dump', 'import', 'present'] - collation: - description: - - Collation mode (sorting). This only applies to new table/databases and - does not update existing ones, this is a limitation of MySQL. - type: str - default: '' - encoding: - description: - - Encoding mode to use, examples include C(utf8) or C(latin1_swedish_ci), - at creation of database, dump or importation of sql script. - type: str - default: '' - target: - description: - - Location, on the remote host, of the dump file to read from or write to. - - Uncompressed SQL files (C(.sql)) as well as bzip2 (C(.bz2)), gzip (C(.gz)) and - xz (Added in 2.0) compressed files are supported. - type: path - single_transaction: - description: - - Execute the dump in a single transaction. - type: bool - default: no - version_added: '2.1' - quick: - description: - - Option used for dumping large tables. - type: bool - default: yes - version_added: '2.1' - ignore_tables: - description: - - A list of table names that will be ignored in the dump - of the form database_name.table_name. - type: list - elements: str - required: no - default: [] - version_added: '2.7' - hex_blob: - description: - - Dump binary columns using hexadecimal notation. - required: no - default: no - type: bool - version_added: '2.10' - force: - description: - - Continue dump or import even if we get an SQL error. - - Used only when I(state) is C(dump) or C(import). - required: no - type: bool - default: no - version_added: '2.10' - master_data: - description: - - Option to dump a master replication server to produce a dump file - that can be used to set up another server as a slave of the master. - - C(0) to not include master data. - - C(1) to generate a 'CHANGE MASTER TO' statement - required on the slave to start the replication process. - - C(2) to generate a commented 'CHANGE MASTER TO'. - - Can be used when I(state=dump). - required: no - type: int - choices: [0, 1, 2] - default: 0 - version_added: '2.10' - skip_lock_tables: - description: - - Skip locking tables for read. Used when I(state=dump), ignored otherwise. - required: no - type: bool - default: no - version_added: '2.10' - dump_extra_args: - description: - - Provide additional arguments for mysqldump. - Used when I(state=dump) only, ignored otherwise. - required: no - type: str - version_added: '2.10' -seealso: -- module: mysql_info -- module: mysql_variables -- module: mysql_user -- module: mysql_replication -- name: MySQL command-line client reference - description: Complete reference of the MySQL command-line client documentation. - link: https://dev.mysql.com/doc/refman/8.0/en/mysql.html -- name: mysqldump reference - description: Complete reference of the ``mysqldump`` client utility documentation. - link: https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html -- name: CREATE DATABASE reference - description: Complete reference of the CREATE DATABASE command documentation. - link: https://dev.mysql.com/doc/refman/8.0/en/create-database.html -- name: DROP DATABASE reference - description: Complete reference of the DROP DATABASE command documentation. - link: https://dev.mysql.com/doc/refman/8.0/en/drop-database.html -author: "Ansible Core Team" -requirements: - - mysql (command line binary) - - mysqldump (command line binary) -notes: - - Requires the mysql and mysqldump binaries on the remote host. - - This module is B(not idempotent) when I(state) is C(import), - and will import the dump file each time if run more than once. -extends_documentation_fragment: mysql -''' - -EXAMPLES = r''' -- name: Create a new database with name 'bobdata' - mysql_db: - name: bobdata - state: present - -- name: Create new databases with names 'foo' and 'bar' - mysql_db: - name: - - foo - - bar - state: present - -# Copy database dump file to remote host and restore it to database 'my_db' -- name: Copy database dump file - copy: - src: dump.sql.bz2 - dest: /tmp - -- name: Restore database - mysql_db: - name: my_db - state: import - target: /tmp/dump.sql.bz2 - -- name: Restore database ignoring errors - mysql_db: - name: my_db - state: import - target: /tmp/dump.sql.bz2 - force: yes - -- name: Dump multiple databases - mysql_db: - state: dump - name: db_1,db_2 - target: /tmp/dump.sql - -- name: Dump multiple databases - mysql_db: - state: dump - name: - - db_1 - - db_2 - target: /tmp/dump.sql - -- name: Dump all databases to hostname.sql - mysql_db: - state: dump - name: all - target: /tmp/dump.sql - -- name: Dump all databases to hostname.sql including master data - mysql_db: - state: dump - name: all - target: /tmp/dump.sql - master_data: 1 - -# Import of sql script with encoding option -- name: > - Import dump.sql with specific latin1 encoding, - similar to mysql -u <username> --default-character-set=latin1 -p <password> < dump.sql - mysql_db: - state: import - name: all - encoding: latin1 - target: /tmp/dump.sql - -# Dump of database with encoding option -- name: > - Dump of Databse with specific latin1 encoding, - similar to mysqldump -u <username> --default-character-set=latin1 -p <password> <database> - mysql_db: - state: dump - name: db_1 - encoding: latin1 - target: /tmp/dump.sql - -- name: Delete database with name 'bobdata' - mysql_db: - name: bobdata - state: absent - -- name: Make sure there is neither a database with name 'foo', nor one with name 'bar' - mysql_db: - name: - - foo - - bar - state: absent - -# Dump database with argument not directly supported by this module -# using dump_extra_args parameter -- name: Dump databases without including triggers - mysql_db: - state: dump - name: foo - target: /tmp/dump.sql - dump_extra_args: --skip-triggers -''' - -RETURN = r''' -db: - description: Database names in string format delimited by white space. - returned: always - type: str - sample: "foo bar" -db_list: - description: List of database names. - returned: always - type: list - sample: ["foo", "bar"] - version_added: '2.9' -executed_commands: - description: List of commands which tried to run. - returned: if executed - type: list - sample: ["CREATE DATABASE acme"] - version_added: '2.10' -''' - -import os -import subprocess -import traceback - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import mysql_quote_identifier -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils.six.moves import shlex_quote -from ansible.module_utils._text import to_native - -executed_commands = [] - -# =========================================== -# MySQL module specific support methods. -# - - -def db_exists(cursor, db): - res = 0 - for each_db in db: - res += cursor.execute("SHOW DATABASES LIKE %s", (each_db.replace("_", r"\_"),)) - return res == len(db) - - -def db_delete(cursor, db): - if not db: - return False - for each_db in db: - query = "DROP DATABASE %s" % mysql_quote_identifier(each_db, 'database') - executed_commands.append(query) - cursor.execute(query) - return True - - -def db_dump(module, host, user, password, db_name, target, all_databases, port, - config_file, socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None, - single_transaction=None, quick=None, ignore_tables=None, hex_blob=None, - encoding=None, force=False, master_data=0, skip_lock_tables=False, dump_extra_args=None): - cmd = module.get_bin_path('mysqldump', True) - # If defined, mysqldump demands --defaults-extra-file be the first option - if config_file: - cmd += " --defaults-extra-file=%s" % shlex_quote(config_file) - if user is not None: - cmd += " --user=%s" % shlex_quote(user) - if password is not None: - cmd += " --password=%s" % shlex_quote(password) - if ssl_cert is not None: - cmd += " --ssl-cert=%s" % shlex_quote(ssl_cert) - if ssl_key is not None: - cmd += " --ssl-key=%s" % shlex_quote(ssl_key) - if ssl_ca is not None: - cmd += " --ssl-ca=%s" % shlex_quote(ssl_ca) - if force: - cmd += " --force" - if socket is not None: - cmd += " --socket=%s" % shlex_quote(socket) - else: - cmd += " --host=%s --port=%i" % (shlex_quote(host), port) - - if all_databases: - cmd += " --all-databases" - elif len(db_name) > 1: - cmd += " --databases {0}".format(' '.join(db_name)) - else: - cmd += " %s" % shlex_quote(' '.join(db_name)) - - if skip_lock_tables: - cmd += " --skip-lock-tables" - if (encoding is not None) and (encoding != ""): - cmd += " --default-character-set=%s" % shlex_quote(encoding) - if single_transaction: - cmd += " --single-transaction=true" - if quick: - cmd += " --quick" - if ignore_tables: - for an_ignored_table in ignore_tables: - cmd += " --ignore-table={0}".format(an_ignored_table) - if hex_blob: - cmd += " --hex-blob" - if master_data: - cmd += " --master-data=%s" % master_data - if dump_extra_args is not None: - cmd += " " + dump_extra_args - - path = None - if os.path.splitext(target)[-1] == '.gz': - path = module.get_bin_path('gzip', True) - elif os.path.splitext(target)[-1] == '.bz2': - path = module.get_bin_path('bzip2', True) - elif os.path.splitext(target)[-1] == '.xz': - path = module.get_bin_path('xz', True) - - if path: - cmd = '%s | %s > %s' % (cmd, path, shlex_quote(target)) - else: - cmd += " > %s" % shlex_quote(target) - - executed_commands.append(cmd) - rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) - return rc, stdout, stderr - - -def db_import(module, host, user, password, db_name, target, all_databases, port, config_file, - socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None, encoding=None, force=False): - if not os.path.exists(target): - return module.fail_json(msg="target %s does not exist on the host" % target) - - cmd = [module.get_bin_path('mysql', True)] - # --defaults-file must go first, or errors out - if config_file: - cmd.append("--defaults-extra-file=%s" % shlex_quote(config_file)) - if user: - cmd.append("--user=%s" % shlex_quote(user)) - if password: - cmd.append("--password=%s" % shlex_quote(password)) - if ssl_cert is not None: - cmd.append("--ssl-cert=%s" % shlex_quote(ssl_cert)) - if ssl_key is not None: - cmd.append("--ssl-key=%s" % shlex_quote(ssl_key)) - if ssl_ca is not None: - cmd.append("--ssl-ca=%s" % shlex_quote(ssl_ca)) - if force: - cmd.append("-f") - if socket is not None: - cmd.append("--socket=%s" % shlex_quote(socket)) - else: - cmd.append("--host=%s" % shlex_quote(host)) - cmd.append("--port=%i" % port) - if (encoding is not None) and (encoding != ""): - cmd.append("--default-character-set=%s" % shlex_quote(encoding)) - if not all_databases: - cmd.append("--one-database") - cmd.append(shlex_quote(''.join(db_name))) - - comp_prog_path = None - if os.path.splitext(target)[-1] == '.gz': - comp_prog_path = module.get_bin_path('gzip', required=True) - elif os.path.splitext(target)[-1] == '.bz2': - comp_prog_path = module.get_bin_path('bzip2', required=True) - elif os.path.splitext(target)[-1] == '.xz': - comp_prog_path = module.get_bin_path('xz', required=True) - if comp_prog_path: - # The line above is for returned data only: - executed_commands.append('%s -dc %s | %s' % (comp_prog_path, target, ' '.join(cmd))) - p1 = subprocess.Popen([comp_prog_path, '-dc', target], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - p2 = subprocess.Popen(cmd, stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - (stdout2, stderr2) = p2.communicate() - p1.stdout.close() - p1.wait() - if p1.returncode != 0: - stderr1 = p1.stderr.read() - return p1.returncode, '', stderr1 - else: - return p2.returncode, stdout2, stderr2 - else: - cmd = ' '.join(cmd) - cmd += " < %s" % shlex_quote(target) - executed_commands.append(cmd) - rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) - return rc, stdout, stderr - - -def db_create(cursor, db, encoding, collation): - if not db: - return False - query_params = dict(enc=encoding, collate=collation) - res = 0 - for each_db in db: - query = ['CREATE DATABASE %s' % mysql_quote_identifier(each_db, 'database')] - if encoding: - query.append("CHARACTER SET %(enc)s") - if collation: - query.append("COLLATE %(collate)s") - query = ' '.join(query) - res += cursor.execute(query, query_params) - try: - executed_commands.append(cursor.mogrify(query, query_params)) - except AttributeError: - executed_commands.append(cursor._executed) - except Exception: - executed_commands.append(query) - return res > 0 - - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(type='str'), - login_password=dict(type='str', no_log=True), - login_host=dict(type='str', default='localhost'), - login_port=dict(type='int', default=3306), - login_unix_socket=dict(type='str'), - name=dict(type='list', required=True, aliases=['db']), - encoding=dict(type='str', default=''), - collation=dict(type='str', default=''), - target=dict(type='path'), - state=dict(type='str', default='present', choices=['absent', 'dump', 'import', 'present']), - client_cert=dict(type='path', aliases=['ssl_cert']), - client_key=dict(type='path', aliases=['ssl_key']), - ca_cert=dict(type='path', aliases=['ssl_ca']), - connect_timeout=dict(type='int', default=30), - config_file=dict(type='path', default='~/.my.cnf'), - single_transaction=dict(type='bool', default=False), - quick=dict(type='bool', default=True), - ignore_tables=dict(type='list', default=[]), - hex_blob=dict(default=False, type='bool'), - force=dict(type='bool', default=False), - master_data=dict(type='int', default=0, choices=[0, 1, 2]), - skip_lock_tables=dict(type='bool', default=False), - dump_extra_args=dict(type='str'), - ), - supports_check_mode=True, - ) - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - db = module.params["name"] - if not db: - module.exit_json(changed=False, db=db, db_list=[]) - db = [each_db.strip() for each_db in db] - - encoding = module.params["encoding"] - collation = module.params["collation"] - state = module.params["state"] - target = module.params["target"] - socket = module.params["login_unix_socket"] - login_port = module.params["login_port"] - if login_port < 0 or login_port > 65535: - module.fail_json(msg="login_port must be a valid unix port number (0-65535)") - ssl_cert = module.params["client_cert"] - ssl_key = module.params["client_key"] - ssl_ca = module.params["ca_cert"] - connect_timeout = module.params['connect_timeout'] - config_file = module.params['config_file'] - login_password = module.params["login_password"] - login_user = module.params["login_user"] - login_host = module.params["login_host"] - ignore_tables = module.params["ignore_tables"] - for a_table in ignore_tables: - if a_table == "": - module.fail_json(msg="Name of ignored table cannot be empty") - single_transaction = module.params["single_transaction"] - quick = module.params["quick"] - hex_blob = module.params["hex_blob"] - force = module.params["force"] - master_data = module.params["master_data"] - skip_lock_tables = module.params["skip_lock_tables"] - dump_extra_args = module.params["dump_extra_args"] - - if len(db) > 1 and state == 'import': - module.fail_json(msg="Multiple databases are not supported with state=import") - db_name = ' '.join(db) - - all_databases = False - if state in ['dump', 'import']: - if target is None: - module.fail_json(msg="with state=%s target is required" % state) - if db == ['all']: - all_databases = True - else: - if db == ['all']: - module.fail_json(msg="name is not allowed to equal 'all' unless state equals import, or dump.") - try: - cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, - connect_timeout=connect_timeout) - except Exception as e: - if os.path.exists(config_file): - module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. " - "Exception message: %s" % (config_file, to_native(e))) - else: - module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e))) - - changed = False - if not os.path.exists(config_file): - config_file = None - - existence_list = [] - non_existence_list = [] - - if not all_databases: - for each_database in db: - if db_exists(cursor, [each_database]): - existence_list.append(each_database) - else: - non_existence_list.append(each_database) - - if state == "absent": - if module.check_mode: - module.exit_json(changed=bool(existence_list), db=db_name, db_list=db) - try: - changed = db_delete(cursor, existence_list) - except Exception as e: - module.fail_json(msg="error deleting database: %s" % to_native(e)) - module.exit_json(changed=changed, db=db_name, db_list=db, executed_commands=executed_commands) - elif state == "present": - if module.check_mode: - module.exit_json(changed=bool(non_existence_list), db=db_name, db_list=db) - changed = False - if non_existence_list: - try: - changed = db_create(cursor, non_existence_list, encoding, collation) - except Exception as e: - module.fail_json(msg="error creating database: %s" % to_native(e), - exception=traceback.format_exc()) - module.exit_json(changed=changed, db=db_name, db_list=db, executed_commands=executed_commands) - elif state == "dump": - if non_existence_list and not all_databases: - module.fail_json(msg="Cannot dump database(s) %r - not found" % (', '.join(non_existence_list))) - if module.check_mode: - module.exit_json(changed=True, db=db_name, db_list=db) - rc, stdout, stderr = db_dump(module, login_host, login_user, - login_password, db, target, all_databases, - login_port, config_file, socket, ssl_cert, ssl_key, - ssl_ca, single_transaction, quick, ignore_tables, - hex_blob, encoding, force, master_data, skip_lock_tables, - dump_extra_args) - if rc != 0: - module.fail_json(msg="%s" % stderr) - module.exit_json(changed=True, db=db_name, db_list=db, msg=stdout, - executed_commands=executed_commands) - elif state == "import": - if module.check_mode: - module.exit_json(changed=True, db=db_name, db_list=db) - if non_existence_list and not all_databases: - try: - db_create(cursor, non_existence_list, encoding, collation) - except Exception as e: - module.fail_json(msg="error creating database: %s" % to_native(e), - exception=traceback.format_exc()) - rc, stdout, stderr = db_import(module, login_host, login_user, - login_password, db, target, - all_databases, - login_port, config_file, - socket, ssl_cert, ssl_key, ssl_ca, encoding, force) - if rc != 0: - module.fail_json(msg="%s" % stderr) - module.exit_json(changed=True, db=db_name, db_list=db, msg=stdout, - executed_commands=executed_commands) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/mysql/mysql_info.py b/lib/ansible/modules/database/mysql/mysql_info.py deleted file mode 100644 index 503ec5e93b..0000000000 --- a/lib/ansible/modules/database/mysql/mysql_info.py +++ /dev/null @@ -1,530 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = r''' ---- -module: mysql_info -short_description: Gather information about MySQL servers -description: -- Gathers information about MySQL servers. -version_added: '2.9' - -options: - filter: - description: - - Limit the collected information by comma separated string or YAML list. - - Allowable values are C(version), C(databases), C(settings), C(global_status), - C(users), C(engines), C(master_status), C(slave_status), C(slave_hosts). - - By default, collects all subsets. - - You can use '!' before value (for example, C(!settings)) to exclude it from the information. - - If you pass including and excluding values to the filter, for example, I(filter=!settings,version), - the excluding values, C(!settings) in this case, will be ignored. - type: list - elements: str - login_db: - description: - - Database name to connect to. - - It makes sense if I(login_user) is allowed to connect to a specific database only. - type: str - exclude_fields: - description: - - List of fields which are not needed to collect. - - "Supports elements: C(db_size). Unsupported elements will be ignored" - type: list - elements: str - version_added: '2.10' - return_empty_dbs: - description: - - Includes names of empty databases to returned dictionary. - type: bool - default: no - version_added: '2.10' - -notes: -- Calculating the size of a database might be slow, depending on the number and size of tables in it. - To avoid this, use I(exclude_fields=db_size). - -seealso: -- module: mysql_variables -- module: mysql_db -- module: mysql_user -- module: mysql_replication - -author: -- Andrew Klychkov (@Andersson007) - -extends_documentation_fragment: mysql -''' - -EXAMPLES = r''' -# Display info from mysql-hosts group (using creds from ~/.my.cnf to connect): -# ansible mysql-hosts -m mysql_info - -# Display only databases and users info: -# ansible mysql-hosts -m mysql_info -a 'filter=databases,users' - -# Display only slave status: -# ansible standby -m mysql_info -a 'filter=slave_status' - -# Display all info from databases group except settings: -# ansible databases -m mysql_info -a 'filter=!settings' - -- name: Collect all possible information using passwordless root access - mysql_info: - login_user: root - -- name: Get MySQL version with non-default credentials - mysql_info: - login_user: mysuperuser - login_password: mysuperpass - filter: version - -- name: Collect all info except settings and users by root - mysql_info: - login_user: root - login_password: rootpass - filter: "!settings,!users" - -- name: Collect info about databases and version using ~/.my.cnf as a credential file - become: yes - mysql_info: - filter: - - databases - - version - -- name: Collect info about databases and version using ~alice/.my.cnf as a credential file - become: yes - mysql_info: - config_file: /home/alice/.my.cnf - filter: - - databases - - version - -- name: Collect info about databases including empty and excluding their sizes - become: yes - mysql_info: - config_file: /home/alice/.my.cnf - filter: - - databases - exclude_fields: db_size - return_empty_dbs: yes -''' - -RETURN = r''' -version: - description: Database server version. - returned: if not excluded by filter - type: dict - sample: { "version": { "major": 5, "minor": 5, "release": 60 } } - contains: - major: - description: Major server version. - returned: if not excluded by filter - type: int - sample: 5 - minor: - description: Minor server version. - returned: if not excluded by filter - type: int - sample: 5 - release: - description: Release server version. - returned: if not excluded by filter - type: int - sample: 60 -databases: - description: Information about databases. - returned: if not excluded by filter - type: dict - sample: - - { "mysql": { "size": 656594 }, "information_schema": { "size": 73728 } } - contains: - size: - description: Database size in bytes. - returned: if not excluded by filter - type: dict - sample: { 'size': 656594 } -settings: - description: Global settings (variables) information. - returned: if not excluded by filter - type: dict - sample: - - { "innodb_open_files": 300, innodb_page_size": 16384 } -global_status: - description: Global status information. - returned: if not excluded by filter - type: dict - sample: - - { "Innodb_buffer_pool_read_requests": 123, "Innodb_buffer_pool_reads": 32 } - version_added: "2.10" -users: - description: Users information. - returned: if not excluded by filter - type: dict - sample: - - { "localhost": { "root": { "Alter_priv": "Y", "Alter_routine_priv": "Y" } } } -engines: - description: Information about the server's storage engines. - returned: if not excluded by filter - type: dict - sample: - - { "CSV": { "Comment": "CSV storage engine", "Savepoints": "NO", "Support": "YES", "Transactions": "NO", "XA": "NO" } } -master_status: - description: Master status information. - returned: if master - type: dict - sample: - - { "Binlog_Do_DB": "", "Binlog_Ignore_DB": "mysql", "File": "mysql-bin.000001", "Position": 769 } -slave_status: - description: Slave status information. - returned: if standby - type: dict - sample: - - { "192.168.1.101": { "3306": { "replication_user": { "Connect_Retry": 60, "Exec_Master_Log_Pos": 769, "Last_Errno": 0 } } } } -slave_hosts: - description: Slave status information. - returned: if master - type: dict - sample: - - { "2": { "Host": "", "Master_id": 1, "Port": 3306 } } -''' - -from decimal import Decimal - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import ( - mysql_connect, - mysql_common_argument_spec, - mysql_driver, - mysql_driver_fail_msg, -) -from ansible.module_utils.six import iteritems -from ansible.module_utils._text import to_native - - -# =========================================== -# MySQL module specific support methods. -# - -class MySQL_Info(object): - - """Class for collection MySQL instance information. - - Arguments: - module (AnsibleModule): Object of AnsibleModule class. - cursor (pymysql/mysql-python): Cursor class for interaction with - the database. - - Note: - If you need to add a new subset: - 1. add a new key with the same name to self.info attr in self.__init__() - 2. add a new private method to get the information - 3. add invocation of the new method to self.__collect() - 4. add info about the new subset to the DOCUMENTATION block - 5. add info about the new subset with an example to RETURN block - """ - - def __init__(self, module, cursor): - self.module = module - self.cursor = cursor - self.info = { - 'version': {}, - 'databases': {}, - 'settings': {}, - 'global_status': {}, - 'engines': {}, - 'users': {}, - 'master_status': {}, - 'slave_hosts': {}, - 'slave_status': {}, - } - - def get_info(self, filter_, exclude_fields, return_empty_dbs): - """Get MySQL instance information based on filter_. - - Arguments: - filter_ (list): List of collected subsets (e.g., databases, users, etc.), - when it is empty, return all available information. - """ - self.__collect(exclude_fields, return_empty_dbs) - - inc_list = [] - exc_list = [] - - if filter_: - partial_info = {} - - for fi in filter_: - if fi.lstrip('!') not in self.info: - self.module.warn('filter element: %s is not allowable, ignored' % fi) - continue - - if fi[0] == '!': - exc_list.append(fi.lstrip('!')) - - else: - inc_list.append(fi) - - if inc_list: - for i in self.info: - if i in inc_list: - partial_info[i] = self.info[i] - - else: - for i in self.info: - if i not in exc_list: - partial_info[i] = self.info[i] - - return partial_info - - else: - return self.info - - def __collect(self, exclude_fields, return_empty_dbs): - """Collect all possible subsets.""" - self.__get_databases(exclude_fields, return_empty_dbs) - self.__get_global_variables() - self.__get_global_status() - self.__get_engines() - self.__get_users() - self.__get_master_status() - self.__get_slave_status() - self.__get_slaves() - - def __get_engines(self): - """Get storage engines info.""" - res = self.__exec_sql('SHOW ENGINES') - - if res: - for line in res: - engine = line['Engine'] - self.info['engines'][engine] = {} - - for vname, val in iteritems(line): - if vname != 'Engine': - self.info['engines'][engine][vname] = val - - def __convert(self, val): - """Convert unserializable data.""" - try: - if isinstance(val, Decimal): - val = float(val) - else: - val = int(val) - - except ValueError: - pass - - except TypeError: - pass - - return val - - def __get_global_variables(self): - """Get global variables (instance settings).""" - res = self.__exec_sql('SHOW GLOBAL VARIABLES') - - if res: - for var in res: - self.info['settings'][var['Variable_name']] = self.__convert(var['Value']) - - ver = self.info['settings']['version'].split('.') - release = ver[2].split('-')[0] - - self.info['version'] = dict( - major=int(ver[0]), - minor=int(ver[1]), - release=int(release), - ) - - def __get_global_status(self): - """Get global status.""" - res = self.__exec_sql('SHOW GLOBAL STATUS') - - if res: - for var in res: - self.info['global_status'][var['Variable_name']] = self.__convert(var['Value']) - - def __get_master_status(self): - """Get master status if the instance is a master.""" - res = self.__exec_sql('SHOW MASTER STATUS') - if res: - for line in res: - for vname, val in iteritems(line): - self.info['master_status'][vname] = self.__convert(val) - - def __get_slave_status(self): - """Get slave status if the instance is a slave.""" - res = self.__exec_sql('SHOW SLAVE STATUS') - if res: - for line in res: - host = line['Master_Host'] - if host not in self.info['slave_status']: - self.info['slave_status'][host] = {} - - port = line['Master_Port'] - if port not in self.info['slave_status'][host]: - self.info['slave_status'][host][port] = {} - - user = line['Master_User'] - if user not in self.info['slave_status'][host][port]: - self.info['slave_status'][host][port][user] = {} - - for vname, val in iteritems(line): - if vname not in ('Master_Host', 'Master_Port', 'Master_User'): - self.info['slave_status'][host][port][user][vname] = self.__convert(val) - - def __get_slaves(self): - """Get slave hosts info if the instance is a master.""" - res = self.__exec_sql('SHOW SLAVE HOSTS') - if res: - for line in res: - srv_id = line['Server_id'] - if srv_id not in self.info['slave_hosts']: - self.info['slave_hosts'][srv_id] = {} - - for vname, val in iteritems(line): - if vname != 'Server_id': - self.info['slave_hosts'][srv_id][vname] = self.__convert(val) - - def __get_users(self): - """Get user info.""" - res = self.__exec_sql('SELECT * FROM mysql.user') - if res: - for line in res: - host = line['Host'] - if host not in self.info['users']: - self.info['users'][host] = {} - - user = line['User'] - self.info['users'][host][user] = {} - - for vname, val in iteritems(line): - if vname not in ('Host', 'User'): - self.info['users'][host][user][vname] = self.__convert(val) - - def __get_databases(self, exclude_fields, return_empty_dbs): - """Get info about databases.""" - if not exclude_fields: - query = ('SELECT table_schema AS "name", ' - 'SUM(data_length + index_length) AS "size" ' - 'FROM information_schema.TABLES GROUP BY table_schema') - else: - if 'db_size' in exclude_fields: - query = ('SELECT table_schema AS "name" ' - 'FROM information_schema.TABLES GROUP BY table_schema') - - res = self.__exec_sql(query) - - if res: - for db in res: - self.info['databases'][db['name']] = {} - - if not exclude_fields or 'db_size' not in exclude_fields: - self.info['databases'][db['name']]['size'] = int(db['size']) - - # If empty dbs are not needed in the returned dict, exit from the method - if not return_empty_dbs: - return None - - # Add info about empty databases (issue #65727): - res = self.__exec_sql('SHOW DATABASES') - if res: - for db in res: - if db['Database'] not in self.info['databases']: - self.info['databases'][db['Database']] = {} - - if not exclude_fields or 'db_size' not in exclude_fields: - self.info['databases'][db['Database']]['size'] = 0 - - def __exec_sql(self, query, ddl=False): - """Execute SQL. - - Arguments: - ddl (bool): If True, return True or False. - Used for queries that don't return any rows - (mainly for DDL queries) (default False). - """ - try: - self.cursor.execute(query) - - if not ddl: - res = self.cursor.fetchall() - return res - return True - - except Exception as e: - self.module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e))) - return False - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = mysql_common_argument_spec() - argument_spec.update( - login_db=dict(type='str'), - filter=dict(type='list'), - exclude_fields=dict(type='list'), - return_empty_dbs=dict(type='bool', default=False), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - db = module.params['login_db'] - connect_timeout = module.params['connect_timeout'] - login_user = module.params['login_user'] - login_password = module.params['login_password'] - ssl_cert = module.params['client_cert'] - ssl_key = module.params['client_key'] - ssl_ca = module.params['ca_cert'] - config_file = module.params['config_file'] - filter_ = module.params['filter'] - exclude_fields = module.params['exclude_fields'] - return_empty_dbs = module.params['return_empty_dbs'] - - if filter_: - filter_ = [f.strip() for f in filter_] - - if exclude_fields: - exclude_fields = set([f.strip() for f in exclude_fields]) - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - try: - cursor, db_conn = mysql_connect(module, login_user, login_password, - config_file, ssl_cert, ssl_key, ssl_ca, db, - connect_timeout=connect_timeout, cursor_class='DictCursor') - except Exception as e: - module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. " - "Exception message: %s" % (config_file, to_native(e))) - - ############################### - # Create object and do main job - - mysql = MySQL_Info(module, cursor) - - module.exit_json(changed=False, **mysql.get_info(filter_, exclude_fields, return_empty_dbs)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/mysql/mysql_query.py b/lib/ansible/modules/database/mysql/mysql_query.py deleted file mode 100644 index 6921254226..0000000000 --- a/lib/ansible/modules/database/mysql/mysql_query.py +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'supported_by': 'community', - 'status': ['preview'] -} - -DOCUMENTATION = r''' ---- -module: mysql_query -short_description: Run MySQL queries -description: -- Runs arbitrary MySQL queries. -- Pay attention, the module does not support check mode! - All queries will be executed in autocommit mode. -version_added: '2.10' -options: - query: - description: - - SQL query to run. Multiple queries can be passed using YAML list syntax. - type: list - elements: str - required: yes - positional_args: - description: - - List of values to be passed as positional arguments to the query. - - Mutually exclusive with I(named_args). - type: list - named_args: - description: - - Dictionary of key-value arguments to pass to the query. - - Mutually exclusive with I(positional_args). - type: dict - login_db: - description: - - Name of database to connect to and run queries against. - type: str - single_transaction: - description: - - Where passed queries run in a single transaction (C(yes)) or commit them one-by-one (C(no)). - type: bool - default: no -notes: -- To pass a query containing commas, use YAML list notation with hyphen (see EXAMPLES block). -author: -- Andrew Klychkov (@Andersson007) -extends_documentation_fragment: mysql -''' - -EXAMPLES = r''' -- name: Simple select query to acme db - mysql_query: - login_db: acme - query: SELECT * FROM orders - -- name: Select query to db acme with positional arguments - mysql_query: - login_db: acme - query: SELECT * FROM acme WHERE id = %s AND story = %s - positional_args: - - 1 - - test - -- name: Select query to test_db with named_args - mysql_query: - login_db: test_db - query: SELECT * FROM test WHERE id = %(id_val)s AND story = %(story_val)s - named_args: - id_val: 1 - story_val: test - -- name: Run several insert queries against db test_db in single transaction - mysql_query: - login_db: test_db - query: - - INSERT INTO articles (id, story) VALUES (2, 'my_long_story') - - INSERT INTO prices (id, price) VALUES (123, '100.00') - single_transaction: yes -''' - -RETURN = r''' -executed_queries: - description: List of executed queries. - returned: always - type: list - sample: ['SELECT * FROM bar', 'UPDATE bar SET id = 1 WHERE id = 2'] -query_result: - description: - - List of lists (sublist for each query) containing dictionaries - in column:value form representing returned rows. - returned: changed - type: list - sample: [[{"Column": "Value1"},{"Column": "Value2"}], [{"ID": 1}, {"ID": 2}]] -rowcount: - description: Number of affected rows for each subquery. - returned: changed - type: list - sample: [5, 1] -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import ( - mysql_connect, - mysql_common_argument_spec, - mysql_driver, - mysql_driver_fail_msg, -) -from ansible.module_utils._text import to_native - -DML_QUERY_KEYWORDS = ('INSERT', 'UPDATE', 'DELETE') -# TRUNCATE is not DDL query but it also returns 0 rows affected: -DDL_QUERY_KEYWORDS = ('CREATE', 'DROP', 'ALTER', 'RENAME', 'TRUNCATE') - - -# =========================================== -# Module execution. -# - -def main(): - argument_spec = mysql_common_argument_spec() - argument_spec.update( - query=dict(type='list', elements='str', required=True), - login_db=dict(type='str'), - positional_args=dict(type='list'), - named_args=dict(type='dict'), - single_transaction=dict(type='bool', default=False), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=( - ('positional_args', 'named_args'), - ), - ) - - db = module.params['login_db'] - connect_timeout = module.params['connect_timeout'] - login_user = module.params['login_user'] - login_password = module.params['login_password'] - ssl_cert = module.params['client_cert'] - ssl_key = module.params['client_key'] - ssl_ca = module.params['ca_cert'] - config_file = module.params['config_file'] - query = module.params["query"] - if module.params["single_transaction"]: - autocommit = False - else: - autocommit = True - # Prepare args: - if module.params.get("positional_args"): - arguments = module.params["positional_args"] - elif module.params.get("named_args"): - arguments = module.params["named_args"] - else: - arguments = None - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - # Connect to DB: - try: - cursor, db_connection = mysql_connect(module, login_user, login_password, - config_file, ssl_cert, ssl_key, ssl_ca, db, - connect_timeout=connect_timeout, - cursor_class='DictCursor', autocommit=autocommit) - except Exception as e: - module.fail_json(msg="unable to connect to database, check login_user and " - "login_password are correct or %s has the credentials. " - "Exception message: %s" % (config_file, to_native(e))) - # Set defaults: - changed = False - - max_keyword_len = len(max(DML_QUERY_KEYWORDS + DDL_QUERY_KEYWORDS, key=len)) - - # Execute query: - query_result = [] - executed_queries = [] - rowcount = [] - for q in query: - try: - cursor.execute(q, arguments) - - except Exception as e: - if not autocommit: - db_connection.rollback() - - cursor.close() - module.fail_json(msg="Cannot execute SQL '%s' args [%s]: %s" % (q, arguments, to_native(e))) - - try: - query_result.append([dict(row) for row in cursor.fetchall()]) - - except Exception as e: - if not autocommit: - db_connection.rollback() - - module.fail_json(msg="Cannot fetch rows from cursor: %s" % to_native(e)) - - # Check DML or DDL keywords in query and set changed accordingly: - q = q.lstrip()[0:max_keyword_len].upper() - for keyword in DML_QUERY_KEYWORDS: - if keyword in q and cursor.rowcount > 0: - changed = True - - for keyword in DDL_QUERY_KEYWORDS: - if keyword in q: - changed = True - - executed_queries.append(cursor._last_executed) - rowcount.append(cursor.rowcount) - - # When the module run with the single_transaction == True: - if not autocommit: - db_connection.commit() - - # Create dict with returned values: - kw = { - 'changed': changed, - 'executed_queries': executed_queries, - 'query_result': query_result, - 'rowcount': rowcount, - } - - # Exit: - module.exit_json(**kw) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/mysql/mysql_replication.py b/lib/ansible/modules/database/mysql/mysql_replication.py deleted file mode 100644 index 93620b60d7..0000000000 --- a/lib/ansible/modules/database/mysql/mysql_replication.py +++ /dev/null @@ -1,579 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2013, Balazs Pocze <banyek@gawker.com> -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# Certain parts are taken from Mark Theunissen's mysqldb module -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: mysql_replication -short_description: Manage MySQL replication -description: -- Manages MySQL server replication, slave, master status, get and change master host. -version_added: '1.3' -author: -- Balazs Pocze (@banyek) -- Andrew Klychkov (@Andersson007) -options: - mode: - description: - - Module operating mode. Could be - C(changemaster) (CHANGE MASTER TO), - C(getmaster) (SHOW MASTER STATUS), - C(getslave) (SHOW SLAVE STATUS), - C(startslave) (START SLAVE), - C(stopslave) (STOP SLAVE), - C(resetmaster) (RESET MASTER) - supported from Ansible 2.10, - C(resetslave) (RESET SLAVE), - C(resetslaveall) (RESET SLAVE ALL). - type: str - choices: - - changemaster - - getmaster - - getslave - - startslave - - stopslave - - resetmaster - - resetslave - - resetslaveall - default: getslave - master_host: - description: - - Same as mysql variable. - type: str - master_user: - description: - - Same as mysql variable. - type: str - master_password: - description: - - Same as mysql variable. - type: str - master_port: - description: - - Same as mysql variable. - type: int - master_connect_retry: - description: - - Same as mysql variable. - type: int - master_log_file: - description: - - Same as mysql variable. - type: str - master_log_pos: - description: - - Same as mysql variable. - type: int - relay_log_file: - description: - - Same as mysql variable. - type: str - relay_log_pos: - description: - - Same as mysql variable. - type: int - master_ssl: - description: - - Same as mysql variable. - type: bool - master_ssl_ca: - description: - - Same as mysql variable. - type: str - master_ssl_capath: - description: - - Same as mysql variable. - type: str - master_ssl_cert: - description: - - Same as mysql variable. - type: str - master_ssl_key: - description: - - Same as mysql variable. - type: str - master_ssl_cipher: - description: - - Same as mysql variable. - type: str - master_auto_position: - description: - - Whether the host uses GTID based replication or not. - type: bool - version_added: '2.0' - master_use_gtid: - description: - - Configures the slave to use the MariaDB Global Transaction ID. - - C(disabled) equals MASTER_USE_GTID=no command. - - To find information about available values see - U(https://mariadb.com/kb/en/library/change-master-to/#master_use_gtid). - - Available since MariaDB 10.0.2. - choices: [current_pos, slave_pos, disabled] - type: str - version_added: '2.10' - master_delay: - description: - - Time lag behind the master's state (in seconds). - - Available from MySQL 5.6. - - For more information see U(https://dev.mysql.com/doc/refman/8.0/en/replication-delayed.html). - type: int - version_added: '2.10' - connection_name: - description: - - Name of the master connection. - - Supported from MariaDB 10.0.1. - - Mutually exclusive with I(channel). - - For more information see U(https://mariadb.com/kb/en/library/multi-source-replication/). - type: str - version_added: '2.10' - channel: - description: - - Name of replication channel. - - Multi-source replication is supported from MySQL 5.7. - - Mutually exclusive with I(connection_name). - - For more information see U(https://dev.mysql.com/doc/refman/8.0/en/replication-multi-source.html). - type: str - version_added: '2.10' - fail_on_error: - description: - - Fails on error when calling mysql. - type: bool - default: False - version_added: '2.10' - -notes: -- If an empty value for the parameter of string type is needed, use an empty string. - -extends_documentation_fragment: -- mysql - -seealso: -- module: mysql_info -- name: MySQL replication reference - description: Complete reference of the MySQL replication documentation. - link: https://dev.mysql.com/doc/refman/8.0/en/replication.html -- name: MariaDB replication reference - description: Complete reference of the MariaDB replication documentation. - link: https://mariadb.com/kb/en/library/setting-up-replication/ -''' - -EXAMPLES = r''' -- name: Stop mysql slave thread - mysql_replication: - mode: stopslave - -- name: Get master binlog file name and binlog position - mysql_replication: - mode: getmaster - -- name: Change master to master server 192.0.2.1 and use binary log 'mysql-bin.000009' with position 4578 - mysql_replication: - mode: changemaster - master_host: 192.0.2.1 - master_log_file: mysql-bin.000009 - master_log_pos: 4578 - -- name: Check slave status using port 3308 - mysql_replication: - mode: getslave - login_host: ansible.example.com - login_port: 3308 - -- name: On MariaDB change master to use GTID current_pos - mysql_replication: - mode: changemaster - master_use_gtid: current_pos - -- name: Change master to use replication delay 3600 seconds - mysql_replication: - mode: changemaster - master_host: 192.0.2.1 - master_delay: 3600 - -- name: Start MariaDB standby with connection name master-1 - mysql_replication: - mode: startslave - connection_name: master-1 - -- name: Stop replication in channel master-1 - mysql_replication: - mode: stopslave - channel: master-1 - -- name: > - Run RESET MASTER command which will delete all existing binary log files - and reset the binary log index file on the master - mysql_replication: - mode: resetmaster - -- name: Run start slave and fail the task on errors - mysql_replication: - mode: startslave - connection_name: master-1 - fail_on_error: yes - -- name: Change master and fail on error (like when slave thread is running) - mysql_replication: - mode: changemaster - fail_on_error: yes - -''' - -RETURN = r''' -queries: - description: List of executed queries which modified DB's state. - returned: always - type: list - sample: ["CHANGE MASTER TO MASTER_HOST='master2.example.com',MASTER_PORT=3306"] - version_added: '2.10' -''' - -import os -import warnings - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils._text import to_native - -executed_queries = [] - - -def get_master_status(cursor): - cursor.execute("SHOW MASTER STATUS") - masterstatus = cursor.fetchone() - return masterstatus - - -def get_slave_status(cursor, connection_name='', channel=''): - if connection_name: - query = "SHOW SLAVE '%s' STATUS" % connection_name - else: - query = "SHOW SLAVE STATUS" - - if channel: - query += " FOR CHANNEL '%s'" % channel - - cursor.execute(query) - slavestatus = cursor.fetchone() - return slavestatus - - -def stop_slave(module, cursor, connection_name='', channel='', fail_on_error=False): - if connection_name: - query = "STOP SLAVE '%s'" % connection_name - else: - query = 'STOP SLAVE' - - if channel: - query += " FOR CHANNEL '%s'" % channel - - try: - executed_queries.append(query) - cursor.execute(query) - stopped = True - except mysql_driver.Warning as e: - stopped = False - except Exception as e: - if fail_on_error: - module.fail_json(msg="STOP SLAVE failed: %s" % to_native(e)) - stopped = False - return stopped - - -def reset_slave(module, cursor, connection_name='', channel='', fail_on_error=False): - if connection_name: - query = "RESET SLAVE '%s'" % connection_name - else: - query = 'RESET SLAVE' - - if channel: - query += " FOR CHANNEL '%s'" % channel - - try: - executed_queries.append(query) - cursor.execute(query) - reset = True - except mysql_driver.Warning as e: - reset = False - except Exception as e: - if fail_on_error: - module.fail_json(msg="RESET SLAVE failed: %s" % to_native(e)) - reset = False - return reset - - -def reset_slave_all(module, cursor, connection_name='', channel='', fail_on_error=False): - if connection_name: - query = "RESET SLAVE '%s' ALL" % connection_name - else: - query = 'RESET SLAVE ALL' - - if channel: - query += " FOR CHANNEL '%s'" % channel - - try: - executed_queries.append(query) - cursor.execute(query) - reset = True - except mysql_driver.Warning as e: - reset = False - except Exception as e: - if fail_on_error: - module.fail_json(msg="RESET SLAVE ALL failed: %s" % to_native(e)) - reset = False - return reset - - -def reset_master(module, cursor, fail_on_error=False): - query = 'RESET MASTER' - try: - executed_queries.append(query) - cursor.execute(query) - reset = True - except mysql_driver.Warning as e: - reset = False - except Exception as e: - if fail_on_error: - module.fail_json(msg="RESET MASTER failed: %s" % to_native(e)) - reset = False - return reset - - -def start_slave(module, cursor, connection_name='', channel='', fail_on_error=False): - if connection_name: - query = "START SLAVE '%s'" % connection_name - else: - query = 'START SLAVE' - - if channel: - query += " FOR CHANNEL '%s'" % channel - - try: - executed_queries.append(query) - cursor.execute(query) - started = True - except mysql_driver.Warning as e: - started = False - except Exception as e: - if fail_on_error: - module.fail_json(msg="START SLAVE failed: %s" % to_native(e)) - started = False - return started - - -def changemaster(cursor, chm, connection_name='', channel=''): - if connection_name: - query = "CHANGE MASTER '%s' TO %s" % (connection_name, ','.join(chm)) - else: - query = 'CHANGE MASTER TO %s' % ','.join(chm) - - if channel: - query += " FOR CHANNEL '%s'" % channel - - executed_queries.append(query) - cursor.execute(query) - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(type='str'), - login_password=dict(type='str', no_log=True), - login_host=dict(type='str', default='localhost'), - login_port=dict(type='int', default=3306), - login_unix_socket=dict(type='str'), - mode=dict(type='str', default='getslave', choices=[ - 'getmaster', 'getslave', 'changemaster', 'stopslave', - 'startslave', 'resetmaster', 'resetslave', 'resetslaveall']), - master_auto_position=dict(type='bool', default=False), - master_host=dict(type='str'), - master_user=dict(type='str'), - master_password=dict(type='str', no_log=True), - master_port=dict(type='int'), - master_connect_retry=dict(type='int'), - master_log_file=dict(type='str'), - master_log_pos=dict(type='int'), - relay_log_file=dict(type='str'), - relay_log_pos=dict(type='int'), - master_ssl=dict(type='bool', default=False), - master_ssl_ca=dict(type='str'), - master_ssl_capath=dict(type='str'), - master_ssl_cert=dict(type='str'), - master_ssl_key=dict(type='str'), - master_ssl_cipher=dict(type='str'), - connect_timeout=dict(type='int', default=30), - config_file=dict(type='path', default='~/.my.cnf'), - client_cert=dict(type='path', aliases=['ssl_cert']), - client_key=dict(type='path', aliases=['ssl_key']), - ca_cert=dict(type='path', aliases=['ssl_ca']), - master_use_gtid=dict(type='str', choices=['current_pos', 'slave_pos', 'disabled']), - master_delay=dict(type='int'), - connection_name=dict(type='str'), - channel=dict(type='str'), - fail_on_error=dict(type='bool', default=False), - ), - mutually_exclusive=[ - ['connection_name', 'channel'] - ], - ) - mode = module.params["mode"] - master_host = module.params["master_host"] - master_user = module.params["master_user"] - master_password = module.params["master_password"] - master_port = module.params["master_port"] - master_connect_retry = module.params["master_connect_retry"] - master_log_file = module.params["master_log_file"] - master_log_pos = module.params["master_log_pos"] - relay_log_file = module.params["relay_log_file"] - relay_log_pos = module.params["relay_log_pos"] - master_ssl = module.params["master_ssl"] - master_ssl_ca = module.params["master_ssl_ca"] - master_ssl_capath = module.params["master_ssl_capath"] - master_ssl_cert = module.params["master_ssl_cert"] - master_ssl_key = module.params["master_ssl_key"] - master_ssl_cipher = module.params["master_ssl_cipher"] - master_auto_position = module.params["master_auto_position"] - ssl_cert = module.params["client_cert"] - ssl_key = module.params["client_key"] - ssl_ca = module.params["ca_cert"] - connect_timeout = module.params['connect_timeout'] - config_file = module.params['config_file'] - master_delay = module.params['master_delay'] - if module.params.get("master_use_gtid") == 'disabled': - master_use_gtid = 'no' - else: - master_use_gtid = module.params["master_use_gtid"] - connection_name = module.params["connection_name"] - channel = module.params['channel'] - fail_on_error = module.params['fail_on_error'] - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - else: - warnings.filterwarnings('error', category=mysql_driver.Warning) - - login_password = module.params["login_password"] - login_user = module.params["login_user"] - - try: - cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, - ssl_cert, ssl_key, ssl_ca, None, cursor_class='DictCursor', - connect_timeout=connect_timeout) - except Exception as e: - if os.path.exists(config_file): - module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. " - "Exception message: %s" % (config_file, to_native(e))) - else: - module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e))) - - if mode in "getmaster": - status = get_master_status(cursor) - if not isinstance(status, dict): - status = dict(Is_Master=False, msg="Server is not configured as mysql master") - else: - status['Is_Master'] = True - module.exit_json(queries=executed_queries, **status) - - elif mode in "getslave": - status = get_slave_status(cursor, connection_name, channel) - if not isinstance(status, dict): - status = dict(Is_Slave=False, msg="Server is not configured as mysql slave") - else: - status['Is_Slave'] = True - module.exit_json(queries=executed_queries, **status) - - elif mode in "changemaster": - chm = [] - result = {} - if master_host is not None: - chm.append("MASTER_HOST='%s'" % master_host) - if master_user is not None: - chm.append("MASTER_USER='%s'" % master_user) - if master_password is not None: - chm.append("MASTER_PASSWORD='%s'" % master_password) - if master_port is not None: - chm.append("MASTER_PORT=%s" % master_port) - if master_connect_retry is not None: - chm.append("MASTER_CONNECT_RETRY=%s" % master_connect_retry) - if master_log_file is not None: - chm.append("MASTER_LOG_FILE='%s'" % master_log_file) - if master_log_pos is not None: - chm.append("MASTER_LOG_POS=%s" % master_log_pos) - if master_delay is not None: - chm.append("MASTER_DELAY=%s" % master_delay) - if relay_log_file is not None: - chm.append("RELAY_LOG_FILE='%s'" % relay_log_file) - if relay_log_pos is not None: - chm.append("RELAY_LOG_POS=%s" % relay_log_pos) - if master_ssl: - chm.append("MASTER_SSL=1") - if master_ssl_ca is not None: - chm.append("MASTER_SSL_CA='%s'" % master_ssl_ca) - if master_ssl_capath is not None: - chm.append("MASTER_SSL_CAPATH='%s'" % master_ssl_capath) - if master_ssl_cert is not None: - chm.append("MASTER_SSL_CERT='%s'" % master_ssl_cert) - if master_ssl_key is not None: - chm.append("MASTER_SSL_KEY='%s'" % master_ssl_key) - if master_ssl_cipher is not None: - chm.append("MASTER_SSL_CIPHER='%s'" % master_ssl_cipher) - if master_auto_position: - chm.append("MASTER_AUTO_POSITION=1") - if master_use_gtid is not None: - chm.append("MASTER_USE_GTID=%s" % master_use_gtid) - try: - changemaster(cursor, chm, connection_name, channel) - except mysql_driver.Warning as e: - result['warning'] = to_native(e) - except Exception as e: - module.fail_json(msg='%s. Query == CHANGE MASTER TO %s' % (to_native(e), chm)) - result['changed'] = True - module.exit_json(queries=executed_queries, **result) - elif mode in "startslave": - started = start_slave(module, cursor, connection_name, channel, fail_on_error) - if started is True: - module.exit_json(msg="Slave started ", changed=True, queries=executed_queries) - else: - module.exit_json(msg="Slave already started (Or cannot be started)", changed=False, queries=executed_queries) - elif mode in "stopslave": - stopped = stop_slave(module, cursor, connection_name, channel, fail_on_error) - if stopped is True: - module.exit_json(msg="Slave stopped", changed=True, queries=executed_queries) - else: - module.exit_json(msg="Slave already stopped", changed=False, queries=executed_queries) - elif mode in "resetmaster": - reset = reset_master(module, cursor, fail_on_error) - if reset is True: - module.exit_json(msg="Master reset", changed=True, queries=executed_queries) - else: - module.exit_json(msg="Master already reset", changed=False, queries=executed_queries) - elif mode in "resetslave": - reset = reset_slave(module, cursor, connection_name, channel, fail_on_error) - if reset is True: - module.exit_json(msg="Slave reset", changed=True, queries=executed_queries) - else: - module.exit_json(msg="Slave already reset", changed=False, queries=executed_queries) - elif mode in "resetslaveall": - reset = reset_slave_all(module, cursor, connection_name, channel, fail_on_error) - if reset is True: - module.exit_json(msg="Slave reset", changed=True, queries=executed_queries) - else: - module.exit_json(msg="Slave already reset", changed=False, queries=executed_queries) - - warnings.simplefilter("ignore") - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/mysql/mysql_user.py b/lib/ansible/modules/database/mysql/mysql_user.py deleted file mode 100644 index f39982ed41..0000000000 --- a/lib/ansible/modules/database/mysql/mysql_user.py +++ /dev/null @@ -1,815 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2012, Mark Theunissen <mark.theunissen@gmail.com> -# Sponsored by Four Kitchens http://fourkitchens.com. -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: mysql_user -short_description: Adds or removes a user from a MySQL database -description: - - Adds or removes a user from a MySQL database. -version_added: "0.6" -options: - name: - description: - - Name of the user (role) to add or remove. - type: str - required: true - password: - description: - - Set the user's password.. - type: str - encrypted: - description: - - Indicate that the 'password' field is a `mysql_native_password` hash. - type: bool - default: no - version_added: "2.0" - host: - description: - - The 'host' part of the MySQL username. - type: str - default: localhost - host_all: - description: - - Override the host option, making ansible apply changes to all hostnames for a given user. - - This option cannot be used when creating users. - type: bool - default: no - version_added: "2.1" - priv: - description: - - "MySQL privileges string in the format: C(db.table:priv1,priv2)." - - "Multiple privileges can be specified by separating each one using - a forward slash: C(db.table:priv/db.table:priv)." - - The format is based on MySQL C(GRANT) statement. - - Database and table names can be quoted, MySQL-style. - - If column privileges are used, the C(priv1,priv2) part must be - exactly as returned by a C(SHOW GRANT) statement. If not followed, - the module will always report changes. It includes grouping columns - by permission (C(SELECT(col1,col2)) instead of C(SELECT(col1),SELECT(col2))). - - Can be passed as a dictionary (see the examples). - type: raw - append_privs: - description: - - Append the privileges defined by priv to the existing ones for this - user instead of overwriting existing ones. - type: bool - default: no - version_added: "1.4" - sql_log_bin: - description: - - Whether binary logging should be enabled or disabled for the connection. - type: bool - default: yes - version_added: "2.1" - state: - description: - - Whether the user should exist. - - When C(absent), removes the user. - type: str - choices: [ absent, present ] - default: present - check_implicit_admin: - description: - - Check if mysql allows login as root/nopassword before trying supplied credentials. - type: bool - default: no - version_added: "1.3" - update_password: - description: - - C(always) will update passwords if they differ. - - C(on_create) will only set the password for newly created users. - type: str - choices: [ always, on_create ] - default: always - version_added: "2.0" - plugin: - description: - - User's plugin to authenticate (``CREATE USER user IDENTIFIED WITH plugin``). - type: str - version_added: '2.10' - plugin_hash_string: - description: - - User's plugin hash string (``CREATE USER user IDENTIFIED WITH plugin AS plugin_hash_string``). - type: str - version_added: '2.10' - plugin_auth_string: - description: - - User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``). - type: str - version_added: '2.10' - -notes: - - "MySQL server installs with default login_user of 'root' and no password. To secure this user - as part of an idempotent playbook, you must create at least two tasks: the first must change the root user's password, - without providing any login_user/login_password details. The second must drop a ~/.my.cnf file containing - the new root credentials. Subsequent runs of the playbook will then succeed by reading the new credentials from - the file." - - Currently, there is only support for the `mysql_native_password` encrypted password hash module. - -seealso: -- module: mysql_info -- name: MySQL access control and account management reference - description: Complete reference of the MySQL access control and account management documentation. - link: https://dev.mysql.com/doc/refman/8.0/en/access-control.html -- name: MySQL provided privileges reference - description: Complete reference of the MySQL provided privileges documentation. - link: https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html - -author: -- Jonathan Mainguy (@Jmainguy) -- Benjamin Malynovytch (@bmalynovytch) -- Lukasz Tomaszkiewicz (@tomaszkiewicz) -extends_documentation_fragment: mysql -''' - -EXAMPLES = r''' -- name: Removes anonymous user account for localhost - mysql_user: - name: '' - host: localhost - state: absent - -- name: Removes all anonymous user accounts - mysql_user: - name: '' - host_all: yes - state: absent - -- name: Create database user with name 'bob' and password '12345' with all database privileges - mysql_user: - name: bob - password: 12345 - priv: '*.*:ALL' - state: present - -- name: Create database user using hashed password with all database privileges - mysql_user: - name: bob - password: '*EE0D72C1085C46C5278932678FBE2C6A782821B4' - encrypted: yes - priv: '*.*:ALL' - state: present - -- name: Create database user with password and all database privileges and 'WITH GRANT OPTION' - mysql_user: - name: bob - password: 12345 - priv: '*.*:ALL,GRANT' - state: present - -- name: Create user with password, all database privileges and 'WITH GRANT OPTION' in db1 and db2 - mysql_user: - state: present - name: bob - password: 12345dd - priv: - 'db1.*': 'ALL,GRANT' - 'db2.*': 'ALL,GRANT' - -# Note that REQUIRESSL is a special privilege that should only apply to *.* by itself. -- name: Modify user to require SSL connections. - mysql_user: - name: bob - append_privs: yes - priv: '*.*:REQUIRESSL' - state: present - -- name: Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials. - mysql_user: - login_user: root - login_password: 123456 - name: sally - state: absent - -- name: Ensure no user named 'sally' exists at all - mysql_user: - name: sally - host_all: yes - state: absent - -- name: Specify grants composed of more than one word - mysql_user: - name: replication - password: 12345 - priv: "*.*:REPLICATION CLIENT" - state: present - -- name: Revoke all privileges for user 'bob' and password '12345' - mysql_user: - name: bob - password: 12345 - priv: "*.*:USAGE" - state: present - -# Example privileges string format -# mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL - -- name: Example using login_unix_socket to connect to server - mysql_user: - name: root - password: abc123 - login_unix_socket: /var/run/mysqld/mysqld.sock - -- name: Example of skipping binary logging while adding user 'bob' - mysql_user: - name: bob - password: 12345 - priv: "*.*:USAGE" - state: present - sql_log_bin: no - -- name: Create user 'bob' authenticated with plugin 'AWSAuthenticationPlugin' - mysql_user: - name: bob - plugin: AWSAuthenticationPlugin - plugin_hash_string: RDS - priv: '*.*:ALL' - state: present - -# Example .my.cnf file for setting the root password -# [client] -# user=root -# password=n<_665{vS43y -''' - -import re -import string - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import SQLParseError -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils.six import iteritems -from ansible.module_utils._text import to_native - - -VALID_PRIVS = frozenset(('CREATE', 'DROP', 'GRANT', 'GRANT OPTION', - 'LOCK TABLES', 'REFERENCES', 'EVENT', 'ALTER', - 'DELETE', 'INDEX', 'INSERT', 'SELECT', 'UPDATE', - 'CREATE TEMPORARY TABLES', 'TRIGGER', 'CREATE VIEW', - 'SHOW VIEW', 'ALTER ROUTINE', 'CREATE ROUTINE', - 'EXECUTE', 'FILE', 'CREATE TABLESPACE', 'CREATE USER', - 'PROCESS', 'PROXY', 'RELOAD', 'REPLICATION CLIENT', - 'REPLICATION SLAVE', 'SHOW DATABASES', 'SHUTDOWN', - 'SUPER', 'ALL', 'ALL PRIVILEGES', 'USAGE', 'REQUIRESSL', - 'CREATE ROLE', 'DROP ROLE', 'APPLICATION_PASSWORD_ADMIN', - 'AUDIT_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN', - 'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN', - 'ENCRYPTION_KEY_ADMIN', 'FIREWALL_ADMIN', 'FIREWALL_USER', - 'GROUP_REPLICATION_ADMIN', 'INNODB_REDO_LOG_ARCHIVE', - 'NDB_STORED_USER', 'PERSIST_RO_VARIABLES_ADMIN', - 'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN', - 'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER', - 'ROLE_ADMIN', 'SESSION_VARIABLES_ADMIN', 'SET_USER_ID', - 'SYSTEM_USER', 'SYSTEM_VARIABLES_ADMIN', 'SYSTEM_USER', - 'TABLE_ENCRYPTION_ADMIN', 'VERSION_TOKEN_ADMIN', - 'XA_RECOVER_ADMIN', 'LOAD FROM S3', 'SELECT INTO S3')) - - -class InvalidPrivsError(Exception): - pass - -# =========================================== -# MySQL module specific support methods. -# - - -# User Authentication Management changed in MySQL 5.7 and MariaDB 10.2.0 -def use_old_user_mgmt(cursor): - cursor.execute("SELECT VERSION()") - result = cursor.fetchone() - version_str = result[0] - version = version_str.split('.') - - if 'mariadb' in version_str.lower(): - # Prior to MariaDB 10.2 - if int(version[0]) * 1000 + int(version[1]) < 10002: - return True - else: - return False - else: - # Prior to MySQL 5.7 - if int(version[0]) * 1000 + int(version[1]) < 5007: - return True - else: - return False - - -def get_mode(cursor): - cursor.execute('SELECT @@GLOBAL.sql_mode') - result = cursor.fetchone() - mode_str = result[0] - if 'ANSI' in mode_str: - mode = 'ANSI' - else: - mode = 'NOTANSI' - return mode - - -def user_exists(cursor, user, host, host_all): - if host_all: - cursor.execute("SELECT count(*) FROM mysql.user WHERE user = %s", ([user])) - else: - cursor.execute("SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s", (user, host)) - - count = cursor.fetchone() - return count[0] > 0 - - -def user_add(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, new_priv, check_mode): - # we cannot create users without a proper hostname - if host_all: - return False - - if check_mode: - return True - - if password and encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password)) - elif password and not encrypted: - cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, password)) - elif plugin and plugin_hash_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string)) - elif plugin and plugin_auth_string: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)) - elif plugin: - cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)) - else: - cursor.execute("CREATE USER %s@%s", (user, host)) - if new_priv is not None: - for db_table, priv in iteritems(new_priv): - privileges_grant(cursor, user, host, db_table, priv) - return True - - -def is_hash(password): - ishash = False - if len(password) == 41 and password[0] == '*': - if frozenset(password[1:]).issubset(string.hexdigits): - ishash = True - return ishash - - -def user_mod(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, new_priv, append_privs, module): - changed = False - msg = "User unchanged" - grant_option = False - - if host_all: - hostnames = user_get_hostnames(cursor, [user]) - else: - hostnames = [host] - - for host in hostnames: - # Handle clear text and hashed passwords. - if bool(password): - # Determine what user management method server uses - old_user_mgmt = use_old_user_mgmt(cursor) - - # Get a list of valid columns in mysql.user table to check if Password and/or authentication_string exist - cursor.execute(""" - SELECT COLUMN_NAME FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME IN ('Password', 'authentication_string') - ORDER BY COLUMN_NAME DESC LIMIT 1 - """) - colA = cursor.fetchone() - - cursor.execute(""" - SELECT COLUMN_NAME FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME IN ('Password', 'authentication_string') - ORDER BY COLUMN_NAME ASC LIMIT 1 - """) - colB = cursor.fetchone() - - # Select hash from either Password or authentication_string, depending which one exists and/or is filled - cursor.execute(""" - SELECT COALESCE( - CASE WHEN %s = '' THEN NULL ELSE %s END, - CASE WHEN %s = '' THEN NULL ELSE %s END - ) - FROM mysql.user WHERE user = %%s AND host = %%s - """ % (colA[0], colA[0], colB[0], colB[0]), (user, host)) - current_pass_hash = cursor.fetchone()[0] - if isinstance(current_pass_hash, bytes): - current_pass_hash = current_pass_hash.decode('ascii') - - if encrypted: - encrypted_password = password - if not is_hash(encrypted_password): - module.fail_json(msg="encrypted was specified however it does not appear to be a valid hash expecting: *SHA1(SHA1(your_password))") - else: - if old_user_mgmt: - cursor.execute("SELECT PASSWORD(%s)", (password,)) - else: - cursor.execute("SELECT CONCAT('*', UCASE(SHA1(UNHEX(SHA1(%s)))))", (password,)) - encrypted_password = cursor.fetchone()[0] - - if current_pass_hash != encrypted_password: - msg = "Password updated" - if module.check_mode: - return (True, msg) - if old_user_mgmt: - cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password)) - msg = "Password updated (old style)" - else: - try: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password)) - msg = "Password updated (new style)" - except (mysql_driver.Error) as e: - # https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql - # Replacing empty root password with new authentication mechanisms fails with error 1396 - if e.args[0] == 1396: - cursor.execute( - "UPDATE user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s", - ('mysql_native_password', encrypted_password, user, host) - ) - cursor.execute("FLUSH PRIVILEGES") - msg = "Password forced update" - else: - raise e - changed = True - - # Handle plugin authentication - if plugin: - cursor.execute("SELECT plugin, authentication_string FROM mysql.user " - "WHERE user = %s AND host = %s", (user, host)) - current_plugin = cursor.fetchone() - - update = False - - if current_plugin[0] != plugin: - update = True - - if plugin_hash_string and current_plugin[1] != plugin_hash_string: - update = True - - if plugin_auth_string and current_plugin[1] != plugin_auth_string: - # this case can cause more updates than expected, - # as plugin can hash auth_string in any way it wants - # and there's no way to figure it out for - # a check, so I prefer to update more often than never - update = True - - if update: - if plugin_hash_string: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string)) - elif plugin_auth_string: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)) - else: - cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)) - changed = True - - # Handle privileges - if new_priv is not None: - curr_priv = privileges_get(cursor, user, host) - - # If the user has privileges on a db.table that doesn't appear at all in - # the new specification, then revoke all privileges on it. - for db_table, priv in iteritems(curr_priv): - # If the user has the GRANT OPTION on a db.table, revoke it first. - if "GRANT" in priv: - grant_option = True - if db_table not in new_priv: - if user != "root" and "PROXY" not in priv and not append_privs: - msg = "Privileges updated" - if module.check_mode: - return (True, msg) - privileges_revoke(cursor, user, host, db_table, priv, grant_option) - changed = True - - # If the user doesn't currently have any privileges on a db.table, then - # we can perform a straight grant operation. - for db_table, priv in iteritems(new_priv): - if db_table not in curr_priv: - msg = "New privileges granted" - if module.check_mode: - return (True, msg) - privileges_grant(cursor, user, host, db_table, priv) - changed = True - - # If the db.table specification exists in both the user's current privileges - # and in the new privileges, then we need to see if there's a difference. - db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys()) - for db_table in db_table_intersect: - priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) - if len(priv_diff) > 0: - msg = "Privileges updated" - if module.check_mode: - return (True, msg) - if not append_privs: - privileges_revoke(cursor, user, host, db_table, curr_priv[db_table], grant_option) - privileges_grant(cursor, user, host, db_table, new_priv[db_table]) - changed = True - - return (changed, msg) - - -def user_delete(cursor, user, host, host_all, check_mode): - if check_mode: - return True - - if host_all: - hostnames = user_get_hostnames(cursor, [user]) - - for hostname in hostnames: - cursor.execute("DROP USER %s@%s", (user, hostname)) - else: - cursor.execute("DROP USER %s@%s", (user, host)) - - return True - - -def user_get_hostnames(cursor, user): - cursor.execute("SELECT Host FROM mysql.user WHERE user = %s", user) - hostnames_raw = cursor.fetchall() - hostnames = [] - - for hostname_raw in hostnames_raw: - hostnames.append(hostname_raw[0]) - - return hostnames - - -def privileges_get(cursor, user, host): - """ MySQL doesn't have a better method of getting privileges aside from the - SHOW GRANTS query syntax, which requires us to then parse the returned string. - Here's an example of the string that is returned from MySQL: - - GRANT USAGE ON *.* TO 'user'@'localhost' IDENTIFIED BY 'pass'; - - This function makes the query and returns a dictionary containing the results. - The dictionary format is the same as that returned by privileges_unpack() below. - """ - output = {} - cursor.execute("SHOW GRANTS FOR %s@%s", (user, host)) - grants = cursor.fetchall() - - def pick(x): - if x == 'ALL PRIVILEGES': - return 'ALL' - else: - return x - - for grant in grants: - res = re.match("""GRANT (.+) ON (.+) TO (['`"]).*\\3@(['`"]).*\\4( IDENTIFIED BY PASSWORD (['`"]).+\\6)? ?(.*)""", grant[0]) - if res is None: - raise InvalidPrivsError('unable to parse the MySQL grant string: %s' % grant[0]) - privileges = res.group(1).split(", ") - privileges = [pick(x) for x in privileges] - if "WITH GRANT OPTION" in res.group(7): - privileges.append('GRANT') - if "REQUIRE SSL" in res.group(7): - privileges.append('REQUIRESSL') - db = res.group(2) - output[db] = privileges - return output - - -def privileges_unpack(priv, mode): - """ Take a privileges string, typically passed as a parameter, and unserialize - it into a dictionary, the same format as privileges_get() above. We have this - custom format to avoid using YAML/JSON strings inside YAML playbooks. Example - of a privileges string: - - mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanother.*:ALL - - The privilege USAGE stands for no privileges, so we add that in on *.* if it's - not specified in the string, as MySQL will always provide this by default. - """ - if mode == 'ANSI': - quote = '"' - else: - quote = '`' - output = {} - privs = [] - for item in priv.strip().split('/'): - pieces = item.strip().rsplit(':', 1) - dbpriv = pieces[0].rsplit(".", 1) - - # Check for FUNCTION or PROCEDURE object types - parts = dbpriv[0].split(" ", 1) - object_type = '' - if len(parts) > 1 and (parts[0] == 'FUNCTION' or parts[0] == 'PROCEDURE'): - object_type = parts[0] + ' ' - dbpriv[0] = parts[1] - - # Do not escape if privilege is for database or table, i.e. - # neither quote *. nor .* - for i, side in enumerate(dbpriv): - if side.strip('`') != '*': - dbpriv[i] = '%s%s%s' % (quote, side.strip('`'), quote) - pieces[0] = object_type + '.'.join(dbpriv) - - if '(' in pieces[1]: - output[pieces[0]] = re.split(r',\s*(?=[^)]*(?:\(|$))', pieces[1].upper()) - for i in output[pieces[0]]: - privs.append(re.sub(r'\s*\(.*\)', '', i)) - else: - output[pieces[0]] = pieces[1].upper().split(',') - privs = output[pieces[0]] - new_privs = frozenset(privs) - if not new_privs.issubset(VALID_PRIVS): - raise InvalidPrivsError('Invalid privileges specified: %s' % new_privs.difference(VALID_PRIVS)) - - if '*.*' not in output: - output['*.*'] = ['USAGE'] - - # if we are only specifying something like REQUIRESSL and/or GRANT (=WITH GRANT OPTION) in *.* - # we still need to add USAGE as a privilege to avoid syntax errors - if 'REQUIRESSL' in priv and not set(output['*.*']).difference(set(['GRANT', 'REQUIRESSL'])): - output['*.*'].append('USAGE') - - return output - - -def privileges_revoke(cursor, user, host, db_table, priv, grant_option): - # Escape '%' since mysql db.execute() uses a format string - db_table = db_table.replace('%', '%%') - if grant_option: - query = ["REVOKE GRANT OPTION ON %s" % db_table] - query.append("FROM %s@%s") - query = ' '.join(query) - cursor.execute(query, (user, host)) - priv_string = ",".join([p for p in priv if p not in ('GRANT', 'REQUIRESSL')]) - query = ["REVOKE %s ON %s" % (priv_string, db_table)] - query.append("FROM %s@%s") - query = ' '.join(query) - cursor.execute(query, (user, host)) - - -def privileges_grant(cursor, user, host, db_table, priv): - # Escape '%' since mysql db.execute uses a format string and the - # specification of db and table often use a % (SQL wildcard) - db_table = db_table.replace('%', '%%') - priv_string = ",".join([p for p in priv if p not in ('GRANT', 'REQUIRESSL')]) - query = ["GRANT %s ON %s" % (priv_string, db_table)] - query.append("TO %s@%s") - if 'REQUIRESSL' in priv: - query.append("REQUIRE SSL") - if 'GRANT' in priv: - query.append("WITH GRANT OPTION") - query = ' '.join(query) - cursor.execute(query, (user, host)) - - -def convert_priv_dict_to_str(priv): - """Converts privs dictionary to string of certain format. - - Args: - priv (dict): Dict of privileges that needs to be converted to string. - - Returns: - priv (str): String representation of input argument. - """ - priv_list = ['%s:%s' % (key, val) for key, val in iteritems(priv)] - - return '/'.join(priv_list) - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(type='str'), - login_password=dict(type='str', no_log=True), - login_host=dict(type='str', default='localhost'), - login_port=dict(type='int', default=3306), - login_unix_socket=dict(type='str'), - user=dict(type='str', required=True, aliases=['name']), - password=dict(type='str', no_log=True), - encrypted=dict(type='bool', default=False), - host=dict(type='str', default='localhost'), - host_all=dict(type="bool", default=False), - state=dict(type='str', default='present', choices=['absent', 'present']), - priv=dict(type='raw'), - append_privs=dict(type='bool', default=False), - check_implicit_admin=dict(type='bool', default=False), - update_password=dict(type='str', default='always', choices=['always', 'on_create']), - connect_timeout=dict(type='int', default=30), - config_file=dict(type='path', default='~/.my.cnf'), - sql_log_bin=dict(type='bool', default=True), - client_cert=dict(type='path', aliases=['ssl_cert']), - client_key=dict(type='path', aliases=['ssl_key']), - ca_cert=dict(type='path', aliases=['ssl_ca']), - plugin=dict(default=None, type='str'), - plugin_hash_string=dict(default=None, type='str'), - plugin_auth_string=dict(default=None, type='str'), - ), - supports_check_mode=True, - ) - login_user = module.params["login_user"] - login_password = module.params["login_password"] - user = module.params["user"] - password = module.params["password"] - encrypted = module.boolean(module.params["encrypted"]) - host = module.params["host"].lower() - host_all = module.params["host_all"] - state = module.params["state"] - priv = module.params["priv"] - check_implicit_admin = module.params['check_implicit_admin'] - connect_timeout = module.params['connect_timeout'] - config_file = module.params['config_file'] - append_privs = module.boolean(module.params["append_privs"]) - update_password = module.params['update_password'] - ssl_cert = module.params["client_cert"] - ssl_key = module.params["client_key"] - ssl_ca = module.params["ca_cert"] - db = '' - sql_log_bin = module.params["sql_log_bin"] - plugin = module.params["plugin"] - plugin_hash_string = module.params["plugin_hash_string"] - plugin_auth_string = module.params["plugin_auth_string"] - if priv and not (isinstance(priv, str) or isinstance(priv, dict)): - module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv)) - - if priv and isinstance(priv, dict): - priv = convert_priv_dict_to_str(priv) - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - cursor = None - try: - if check_implicit_admin: - try: - cursor, db_conn = mysql_connect(module, 'root', '', config_file, ssl_cert, ssl_key, ssl_ca, db, - connect_timeout=connect_timeout) - except Exception: - pass - - if not cursor: - cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, db, - connect_timeout=connect_timeout) - except Exception as e: - module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. " - "Exception message: %s" % (config_file, to_native(e))) - - if not sql_log_bin: - cursor.execute("SET SQL_LOG_BIN=0;") - - if priv is not None: - try: - mode = get_mode(cursor) - except Exception as e: - module.fail_json(msg=to_native(e)) - try: - priv = privileges_unpack(priv, mode) - except Exception as e: - module.fail_json(msg="invalid privileges string: %s" % to_native(e)) - - if state == "present": - if user_exists(cursor, user, host, host_all): - try: - if update_password == 'always': - changed, msg = user_mod(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, - priv, append_privs, module) - else: - changed, msg = user_mod(cursor, user, host, host_all, None, encrypted, - plugin, plugin_hash_string, plugin_auth_string, - priv, append_privs, module) - - except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: - module.fail_json(msg=to_native(e)) - else: - if host_all: - module.fail_json(msg="host_all parameter cannot be used when adding a user") - try: - changed = user_add(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, - priv, module.check_mode) - if changed: - msg = "User added" - - except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: - module.fail_json(msg=to_native(e)) - elif state == "absent": - if user_exists(cursor, user, host, host_all): - changed = user_delete(cursor, user, host, host_all, module.check_mode) - msg = "User deleted" - else: - changed = False - msg = "User doesn't exist" - module.exit_json(changed=changed, user=user, msg=msg) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/mysql/mysql_variables.py b/lib/ansible/modules/database/mysql/mysql_variables.py deleted file mode 100644 index aa03f8990a..0000000000 --- a/lib/ansible/modules/database/mysql/mysql_variables.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2013, Balazs Pocze <banyek@gawker.com> -# Certain parts are taken from Mark Theunissen's mysqldb module -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: mysql_variables - -short_description: Manage MySQL global variables -description: -- Query / Set MySQL variables. -version_added: '1.3' -author: -- Balazs Pocze (@banyek) -options: - variable: - description: - - Variable name to operate - type: str - required: yes - value: - description: - - If set, then sets variable value to this - type: str - mode: - description: - - C(global) assigns C(value) to a global system variable which will be changed at runtime - but won't persist across server restarts. - - C(persist) assigns C(value) to a global system variable and persists it to - the mysqld-auto.cnf option file in the data directory - (the variable will survive service restarts). - - C(persist_only) persists C(value) to the mysqld-auto.cnf option file in the data directory - but without setting the global variable runtime value - (the value will be changed after the next service restart). - - Supported by MySQL 8.0 or later. - - For more information see U(https://dev.mysql.com/doc/refman/8.0/en/set-variable.html). - type: str - choices: ['global', 'persist', 'persist_only'] - default: global - version_added: '2.10' - -seealso: -- module: mysql_info -- name: MySQL SET command reference - description: Complete reference of the MySQL SET command documentation. - link: https://dev.mysql.com/doc/refman/8.0/en/set-statement.html - -extends_documentation_fragment: -- mysql -''' - -EXAMPLES = r''' -- name: Check for sync_binlog setting - mysql_variables: - variable: sync_binlog - -- name: Set read_only variable to 1 persistently - mysql_variables: - variable: read_only - value: 1 - mode: persist -''' - -RETURN = r''' -queries: - description: List of executed queries which modified DB's state. - returned: if executed - type: list - sample: ["SET GLOBAL `read_only` = 1"] - version_added: '2.10' -''' - -import os -import warnings -from re import match - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import SQLParseError, mysql_quote_identifier -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils._text import to_native - -executed_queries = [] - - -def check_mysqld_auto(module, cursor, mysqlvar): - """Check variable's value in mysqld-auto.cnf.""" - query = ("SELECT VARIABLE_VALUE " - "FROM performance_schema.persisted_variables " - "WHERE VARIABLE_NAME = %s") - try: - cursor.execute(query, (mysqlvar,)) - res = cursor.fetchone() - except Exception as e: - if "Table 'performance_schema.persisted_variables' doesn't exist" in str(e): - module.fail_json(msg='Server version must be 8.0 or greater.') - - if res: - return res[0] - else: - return None - - -def typedvalue(value): - """ - Convert value to number whenever possible, return same value - otherwise. - - >>> typedvalue('3') - 3 - >>> typedvalue('3.0') - 3.0 - >>> typedvalue('foobar') - 'foobar' - - """ - try: - return int(value) - except ValueError: - pass - - try: - return float(value) - except ValueError: - pass - - return value - - -def getvariable(cursor, mysqlvar): - cursor.execute("SHOW VARIABLES WHERE Variable_name = %s", (mysqlvar,)) - mysqlvar_val = cursor.fetchall() - if len(mysqlvar_val) == 1: - return mysqlvar_val[0][1] - else: - return None - - -def setvariable(cursor, mysqlvar, value, mode='global'): - """ Set a global mysql variable to a given value - - The DB driver will handle quoting of the given value based on its - type, thus numeric strings like '3.0' or '8' are illegal, they - should be passed as numeric literals. - - """ - if mode == 'persist': - query = "SET PERSIST %s = " % mysql_quote_identifier(mysqlvar, 'vars') - elif mode == 'global': - query = "SET GLOBAL %s = " % mysql_quote_identifier(mysqlvar, 'vars') - elif mode == 'persist_only': - query = "SET PERSIST_ONLY %s = " % mysql_quote_identifier(mysqlvar, 'vars') - - try: - cursor.execute(query + "%s", (value,)) - executed_queries.append(query + "%s" % value) - cursor.fetchall() - result = True - except Exception as e: - result = to_native(e) - - return result - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(type='str'), - login_password=dict(type='str', no_log=True), - login_host=dict(type='str', default='localhost'), - login_port=dict(type='int', default=3306), - login_unix_socket=dict(type='str'), - variable=dict(type='str'), - value=dict(type='str'), - client_cert=dict(type='path', aliases=['ssl_cert']), - client_key=dict(type='path', aliases=['ssl_key']), - ca_cert=dict(type='path', aliases=['ssl_ca']), - connect_timeout=dict(type='int', default=30), - config_file=dict(type='path', default='~/.my.cnf'), - mode=dict(type='str', choices=['global', 'persist', 'persist_only'], default='global'), - ), - ) - user = module.params["login_user"] - password = module.params["login_password"] - connect_timeout = module.params['connect_timeout'] - ssl_cert = module.params["client_cert"] - ssl_key = module.params["client_key"] - ssl_ca = module.params["ca_cert"] - config_file = module.params['config_file'] - db = 'mysql' - - mysqlvar = module.params["variable"] - value = module.params["value"] - mode = module.params["mode"] - - if mysqlvar is None: - module.fail_json(msg="Cannot run without variable to operate with") - if match('^[0-9a-z_.]+$', mysqlvar) is None: - module.fail_json(msg="invalid variable name \"%s\"" % mysqlvar) - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - else: - warnings.filterwarnings('error', category=mysql_driver.Warning) - - try: - cursor, db_conn = mysql_connect(module, user, password, config_file, ssl_cert, ssl_key, ssl_ca, db, - connect_timeout=connect_timeout) - except Exception as e: - if os.path.exists(config_file): - module.fail_json(msg=("unable to connect to database, check login_user and " - "login_password are correct or %s has the credentials. " - "Exception message: %s" % (config_file, to_native(e)))) - else: - module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e))) - - mysqlvar_val = None - var_in_mysqld_auto_cnf = None - - mysqlvar_val = getvariable(cursor, mysqlvar) - if mysqlvar_val is None: - module.fail_json(msg="Variable not available \"%s\"" % mysqlvar, changed=False) - - if value is None: - module.exit_json(msg=mysqlvar_val) - - if mode in ('persist', 'persist_only'): - var_in_mysqld_auto_cnf = check_mysqld_auto(module, cursor, mysqlvar) - - if mode == 'persist_only': - if var_in_mysqld_auto_cnf is None: - mysqlvar_val = False - else: - mysqlvar_val = var_in_mysqld_auto_cnf - - # Type values before using them - value_wanted = typedvalue(value) - value_actual = typedvalue(mysqlvar_val) - value_in_auto_cnf = None - if var_in_mysqld_auto_cnf is not None: - value_in_auto_cnf = typedvalue(var_in_mysqld_auto_cnf) - - if value_wanted == value_actual and mode in ('global', 'persist'): - if mode == 'persist' and value_wanted == value_in_auto_cnf: - module.exit_json(msg="Variable is already set to requested value globally" - "and stored into mysqld-auto.cnf file.", changed=False) - - elif mode == 'global': - module.exit_json(msg="Variable is already set to requested value.", changed=False) - - if mode == 'persist_only' and value_wanted == value_in_auto_cnf: - module.exit_json(msg="Variable is already stored into mysqld-auto.cnf " - "with requested value.", changed=False) - - try: - result = setvariable(cursor, mysqlvar, value_wanted, mode) - except SQLParseError as e: - result = to_native(e) - - if result is True: - module.exit_json(msg="Variable change succeeded prev_value=%s" % value_actual, - changed=True, queries=executed_queries) - else: - module.fail_json(msg=result, changed=False) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_copy.py b/lib/ansible/modules/database/postgresql/postgresql_copy.py deleted file mode 100644 index 9c5a3766fb..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_copy.py +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'supported_by': 'community', - 'status': ['preview'] -} - -DOCUMENTATION = r''' ---- -module: postgresql_copy -short_description: Copy data between a file/program and a PostgreSQL table -description: -- Copy data between a file/program and a PostgreSQL table. -version_added: '2.9' - -options: - copy_to: - description: - - Copy the contents of a table to a file. - - Can also copy the results of a SELECT query. - - Mutually exclusive with I(copy_from) and I(dst). - type: path - aliases: [ to ] - copy_from: - description: - - Copy data from a file to a table (appending the data to whatever is in the table already). - - Mutually exclusive with I(copy_to) and I(src). - type: path - aliases: [ from ] - src: - description: - - Copy data from I(copy_from) to I(src=tablename). - - Used with I(copy_to) only. - type: str - aliases: [ source ] - dst: - description: - - Copy data to I(dst=tablename) from I(copy_from=/path/to/data.file). - - Used with I(copy_from) only. - type: str - aliases: [ destination ] - columns: - description: - - List of column names for the src/dst table to COPY FROM/TO. - type: list - elements: str - aliases: [ column ] - program: - description: - - Mark I(src)/I(dst) as a program. Data will be copied to/from a program. - - See block Examples and PROGRAM arg description U(https://www.postgresql.org/docs/current/sql-copy.html). - type: bool - default: no - options: - description: - - Options of COPY command. - - See the full list of available options U(https://www.postgresql.org/docs/current/sql-copy.html). - type: dict - db: - description: - - Name of database to connect to. - type: str - aliases: [ login_db ] - session_role: - description: - - Switch to session_role after connecting. - The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str - -notes: -- Supports PostgreSQL version 9.4+. -- COPY command is only allowed to database superusers. -- if I(check_mode=yes), we just check the src/dst table availability - and return the COPY query that actually has not been executed. -- If i(check_mode=yes) and the source has been passed as SQL, the module - will execute it and rolled the transaction back but pay attention - it can affect database performance (e.g., if SQL collects a lot of data). - -seealso: -- name: COPY command reference - description: Complete reference of the COPY command documentation. - link: https://www.postgresql.org/docs/current/sql-copy.html - -author: -- Andrew Klychkov (@Andersson007) - -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Copy text TAB-separated data from file /tmp/data.txt to acme table - postgresql_copy: - copy_from: /tmp/data.txt - dst: acme - -- name: Copy CSV (comma-separated) data from file /tmp/data.csv to columns id, name of table acme - postgresql_copy: - copy_from: /tmp/data.csv - dst: acme - columns: id,name - options: - format: csv - -- name: > - Copy text vertical-bar-separated data from file /tmp/data.txt to bar table. - The NULL values are specified as N - postgresql_copy: - copy_from: /tmp/data.csv - dst: bar - options: - delimiter: '|' - null: 'N' - -- name: Copy data from acme table to file /tmp/data.txt in text format, TAB-separated - postgresql_copy: - src: acme - copy_to: /tmp/data.txt - -- name: Copy data from SELECT query to/tmp/data.csv in CSV format - postgresql_copy: - src: 'SELECT * FROM acme' - copy_to: /tmp/data.csv - options: - format: csv - -- name: Copy CSV data from my_table to gzip - postgresql_copy: - src: my_table - copy_to: 'gzip > /tmp/data.csv.gz' - program: yes - options: - format: csv - -- name: > - Copy data from columns id, name of table bar to /tmp/data.txt. - Output format is text, vertical-bar-separated, NULL as N - postgresql_copy: - src: bar - columns: - - id - - name - copy_to: /tmp/data.csv - options: - delimiter: '|' - null: 'N' -''' - -RETURN = r''' -queries: - description: List of executed queries. - returned: always - type: str - sample: [ "COPY test_table FROM '/tmp/data_file.txt' (FORMAT csv, DELIMITER ',', NULL 'NULL')" ] -src: - description: Data source. - returned: always - type: str - sample: "mytable" -dst: - description: Data destination. - returned: always - type: str - sample: "/tmp/data.csv" -''' - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import pg_quote_identifier -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) -from ansible.module_utils.six import iteritems - - -class PgCopyData(object): - - """Implements behavior of COPY FROM, COPY TO PostgreSQL command. - - Arguments: - module (AnsibleModule) -- object of AnsibleModule class - cursor (cursor) -- cursor object of psycopg2 library - - Attributes: - module (AnsibleModule) -- object of AnsibleModule class - cursor (cursor) -- cursor object of psycopg2 library - changed (bool) -- something was changed after execution or not - executed_queries (list) -- executed queries - dst (str) -- data destination table (when copy_from) - src (str) -- data source table (when copy_to) - opt_need_quotes (tuple) -- values of these options must be passed - to SQL in quotes - """ - - def __init__(self, module, cursor): - self.module = module - self.cursor = cursor - self.executed_queries = [] - self.changed = False - self.dst = '' - self.src = '' - self.opt_need_quotes = ( - 'DELIMITER', - 'NULL', - 'QUOTE', - 'ESCAPE', - 'ENCODING', - ) - - def copy_from(self): - """Implements COPY FROM command behavior.""" - self.src = self.module.params['copy_from'] - self.dst = self.module.params['dst'] - - query_fragments = ['COPY %s' % pg_quote_identifier(self.dst, 'table')] - - if self.module.params.get('columns'): - query_fragments.append('(%s)' % ','.join(self.module.params['columns'])) - - query_fragments.append('FROM') - - if self.module.params.get('program'): - query_fragments.append('PROGRAM') - - query_fragments.append("'%s'" % self.src) - - if self.module.params.get('options'): - query_fragments.append(self.__transform_options()) - - # Note: check mode is implemented here: - if self.module.check_mode: - self.changed = self.__check_table(self.dst) - - if self.changed: - self.executed_queries.append(' '.join(query_fragments)) - else: - if exec_sql(self, ' '.join(query_fragments), ddl=True): - self.changed = True - - def copy_to(self): - """Implements COPY TO command behavior.""" - self.src = self.module.params['src'] - self.dst = self.module.params['copy_to'] - - if 'SELECT ' in self.src.upper(): - # If src is SQL SELECT statement: - query_fragments = ['COPY (%s)' % self.src] - else: - # If src is a table: - query_fragments = ['COPY %s' % pg_quote_identifier(self.src, 'table')] - - if self.module.params.get('columns'): - query_fragments.append('(%s)' % ','.join(self.module.params['columns'])) - - query_fragments.append('TO') - - if self.module.params.get('program'): - query_fragments.append('PROGRAM') - - query_fragments.append("'%s'" % self.dst) - - if self.module.params.get('options'): - query_fragments.append(self.__transform_options()) - - # Note: check mode is implemented here: - if self.module.check_mode: - self.changed = self.__check_table(self.src) - - if self.changed: - self.executed_queries.append(' '.join(query_fragments)) - else: - if exec_sql(self, ' '.join(query_fragments), ddl=True): - self.changed = True - - def __transform_options(self): - """Transform options dict into a suitable string.""" - for (key, val) in iteritems(self.module.params['options']): - if key.upper() in self.opt_need_quotes: - self.module.params['options'][key] = "'%s'" % val - - opt = ['%s %s' % (key, val) for (key, val) in iteritems(self.module.params['options'])] - return '(%s)' % ', '.join(opt) - - def __check_table(self, table): - """Check table or SQL in transaction mode for check_mode. - - Return True if it is OK. - - Arguments: - table (str) - Table name that needs to be checked. - It can be SQL SELECT statement that was passed - instead of the table name. - """ - if 'SELECT ' in table.upper(): - # In this case table is actually SQL SELECT statement. - # If SQL fails, it's handled by exec_sql(): - exec_sql(self, table, add_to_executed=False) - # If exec_sql was passed, it means all is OK: - return True - - exec_sql(self, 'SELECT 1 FROM %s' % pg_quote_identifier(table, 'table'), - add_to_executed=False) - # If SQL was executed successfully: - return True - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - copy_to=dict(type='path', aliases=['to']), - copy_from=dict(type='path', aliases=['from']), - src=dict(type='str', aliases=['source']), - dst=dict(type='str', aliases=['destination']), - columns=dict(type='list', elements='str', aliases=['column']), - options=dict(type='dict'), - program=dict(type='bool', default=False), - db=dict(type='str', aliases=['login_db']), - session_role=dict(type='str'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[ - ['copy_from', 'copy_to'], - ['copy_from', 'src'], - ['copy_to', 'dst'], - ] - ) - - # Note: we don't need to check mutually exclusive params here, because they are - # checked automatically by AnsibleModule (mutually_exclusive=[] list above). - if module.params.get('copy_from') and not module.params.get('dst'): - module.fail_json(msg='dst param is necessary with copy_from') - - elif module.params.get('copy_to') and not module.params.get('src'): - module.fail_json(msg='src param is necessary with copy_to') - - # Connect to DB and make cursor object: - conn_params = get_conn_params(module, module.params) - db_connection = connect_to_db(module, conn_params, autocommit=False) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - ############## - # Create the object and do main job: - data = PgCopyData(module, cursor) - - # Note: parameters like dst, src, etc. are got - # from module object into data object of PgCopyData class. - # Therefore not need to pass args to the methods below. - # Note: check mode is implemented inside the methods below - # by checking passed module.check_mode arg. - if module.params.get('copy_to'): - data.copy_to() - - elif module.params.get('copy_from'): - data.copy_from() - - # Finish: - if module.check_mode: - db_connection.rollback() - else: - db_connection.commit() - - cursor.close() - db_connection.close() - - # Return some values: - module.exit_json( - changed=data.changed, - queries=data.executed_queries, - src=data.src, - dst=data.dst, - ) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_db.py b/lib/ansible/modules/database/postgresql/postgresql_db.py deleted file mode 100644 index 40858d9974..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_db.py +++ /dev/null @@ -1,657 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: postgresql_db -short_description: Add or remove PostgreSQL databases from a remote host. -description: - - Add or remove PostgreSQL databases from a remote host. -version_added: '0.6' -options: - name: - description: - - Name of the database to add or remove - type: str - required: true - aliases: [ db ] - port: - description: - - Database port to connect (if needed) - type: int - default: 5432 - aliases: - - login_port - owner: - description: - - Name of the role to set as owner of the database - type: str - template: - description: - - Template used to create the database - type: str - encoding: - description: - - Encoding of the database - type: str - lc_collate: - description: - - Collation order (LC_COLLATE) to use in the database. Must match collation order of template database unless C(template0) is used as template. - type: str - lc_ctype: - description: - - Character classification (LC_CTYPE) to use in the database (e.g. lower, upper, ...) Must match LC_CTYPE of template database unless C(template0) - is used as template. - type: str - session_role: - description: - - Switch to session_role after connecting. The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally. - type: str - version_added: '2.8' - state: - description: - - The database state. - - C(present) implies that the database should be created if necessary. - - C(absent) implies that the database should be removed if present. - - C(dump) requires a target definition to which the database will be backed up. (Added in Ansible 2.4) - Note that in some PostgreSQL versions of pg_dump, which is an embedded PostgreSQL utility and is used by the module, - returns rc 0 even when errors occurred (e.g. the connection is forbidden by pg_hba.conf, etc.), - so the module returns changed=True but the dump has not actually been done. Please, be sure that your version of - pg_dump returns rc 1 in this case. - - C(restore) also requires a target definition from which the database will be restored. (Added in Ansible 2.4) - - The format of the backup will be detected based on the target name. - - Supported compression formats for dump and restore include C(.pgc), C(.bz2), C(.gz) and C(.xz) - - Supported formats for dump and restore include C(.sql) and C(.tar) - type: str - choices: [ absent, dump, present, restore ] - default: present - target: - description: - - File to back up or restore from. - - Used when I(state) is C(dump) or C(restore). - type: path - version_added: '2.4' - target_opts: - description: - - Further arguments for pg_dump or pg_restore. - - Used when I(state) is C(dump) or C(restore). - type: str - version_added: '2.4' - maintenance_db: - description: - - The value specifies the initial database (which is also called as maintenance DB) that Ansible connects to. - type: str - default: postgres - version_added: '2.5' - conn_limit: - description: - - Specifies the database connection limit. - type: str - version_added: '2.8' - tablespace: - description: - - The tablespace to set for the database - U(https://www.postgresql.org/docs/current/sql-alterdatabase.html). - - If you want to move the database back to the default tablespace, - explicitly set this to pg_default. - type: path - version_added: '2.9' - dump_extra_args: - description: - - Provides additional arguments when I(state) is C(dump). - - Cannot be used with dump-file-format-related arguments like ``--format=d``. - type: str - version_added: '2.10' -seealso: -- name: CREATE DATABASE reference - description: Complete reference of the CREATE DATABASE command documentation. - link: https://www.postgresql.org/docs/current/sql-createdatabase.html -- name: DROP DATABASE reference - description: Complete reference of the DROP DATABASE command documentation. - link: https://www.postgresql.org/docs/current/sql-dropdatabase.html -- name: pg_dump reference - description: Complete reference of pg_dump documentation. - link: https://www.postgresql.org/docs/current/app-pgdump.html -- name: pg_restore reference - description: Complete reference of pg_restore documentation. - link: https://www.postgresql.org/docs/current/app-pgrestore.html -- module: postgresql_tablespace -- module: postgresql_info -- module: postgresql_ping -notes: -- State C(dump) and C(restore) don't require I(psycopg2) since version 2.8. -author: "Ansible Core Team" -extends_documentation_fragment: -- postgres -''' - -EXAMPLES = r''' -- name: Create a new database with name "acme" - postgresql_db: - name: acme - -# Note: If a template different from "template0" is specified, encoding and locale settings must match those of the template. -- name: Create a new database with name "acme" and specific encoding and locale # settings. - postgresql_db: - name: acme - encoding: UTF-8 - lc_collate: de_DE.UTF-8 - lc_ctype: de_DE.UTF-8 - template: template0 - -# Note: Default limit for the number of concurrent connections to a specific database is "-1", which means "unlimited" -- name: Create a new database with name "acme" which has a limit of 100 concurrent connections - postgresql_db: - name: acme - conn_limit: "100" - -- name: Dump an existing database to a file - postgresql_db: - name: acme - state: dump - target: /tmp/acme.sql - -- name: Dump an existing database to a file excluding the test table - postgresql_db: - name: acme - state: dump - target: /tmp/acme.sql - dump_extra_args: --exclude-table=test - -- name: Dump an existing database to a file (with compression) - postgresql_db: - name: acme - state: dump - target: /tmp/acme.sql.gz - -- name: Dump a single schema for an existing database - postgresql_db: - name: acme - state: dump - target: /tmp/acme.sql - target_opts: "-n public" - -# Note: In the example below, if database foo exists and has another tablespace -# the tablespace will be changed to foo. Access to the database will be locked -# until the copying of database files is finished. -- name: Create a new database called foo in tablespace bar - postgresql_db: - name: foo - tablespace: bar -''' - -RETURN = r''' -executed_commands: - description: List of commands which tried to run. - returned: always - type: list - sample: ["CREATE DATABASE acme"] - version_added: '2.10' -''' - - -import os -import subprocess -import traceback - -try: - import psycopg2 - import psycopg2.extras -except ImportError: - HAS_PSYCOPG2 = False -else: - HAS_PSYCOPG2 = True - -import ansible.module_utils.postgres as pgutils -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import SQLParseError, pg_quote_identifier -from ansible.module_utils.six import iteritems -from ansible.module_utils.six.moves import shlex_quote -from ansible.module_utils._text import to_native - -executed_commands = [] - - -class NotSupportedError(Exception): - pass - -# =========================================== -# PostgreSQL module specific support methods. -# - - -def set_owner(cursor, db, owner): - query = 'ALTER DATABASE %s OWNER TO "%s"' % ( - pg_quote_identifier(db, 'database'), - owner) - executed_commands.append(query) - cursor.execute(query) - return True - - -def set_conn_limit(cursor, db, conn_limit): - query = "ALTER DATABASE %s CONNECTION LIMIT %s" % ( - pg_quote_identifier(db, 'database'), - conn_limit) - executed_commands.append(query) - cursor.execute(query) - return True - - -def get_encoding_id(cursor, encoding): - query = "SELECT pg_char_to_encoding(%(encoding)s) AS encoding_id;" - cursor.execute(query, {'encoding': encoding}) - return cursor.fetchone()['encoding_id'] - - -def get_db_info(cursor, db): - query = """ - SELECT rolname AS owner, - pg_encoding_to_char(encoding) AS encoding, encoding AS encoding_id, - datcollate AS lc_collate, datctype AS lc_ctype, pg_database.datconnlimit AS conn_limit, - spcname AS tablespace - FROM pg_database - JOIN pg_roles ON pg_roles.oid = pg_database.datdba - JOIN pg_tablespace ON pg_tablespace.oid = pg_database.dattablespace - WHERE datname = %(db)s - """ - cursor.execute(query, {'db': db}) - return cursor.fetchone() - - -def db_exists(cursor, db): - query = "SELECT * FROM pg_database WHERE datname=%(db)s" - cursor.execute(query, {'db': db}) - return cursor.rowcount == 1 - - -def db_delete(cursor, db): - if db_exists(cursor, db): - query = "DROP DATABASE %s" % pg_quote_identifier(db, 'database') - executed_commands.append(query) - cursor.execute(query) - return True - else: - return False - - -def db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace): - params = dict(enc=encoding, collate=lc_collate, ctype=lc_ctype, conn_limit=conn_limit, tablespace=tablespace) - if not db_exists(cursor, db): - query_fragments = ['CREATE DATABASE %s' % pg_quote_identifier(db, 'database')] - if owner: - query_fragments.append('OWNER "%s"' % owner) - if template: - query_fragments.append('TEMPLATE %s' % pg_quote_identifier(template, 'database')) - if encoding: - query_fragments.append('ENCODING %(enc)s') - if lc_collate: - query_fragments.append('LC_COLLATE %(collate)s') - if lc_ctype: - query_fragments.append('LC_CTYPE %(ctype)s') - if tablespace: - query_fragments.append('TABLESPACE %s' % pg_quote_identifier(tablespace, 'tablespace')) - if conn_limit: - query_fragments.append("CONNECTION LIMIT %(conn_limit)s" % {"conn_limit": conn_limit}) - query = ' '.join(query_fragments) - executed_commands.append(cursor.mogrify(query, params)) - cursor.execute(query, params) - return True - else: - db_info = get_db_info(cursor, db) - if (encoding and get_encoding_id(cursor, encoding) != db_info['encoding_id']): - raise NotSupportedError( - 'Changing database encoding is not supported. ' - 'Current encoding: %s' % db_info['encoding'] - ) - elif lc_collate and lc_collate != db_info['lc_collate']: - raise NotSupportedError( - 'Changing LC_COLLATE is not supported. ' - 'Current LC_COLLATE: %s' % db_info['lc_collate'] - ) - elif lc_ctype and lc_ctype != db_info['lc_ctype']: - raise NotSupportedError( - 'Changing LC_CTYPE is not supported.' - 'Current LC_CTYPE: %s' % db_info['lc_ctype'] - ) - else: - changed = False - - if owner and owner != db_info['owner']: - changed = set_owner(cursor, db, owner) - - if conn_limit and conn_limit != str(db_info['conn_limit']): - changed = set_conn_limit(cursor, db, conn_limit) - - if tablespace and tablespace != db_info['tablespace']: - changed = set_tablespace(cursor, db, tablespace) - - return changed - - -def db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace): - if not db_exists(cursor, db): - return False - else: - db_info = get_db_info(cursor, db) - if (encoding and get_encoding_id(cursor, encoding) != db_info['encoding_id']): - return False - elif lc_collate and lc_collate != db_info['lc_collate']: - return False - elif lc_ctype and lc_ctype != db_info['lc_ctype']: - return False - elif owner and owner != db_info['owner']: - return False - elif conn_limit and conn_limit != str(db_info['conn_limit']): - return False - elif tablespace and tablespace != db_info['tablespace']: - return False - else: - return True - - -def db_dump(module, target, target_opts="", - db=None, - dump_extra_args=None, - user=None, - password=None, - host=None, - port=None, - **kw): - - flags = login_flags(db, host, port, user, db_prefix=False) - cmd = module.get_bin_path('pg_dump', True) - comp_prog_path = None - - if os.path.splitext(target)[-1] == '.tar': - flags.append(' --format=t') - elif os.path.splitext(target)[-1] == '.pgc': - flags.append(' --format=c') - if os.path.splitext(target)[-1] == '.gz': - if module.get_bin_path('pigz'): - comp_prog_path = module.get_bin_path('pigz', True) - else: - comp_prog_path = module.get_bin_path('gzip', True) - elif os.path.splitext(target)[-1] == '.bz2': - comp_prog_path = module.get_bin_path('bzip2', True) - elif os.path.splitext(target)[-1] == '.xz': - comp_prog_path = module.get_bin_path('xz', True) - - cmd += "".join(flags) - - if dump_extra_args: - cmd += " {0} ".format(dump_extra_args) - - if target_opts: - cmd += " {0} ".format(target_opts) - - if comp_prog_path: - # Use a fifo to be notified of an error in pg_dump - # Using shell pipe has no way to return the code of the first command - # in a portable way. - fifo = os.path.join(module.tmpdir, 'pg_fifo') - os.mkfifo(fifo) - cmd = '{1} <{3} > {2} & {0} >{3}'.format(cmd, comp_prog_path, shlex_quote(target), fifo) - else: - cmd = '{0} > {1}'.format(cmd, shlex_quote(target)) - - return do_with_password(module, cmd, password) - - -def db_restore(module, target, target_opts="", - db=None, - user=None, - password=None, - host=None, - port=None, - **kw): - - flags = login_flags(db, host, port, user) - comp_prog_path = None - cmd = module.get_bin_path('psql', True) - - if os.path.splitext(target)[-1] == '.sql': - flags.append(' --file={0}'.format(target)) - - elif os.path.splitext(target)[-1] == '.tar': - flags.append(' --format=Tar') - cmd = module.get_bin_path('pg_restore', True) - - elif os.path.splitext(target)[-1] == '.pgc': - flags.append(' --format=Custom') - cmd = module.get_bin_path('pg_restore', True) - - elif os.path.splitext(target)[-1] == '.gz': - comp_prog_path = module.get_bin_path('zcat', True) - - elif os.path.splitext(target)[-1] == '.bz2': - comp_prog_path = module.get_bin_path('bzcat', True) - - elif os.path.splitext(target)[-1] == '.xz': - comp_prog_path = module.get_bin_path('xzcat', True) - - cmd += "".join(flags) - if target_opts: - cmd += " {0} ".format(target_opts) - - if comp_prog_path: - env = os.environ.copy() - if password: - env = {"PGPASSWORD": password} - p1 = subprocess.Popen([comp_prog_path, target], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - p2 = subprocess.Popen(cmd, stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=env) - (stdout2, stderr2) = p2.communicate() - p1.stdout.close() - p1.wait() - if p1.returncode != 0: - stderr1 = p1.stderr.read() - return p1.returncode, '', stderr1, 'cmd: ****' - else: - return p2.returncode, '', stderr2, 'cmd: ****' - else: - cmd = '{0} < {1}'.format(cmd, shlex_quote(target)) - - return do_with_password(module, cmd, password) - - -def login_flags(db, host, port, user, db_prefix=True): - """ - returns a list of connection argument strings each prefixed - with a space and quoted where necessary to later be combined - in a single shell string with `"".join(rv)` - - db_prefix determines if "--dbname" is prefixed to the db argument, - since the argument was introduced in 9.3. - """ - flags = [] - if db: - if db_prefix: - flags.append(' --dbname={0}'.format(shlex_quote(db))) - else: - flags.append(' {0}'.format(shlex_quote(db))) - if host: - flags.append(' --host={0}'.format(host)) - if port: - flags.append(' --port={0}'.format(port)) - if user: - flags.append(' --username={0}'.format(user)) - return flags - - -def do_with_password(module, cmd, password): - env = {} - if password: - env = {"PGPASSWORD": password} - executed_commands.append(cmd) - rc, stderr, stdout = module.run_command(cmd, use_unsafe_shell=True, environ_update=env) - return rc, stderr, stdout, cmd - - -def set_tablespace(cursor, db, tablespace): - query = "ALTER DATABASE %s SET TABLESPACE %s" % ( - pg_quote_identifier(db, 'database'), - pg_quote_identifier(tablespace, 'tablespace')) - executed_commands.append(query) - cursor.execute(query) - return True - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = pgutils.postgres_common_argument_spec() - argument_spec.update( - db=dict(type='str', required=True, aliases=['name']), - owner=dict(type='str', default=''), - template=dict(type='str', default=''), - encoding=dict(type='str', default=''), - lc_collate=dict(type='str', default=''), - lc_ctype=dict(type='str', default=''), - state=dict(type='str', default='present', choices=['absent', 'dump', 'present', 'restore']), - target=dict(type='path', default=''), - target_opts=dict(type='str', default=''), - maintenance_db=dict(type='str', default="postgres"), - session_role=dict(type='str'), - conn_limit=dict(type='str', default=''), - tablespace=dict(type='path', default=''), - dump_extra_args=dict(type='str', default=None), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - db = module.params["db"] - owner = module.params["owner"] - template = module.params["template"] - encoding = module.params["encoding"] - lc_collate = module.params["lc_collate"] - lc_ctype = module.params["lc_ctype"] - target = module.params["target"] - target_opts = module.params["target_opts"] - state = module.params["state"] - changed = False - maintenance_db = module.params['maintenance_db'] - session_role = module.params["session_role"] - conn_limit = module.params['conn_limit'] - tablespace = module.params['tablespace'] - dump_extra_args = module.params['dump_extra_args'] - - raw_connection = state in ("dump", "restore") - - if not raw_connection: - pgutils.ensure_required_libs(module) - - # To use defaults values, keyword arguments must be absent, so - # check which values are empty and don't include in the **kw - # dictionary - params_map = { - "login_host": "host", - "login_user": "user", - "login_password": "password", - "port": "port", - "ssl_mode": "sslmode", - "ca_cert": "sslrootcert" - } - kw = dict((params_map[k], v) for (k, v) in iteritems(module.params) - if k in params_map and v != '' and v is not None) - - # If a login_unix_socket is specified, incorporate it here. - is_localhost = "host" not in kw or kw["host"] == "" or kw["host"] == "localhost" - - if is_localhost and module.params["login_unix_socket"] != "": - kw["host"] = module.params["login_unix_socket"] - - if target == "": - target = "{0}/{1}.sql".format(os.getcwd(), db) - target = os.path.expanduser(target) - - if not raw_connection: - try: - db_connection = psycopg2.connect(database=maintenance_db, **kw) - - # Enable autocommit so we can create databases - if psycopg2.__version__ >= '2.4.2': - db_connection.autocommit = True - else: - db_connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) - cursor = db_connection.cursor(cursor_factory=psycopg2.extras.DictCursor) - - except TypeError as e: - if 'sslrootcert' in e.args[0]: - module.fail_json(msg='Postgresql server must be at least version 8.4 to support sslrootcert. Exception: {0}'.format(to_native(e)), - exception=traceback.format_exc()) - module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) - - except Exception as e: - module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) - - if session_role: - try: - cursor.execute('SET ROLE "%s"' % session_role) - except Exception as e: - module.fail_json(msg="Could not switch role: %s" % to_native(e), exception=traceback.format_exc()) - - try: - if module.check_mode: - if state == "absent": - changed = db_exists(cursor, db) - elif state == "present": - changed = not db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace) - module.exit_json(changed=changed, db=db, executed_commands=executed_commands) - - if state == "absent": - try: - changed = db_delete(cursor, db) - except SQLParseError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - elif state == "present": - try: - changed = db_create(cursor, db, owner, template, encoding, lc_collate, lc_ctype, conn_limit, tablespace) - except SQLParseError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - elif state in ("dump", "restore"): - method = state == "dump" and db_dump or db_restore - try: - if state == 'dump': - rc, stdout, stderr, cmd = method(module, target, target_opts, db, dump_extra_args, **kw) - else: - rc, stdout, stderr, cmd = method(module, target, target_opts, db, **kw) - - if rc != 0: - module.fail_json(msg=stderr, stdout=stdout, rc=rc, cmd=cmd) - else: - module.exit_json(changed=True, msg=stdout, stderr=stderr, rc=rc, cmd=cmd, - executed_commands=executed_commands) - except SQLParseError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - except NotSupportedError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - except SystemExit: - # Avoid catching this on Python 2.4 - raise - except Exception as e: - module.fail_json(msg="Database query failed: %s" % to_native(e), exception=traceback.format_exc()) - - module.exit_json(changed=changed, db=db, executed_commands=executed_commands) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_ext.py b/lib/ansible/modules/database/postgresql/postgresql_ext.py deleted file mode 100644 index 80e10a60d8..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_ext.py +++ /dev/null @@ -1,415 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: postgresql_ext -short_description: Add or remove PostgreSQL extensions from a database -description: -- Add or remove PostgreSQL extensions from a database. -version_added: '1.9' -options: - name: - description: - - Name of the extension to add or remove. - required: true - type: str - aliases: - - ext - db: - description: - - Name of the database to add or remove the extension to/from. - required: true - type: str - aliases: - - login_db - schema: - description: - - Name of the schema to add the extension to. - version_added: '2.8' - type: str - session_role: - description: - - Switch to session_role after connecting. - - The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally. - type: str - version_added: '2.8' - state: - description: - - The database extension state. - default: present - choices: [ absent, present ] - type: str - cascade: - description: - - Automatically install/remove any extensions that this extension depends on - that are not already installed/removed (supported since PostgreSQL 9.6). - type: bool - default: no - version_added: '2.8' - login_unix_socket: - description: - - Path to a Unix domain socket for local connections. - type: str - version_added: '2.8' - ssl_mode: - description: - - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. - - See U(https://www.postgresql.org/docs/current/static/libpq-ssl.html) for more information on the modes. - - Default of C(prefer) matches libpq default. - type: str - default: prefer - choices: [ allow, disable, prefer, require, verify-ca, verify-full ] - version_added: '2.8' - ca_cert: - description: - - Specifies the name of a file containing SSL certificate authority (CA) certificate(s). - - If the file exists, the server's certificate will be verified to be signed by one of these authorities. - type: str - aliases: [ ssl_rootcert ] - version_added: '2.8' - version: - description: - - Extension version to add or update to. Has effect with I(state=present) only. - - If not specified, the latest extension version will be created. - - It can't downgrade an extension version. - When version downgrade is needed, remove the extension and create new one with appropriate version. - - Set I(version=latest) to update the extension to the latest available version. - type: str - version_added: '2.9' -seealso: -- name: PostgreSQL extensions - description: General information about PostgreSQL extensions. - link: https://www.postgresql.org/docs/current/external-extensions.html -- name: CREATE EXTENSION reference - description: Complete reference of the CREATE EXTENSION command documentation. - link: https://www.postgresql.org/docs/current/sql-createextension.html -- name: ALTER EXTENSION reference - description: Complete reference of the ALTER EXTENSION command documentation. - link: https://www.postgresql.org/docs/current/sql-alterextension.html -- name: DROP EXTENSION reference - description: Complete reference of the DROP EXTENSION command documentation. - link: https://www.postgresql.org/docs/current/sql-droppublication.html -notes: -- The default authentication assumes that you are either logging in as - or sudo'ing to the C(postgres) account on the host. -- This module uses I(psycopg2), a Python PostgreSQL database adapter. -- You must ensure that C(psycopg2) is installed on the host before using this module. -- If the remote host is the PostgreSQL server (which is the default case), - then PostgreSQL must also be installed on the remote host. -- For Ubuntu-based systems, install the C(postgresql), C(libpq-dev), - and C(python-psycopg2) packages on the remote host before using this module. -requirements: [ psycopg2 ] -author: -- Daniel Schep (@dschep) -- Thomas O'Donnell (@andytom) -- Sandro Santilli (@strk) -- Andrew Klychkov (@Andersson007) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Adds postgis extension to the database acme in the schema foo - postgresql_ext: - name: postgis - db: acme - schema: foo - -- name: Removes postgis extension to the database acme - postgresql_ext: - name: postgis - db: acme - state: absent - -- name: Adds earthdistance extension to the database template1 cascade - postgresql_ext: - name: earthdistance - db: template1 - cascade: true - -# In the example below, if earthdistance extension is installed, -# it will be removed too because it depends on cube: -- name: Removes cube extension from the database acme cascade - postgresql_ext: - name: cube - db: acme - cascade: yes - state: absent - -- name: Create extension foo of version 1.2 or update it if it's already created - postgresql_ext: - db: acme - name: foo - version: 1.2 - -- name: Assuming extension foo is created, update it to the latest version - postgresql_ext: - db: acme - name: foo - version: latest -''' - -RETURN = r''' -query: - description: List of executed queries. - returned: always - type: list - sample: ["DROP EXTENSION \"acme\""] - -''' - -import traceback - -from distutils.version import LooseVersion - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - get_conn_params, - postgres_common_argument_spec, -) -from ansible.module_utils._text import to_native - -executed_queries = [] - - -class NotSupportedError(Exception): - pass - - -# =========================================== -# PostgreSQL module specific support methods. -# - -def ext_exists(cursor, ext): - query = "SELECT * FROM pg_extension WHERE extname=%(ext)s" - cursor.execute(query, {'ext': ext}) - return cursor.rowcount == 1 - - -def ext_delete(cursor, ext, cascade): - if ext_exists(cursor, ext): - query = "DROP EXTENSION \"%s\"" % ext - if cascade: - query += " CASCADE" - cursor.execute(query) - executed_queries.append(query) - return True - else: - return False - - -def ext_update_version(cursor, ext, version): - """Update extension version. - - Return True if success. - - Args: - cursor (cursor) -- cursor object of psycopg2 library - ext (str) -- extension name - version (str) -- extension version - """ - if version != 'latest': - query = ("ALTER EXTENSION \"%s\"" % ext) - cursor.execute(query + " UPDATE TO %(ver)s", {'ver': version}) - executed_queries.append(cursor.mogrify(query + " UPDATE TO %(ver)s", {'ver': version})) - else: - query = ("ALTER EXTENSION \"%s\" UPDATE" % ext) - cursor.execute(query) - executed_queries.append(query) - return True - - -def ext_create(cursor, ext, schema, cascade, version): - query = "CREATE EXTENSION \"%s\"" % ext - if schema: - query += " WITH SCHEMA \"%s\"" % schema - if version: - query += " VERSION %(ver)s" - if cascade: - query += " CASCADE" - - if version: - cursor.execute(query, {'ver': version}) - executed_queries.append(cursor.mogrify(query, {'ver': version})) - else: - cursor.execute(query) - executed_queries.append(query) - return True - - -def ext_get_versions(cursor, ext): - """ - Get the current created extension version and available versions. - - Return tuple (current_version, [list of available versions]). - - Note: the list of available versions contains only versions - that higher than the current created version. - If the extension is not created, this list will contain all - available versions. - - Args: - cursor (cursor) -- cursor object of psycopg2 library - ext (str) -- extension name - """ - - # 1. Get the current extension version: - query = ("SELECT extversion FROM pg_catalog.pg_extension " - "WHERE extname = %(ext)s") - - current_version = '0' - cursor.execute(query, {'ext': ext}) - res = cursor.fetchone() - if res: - current_version = res[0] - - # 2. Get available versions: - query = ("SELECT version FROM pg_available_extension_versions " - "WHERE name = %(ext)s") - cursor.execute(query, {'ext': ext}) - res = cursor.fetchall() - - available_versions = [] - if res: - # Make the list of available versions: - for line in res: - if LooseVersion(line[0]) > LooseVersion(current_version): - available_versions.append(line['version']) - - if current_version == '0': - current_version = False - - return (current_version, available_versions) - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - db=dict(type="str", required=True, aliases=["login_db"]), - ext=dict(type="str", required=True, aliases=["name"]), - schema=dict(type="str"), - state=dict(type="str", default="present", choices=["absent", "present"]), - cascade=dict(type="bool", default=False), - session_role=dict(type="str"), - version=dict(type="str"), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - ext = module.params["ext"] - schema = module.params["schema"] - state = module.params["state"] - cascade = module.params["cascade"] - version = module.params["version"] - changed = False - - if version and state == 'absent': - module.warn("Parameter version is ignored when state=absent") - - conn_params = get_conn_params(module, module.params) - db_connection = connect_to_db(module, conn_params, autocommit=True) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - try: - # Get extension info and available versions: - curr_version, available_versions = ext_get_versions(cursor, ext) - - if state == "present": - if version == 'latest': - if available_versions: - version = available_versions[-1] - else: - version = '' - - if version: - # If the specific version is passed and it is not available for update: - if version not in available_versions: - if not curr_version: - module.fail_json(msg="Passed version '%s' is not available" % version) - - elif LooseVersion(curr_version) == LooseVersion(version): - changed = False - - else: - module.fail_json(msg="Passed version '%s' is lower than " - "the current created version '%s' or " - "the passed version is not available" % (version, curr_version)) - - # If the specific version is passed and it is higher that the current version: - if curr_version and version: - if LooseVersion(curr_version) < LooseVersion(version): - if module.check_mode: - changed = True - else: - changed = ext_update_version(cursor, ext, version) - - # If the specific version is passed and it is created now: - if curr_version == version: - changed = False - - # If the ext doesn't exist and installed: - elif not curr_version and available_versions: - if module.check_mode: - changed = True - else: - changed = ext_create(cursor, ext, schema, cascade, version) - - # If version is not passed: - else: - if not curr_version: - # If the ext doesn't exist and it's installed: - if available_versions: - if module.check_mode: - changed = True - else: - changed = ext_create(cursor, ext, schema, cascade, version) - - # If the ext doesn't exist and not installed: - else: - module.fail_json(msg="Extension %s is not installed" % ext) - - elif state == "absent": - if curr_version: - if module.check_mode: - changed = True - else: - changed = ext_delete(cursor, ext, cascade) - else: - changed = False - - except Exception as e: - db_connection.close() - module.fail_json(msg="Database query failed: %s" % to_native(e), exception=traceback.format_exc()) - - db_connection.close() - module.exit_json(changed=changed, db=module.params["db"], ext=ext, queries=executed_queries) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_idx.py b/lib/ansible/modules/database/postgresql/postgresql_idx.py deleted file mode 100644 index baf96e0ba3..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_idx.py +++ /dev/null @@ -1,586 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2018-2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = r''' ---- -module: postgresql_idx -short_description: Create or drop indexes from a PostgreSQL database -description: -- Create or drop indexes from a PostgreSQL database. -version_added: '2.8' - -options: - idxname: - description: - - Name of the index to create or drop. - type: str - required: true - aliases: - - name - db: - description: - - Name of database to connect to and where the index will be created/dropped. - type: str - aliases: - - login_db - session_role: - description: - - Switch to session_role after connecting. - The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str - schema: - description: - - Name of a database schema where the index will be created. - type: str - state: - description: - - Index state. - - C(present) implies the index will be created if it does not exist. - - C(absent) implies the index will be dropped if it exists. - type: str - default: present - choices: [ absent, present ] - table: - description: - - Table to create index on it. - - Mutually exclusive with I(state=absent). - type: str - columns: - description: - - List of index columns that need to be covered by index. - - Mutually exclusive with I(state=absent). - type: list - elements: str - aliases: - - column - cond: - description: - - Index conditions. - - Mutually exclusive with I(state=absent). - type: str - idxtype: - description: - - Index type (like btree, gist, gin, etc.). - - Mutually exclusive with I(state=absent). - type: str - aliases: - - type - concurrent: - description: - - Enable or disable concurrent mode (CREATE / DROP INDEX CONCURRENTLY). - - Pay attention, if I(concurrent=no), the table will be locked (ACCESS EXCLUSIVE) during the building process. - For more information about the lock levels see U(https://www.postgresql.org/docs/current/explicit-locking.html). - - If the building process was interrupted for any reason when I(cuncurrent=yes), the index becomes invalid. - In this case it should be dropped and created again. - - Mutually exclusive with I(cascade=yes). - type: bool - default: yes - unique: - description: - - Enable unique index. - - Only btree currently supports unique indexes. - type: bool - default: no - version_added: '2.10' - tablespace: - description: - - Set a tablespace for the index. - - Mutually exclusive with I(state=absent). - required: false - type: str - storage_params: - description: - - Storage parameters like fillfactor, vacuum_cleanup_index_scale_factor, etc. - - Mutually exclusive with I(state=absent). - type: list - elements: str - cascade: - description: - - Automatically drop objects that depend on the index, - and in turn all objects that depend on those objects. - - It used only with I(state=absent). - - Mutually exclusive with I(concurrent=yes) - type: bool - default: no - -seealso: -- module: postgresql_table -- module: postgresql_tablespace -- name: PostgreSQL indexes reference - description: General information about PostgreSQL indexes. - link: https://www.postgresql.org/docs/current/indexes.html -- name: CREATE INDEX reference - description: Complete reference of the CREATE INDEX command documentation. - link: https://www.postgresql.org/docs/current/sql-createindex.html -- name: ALTER INDEX reference - description: Complete reference of the ALTER INDEX command documentation. - link: https://www.postgresql.org/docs/current/sql-alterindex.html -- name: DROP INDEX reference - description: Complete reference of the DROP INDEX command documentation. - link: https://www.postgresql.org/docs/current/sql-dropindex.html - -notes: -- The index building process can affect database performance. -- To avoid table locks on production databases, use I(concurrent=yes) (default behavior). - -author: -- Andrew Klychkov (@Andersson007) - -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Create btree index if not exists test_idx concurrently covering columns id and name of table products - postgresql_idx: - db: acme - table: products - columns: id,name - name: test_idx - -- name: Create btree index test_idx concurrently with tablespace called ssd and storage parameter - postgresql_idx: - db: acme - table: products - columns: - - id - - name - idxname: test_idx - tablespace: ssd - storage_params: - - fillfactor=90 - -- name: Create gist index test_gist_idx concurrently on column geo_data of table map - postgresql_idx: - db: somedb - table: map - idxtype: gist - columns: geo_data - idxname: test_gist_idx - -# Note: for the example below pg_trgm extension must be installed for gin_trgm_ops -- name: Create gin index gin0_idx not concurrently on column comment of table test - postgresql_idx: - idxname: gin0_idx - table: test - columns: comment gin_trgm_ops - concurrent: no - idxtype: gin - -- name: Drop btree test_idx concurrently - postgresql_idx: - db: mydb - idxname: test_idx - state: absent - -- name: Drop test_idx cascade - postgresql_idx: - db: mydb - idxname: test_idx - state: absent - cascade: yes - concurrent: no - -- name: Create btree index test_idx concurrently on columns id,comment where column id > 1 - postgresql_idx: - db: mydb - table: test - columns: id,comment - idxname: test_idx - cond: id > 1 - -- name: Create unique btree index if not exists test_unique_idx on column name of table products - postgresql_idx: - db: acme - table: products - columns: name - name: test_unique_idx - unique: yes - concurrent: no -''' - -RETURN = r''' -name: - description: Index name. - returned: always - type: str - sample: 'foo_idx' -state: - description: Index state. - returned: always - type: str - sample: 'present' -schema: - description: Schema where index exists. - returned: always - type: str - sample: 'public' -tablespace: - description: Tablespace where index exists. - returned: always - type: str - sample: 'ssd' -query: - description: Query that was tried to be executed. - returned: always - type: str - sample: 'CREATE INDEX CONCURRENTLY foo_idx ON test_table USING BTREE (id)' -storage_params: - description: Index storage parameters. - returned: always - type: list - sample: [ "fillfactor=90" ] -valid: - description: Index validity. - returned: always - type: bool - sample: true -''' - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) - - -VALID_IDX_TYPES = ('BTREE', 'HASH', 'GIST', 'SPGIST', 'GIN', 'BRIN') - - -# =========================================== -# PostgreSQL module specific support methods. -# - -class Index(object): - - """Class for working with PostgreSQL indexes. - - TODO: - 1. Add possibility to change ownership - 2. Add possibility to change tablespace - 3. Add list called executed_queries (executed_query should be left too) - 4. Use self.module instead of passing arguments to the methods whenever possible - - Args: - module (AnsibleModule) -- object of AnsibleModule class - cursor (cursor) -- cursor object of psycopg2 library - schema (str) -- name of the index schema - name (str) -- name of the index - - Attrs: - module (AnsibleModule) -- object of AnsibleModule class - cursor (cursor) -- cursor object of psycopg2 library - schema (str) -- name of the index schema - name (str) -- name of the index - exists (bool) -- flag the index exists in the DB or not - info (dict) -- dict that contents information about the index - executed_query (str) -- executed query - """ - - def __init__(self, module, cursor, schema, name): - self.name = name - if schema: - self.schema = schema - else: - self.schema = 'public' - self.module = module - self.cursor = cursor - self.info = { - 'name': self.name, - 'state': 'absent', - 'schema': '', - 'tblname': '', - 'tblspace': '', - 'valid': True, - 'storage_params': [], - } - self.exists = False - self.__exists_in_db() - self.executed_query = '' - - def get_info(self): - """Refresh index info. - - Return self.info dict. - """ - self.__exists_in_db() - return self.info - - def __exists_in_db(self): - """Check index existence, collect info, add it to self.info dict. - - Return True if the index exists, otherwise, return False. - """ - query = ("SELECT i.schemaname, i.tablename, i.tablespace, " - "pi.indisvalid, c.reloptions " - "FROM pg_catalog.pg_indexes AS i " - "JOIN pg_catalog.pg_class AS c " - "ON i.indexname = c.relname " - "JOIN pg_catalog.pg_index AS pi " - "ON c.oid = pi.indexrelid " - "WHERE i.indexname = %(name)s") - - res = exec_sql(self, query, query_params={'name': self.name}, add_to_executed=False) - if res: - self.exists = True - self.info = dict( - name=self.name, - state='present', - schema=res[0][0], - tblname=res[0][1], - tblspace=res[0][2] if res[0][2] else '', - valid=res[0][3], - storage_params=res[0][4] if res[0][4] else [], - ) - return True - - else: - self.exists = False - return False - - def create(self, tblname, idxtype, columns, cond, tblspace, storage_params, concurrent=True, unique=False): - """Create PostgreSQL index. - - Return True if success, otherwise, return False. - - Args: - tblname (str) -- name of a table for the index - idxtype (str) -- type of the index like BTREE, BRIN, etc - columns (str) -- string of comma-separated columns that need to be covered by index - tblspace (str) -- tablespace for storing the index - storage_params (str) -- string of comma-separated storage parameters - - Kwargs: - concurrent (bool) -- build index in concurrent mode, default True - """ - if self.exists: - return False - - if idxtype is None: - idxtype = "BTREE" - - query = 'CREATE' - - if unique: - query += ' UNIQUE' - - query += ' INDEX' - - if concurrent: - query += ' CONCURRENTLY' - - query += ' %s' % self.name - - if self.schema: - query += ' ON %s.%s ' % (self.schema, tblname) - else: - query += 'public.%s ' % tblname - - query += 'USING %s (%s)' % (idxtype, columns) - - if storage_params: - query += ' WITH (%s)' % storage_params - - if tblspace: - query += ' TABLESPACE %s' % tblspace - - if cond: - query += ' WHERE %s' % cond - - self.executed_query = query - - if exec_sql(self, query, ddl=True, add_to_executed=False): - return True - - return False - - def drop(self, schema, cascade=False, concurrent=True): - """Drop PostgreSQL index. - - Return True if success, otherwise, return False. - - Args: - schema (str) -- name of the index schema - - Kwargs: - cascade (bool) -- automatically drop objects that depend on the index, - default False - concurrent (bool) -- build index in concurrent mode, default True - """ - if not self.exists: - return False - - query = 'DROP INDEX' - - if concurrent: - query += ' CONCURRENTLY' - - if not schema: - query += ' public.%s' % self.name - else: - query += ' %s.%s' % (schema, self.name) - - if cascade: - query += ' CASCADE' - - self.executed_query = query - - if exec_sql(self, query, ddl=True, add_to_executed=False): - return True - - return False - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - idxname=dict(type='str', required=True, aliases=['name']), - db=dict(type='str', aliases=['login_db']), - state=dict(type='str', default='present', choices=['absent', 'present']), - concurrent=dict(type='bool', default=True), - unique=dict(type='bool', default=False), - table=dict(type='str'), - idxtype=dict(type='str', aliases=['type']), - columns=dict(type='list', elements='str', aliases=['column']), - cond=dict(type='str'), - session_role=dict(type='str'), - tablespace=dict(type='str'), - storage_params=dict(type='list', elements='str'), - cascade=dict(type='bool', default=False), - schema=dict(type='str'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - idxname = module.params["idxname"] - state = module.params["state"] - concurrent = module.params["concurrent"] - unique = module.params["unique"] - table = module.params["table"] - idxtype = module.params["idxtype"] - columns = module.params["columns"] - cond = module.params["cond"] - tablespace = module.params["tablespace"] - storage_params = module.params["storage_params"] - cascade = module.params["cascade"] - schema = module.params["schema"] - - if concurrent and cascade: - module.fail_json(msg="Concurrent mode and cascade parameters are mutually exclusive") - - if unique and (idxtype and idxtype != 'btree'): - module.fail_json(msg="Only btree currently supports unique indexes") - - if state == 'present': - if not table: - module.fail_json(msg="Table must be specified") - if not columns: - module.fail_json(msg="At least one column must be specified") - else: - if table or columns or cond or idxtype or tablespace: - module.fail_json(msg="Index %s is going to be removed, so it does not " - "make sense to pass a table name, columns, conditions, " - "index type, or tablespace" % idxname) - - if cascade and state != 'absent': - module.fail_json(msg="cascade parameter used only with state=absent") - - conn_params = get_conn_params(module, module.params) - db_connection = connect_to_db(module, conn_params, autocommit=True) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - # Set defaults: - changed = False - - # Do job: - index = Index(module, cursor, schema, idxname) - kw = index.get_info() - kw['query'] = '' - - # - # check_mode start - if module.check_mode: - if state == 'present' and index.exists: - kw['changed'] = False - module.exit_json(**kw) - - elif state == 'present' and not index.exists: - kw['changed'] = True - module.exit_json(**kw) - - elif state == 'absent' and not index.exists: - kw['changed'] = False - module.exit_json(**kw) - - elif state == 'absent' and index.exists: - kw['changed'] = True - module.exit_json(**kw) - # check_mode end - # - - if state == "present": - if idxtype and idxtype.upper() not in VALID_IDX_TYPES: - module.fail_json(msg="Index type '%s' of %s is not in valid types" % (idxtype, idxname)) - - columns = ','.join(columns) - - if storage_params: - storage_params = ','.join(storage_params) - - changed = index.create(table, idxtype, columns, cond, tablespace, storage_params, concurrent, unique) - - if changed: - kw = index.get_info() - kw['state'] = 'present' - kw['query'] = index.executed_query - - else: - changed = index.drop(schema, cascade, concurrent) - - if changed: - kw['state'] = 'absent' - kw['query'] = index.executed_query - - if not kw['valid']: - db_connection.rollback() - module.warn("Index %s is invalid! ROLLBACK" % idxname) - - if not concurrent: - db_connection.commit() - - kw['changed'] = changed - db_connection.close() - module.exit_json(**kw) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_info.py b/lib/ansible/modules/database/postgresql/postgresql_info.py deleted file mode 100644 index 654c2e160c..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_info.py +++ /dev/null @@ -1,1011 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = r''' ---- -module: postgresql_info -short_description: Gather information about PostgreSQL servers -description: -- Gathers information about PostgreSQL servers. -version_added: '2.8' -options: - filter: - description: - - Limit the collected information by comma separated string or YAML list. - - Allowable values are C(version), - C(databases), C(settings), C(tablespaces), C(roles), - C(replications), C(repl_slots). - - By default, collects all subsets. - - You can use shell-style (fnmatch) wildcard to pass groups of values (see Examples). - - You can use '!' before value (for example, C(!settings)) to exclude it from the information. - - If you pass including and excluding values to the filter, for example, I(filter=!settings,ver), - the excluding values will be ignored. - type: list - elements: str - db: - description: - - Name of database to connect. - type: str - aliases: - - login_db - session_role: - description: - - Switch to session_role after connecting. The specified session_role must - be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str -seealso: -- module: postgresql_ping -author: -- Andrew Klychkov (@Andersson007) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -# Display info from postgres hosts. -# ansible postgres -m postgresql_info - -# Display only databases and roles info from all hosts using shell-style wildcards: -# ansible all -m postgresql_info -a 'filter=dat*,rol*' - -# Display only replications and repl_slots info from standby hosts using shell-style wildcards: -# ansible standby -m postgresql_info -a 'filter=repl*' - -# Display all info from databases hosts except settings: -# ansible databases -m postgresql_info -a 'filter=!settings' - -- name: Collect PostgreSQL version and extensions - become: yes - become_user: postgres - postgresql_info: - filter: ver*,ext* - -- name: Collect all info except settings and roles - become: yes - become_user: postgres - postgresql_info: - filter: "!settings,!roles" - -# On FreeBSD with PostgreSQL 9.5 version and lower use pgsql user to become -# and pass "postgres" as a database to connect to -- name: Collect tablespaces and repl_slots info - become: yes - become_user: pgsql - postgresql_info: - db: postgres - filter: - - tablesp* - - repl_sl* - -- name: Collect all info except databases - become: yes - become_user: postgres - postgresql_info: - filter: - - "!databases" -''' - -RETURN = r''' -version: - description: Database server version U(https://www.postgresql.org/support/versioning/). - returned: always - type: dict - sample: { "version": { "major": 10, "minor": 6 } } - contains: - major: - description: Major server version. - returned: always - type: int - sample: 11 - minor: - description: Minor server version. - returned: always - type: int - sample: 1 -databases: - description: Information about databases. - returned: always - type: dict - sample: - - { "postgres": { "access_priv": "", "collate": "en_US.UTF-8", - "ctype": "en_US.UTF-8", "encoding": "UTF8", "owner": "postgres", "size": "7997 kB" } } - contains: - database_name: - description: Database name. - returned: always - type: dict - sample: template1 - contains: - access_priv: - description: Database access privileges. - returned: always - type: str - sample: "=c/postgres_npostgres=CTc/postgres" - collate: - description: - - Database collation U(https://www.postgresql.org/docs/current/collation.html). - returned: always - type: str - sample: en_US.UTF-8 - ctype: - description: - - Database LC_CTYPE U(https://www.postgresql.org/docs/current/multibyte.html). - returned: always - type: str - sample: en_US.UTF-8 - encoding: - description: - - Database encoding U(https://www.postgresql.org/docs/current/multibyte.html). - returned: always - type: str - sample: UTF8 - owner: - description: - - Database owner U(https://www.postgresql.org/docs/current/sql-createdatabase.html). - returned: always - type: str - sample: postgres - size: - description: Database size in bytes. - returned: always - type: str - sample: 8189415 - extensions: - description: - - Extensions U(https://www.postgresql.org/docs/current/sql-createextension.html). - returned: always - type: dict - sample: - - { "plpgsql": { "description": "PL/pgSQL procedural language", - "extversion": { "major": 1, "minor": 0 } } } - contains: - extdescription: - description: Extension description. - returned: if existent - type: str - sample: PL/pgSQL procedural language - extversion: - description: Extension description. - returned: always - type: dict - contains: - major: - description: Extension major version. - returned: always - type: int - sample: 1 - minor: - description: Extension minor version. - returned: always - type: int - sample: 0 - nspname: - description: Namespace where the extension is. - returned: always - type: str - sample: pg_catalog - languages: - description: Procedural languages U(https://www.postgresql.org/docs/current/xplang.html). - returned: always - type: dict - sample: { "sql": { "lanacl": "", "lanowner": "postgres" } } - contains: - lanacl: - description: - - Language access privileges - U(https://www.postgresql.org/docs/current/catalog-pg-language.html). - returned: always - type: str - sample: "{postgres=UC/postgres,=U/postgres}" - lanowner: - description: - - Language owner U(https://www.postgresql.org/docs/current/catalog-pg-language.html). - returned: always - type: str - sample: postgres - namespaces: - description: - - Namespaces (schema) U(https://www.postgresql.org/docs/current/sql-createschema.html). - returned: always - type: dict - sample: { "pg_catalog": { "nspacl": "{postgres=UC/postgres,=U/postgres}", "nspowner": "postgres" } } - contains: - nspacl: - description: - - Access privileges U(https://www.postgresql.org/docs/current/catalog-pg-namespace.html). - returned: always - type: str - sample: "{postgres=UC/postgres,=U/postgres}" - nspowner: - description: - - Schema owner U(https://www.postgresql.org/docs/current/catalog-pg-namespace.html). - returned: always - type: str - sample: postgres - publications: - description: - - Information about logical replication publications (available for PostgreSQL 10 and higher) - U(https://www.postgresql.org/docs/current/logical-replication-publication.html). - - Content depends on PostgreSQL server version. - returned: if configured - type: dict - version_added: "2.10" - sample: { "pub1": { "ownername": "postgres", "puballtables": true, "pubinsert": true, "pubupdate": true } } - subscriptions: - description: - - Information about replication subscriptions (available for PostgreSQL 10 and higher) - U(https://www.postgresql.org/docs/current/logical-replication-subscription.html). - - Content depends on PostgreSQL server version. - returned: if configured - type: dict - version_added: "2.10" - sample: - - { "my_subscription": {"ownername": "postgres", "subenabled": true, "subpublications": ["first_publication"] } } -repl_slots: - description: - - Replication slots (available in 9.4 and later) - U(https://www.postgresql.org/docs/current/view-pg-replication-slots.html). - returned: if existent - type: dict - sample: { "slot0": { "active": false, "database": null, "plugin": null, "slot_type": "physical" } } - contains: - active: - description: - - True means that a receiver has connected to it, and it is currently reserving archives. - returned: always - type: bool - sample: true - database: - description: Database name this slot is associated with, or null. - returned: always - type: str - sample: acme - plugin: - description: - - Base name of the shared object containing the output plugin - this logical slot is using, or null for physical slots. - returned: always - type: str - sample: pgoutput - slot_type: - description: The slot type - physical or logical. - returned: always - type: str - sample: logical -replications: - description: - - Information about the current replications by process PIDs - U(https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-STATS-VIEWS-TABLE). - returned: if pg_stat_replication view existent - type: dict - sample: - - { 76580: { "app_name": "standby1", "backend_start": "2019-02-03 00:14:33.908593+03", - "client_addr": "10.10.10.2", "client_hostname": "", "state": "streaming", "usename": "postgres" } } - contains: - usename: - description: - - Name of the user logged into this WAL sender process ('usename' is a column name in pg_stat_replication view). - returned: always - type: str - sample: replication_user - app_name: - description: Name of the application that is connected to this WAL sender. - returned: if existent - type: str - sample: acme_srv - client_addr: - description: - - IP address of the client connected to this WAL sender. - - If this field is null, it indicates that the client is connected - via a Unix socket on the server machine. - returned: always - type: str - sample: 10.0.0.101 - client_hostname: - description: - - Host name of the connected client, as reported by a reverse DNS lookup of client_addr. - - This field will only be non-null for IP connections, and only when log_hostname is enabled. - returned: always - type: str - sample: dbsrv1 - backend_start: - description: Time when this process was started, i.e., when the client connected to this WAL sender. - returned: always - type: str - sample: "2019-02-03 00:14:33.908593+03" - state: - description: Current WAL sender state. - returned: always - type: str - sample: streaming -tablespaces: - description: - - Information about tablespaces U(https://www.postgresql.org/docs/current/catalog-pg-tablespace.html). - returned: always - type: dict - sample: - - { "test": { "spcacl": "{postgres=C/postgres,andreyk=C/postgres}", "spcoptions": [ "seq_page_cost=1" ], - "spcowner": "postgres" } } - contains: - spcacl: - description: Tablespace access privileges. - returned: always - type: str - sample: "{postgres=C/postgres,andreyk=C/postgres}" - spcoptions: - description: Tablespace-level options. - returned: always - type: list - sample: [ "seq_page_cost=1" ] - spcowner: - description: Owner of the tablespace. - returned: always - type: str - sample: test_user -roles: - description: - - Information about roles U(https://www.postgresql.org/docs/current/user-manag.html). - returned: always - type: dict - sample: - - { "test_role": { "canlogin": true, "member_of": [ "user_ro" ], "superuser": false, - "valid_until": "9999-12-31T23:59:59.999999+00:00" } } - contains: - canlogin: - description: Login privilege U(https://www.postgresql.org/docs/current/role-attributes.html). - returned: always - type: bool - sample: true - member_of: - description: - - Role membership U(https://www.postgresql.org/docs/current/role-membership.html). - returned: always - type: list - sample: [ "read_only_users" ] - superuser: - description: User is a superuser or not. - returned: always - type: bool - sample: false - valid_until: - description: - - Password expiration date U(https://www.postgresql.org/docs/current/sql-alterrole.html). - returned: always - type: str - sample: "9999-12-31T23:59:59.999999+00:00" -pending_restart_settings: - description: - - List of settings that are pending restart to be set. - returned: always - type: list - sample: [ "shared_buffers" ] -settings: - description: - - Information about run-time server parameters - U(https://www.postgresql.org/docs/current/view-pg-settings.html). - returned: always - type: dict - sample: - - { "work_mem": { "boot_val": "4096", "context": "user", "max_val": "2147483647", - "min_val": "64", "setting": "8192", "sourcefile": "/var/lib/pgsql/10/data/postgresql.auto.conf", - "unit": "kB", "vartype": "integer", "val_in_bytes": 4194304 } } - contains: - setting: - description: Current value of the parameter. - returned: always - type: str - sample: 49152 - unit: - description: Implicit unit of the parameter. - returned: always - type: str - sample: kB - boot_val: - description: - - Parameter value assumed at server startup if the parameter is not otherwise set. - returned: always - type: str - sample: 4096 - min_val: - description: - - Minimum allowed value of the parameter (null for non-numeric values). - returned: always - type: str - sample: 64 - max_val: - description: - - Maximum allowed value of the parameter (null for non-numeric values). - returned: always - type: str - sample: 2147483647 - sourcefile: - description: - - Configuration file the current value was set in. - - Null for values set from sources other than configuration files, - or when examined by a user who is neither a superuser or a member of pg_read_all_settings. - - Helpful when using include directives in configuration files. - returned: always - type: str - sample: /var/lib/pgsql/10/data/postgresql.auto.conf - context: - description: - - Context required to set the parameter's value. - - For more information see U(https://www.postgresql.org/docs/current/view-pg-settings.html). - returned: always - type: str - sample: user - vartype: - description: - - Parameter type (bool, enum, integer, real, or string). - returned: always - type: str - sample: integer - val_in_bytes: - description: - - Current value of the parameter in bytes. - returned: if supported - type: int - sample: 2147483647 - pretty_val: - description: - - Value presented in the pretty form. - returned: always - type: str - sample: 2MB - pending_restart: - description: - - True if the value has been changed in the configuration file but needs a restart; or false otherwise. - - Returns only if C(settings) is passed. - returned: always - type: bool - sample: false -''' - -from fnmatch import fnmatch - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - get_conn_params, - postgres_common_argument_spec, -) -from ansible.module_utils.six import iteritems -from ansible.module_utils._text import to_native - - -# =========================================== -# PostgreSQL module specific support methods. -# - -class PgDbConn(object): - - """Auxiliary class for working with PostgreSQL connection objects. - - Arguments: - module (AnsibleModule): Object of AnsibleModule class that - contains connection parameters. - """ - - def __init__(self, module): - self.module = module - self.db_conn = None - self.cursor = None - - def connect(self): - """Connect to a PostgreSQL database and return a cursor object. - - Note: connection parameters are passed by self.module object. - """ - conn_params = get_conn_params(self.module, self.module.params, warn_db_default=False) - self.db_conn = connect_to_db(self.module, conn_params) - return self.db_conn.cursor(cursor_factory=DictCursor) - - def reconnect(self, dbname): - """Reconnect to another database and return a PostgreSQL cursor object. - - Arguments: - dbname (string): Database name to connect to. - """ - self.db_conn.close() - - self.module.params['database'] = dbname - return self.connect() - - -class PgClusterInfo(object): - - """Class for collection information about a PostgreSQL instance. - - Arguments: - module (AnsibleModule): Object of AnsibleModule class. - db_conn_obj (psycopg2.connect): PostgreSQL connection object. - """ - - def __init__(self, module, db_conn_obj): - self.module = module - self.db_obj = db_conn_obj - self.cursor = db_conn_obj.connect() - self.pg_info = { - "version": {}, - "tablespaces": {}, - "databases": {}, - "replications": {}, - "repl_slots": {}, - "settings": {}, - "roles": {}, - "pending_restart_settings": [], - } - - def collect(self, val_list=False): - """Collect information based on 'filter' option.""" - subset_map = { - "version": self.get_pg_version, - "tablespaces": self.get_tablespaces, - "databases": self.get_db_info, - "replications": self.get_repl_info, - "repl_slots": self.get_rslot_info, - "settings": self.get_settings, - "roles": self.get_role_info, - } - - incl_list = [] - excl_list = [] - # Notice: incl_list and excl_list - # don't make sense together, therefore, - # if incl_list is not empty, we collect - # only values from it: - if val_list: - for i in val_list: - if i[0] != '!': - incl_list.append(i) - else: - excl_list.append(i.lstrip('!')) - - if incl_list: - for s in subset_map: - for i in incl_list: - if fnmatch(s, i): - subset_map[s]() - break - elif excl_list: - found = False - # Collect info: - for s in subset_map: - for e in excl_list: - if fnmatch(s, e): - found = True - - if not found: - subset_map[s]() - else: - found = False - - # Default behaviour, if include or exclude is not passed: - else: - # Just collect info for each item: - for s in subset_map: - subset_map[s]() - - return self.pg_info - - def get_pub_info(self): - """Get publication statistics.""" - query = ("SELECT p.*, r.rolname AS ownername " - "FROM pg_catalog.pg_publication AS p " - "JOIN pg_catalog.pg_roles AS r " - "ON p.pubowner = r.oid") - - result = self.__exec_sql(query) - - if result: - result = [dict(row) for row in result] - else: - return {} - - publications = {} - - for elem in result: - if not publications.get(elem['pubname']): - publications[elem['pubname']] = {} - - for key, val in iteritems(elem): - if key != 'pubname': - publications[elem['pubname']][key] = val - - return publications - - def get_subscr_info(self): - """Get subscription statistics.""" - query = ("SELECT s.*, r.rolname AS ownername, d.datname AS dbname " - "FROM pg_catalog.pg_subscription s " - "JOIN pg_catalog.pg_database d " - "ON s.subdbid = d.oid " - "JOIN pg_catalog.pg_roles AS r " - "ON s.subowner = r.oid") - - result = self.__exec_sql(query) - - if result: - result = [dict(row) for row in result] - else: - return {} - - subscr_info = {} - - for elem in result: - if not subscr_info.get(elem['dbname']): - subscr_info[elem['dbname']] = {} - - if not subscr_info[elem['dbname']].get(elem['subname']): - subscr_info[elem['dbname']][elem['subname']] = {} - - for key, val in iteritems(elem): - if key not in ('subname', 'dbname'): - subscr_info[elem['dbname']][elem['subname']][key] = val - - return subscr_info - - def get_tablespaces(self): - """Get information about tablespaces.""" - # Check spcoption exists: - opt = self.__exec_sql("SELECT column_name " - "FROM information_schema.columns " - "WHERE table_name = 'pg_tablespace' " - "AND column_name = 'spcoptions'") - - if not opt: - query = ("SELECT s.spcname, a.rolname, s.spcacl " - "FROM pg_tablespace AS s " - "JOIN pg_authid AS a ON s.spcowner = a.oid") - else: - query = ("SELECT s.spcname, a.rolname, s.spcacl, s.spcoptions " - "FROM pg_tablespace AS s " - "JOIN pg_authid AS a ON s.spcowner = a.oid") - - res = self.__exec_sql(query) - ts_dict = {} - for i in res: - ts_name = i[0] - ts_info = dict( - spcowner=i[1], - spcacl=i[2] if i[2] else '', - ) - if opt: - ts_info['spcoptions'] = i[3] if i[3] else [] - - ts_dict[ts_name] = ts_info - - self.pg_info["tablespaces"] = ts_dict - - def get_ext_info(self): - """Get information about existing extensions.""" - # Check that pg_extension exists: - res = self.__exec_sql("SELECT EXISTS (SELECT 1 FROM " - "information_schema.tables " - "WHERE table_name = 'pg_extension')") - if not res[0][0]: - return True - - query = ("SELECT e.extname, e.extversion, n.nspname, c.description " - "FROM pg_catalog.pg_extension AS e " - "LEFT JOIN pg_catalog.pg_namespace AS n " - "ON n.oid = e.extnamespace " - "LEFT JOIN pg_catalog.pg_description AS c " - "ON c.objoid = e.oid " - "AND c.classoid = 'pg_catalog.pg_extension'::pg_catalog.regclass") - res = self.__exec_sql(query) - ext_dict = {} - for i in res: - ext_ver = i[1].split('.') - - ext_dict[i[0]] = dict( - extversion=dict( - major=int(ext_ver[0]), - minor=int(ext_ver[1]), - ), - nspname=i[2], - description=i[3], - ) - - return ext_dict - - def get_role_info(self): - """Get information about roles (in PgSQL groups and users are roles).""" - query = ("SELECT r.rolname, r.rolsuper, r.rolcanlogin, " - "r.rolvaliduntil, " - "ARRAY(SELECT b.rolname " - "FROM pg_catalog.pg_auth_members AS m " - "JOIN pg_catalog.pg_roles AS b ON (m.roleid = b.oid) " - "WHERE m.member = r.oid) AS memberof " - "FROM pg_catalog.pg_roles AS r " - "WHERE r.rolname !~ '^pg_'") - - res = self.__exec_sql(query) - rol_dict = {} - for i in res: - rol_dict[i[0]] = dict( - superuser=i[1], - canlogin=i[2], - valid_until=i[3] if i[3] else '', - member_of=i[4] if i[4] else [], - ) - - self.pg_info["roles"] = rol_dict - - def get_rslot_info(self): - """Get information about replication slots if exist.""" - # Check that pg_replication_slots exists: - res = self.__exec_sql("SELECT EXISTS (SELECT 1 FROM " - "information_schema.tables " - "WHERE table_name = 'pg_replication_slots')") - if not res[0][0]: - return True - - query = ("SELECT slot_name, plugin, slot_type, database, " - "active FROM pg_replication_slots") - res = self.__exec_sql(query) - - # If there is no replication: - if not res: - return True - - rslot_dict = {} - for i in res: - rslot_dict[i[0]] = dict( - plugin=i[1], - slot_type=i[2], - database=i[3], - active=i[4], - ) - - self.pg_info["repl_slots"] = rslot_dict - - def get_settings(self): - """Get server settings.""" - # Check pending restart column exists: - pend_rest_col_exists = self.__exec_sql("SELECT 1 FROM information_schema.columns " - "WHERE table_name = 'pg_settings' " - "AND column_name = 'pending_restart'") - if not pend_rest_col_exists: - query = ("SELECT name, setting, unit, context, vartype, " - "boot_val, min_val, max_val, sourcefile " - "FROM pg_settings") - else: - query = ("SELECT name, setting, unit, context, vartype, " - "boot_val, min_val, max_val, sourcefile, pending_restart " - "FROM pg_settings") - - res = self.__exec_sql(query) - - set_dict = {} - for i in res: - val_in_bytes = None - setting = i[1] - if i[2]: - unit = i[2] - else: - unit = '' - - if unit == 'kB': - val_in_bytes = int(setting) * 1024 - - elif unit == '8kB': - val_in_bytes = int(setting) * 1024 * 8 - - elif unit == 'MB': - val_in_bytes = int(setting) * 1024 * 1024 - - if val_in_bytes is not None and val_in_bytes < 0: - val_in_bytes = 0 - - setting_name = i[0] - pretty_val = self.__get_pretty_val(setting_name) - - pending_restart = None - if pend_rest_col_exists: - pending_restart = i[9] - - set_dict[setting_name] = dict( - setting=setting, - unit=unit, - context=i[3], - vartype=i[4], - boot_val=i[5] if i[5] else '', - min_val=i[6] if i[6] else '', - max_val=i[7] if i[7] else '', - sourcefile=i[8] if i[8] else '', - pretty_val=pretty_val, - ) - if val_in_bytes is not None: - set_dict[setting_name]['val_in_bytes'] = val_in_bytes - - if pending_restart is not None: - set_dict[setting_name]['pending_restart'] = pending_restart - if pending_restart: - self.pg_info["pending_restart_settings"].append(setting_name) - - self.pg_info["settings"] = set_dict - - def get_repl_info(self): - """Get information about replication if the server is a master.""" - # Check that pg_replication_slots exists: - res = self.__exec_sql("SELECT EXISTS (SELECT 1 FROM " - "information_schema.tables " - "WHERE table_name = 'pg_stat_replication')") - if not res[0][0]: - return True - - query = ("SELECT r.pid, a.rolname, r.application_name, r.client_addr, " - "r.client_hostname, r.backend_start::text, r.state " - "FROM pg_stat_replication AS r " - "JOIN pg_authid AS a ON r.usesysid = a.oid") - res = self.__exec_sql(query) - - # If there is no replication: - if not res: - return True - - repl_dict = {} - for i in res: - repl_dict[i[0]] = dict( - usename=i[1], - app_name=i[2] if i[2] else '', - client_addr=i[3], - client_hostname=i[4] if i[4] else '', - backend_start=i[5], - state=i[6], - ) - - self.pg_info["replications"] = repl_dict - - def get_lang_info(self): - """Get information about current supported languages.""" - query = ("SELECT l.lanname, a.rolname, l.lanacl " - "FROM pg_language AS l " - "JOIN pg_authid AS a ON l.lanowner = a.oid") - res = self.__exec_sql(query) - lang_dict = {} - for i in res: - lang_dict[i[0]] = dict( - lanowner=i[1], - lanacl=i[2] if i[2] else '', - ) - - return lang_dict - - def get_namespaces(self): - """Get information about namespaces.""" - query = ("SELECT n.nspname, a.rolname, n.nspacl " - "FROM pg_catalog.pg_namespace AS n " - "JOIN pg_authid AS a ON a.oid = n.nspowner") - res = self.__exec_sql(query) - - nsp_dict = {} - for i in res: - nsp_dict[i[0]] = dict( - nspowner=i[1], - nspacl=i[2] if i[2] else '', - ) - - return nsp_dict - - def get_pg_version(self): - """Get major and minor PostgreSQL server version.""" - query = "SELECT version()" - raw = self.__exec_sql(query)[0][0] - raw = raw.split()[1].split('.') - self.pg_info["version"] = dict( - major=int(raw[0]), - minor=int(raw[1]), - ) - - def get_db_info(self): - """Get information about the current database.""" - # Following query returns: - # Name, Owner, Encoding, Collate, Ctype, Access Priv, Size - query = ("SELECT d.datname, " - "pg_catalog.pg_get_userbyid(d.datdba), " - "pg_catalog.pg_encoding_to_char(d.encoding), " - "d.datcollate, " - "d.datctype, " - "pg_catalog.array_to_string(d.datacl, E'\n'), " - "CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') " - "THEN pg_catalog.pg_database_size(d.datname)::text " - "ELSE 'No Access' END, " - "t.spcname " - "FROM pg_catalog.pg_database AS d " - "JOIN pg_catalog.pg_tablespace t ON d.dattablespace = t.oid " - "WHERE d.datname != 'template0'") - - res = self.__exec_sql(query) - - db_dict = {} - for i in res: - db_dict[i[0]] = dict( - owner=i[1], - encoding=i[2], - collate=i[3], - ctype=i[4], - access_priv=i[5] if i[5] else '', - size=i[6], - ) - - if self.cursor.connection.server_version >= 100000: - subscr_info = self.get_subscr_info() - - for datname in db_dict: - self.cursor = self.db_obj.reconnect(datname) - db_dict[datname]['namespaces'] = self.get_namespaces() - db_dict[datname]['extensions'] = self.get_ext_info() - db_dict[datname]['languages'] = self.get_lang_info() - if self.cursor.connection.server_version >= 100000: - db_dict[datname]['publications'] = self.get_pub_info() - db_dict[datname]['subscriptions'] = subscr_info.get(datname, {}) - - self.pg_info["databases"] = db_dict - - def __get_pretty_val(self, setting): - """Get setting's value represented by SHOW command.""" - return self.__exec_sql("SHOW %s" % setting)[0][0] - - def __exec_sql(self, query): - """Execute SQL and return the result.""" - try: - self.cursor.execute(query) - res = self.cursor.fetchall() - if res: - return res - except Exception as e: - self.module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e))) - self.cursor.close() - return False - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - db=dict(type='str', aliases=['login_db']), - filter=dict(type='list', elements='str'), - session_role=dict(type='str'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - filter_ = module.params["filter"] - - db_conn_obj = PgDbConn(module) - - # Do job: - pg_info = PgClusterInfo(module, db_conn_obj) - - module.exit_json(**pg_info.collect(filter_)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_lang.py b/lib/ansible/modules/database/postgresql/postgresql_lang.py deleted file mode 100644 index 6c85b197ce..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_lang.py +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# (c) 2014, Jens Depuydt <http://www.jensd.be> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: postgresql_lang -short_description: Adds, removes or changes procedural languages with a PostgreSQL database -description: -- Adds, removes or changes procedural languages with a PostgreSQL database. -- This module allows you to add a language, remote a language or change the trust - relationship with a PostgreSQL database. -- The module can be used on the machine where executed or on a remote host. -- When removing a language from a database, it is possible that dependencies prevent - the database from being removed. In that case, you can specify I(cascade=yes) to - automatically drop objects that depend on the language (such as functions in the - language). -- In case the language can't be deleted because it is required by the - database system, you can specify I(fail_on_drop=no) to ignore the error. -- Be careful when marking a language as trusted since this could be a potential - security breach. Untrusted languages allow only users with the PostgreSQL superuser - privilege to use this language to create new functions. -version_added: '1.7' -options: - lang: - description: - - Name of the procedural language to add, remove or change. - required: true - type: str - aliases: - - name - trust: - description: - - Make this language trusted for the selected db. - type: bool - default: 'no' - db: - description: - - Name of database to connect to and where the language will be added, removed or changed. - type: str - aliases: - - login_db - required: true - force_trust: - description: - - Marks the language as trusted, even if it's marked as untrusted in pg_pltemplate. - - Use with care! - type: bool - default: 'no' - fail_on_drop: - description: - - If C(yes), fail when removing a language. Otherwise just log and continue. - - In some cases, it is not possible to remove a language (used by the db-system). - - When dependencies block the removal, consider using I(cascade). - type: bool - default: 'yes' - cascade: - description: - - When dropping a language, also delete object that depend on this language. - - Only used when I(state=absent). - type: bool - default: 'no' - session_role: - version_added: '2.8' - description: - - Switch to session_role after connecting. - - The specified I(session_role) must be a role that the current I(login_user) is a member of. - - Permissions checking for SQL commands is carried out as though the I(session_role) were the one that had logged in originally. - type: str - state: - description: - - The state of the language for the selected database. - type: str - default: present - choices: [ absent, present ] - login_unix_socket: - description: - - Path to a Unix domain socket for local connections. - type: str - version_added: '2.8' - ssl_mode: - description: - - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. - - See U(https://www.postgresql.org/docs/current/static/libpq-ssl.html) for more information on the modes. - - Default of C(prefer) matches libpq default. - type: str - default: prefer - choices: [ allow, disable, prefer, require, verify-ca, verify-full ] - version_added: '2.8' - ca_cert: - description: - - Specifies the name of a file containing SSL certificate authority (CA) certificate(s). - - If the file exists, the server's certificate will be verified to be signed by one of these authorities. - type: str - aliases: [ ssl_rootcert ] - version_added: '2.8' - owner: - description: - - Set an owner for the language. - - Ignored when I(state=absent). - type: str - version_added: '2.10' -seealso: -- name: PostgreSQL languages - description: General information about PostgreSQL languages. - link: https://www.postgresql.org/docs/current/xplang.html -- name: CREATE LANGUAGE reference - description: Complete reference of the CREATE LANGUAGE command documentation. - link: https://www.postgresql.org/docs/current/sql-createlanguage.html -- name: ALTER LANGUAGE reference - description: Complete reference of the ALTER LANGUAGE command documentation. - link: https://www.postgresql.org/docs/current/sql-alterlanguage.html -- name: DROP LANGUAGE reference - description: Complete reference of the DROP LANGUAGE command documentation. - link: https://www.postgresql.org/docs/current/sql-droplanguage.html -author: -- Jens Depuydt (@jensdepuydt) -- Thomas O'Donnell (@andytom) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Add language pltclu to database testdb if it doesn't exist - postgresql_lang: db=testdb lang=pltclu state=present - -# Add language pltclu to database testdb if it doesn't exist and mark it as trusted. -# Marks the language as trusted if it exists but isn't trusted yet. -# force_trust makes sure that the language will be marked as trusted -- name: Add language pltclu to database testdb if it doesn't exist and mark it as trusted - postgresql_lang: - db: testdb - lang: pltclu - state: present - trust: yes - force_trust: yes - -- name: Remove language pltclu from database testdb - postgresql_lang: - db: testdb - lang: pltclu - state: absent - -- name: Remove language pltclu from database testdb and remove all dependencies - postgresql_lang: - db: testdb - lang: pltclu - state: absent - cascade: yes - -- name: Remove language c from database testdb but ignore errors if something prevents the removal - postgresql_lang: - db: testdb - lang: pltclu - state: absent - fail_on_drop: no - -- name: In testdb change owner of mylang to alice - postgresql_lang: - db: testdb - lang: mylang - owner: alice -''' - -RETURN = r''' -queries: - description: List of executed queries. - returned: always - type: list - sample: ['CREATE LANGUAGE "acme"'] - version_added: '2.8' -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - get_conn_params, - postgres_common_argument_spec, -) - -executed_queries = [] - - -def lang_exists(cursor, lang): - """Checks if language exists for db""" - query = "SELECT lanname FROM pg_language WHERE lanname = %(lang)s" - cursor.execute(query, {'lang': lang}) - return cursor.rowcount > 0 - - -def lang_istrusted(cursor, lang): - """Checks if language is trusted for db""" - query = "SELECT lanpltrusted FROM pg_language WHERE lanname = %(lang)s" - cursor.execute(query, {'lang': lang}) - return cursor.fetchone()[0] - - -def lang_altertrust(cursor, lang, trust): - """Changes if language is trusted for db""" - query = "UPDATE pg_language SET lanpltrusted = %(trust)s WHERE lanname = %(lang)s" - cursor.execute(query, {'trust': trust, 'lang': lang}) - executed_queries.append(cursor.mogrify(query, {'trust': trust, 'lang': lang})) - return True - - -def lang_add(cursor, lang, trust): - """Adds language for db""" - if trust: - query = 'CREATE TRUSTED LANGUAGE "%s"' % lang - else: - query = 'CREATE LANGUAGE "%s"' % lang - executed_queries.append(query) - cursor.execute(query) - return True - - -def lang_drop(cursor, lang, cascade): - """Drops language for db""" - cursor.execute("SAVEPOINT ansible_pgsql_lang_drop") - try: - if cascade: - query = "DROP LANGUAGE \"%s\" CASCADE" % lang - else: - query = "DROP LANGUAGE \"%s\"" % lang - executed_queries.append(query) - cursor.execute(query) - except Exception: - cursor.execute("ROLLBACK TO SAVEPOINT ansible_pgsql_lang_drop") - cursor.execute("RELEASE SAVEPOINT ansible_pgsql_lang_drop") - return False - cursor.execute("RELEASE SAVEPOINT ansible_pgsql_lang_drop") - return True - - -def get_lang_owner(cursor, lang): - """Get language owner. - - Args: - cursor (cursor): psycopg2 cursor object. - lang (str): language name. - """ - query = ("SELECT r.rolname FROM pg_language l " - "JOIN pg_roles r ON l.lanowner = r.oid " - "WHERE l.lanname = %(lang)s") - cursor.execute(query, {'lang': lang}) - return cursor.fetchone()[0] - - -def set_lang_owner(cursor, lang, owner): - """Set language owner. - - Args: - cursor (cursor): psycopg2 cursor object. - lang (str): language name. - owner (str): name of new owner. - """ - query = "ALTER LANGUAGE \"%s\" OWNER TO %s" % (lang, owner) - executed_queries.append(query) - cursor.execute(query) - return True - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - db=dict(type="str", required=True, aliases=["login_db"]), - lang=dict(type="str", required=True, aliases=["name"]), - state=dict(type="str", default="present", choices=["absent", "present"]), - trust=dict(type="bool", default="no"), - force_trust=dict(type="bool", default="no"), - cascade=dict(type="bool", default="no"), - fail_on_drop=dict(type="bool", default="yes"), - session_role=dict(type="str"), - owner=dict(type="str"), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - db = module.params["db"] - lang = module.params["lang"] - state = module.params["state"] - trust = module.params["trust"] - force_trust = module.params["force_trust"] - cascade = module.params["cascade"] - fail_on_drop = module.params["fail_on_drop"] - owner = module.params["owner"] - - conn_params = get_conn_params(module, module.params) - db_connection = connect_to_db(module, conn_params, autocommit=False) - cursor = db_connection.cursor() - - changed = False - kw = {'db': db, 'lang': lang, 'trust': trust} - - if state == "present": - if lang_exists(cursor, lang): - lang_trusted = lang_istrusted(cursor, lang) - if (lang_trusted and not trust) or (not lang_trusted and trust): - if module.check_mode: - changed = True - else: - changed = lang_altertrust(cursor, lang, trust) - else: - if module.check_mode: - changed = True - else: - changed = lang_add(cursor, lang, trust) - if force_trust: - changed = lang_altertrust(cursor, lang, trust) - - else: - if lang_exists(cursor, lang): - if module.check_mode: - changed = True - kw['lang_dropped'] = True - else: - changed = lang_drop(cursor, lang, cascade) - if fail_on_drop and not changed: - msg = ("unable to drop language, use cascade " - "to delete dependencies or fail_on_drop=no to ignore") - module.fail_json(msg=msg) - kw['lang_dropped'] = changed - - if owner and state == 'present': - if lang_exists(cursor, lang): - if owner != get_lang_owner(cursor, lang): - changed = set_lang_owner(cursor, lang, owner) - - if changed: - if module.check_mode: - db_connection.rollback() - else: - db_connection.commit() - - kw['changed'] = changed - kw['queries'] = executed_queries - db_connection.close() - module.exit_json(**kw) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_membership.py b/lib/ansible/modules/database/postgresql/postgresql_membership.py deleted file mode 100644 index 5d2ba25b5e..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_membership.py +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'supported_by': 'community', - 'status': ['preview'] -} - -DOCUMENTATION = r''' ---- -module: postgresql_membership -short_description: Add or remove PostgreSQL roles from groups -description: -- Adds or removes PostgreSQL roles from groups (other roles). -- Users are roles with login privilege. -- Groups are PostgreSQL roles usually without LOGIN privilege. -- "Common use case:" -- 1) add a new group (groups) by M(postgresql_user) module with I(role_attr_flags=NOLOGIN) -- 2) grant them desired privileges by M(postgresql_privs) module -- 3) add desired PostgreSQL users to the new group (groups) by this module -version_added: '2.8' -options: - groups: - description: - - The list of groups (roles) that need to be granted to or revoked from I(target_roles). - required: yes - type: list - elements: str - aliases: - - group - - source_role - - source_roles - target_roles: - description: - - The list of target roles (groups will be granted to them). - required: yes - type: list - elements: str - aliases: - - target_role - - users - - user - fail_on_role: - description: - - If C(yes), fail when group or target_role doesn't exist. If C(no), just warn and continue. - default: yes - type: bool - state: - description: - - Membership state. - - I(state=present) implies the I(groups)must be granted to I(target_roles). - - I(state=absent) implies the I(groups) must be revoked from I(target_roles). - type: str - default: present - choices: [ absent, present ] - db: - description: - - Name of database to connect to. - type: str - aliases: - - login_db - session_role: - description: - - Switch to session_role after connecting. - The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str -seealso: -- module: postgresql_user -- module: postgresql_privs -- module: postgresql_owner -- name: PostgreSQL role membership reference - description: Complete reference of the PostgreSQL role membership documentation. - link: https://www.postgresql.org/docs/current/role-membership.html -- name: PostgreSQL role attributes reference - description: Complete reference of the PostgreSQL role attributes documentation. - link: https://www.postgresql.org/docs/current/role-attributes.html -author: -- Andrew Klychkov (@Andersson007) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Grant role read_only to alice and bob - postgresql_membership: - group: read_only - target_roles: - - alice - - bob - state: present - -# you can also use target_roles: alice,bob,etc to pass the role list - -- name: Revoke role read_only and exec_func from bob. Ignore if roles don't exist - postgresql_membership: - groups: - - read_only - - exec_func - target_role: bob - fail_on_role: no - state: absent -''' - -RETURN = r''' -queries: - description: List of executed queries. - returned: always - type: str - sample: [ "GRANT \"user_ro\" TO \"alice\"" ] -granted: - description: Dict of granted groups and roles. - returned: if I(state=present) - type: dict - sample: { "ro_group": [ "alice", "bob" ] } -revoked: - description: Dict of revoked groups and roles. - returned: if I(state=absent) - type: dict - sample: { "ro_group": [ "alice", "bob" ] } -state: - description: Membership state that tried to be set. - returned: always - type: str - sample: "present" -''' - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - PgMembership, - postgres_common_argument_spec, -) - - -# =========================================== -# Module execution. -# - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - groups=dict(type='list', elements='str', required=True, aliases=['group', 'source_role', 'source_roles']), - target_roles=dict(type='list', elements='str', required=True, aliases=['target_role', 'user', 'users']), - fail_on_role=dict(type='bool', default=True), - state=dict(type='str', default='present', choices=['absent', 'present']), - db=dict(type='str', aliases=['login_db']), - session_role=dict(type='str'), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - groups = module.params['groups'] - target_roles = module.params['target_roles'] - fail_on_role = module.params['fail_on_role'] - state = module.params['state'] - - conn_params = get_conn_params(module, module.params, warn_db_default=False) - db_connection = connect_to_db(module, conn_params, autocommit=False) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - ############## - # Create the object and do main job: - - pg_membership = PgMembership(module, cursor, groups, target_roles, fail_on_role) - - if state == 'present': - pg_membership.grant() - - elif state == 'absent': - pg_membership.revoke() - - # Rollback if it's possible and check_mode: - if module.check_mode: - db_connection.rollback() - else: - db_connection.commit() - - cursor.close() - db_connection.close() - - # Make return values: - return_dict = dict( - changed=pg_membership.changed, - state=state, - groups=pg_membership.groups, - target_roles=pg_membership.target_roles, - queries=pg_membership.executed_queries, - ) - - if state == 'present': - return_dict['granted'] = pg_membership.granted - elif state == 'absent': - return_dict['revoked'] = pg_membership.revoked - - module.exit_json(**return_dict) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_owner.py b/lib/ansible/modules/database/postgresql/postgresql_owner.py deleted file mode 100644 index 943ed55da7..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_owner.py +++ /dev/null @@ -1,444 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'supported_by': 'community', - 'status': ['preview'] -} - -DOCUMENTATION = r''' ---- -module: postgresql_owner -short_description: Change an owner of PostgreSQL database object -description: -- Change an owner of PostgreSQL database object. -- Also allows to reassign the ownership of database objects owned by a database role to another role. -version_added: '2.8' - -options: - new_owner: - description: - - Role (user/group) to set as an I(obj_name) owner. - type: str - required: yes - obj_name: - description: - - Name of a database object to change ownership. - - Mutually exclusive with I(reassign_owned_by). - type: str - obj_type: - description: - - Type of a database object. - - Mutually exclusive with I(reassign_owned_by). - type: str - choices: [ database, function, matview, sequence, schema, table, tablespace, view ] - aliases: - - type - reassign_owned_by: - description: - - The list of role names. The ownership of all the objects within the current database, - and of all shared objects (databases, tablespaces), owned by this role(s) will be reassigned to I(owner). - - Pay attention - it reassigns all objects owned by this role(s) in the I(db)! - - If role(s) exists, always returns changed True. - - Cannot reassign ownership of objects that are required by the database system. - - Mutually exclusive with C(obj_type). - type: list - elements: str - fail_on_role: - description: - - If C(yes), fail when I(reassign_owned_by) role does not exist. - Otherwise just warn and continue. - - Mutually exclusive with I(obj_name) and I(obj_type). - default: yes - type: bool - db: - description: - - Name of database to connect to. - type: str - aliases: - - login_db - session_role: - description: - - Switch to session_role after connecting. - The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str -seealso: -- module: postgresql_user -- module: postgresql_privs -- module: postgresql_membership -- name: PostgreSQL REASSIGN OWNED command reference - description: Complete reference of the PostgreSQL REASSIGN OWNED command documentation. - link: https://www.postgresql.org/docs/current/sql-reassign-owned.html -author: -- Andrew Klychkov (@Andersson007) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -# Set owner as alice for function myfunc in database bar by ansible ad-hoc command: -# ansible -m postgresql_owner -a "db=bar new_owner=alice obj_name=myfunc obj_type=function" - -- name: The same as above by playbook - postgresql_owner: - db: bar - new_owner: alice - obj_name: myfunc - obj_type: function - -- name: Set owner as bob for table acme in database bar - postgresql_owner: - db: bar - new_owner: bob - obj_name: acme - obj_type: table - -- name: Set owner as alice for view test_view in database bar - postgresql_owner: - db: bar - new_owner: alice - obj_name: test_view - obj_type: view - -- name: Set owner as bob for tablespace ssd in database foo - postgresql_owner: - db: foo - new_owner: bob - obj_name: ssd - obj_type: tablespace - -- name: Reassign all object in database bar owned by bob to alice - postgresql_owner: - db: bar - new_owner: alice - reassign_owned_by: bob - -- name: Reassign all object in database bar owned by bob and bill to alice - postgresql_owner: - db: bar - new_owner: alice - reassign_owned_by: - - bob - - bill -''' - -RETURN = r''' -queries: - description: List of executed queries. - returned: always - type: str - sample: [ 'REASSIGN OWNED BY "bob" TO "alice"' ] -''' - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import pg_quote_identifier -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) - - -class PgOwnership(object): - - """Class for changing ownership of PostgreSQL objects. - - Arguments: - module (AnsibleModule): Object of Ansible module class. - cursor (psycopg2.connect.cursor): Cursor object for interaction with the database. - role (str): Role name to set as a new owner of objects. - - Important: - If you want to add handling of a new type of database objects: - 1. Add a specific method for this like self.__set_db_owner(), etc. - 2. Add a condition with a check of ownership for new type objects to self.__is_owner() - 3. Add a condition with invocation of the specific method to self.set_owner() - 4. Add the information to the module documentation - That's all. - """ - - def __init__(self, module, cursor, role): - self.module = module - self.cursor = cursor - self.check_role_exists(role) - self.role = role - self.changed = False - self.executed_queries = [] - self.obj_name = '' - self.obj_type = '' - - def check_role_exists(self, role, fail_on_role=True): - """Check the role exists or not. - - Arguments: - role (str): Role name. - fail_on_role (bool): If True, fail when the role does not exist. - Otherwise just warn and continue. - """ - if not self.__role_exists(role): - if fail_on_role: - self.module.fail_json(msg="Role '%s' does not exist" % role) - else: - self.module.warn("Role '%s' does not exist, pass" % role) - - return False - - else: - return True - - def reassign(self, old_owners, fail_on_role): - """Implements REASSIGN OWNED BY command. - - If success, set self.changed as True. - - Arguments: - old_owners (list): The ownership of all the objects within - the current database, and of all shared objects (databases, tablespaces), - owned by these roles will be reassigned to self.role. - fail_on_role (bool): If True, fail when a role from old_owners does not exist. - Otherwise just warn and continue. - """ - roles = [] - for r in old_owners: - if self.check_role_exists(r, fail_on_role): - roles.append(pg_quote_identifier(r, 'role')) - - # Roles do not exist, nothing to do, exit: - if not roles: - return False - - old_owners = ','.join(roles) - - query = ['REASSIGN OWNED BY'] - query.append(old_owners) - query.append('TO %s' % pg_quote_identifier(self.role, 'role')) - query = ' '.join(query) - - self.changed = exec_sql(self, query, ddl=True) - - def set_owner(self, obj_type, obj_name): - """Change owner of a database object. - - Arguments: - obj_type (str): Type of object (like database, table, view, etc.). - obj_name (str): Object name. - """ - self.obj_name = obj_name - self.obj_type = obj_type - - # if a new_owner is the object owner now, - # nothing to do: - if self.__is_owner(): - return False - - if obj_type == 'database': - self.__set_db_owner() - - elif obj_type == 'function': - self.__set_func_owner() - - elif obj_type == 'sequence': - self.__set_seq_owner() - - elif obj_type == 'schema': - self.__set_schema_owner() - - elif obj_type == 'table': - self.__set_table_owner() - - elif obj_type == 'tablespace': - self.__set_tablespace_owner() - - elif obj_type == 'view': - self.__set_view_owner() - - elif obj_type == 'matview': - self.__set_mat_view_owner() - - def __is_owner(self): - """Return True if self.role is the current object owner.""" - if self.obj_type == 'table': - query = ("SELECT 1 FROM pg_tables " - "WHERE tablename = %(obj_name)s " - "AND tableowner = %(role)s") - - elif self.obj_type == 'database': - query = ("SELECT 1 FROM pg_database AS d " - "JOIN pg_roles AS r ON d.datdba = r.oid " - "WHERE d.datname = %(obj_name)s " - "AND r.rolname = %(role)s") - - elif self.obj_type == 'function': - query = ("SELECT 1 FROM pg_proc AS f " - "JOIN pg_roles AS r ON f.proowner = r.oid " - "WHERE f.proname = %(obj_name)s " - "AND r.rolname = %(role)s") - - elif self.obj_type == 'sequence': - query = ("SELECT 1 FROM pg_class AS c " - "JOIN pg_roles AS r ON c.relowner = r.oid " - "WHERE c.relkind = 'S' AND c.relname = %(obj_name)s " - "AND r.rolname = %(role)s") - - elif self.obj_type == 'schema': - query = ("SELECT 1 FROM information_schema.schemata " - "WHERE schema_name = %(obj_name)s " - "AND schema_owner = %(role)s") - - elif self.obj_type == 'tablespace': - query = ("SELECT 1 FROM pg_tablespace AS t " - "JOIN pg_roles AS r ON t.spcowner = r.oid " - "WHERE t.spcname = %(obj_name)s " - "AND r.rolname = %(role)s") - - elif self.obj_type == 'view': - query = ("SELECT 1 FROM pg_views " - "WHERE viewname = %(obj_name)s " - "AND viewowner = %(role)s") - - elif self.obj_type == 'matview': - query = ("SELECT 1 FROM pg_matviews " - "WHERE matviewname = %(obj_name)s " - "AND matviewowner = %(role)s") - - query_params = {'obj_name': self.obj_name, 'role': self.role} - return exec_sql(self, query, query_params, add_to_executed=False) - - def __set_db_owner(self): - """Set the database owner.""" - query = "ALTER DATABASE %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'database'), - pg_quote_identifier(self.role, 'role')) - self.changed = exec_sql(self, query, ddl=True) - - def __set_func_owner(self): - """Set the function owner.""" - query = "ALTER FUNCTION %s OWNER TO %s" % (self.obj_name, - pg_quote_identifier(self.role, 'role')) - self.changed = exec_sql(self, query, ddl=True) - - def __set_seq_owner(self): - """Set the sequence owner.""" - query = "ALTER SEQUENCE %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'table'), - pg_quote_identifier(self.role, 'role')) - self.changed = exec_sql(self, query, ddl=True) - - def __set_schema_owner(self): - """Set the schema owner.""" - query = "ALTER SCHEMA %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'schema'), - pg_quote_identifier(self.role, 'role')) - self.changed = exec_sql(self, query, ddl=True) - - def __set_table_owner(self): - """Set the table owner.""" - query = "ALTER TABLE %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'table'), - pg_quote_identifier(self.role, 'role')) - self.changed = exec_sql(self, query, ddl=True) - - def __set_tablespace_owner(self): - """Set the tablespace owner.""" - query = "ALTER TABLESPACE %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'database'), - pg_quote_identifier(self.role, 'role')) - self.changed = exec_sql(self, query, ddl=True) - - def __set_view_owner(self): - """Set the view owner.""" - query = "ALTER VIEW %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'table'), - pg_quote_identifier(self.role, 'role')) - self.changed = exec_sql(self, query, ddl=True) - - def __set_mat_view_owner(self): - """Set the materialized view owner.""" - query = "ALTER MATERIALIZED VIEW %s OWNER TO %s" % (pg_quote_identifier(self.obj_name, 'table'), - pg_quote_identifier(self.role, 'role')) - self.changed = exec_sql(self, query, ddl=True) - - def __role_exists(self, role): - """Return True if role exists, otherwise return False.""" - query_params = {'role': role} - query = "SELECT 1 FROM pg_roles WHERE rolname = %(role)s" - return exec_sql(self, query, query_params, add_to_executed=False) - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - new_owner=dict(type='str', required=True), - obj_name=dict(type='str'), - obj_type=dict(type='str', aliases=['type'], choices=[ - 'database', 'function', 'matview', 'sequence', 'schema', 'table', 'tablespace', 'view']), - reassign_owned_by=dict(type='list', elements='str'), - fail_on_role=dict(type='bool', default=True), - db=dict(type='str', aliases=['login_db']), - session_role=dict(type='str'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[ - ['obj_name', 'reassign_owned_by'], - ['obj_type', 'reassign_owned_by'], - ['obj_name', 'fail_on_role'], - ['obj_type', 'fail_on_role'], - ], - supports_check_mode=True, - ) - - new_owner = module.params['new_owner'] - obj_name = module.params['obj_name'] - obj_type = module.params['obj_type'] - reassign_owned_by = module.params['reassign_owned_by'] - fail_on_role = module.params['fail_on_role'] - - conn_params = get_conn_params(module, module.params) - db_connection = connect_to_db(module, conn_params, autocommit=False) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - ############## - # Create the object and do main job: - pg_ownership = PgOwnership(module, cursor, new_owner) - - # if we want to change ownership: - if obj_name: - pg_ownership.set_owner(obj_type, obj_name) - - # if we want to reassign objects owned by roles: - elif reassign_owned_by: - pg_ownership.reassign(reassign_owned_by, fail_on_role) - - # Rollback if it's possible and check_mode: - if module.check_mode: - db_connection.rollback() - else: - db_connection.commit() - - cursor.close() - db_connection.close() - - module.exit_json( - changed=pg_ownership.changed, - queries=pg_ownership.executed_queries, - ) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_pg_hba.py b/lib/ansible/modules/database/postgresql/postgresql_pg_hba.py deleted file mode 100644 index 7bba719bb7..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_pg_hba.py +++ /dev/null @@ -1,749 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2019, Sebastiaan Mannem (@sebasmannem) <sebastiaan.mannem@enterprisedb.com> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function - -''' -This module is used to manage postgres pg_hba files with Ansible. -''' - -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: postgresql_pg_hba -short_description: Add, remove or modify a rule in a pg_hba file -description: - - The fundamental function of the module is to create, or delete lines in pg_hba files. - - The lines in the file should be in a typical pg_hba form and lines should be unique per key (type, databases, users, source). - If they are not unique and the SID is 'the one to change', only one for C(state=present) or none for C(state=absent) of the SID's will remain. -extends_documentation_fragment: files -version_added: '2.8' -options: - address: - description: - - The source address/net where the connections could come from. - - Will not be used for entries of I(type)=C(local). - - You can also use keywords C(all), C(samehost), and C(samenet). - default: samehost - type: str - aliases: [ source, src ] - backup: - description: - - If set, create a backup of the C(pg_hba) file before it is modified. - The location of the backup is returned in the (backup) variable by this module. - default: false - type: bool - backup_file: - description: - - Write backup to a specific backupfile rather than a temp file. - type: str - create: - description: - - Create an C(pg_hba) file if none exists. - - When set to false, an error is raised when the C(pg_hba) file doesn't exist. - default: false - type: bool - contype: - description: - - Type of the rule. If not set, C(postgresql_pg_hba) will only return contents. - type: str - choices: [ local, host, hostnossl, hostssl ] - databases: - description: - - Databases this line applies to. - default: all - type: str - dest: - description: - - Path to C(pg_hba) file to modify. - type: path - required: true - method: - description: - - Authentication method to be used. - type: str - choices: [ cert, gss, ident, krb5, ldap, md5, pam, password, peer, radius, reject, scram-sha-256 , sspi, trust ] - default: md5 - netmask: - description: - - The netmask of the source address. - type: str - options: - description: - - Additional options for the authentication I(method). - type: str - order: - description: - - The entries will be written out in a specific order. - With this option you can control by which field they are ordered first, second and last. - s=source, d=databases, u=users. - This option is deprecated since 2.9 and will be removed in 2.11. - Sortorder is now hardcoded to sdu. - type: str - default: sdu - choices: [ sdu, sud, dsu, dus, usd, uds ] - state: - description: - - The lines will be added/modified when C(state=present) and removed when C(state=absent). - type: str - default: present - choices: [ absent, present ] - users: - description: - - Users this line applies to. - type: str - default: all - -notes: - - The default authentication assumes that on the host, you are either logging in as or - sudo'ing to an account with appropriate permissions to read and modify the file. - - This module also returns the pg_hba info. You can use this module to only retrieve it by only specifying I(dest). - The info can be found in the returned data under key pg_hba, being a list, containing a dict per rule. - - This module will sort resulting C(pg_hba) files if a rule change is required. - This could give unexpected results with manual created hba files, if it was improperly sorted. - For example a rule was created for a net first and for a ip in that net range next. - In that situation, the 'ip specific rule' will never hit, it is in the C(pg_hba) file obsolete. - After the C(pg_hba) file is rewritten by the M(postgresql_pg_hba) module, the ip specific rule will be sorted above the range rule. - And then it will hit, which will give unexpected results. - - With the 'order' parameter you can control which field is used to sort first, next and last. - - The module supports a check mode and a diff mode. - -seealso: -- name: PostgreSQL pg_hba.conf file reference - description: Complete reference of the PostgreSQL pg_hba.conf file documentation. - link: https://www.postgresql.org/docs/current/auth-pg-hba-conf.html - -requirements: - - ipaddress - -author: Sebastiaan Mannem (@sebasmannem) -''' - -EXAMPLES = ''' -- name: Grant users joe and simon access to databases sales and logistics from ipv6 localhost ::1/128 using peer authentication. - postgresql_pg_hba: - dest: /var/lib/postgres/data/pg_hba.conf - contype: host - users: joe,simon - source: ::1 - databases: sales,logistics - method: peer - create: true - -- name: Grant user replication from network 192.168.0.100/24 access for replication with client cert authentication. - postgresql_pg_hba: - dest: /var/lib/postgres/data/pg_hba.conf - contype: host - users: replication - source: 192.168.0.100/24 - databases: replication - method: cert - -- name: Revoke access from local user mary on database mydb. - postgresql_pg_hba: - dest: /var/lib/postgres/data/pg_hba.conf - contype: local - users: mary - databases: mydb - state: absent -''' - -RETURN = r''' -msgs: - description: List of textual messages what was done - returned: always - type: list - sample: - "msgs": [ - "Removing", - "Changed", - "Writing" - ] -backup_file: - description: File that the original pg_hba file was backed up to - returned: changed - type: str - sample: /tmp/pg_hba_jxobj_p -pg_hba: - description: List of the pg_hba rules as they are configured in the specified hba file - returned: always - type: list - sample: - "pg_hba": [ - { - "db": "all", - "method": "md5", - "src": "samehost", - "type": "host", - "usr": "all" - } - ] -''' - -import os -import re -import traceback - -IPADDRESS_IMP_ERR = None -try: - import ipaddress -except ImportError: - IPADDRESS_IMP_ERR = traceback.format_exc() - -import tempfile -import shutil -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -# from ansible.module_utils.postgres import postgres_common_argument_spec - -PG_HBA_METHODS = ["trust", "reject", "md5", "password", "gss", "sspi", "krb5", "ident", "peer", - "ldap", "radius", "cert", "pam", "scram-sha-256"] -PG_HBA_TYPES = ["local", "host", "hostssl", "hostnossl"] -PG_HBA_ORDERS = ["sdu", "sud", "dsu", "dus", "usd", "uds"] -PG_HBA_HDR = ['type', 'db', 'usr', 'src', 'mask', 'method', 'options'] - -WHITESPACES_RE = re.compile(r'\s+') - - -class PgHbaError(Exception): - ''' - This exception is raised when parsing the pg_hba file ends in an error. - ''' - - -class PgHbaRuleError(PgHbaError): - ''' - This exception is raised when parsing the pg_hba file ends in an error. - ''' - - -class PgHbaRuleChanged(PgHbaRuleError): - ''' - This exception is raised when a new parsed rule is a changed version of an existing rule. - ''' - - -class PgHbaValueError(PgHbaError): - ''' - This exception is raised when a new parsed rule is a changed version of an existing rule. - ''' - - -class PgHbaRuleValueError(PgHbaRuleError): - ''' - This exception is raised when a new parsed rule is a changed version of an existing rule. - ''' - - -class PgHba(object): - """ - PgHba object to read/write entries to/from. - pg_hba_file - the pg_hba file almost always /etc/pg_hba - """ - def __init__(self, pg_hba_file=None, order="sdu", backup=False, create=False): - if order not in PG_HBA_ORDERS: - msg = "invalid order setting {0} (should be one of '{1}')." - raise PgHbaError(msg.format(order, "', '".join(PG_HBA_ORDERS))) - self.pg_hba_file = pg_hba_file - self.rules = None - self.comment = None - self.order = order - self.backup = backup - self.last_backup = None - self.create = create - self.unchanged() - # self.databases will be update by add_rule and gives some idea of the number of databases - # (at least that are handled by this pg_hba) - self.databases = set(['postgres', 'template0', 'template1']) - - # self.databases will be update by add_rule and gives some idea of the number of users - # (at least that are handled by this pg_hba) since this might also be groups with multiple - # users, this might be totally off, but at least it is some info... - self.users = set(['postgres']) - - self.read() - - def unchanged(self): - ''' - This method resets self.diff to a empty default - ''' - self.diff = {'before': {'file': self.pg_hba_file, 'pg_hba': []}, - 'after': {'file': self.pg_hba_file, 'pg_hba': []}} - - def read(self): - ''' - Read in the pg_hba from the system - ''' - self.rules = {} - self.comment = [] - # read the pg_hbafile - try: - with open(self.pg_hba_file, 'r') as file: - for line in file: - line = line.strip() - # uncomment - if '#' in line: - line, comment = line.split('#', 1) - self.comment.append('#' + comment) - try: - self.add_rule(PgHbaRule(line=line)) - except PgHbaRuleError: - pass - self.unchanged() - except IOError: - pass - - def write(self, backup_file=''): - ''' - This method writes the PgHba rules (back) to a file. - ''' - if not self.changed(): - return False - - contents = self.render() - if self.pg_hba_file: - if not (os.path.isfile(self.pg_hba_file) or self.create): - raise PgHbaError("pg_hba file '{0}' doesn't exist. " - "Use create option to autocreate.".format(self.pg_hba_file)) - if self.backup and os.path.isfile(self.pg_hba_file): - if backup_file: - self.last_backup = backup_file - else: - __backup_file_h, self.last_backup = tempfile.mkstemp(prefix='pg_hba') - shutil.copy(self.pg_hba_file, self.last_backup) - fileh = open(self.pg_hba_file, 'w') - else: - filed, __path = tempfile.mkstemp(prefix='pg_hba') - fileh = os.fdopen(filed, 'w') - - fileh.write(contents) - self.unchanged() - fileh.close() - return True - - def add_rule(self, rule): - ''' - This method can be used to add a rule to the list of rules in this PgHba object - ''' - key = rule.key() - try: - try: - oldrule = self.rules[key] - except KeyError: - raise PgHbaRuleChanged - ekeys = set(list(oldrule.keys()) + list(rule.keys())) - ekeys.remove('line') - for k in ekeys: - if oldrule[k] != rule[k]: - raise PgHbaRuleChanged('{0} changes {1}'.format(rule, oldrule)) - except PgHbaRuleChanged: - self.rules[key] = rule - self.diff['after']['pg_hba'].append(rule.line()) - if rule['db'] not in ['all', 'samerole', 'samegroup', 'replication']: - databases = set(rule['db'].split(',')) - self.databases.update(databases) - if rule['usr'] != 'all': - user = rule['usr'] - if user[0] == '+': - user = user[1:] - self.users.add(user) - - def remove_rule(self, rule): - ''' - This method can be used to find and remove a rule. It doesn't look for the exact rule, only - the rule with the same key. - ''' - keys = rule.key() - try: - del self.rules[keys] - self.diff['before']['pg_hba'].append(rule.line()) - except KeyError: - pass - - def get_rules(self, with_lines=False): - ''' - This method returns all the rules of the PgHba object - ''' - rules = sorted(self.rules.values()) - for rule in rules: - ret = {} - for key, value in rule.items(): - ret[key] = value - if not with_lines: - if 'line' in ret: - del ret['line'] - else: - ret['line'] = rule.line() - - yield ret - - def render(self): - ''' - This method renders the content of the PgHba rules and comments. - The returning value can be used directly to write to a new file. - ''' - comment = '\n'.join(self.comment) - rule_lines = '\n'.join([rule['line'] for rule in self.get_rules(with_lines=True)]) - result = comment + '\n' + rule_lines - # End it properly with a linefeed (if not already). - if result and result[-1] not in ['\n', '\r']: - result += '\n' - return result - - def changed(self): - ''' - This method can be called to detect if the PgHba file has been changed. - ''' - return bool(self.diff['before']['pg_hba'] or self.diff['after']['pg_hba']) - - -class PgHbaRule(dict): - ''' - This class represents one rule as defined in a line in a PgHbaFile. - ''' - - def __init__(self, contype=None, databases=None, users=None, source=None, netmask=None, - method=None, options=None, line=None): - ''' - This function can be called with a comma seperated list of databases and a comma seperated - list of users and it will act as a generator that returns a expanded list of rules one by - one. - ''' - - super(PgHbaRule, self).__init__() - - if line: - # Read values from line if parsed - self.fromline(line) - - # read rule cols from parsed items - rule = dict(zip(PG_HBA_HDR, [contype, databases, users, source, netmask, method, options])) - for key, value in rule.items(): - if value: - self[key] = value - - # Some sanity checks - for key in ['method', 'type']: - if key not in self: - raise PgHbaRuleError('Missing {0} in rule {1}'.format(key, self)) - - if self['method'] not in PG_HBA_METHODS: - msg = "invalid method {0} (should be one of '{1}')." - raise PgHbaRuleValueError(msg.format(self['method'], "', '".join(PG_HBA_METHODS))) - - if self['type'] not in PG_HBA_TYPES: - msg = "invalid connection type {0} (should be one of '{1}')." - raise PgHbaRuleValueError(msg.format(self['type'], "', '".join(PG_HBA_TYPES))) - - if self['type'] == 'local': - self.unset('src') - self.unset('mask') - elif 'src' not in self: - raise PgHbaRuleError('Missing src in rule {1}'.format(self)) - elif '/' in self['src']: - self.unset('mask') - else: - self['src'] = str(self.source()) - self.unset('mask') - - def unset(self, key): - ''' - This method is used to unset certain columns if they exist - ''' - if key in self: - del self[key] - - def line(self): - ''' - This method can be used to return (or generate) the line - ''' - try: - return self['line'] - except KeyError: - self['line'] = "\t".join([self[k] for k in PG_HBA_HDR if k in self.keys()]) - return self['line'] - - def fromline(self, line): - ''' - split into 'type', 'db', 'usr', 'src', 'mask', 'method', 'options' cols - ''' - if WHITESPACES_RE.sub('', line) == '': - # empty line. skip this one... - return - cols = WHITESPACES_RE.split(line) - if len(cols) < 4: - msg = "Rule {0} has too few columns." - raise PgHbaValueError(msg.format(line)) - if cols[0] not in PG_HBA_TYPES: - msg = "Rule {0} has unknown type: {1}." - raise PgHbaValueError(msg.format(line, cols[0])) - if cols[0] == 'local': - cols.insert(3, None) # No address - cols.insert(3, None) # No IP-mask - if len(cols) < 6: - cols.insert(4, None) # No IP-mask - elif cols[5] not in PG_HBA_METHODS: - cols.insert(4, None) # No IP-mask - if cols[5] not in PG_HBA_METHODS: - raise PgHbaValueError("Rule {0} of '{1}' type has invalid auth-method '{2}'".format(line, cols[0], cols[5])) - - if len(cols) < 7: - cols.insert(6, None) # No auth-options - else: - cols[6] = " ".join(cols[6:]) # combine all auth-options - rule = dict(zip(PG_HBA_HDR, cols[:7])) - for key, value in rule.items(): - if value: - self[key] = value - - def key(self): - ''' - This method can be used to get the key from a rule. - ''' - if self['type'] == 'local': - source = 'local' - else: - source = str(self.source()) - return (source, self['db'], self['usr']) - - def source(self): - ''' - This method is used to get the source of a rule as an ipaddress object if possible. - ''' - if 'mask' in self.keys(): - try: - ipaddress.ip_address(u'{0}'.format(self['src'])) - except ValueError: - raise PgHbaValueError('Mask was specified, but source "{0}" ' - 'is no valid ip'.format(self['src'])) - # ipaddress module cannot work with ipv6 netmask, so lets convert it to prefixlen - # furthermore ipv4 with bad netmask throws 'Rule {} doesn't seem to be an ip, but has a - # mask error that doesn't seem to describe what is going on. - try: - mask_as_ip = ipaddress.ip_address(u'{0}'.format(self['mask'])) - except ValueError: - raise PgHbaValueError('Mask {0} seems to be invalid'.format(self['mask'])) - binvalue = "{0:b}".format(int(mask_as_ip)) - if '01' in binvalue: - raise PgHbaValueError('IP mask {0} seems invalid ' - '(binary value has 1 after 0)'.format(self['mask'])) - prefixlen = binvalue.count('1') - sourcenw = '{0}/{1}'.format(self['src'], prefixlen) - try: - return ipaddress.ip_network(u'{0}'.format(sourcenw), strict=False) - except ValueError: - raise PgHbaValueError('{0} is no valid address range'.format(sourcenw)) - - try: - return ipaddress.ip_network(u'{0}'.format(self['src']), strict=False) - except ValueError: - return self['src'] - - def __lt__(self, other): - """This function helps sorted to decide how to sort. - - It just checks itself against the other and decides on some key values - if it should be sorted higher or lower in the list. - The way it works: - For networks, every 1 in 'netmask in binary' makes the subnet more specific. - Therefore I chose to use prefix as the weight. - So a single IP (/32) should have twice the weight of a /16 network. - To keep everything in the same weight scale, - - for ipv6, we use a weight scale of 0 (all possible ipv6 addresses) to 128 (single ip) - - for ipv4, we use a weight scale of 0 (all possible ipv4 addresses) to 128 (single ip) - Therefore for ipv4, we use prefixlen (0-32) * 4 for weight, - which corresponds to ipv6 (0-128). - """ - myweight = self.source_weight() - hisweight = other.source_weight() - if myweight != hisweight: - return myweight > hisweight - - myweight = self.db_weight() - hisweight = other.db_weight() - if myweight != hisweight: - return myweight < hisweight - - myweight = self.user_weight() - hisweight = other.user_weight() - if myweight != hisweight: - return myweight < hisweight - try: - return self['src'] < other['src'] - except TypeError: - return self.source_type_weight() < other.source_type_weight() - except Exception: - # When all else fails, just compare the exact line. - return self.line() < other.line() - - def source_weight(self): - """Report the weight of this source net. - - Basically this is the netmask, where IPv4 is normalized to IPv6 - (IPv4/32 has the same weight as IPv6/128). - """ - if self['type'] == 'local': - return 130 - - sourceobj = self.source() - if isinstance(sourceobj, ipaddress.IPv4Network): - return sourceobj.prefixlen * 4 - if isinstance(sourceobj, ipaddress.IPv6Network): - return sourceobj.prefixlen - if isinstance(sourceobj, str): - # You can also write all to match any IP address, - # samehost to match any of the server's own IP addresses, - # or samenet to match any address in any subnet that the server is connected to. - if sourceobj == 'all': - # (all is considered the full range of all ips, which has a weight of 0) - return 0 - if sourceobj == 'samehost': - # (sort samehost second after local) - return 129 - if sourceobj == 'samenet': - # Might write some fancy code to determine all prefix's - # from all interfaces and find a sane value for this one. - # For now, let's assume IPv4/24 or IPv6/96 (both have weight 96). - return 96 - if sourceobj[0] == '.': - # suffix matching (domain name), let's assume a very large scale - # and therefore a very low weight IPv4/16 or IPv6/64 (both have weight 64). - return 64 - # hostname, let's assume only one host matches, which is - # IPv4/32 or IPv6/128 (both have weight 128) - return 128 - raise PgHbaValueError('Cannot deduct the source weight of this source {1}'.format(sourceobj)) - - def source_type_weight(self): - """Give a weight on the type of this source. - - Basically make sure that IPv6Networks are sorted higher than IPv4Networks. - This is a 'when all else fails' solution in __lt__. - """ - if self['type'] == 'local': - return 3 - - sourceobj = self.source() - if isinstance(sourceobj, ipaddress.IPv4Network): - return 2 - if isinstance(sourceobj, ipaddress.IPv6Network): - return 1 - if isinstance(sourceobj, str): - return 0 - raise PgHbaValueError('This source {0} is of an unknown type...'.format(sourceobj)) - - def db_weight(self): - """Report the weight of the database. - - Normally, just 1, but for replication this is 0, and for 'all', this is more than 2. - """ - if self['db'] == 'all': - return 100000 - if self['db'] == 'replication': - return 0 - if self['db'] in ['samerole', 'samegroup']: - return 1 - return 1 + self['db'].count(',') - - def user_weight(self): - """Report weight when comparing users.""" - if self['usr'] == 'all': - return 1000000 - return 1 - - -def main(): - ''' - This function is the main function of this module - ''' - # argument_spec = postgres_common_argument_spec() - argument_spec = dict() - argument_spec.update( - address=dict(type='str', default='samehost', aliases=['source', 'src']), - backup=dict(type='bool', default=False), - backup_file=dict(type='str'), - contype=dict(type='str', default=None, choices=PG_HBA_TYPES), - create=dict(type='bool', default=False), - databases=dict(type='str', default='all'), - dest=dict(type='path', required=True), - method=dict(type='str', default='md5', choices=PG_HBA_METHODS), - netmask=dict(type='str'), - options=dict(type='str'), - order=dict(type='str', default="sdu", choices=PG_HBA_ORDERS), - state=dict(type='str', default="present", choices=["absent", "present"]), - users=dict(type='str', default='all') - ) - module = AnsibleModule( - argument_spec=argument_spec, - add_file_common_args=True, - supports_check_mode=True - ) - if IPADDRESS_IMP_ERR is not None: - module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMP_ERR) - - contype = module.params["contype"] - create = bool(module.params["create"] or module.check_mode) - if module.check_mode: - backup = False - else: - backup = module.params['backup'] - backup_file = module.params['backup_file'] - databases = module.params["databases"] - dest = module.params["dest"] - - method = module.params["method"] - netmask = module.params["netmask"] - options = module.params["options"] - order = module.params["order"] - source = module.params["address"] - state = module.params["state"] - users = module.params["users"] - - ret = {'msgs': []} - try: - pg_hba = PgHba(dest, order, backup=backup, create=create) - except PgHbaError as error: - module.fail_json(msg='Error reading file:\n{0}'.format(error)) - - if contype: - try: - for database in databases.split(','): - for user in users.split(','): - rule = PgHbaRule(contype, database, user, source, netmask, method, options) - if state == "present": - ret['msgs'].append('Adding') - pg_hba.add_rule(rule) - else: - ret['msgs'].append('Removing') - pg_hba.remove_rule(rule) - except PgHbaError as error: - module.fail_json(msg='Error modifying rules:\n{0}'.format(error)) - file_args = module.load_file_common_arguments(module.params) - ret['changed'] = changed = pg_hba.changed() - if changed: - ret['msgs'].append('Changed') - ret['diff'] = pg_hba.diff - - if not module.check_mode: - ret['msgs'].append('Writing') - try: - if pg_hba.write(backup_file): - module.set_fs_attributes_if_different(file_args, True, pg_hba.diff, - expand=False) - except PgHbaError as error: - module.fail_json(msg='Error writing file:\n{0}'.format(error)) - if pg_hba.last_backup: - ret['backup_file'] = pg_hba.last_backup - - ret['pg_hba'] = list(pg_hba.get_rules()) - module.exit_json(**ret) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_ping.py b/lib/ansible/modules/database/postgresql/postgresql_ping.py deleted file mode 100644 index cbd4d6e39f..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_ping.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2018, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = r''' ---- -module: postgresql_ping -short_description: Check remote PostgreSQL server availability -description: -- Simple module to check remote PostgreSQL server availability. -version_added: '2.8' -options: - db: - description: - - Name of a database to connect to. - type: str - aliases: - - login_db -seealso: -- module: postgresql_info -author: -- Andrew Klychkov (@Andersson007) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -# PostgreSQL ping dbsrv server from the shell: -# ansible dbsrv -m postgresql_ping - -# In the example below you need to generate certificates previously. -# See https://www.postgresql.org/docs/current/libpq-ssl.html for more information. -- name: PostgreSQL ping dbsrv server using not default credentials and ssl - postgresql_ping: - db: protected_db - login_host: dbsrv - login_user: secret - login_password: secret_pass - ca_cert: /root/root.crt - ssl_mode: verify-full -''' - -RETURN = r''' -is_available: - description: PostgreSQL server availability. - returned: always - type: bool - sample: true -server_version: - description: PostgreSQL server version. - returned: always - type: dict - sample: { major: 10, minor: 1 } -''' - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) - - -# =========================================== -# PostgreSQL module specific support methods. -# - - -class PgPing(object): - def __init__(self, module, cursor): - self.module = module - self.cursor = cursor - self.is_available = False - self.version = {} - - def do(self): - self.get_pg_version() - return (self.is_available, self.version) - - def get_pg_version(self): - query = "SELECT version()" - raw = exec_sql(self, query, add_to_executed=False)[0][0] - if raw: - self.is_available = True - raw = raw.split()[1].split('.') - self.version = dict( - major=int(raw[0]), - minor=int(raw[1]), - ) - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - db=dict(type='str', aliases=['login_db']), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - # Set some default values: - cursor = False - db_connection = False - result = dict( - changed=False, - is_available=False, - server_version=dict(), - ) - - conn_params = get_conn_params(module, module.params, warn_db_default=False) - db_connection = connect_to_db(module, conn_params, fail_on_conn=False) - - if db_connection is not None: - cursor = db_connection.cursor(cursor_factory=DictCursor) - - # Do job: - pg_ping = PgPing(module, cursor) - if cursor: - # If connection established: - result["is_available"], result["server_version"] = pg_ping.do() - db_connection.rollback() - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_privs.py b/lib/ansible/modules/database/postgresql/postgresql_privs.py deleted file mode 100644 index c7cffe9887..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_privs.py +++ /dev/null @@ -1,1097 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# Copyright: (c) 2019, Tobias Birkefeld (@tcraxs) <t@craxs.de> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: postgresql_privs -version_added: '1.2' -short_description: Grant or revoke privileges on PostgreSQL database objects -description: -- Grant or revoke privileges on PostgreSQL database objects. -- This module is basically a wrapper around most of the functionality of - PostgreSQL's GRANT and REVOKE statements with detection of changes - (GRANT/REVOKE I(privs) ON I(type) I(objs) TO/FROM I(roles)). -options: - database: - description: - - Name of database to connect to. - required: yes - type: str - aliases: - - db - - login_db - state: - description: - - If C(present), the specified privileges are granted, if C(absent) they are revoked. - type: str - default: present - choices: [ absent, present ] - privs: - description: - - Comma separated list of privileges to grant/revoke. - type: str - aliases: - - priv - type: - description: - - Type of database object to set privileges on. - - The C(default_privs) choice is available starting at version 2.7. - - The C(foreign_data_wrapper) and C(foreign_server) object types are available from Ansible version '2.8'. - - The C(type) choice is available from Ansible version '2.10'. - type: str - default: table - choices: [ database, default_privs, foreign_data_wrapper, foreign_server, function, - group, language, table, tablespace, schema, sequence, type ] - objs: - description: - - Comma separated list of database objects to set privileges on. - - If I(type) is C(table), C(partition table), C(sequence) or C(function), - the special valueC(ALL_IN_SCHEMA) can be provided instead to specify all - database objects of type I(type) in the schema specified via I(schema). - (This also works with PostgreSQL < 9.0.) (C(ALL_IN_SCHEMA) is available - for C(function) and C(partition table) from version 2.8) - - If I(type) is C(database), this parameter can be omitted, in which case - privileges are set for the database specified via I(database). - - 'If I(type) is I(function), colons (":") in object names will be - replaced with commas (needed to specify function signatures, see examples)' - type: str - aliases: - - obj - schema: - description: - - Schema that contains the database objects specified via I(objs). - - May only be provided if I(type) is C(table), C(sequence), C(function), C(type), - or C(default_privs). Defaults to C(public) in these cases. - - Pay attention, for embedded types when I(type=type) - I(schema) can be C(pg_catalog) or C(information_schema) respectively. - type: str - roles: - description: - - Comma separated list of role (user/group) names to set permissions for. - - The special value C(PUBLIC) can be provided instead to set permissions - for the implicitly defined PUBLIC group. - type: str - required: yes - aliases: - - role - fail_on_role: - version_added: '2.8' - description: - - If C(yes), fail when target role (for whom privs need to be granted) does not exist. - Otherwise just warn and continue. - default: yes - type: bool - session_role: - version_added: '2.8' - description: - - Switch to session_role after connecting. - - The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally. - type: str - target_roles: - description: - - A list of existing role (user/group) names to set as the - default permissions for database objects subsequently created by them. - - Parameter I(target_roles) is only available with C(type=default_privs). - type: str - version_added: '2.8' - grant_option: - description: - - Whether C(role) may grant/revoke the specified privileges/group memberships to others. - - Set to C(no) to revoke GRANT OPTION, leave unspecified to make no changes. - - I(grant_option) only has an effect if I(state) is C(present). - type: bool - aliases: - - admin_option - host: - description: - - Database host address. If unspecified, connect via Unix socket. - type: str - aliases: - - login_host - port: - description: - - Database port to connect to. - type: int - default: 5432 - aliases: - - login_port - unix_socket: - description: - - Path to a Unix domain socket for local connections. - type: str - aliases: - - login_unix_socket - login: - description: - - The username to authenticate with. - type: str - default: postgres - aliases: - - login_user - password: - description: - - The password to authenticate with. - type: str - aliases: - - login_password - ssl_mode: - description: - - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. - - See U(https://www.postgresql.org/docs/current/static/libpq-ssl.html) for more information on the modes. - - Default of C(prefer) matches libpq default. - type: str - default: prefer - choices: [ allow, disable, prefer, require, verify-ca, verify-full ] - version_added: '2.3' - ca_cert: - description: - - Specifies the name of a file containing SSL certificate authority (CA) certificate(s). - - If the file exists, the server's certificate will be verified to be signed by one of these authorities. - version_added: '2.3' - type: str - aliases: - - ssl_rootcert - -notes: -- Parameters that accept comma separated lists (I(privs), I(objs), I(roles)) - have singular alias names (I(priv), I(obj), I(role)). -- To revoke only C(GRANT OPTION) for a specific object, set I(state) to - C(present) and I(grant_option) to C(no) (see examples). -- Note that when revoking privileges from a role R, this role may still have - access via privileges granted to any role R is a member of including C(PUBLIC). -- Note that when revoking privileges from a role R, you do so as the user - specified via I(login). If R has been granted the same privileges by - another user also, R can still access database objects via these privileges. -- When revoking privileges, C(RESTRICT) is assumed (see PostgreSQL docs). - -seealso: -- module: postgresql_user -- module: postgresql_owner -- module: postgresql_membership -- name: PostgreSQL privileges - description: General information about PostgreSQL privileges. - link: https://www.postgresql.org/docs/current/ddl-priv.html -- name: PostgreSQL GRANT command reference - description: Complete reference of the PostgreSQL GRANT command documentation. - link: https://www.postgresql.org/docs/current/sql-grant.html -- name: PostgreSQL REVOKE command reference - description: Complete reference of the PostgreSQL REVOKE command documentation. - link: https://www.postgresql.org/docs/current/sql-revoke.html - -extends_documentation_fragment: -- postgres - -author: -- Bernhard Weitzhofer (@b6d) -- Tobias Birkefeld (@tcraxs) -''' - -EXAMPLES = r''' -# On database "library": -# GRANT SELECT, INSERT, UPDATE ON TABLE public.books, public.authors -# TO librarian, reader WITH GRANT OPTION -- name: Grant privs to librarian and reader on database library - postgresql_privs: - database: library - state: present - privs: SELECT,INSERT,UPDATE - type: table - objs: books,authors - schema: public - roles: librarian,reader - grant_option: yes - -- name: Same as above leveraging default values - postgresql_privs: - db: library - privs: SELECT,INSERT,UPDATE - objs: books,authors - roles: librarian,reader - grant_option: yes - -# REVOKE GRANT OPTION FOR INSERT ON TABLE books FROM reader -# Note that role "reader" will be *granted* INSERT privilege itself if this -# isn't already the case (since state: present). -- name: Revoke privs from reader - postgresql_privs: - db: library - state: present - priv: INSERT - obj: books - role: reader - grant_option: no - -# "public" is the default schema. This also works for PostgreSQL 8.x. -- name: REVOKE INSERT, UPDATE ON ALL TABLES IN SCHEMA public FROM reader - postgresql_privs: - db: library - state: absent - privs: INSERT,UPDATE - objs: ALL_IN_SCHEMA - role: reader - -- name: GRANT ALL PRIVILEGES ON SCHEMA public, math TO librarian - postgresql_privs: - db: library - privs: ALL - type: schema - objs: public,math - role: librarian - -# Note the separation of arguments with colons. -- name: GRANT ALL PRIVILEGES ON FUNCTION math.add(int, int) TO librarian, reader - postgresql_privs: - db: library - privs: ALL - type: function - obj: add(int:int) - schema: math - roles: librarian,reader - -# Note that group role memberships apply cluster-wide and therefore are not -# restricted to database "library" here. -- name: GRANT librarian, reader TO alice, bob WITH ADMIN OPTION - postgresql_privs: - db: library - type: group - objs: librarian,reader - roles: alice,bob - admin_option: yes - -# Note that here "db: postgres" specifies the database to connect to, not the -# database to grant privileges on (which is specified via the "objs" param) -- name: GRANT ALL PRIVILEGES ON DATABASE library TO librarian - postgresql_privs: - db: postgres - privs: ALL - type: database - obj: library - role: librarian - -# If objs is omitted for type "database", it defaults to the database -# to which the connection is established -- name: GRANT ALL PRIVILEGES ON DATABASE library TO librarian - postgresql_privs: - db: library - privs: ALL - type: database - role: librarian - -# Available since version 2.7 -# Objs must be set, ALL_DEFAULT to TABLES/SEQUENCES/TYPES/FUNCTIONS -# ALL_DEFAULT works only with privs=ALL -# For specific -- name: ALTER DEFAULT PRIVILEGES ON DATABASE library TO librarian - postgresql_privs: - db: library - objs: ALL_DEFAULT - privs: ALL - type: default_privs - role: librarian - grant_option: yes - -# Available since version 2.7 -# Objs must be set, ALL_DEFAULT to TABLES/SEQUENCES/TYPES/FUNCTIONS -# ALL_DEFAULT works only with privs=ALL -# For specific -- name: ALTER DEFAULT PRIVILEGES ON DATABASE library TO reader, step 1 - postgresql_privs: - db: library - objs: TABLES,SEQUENCES - privs: SELECT - type: default_privs - role: reader - -- name: ALTER DEFAULT PRIVILEGES ON DATABASE library TO reader, step 2 - postgresql_privs: - db: library - objs: TYPES - privs: USAGE - type: default_privs - role: reader - -# Available since version 2.8 -- name: GRANT ALL PRIVILEGES ON FOREIGN DATA WRAPPER fdw TO reader - postgresql_privs: - db: test - objs: fdw - privs: ALL - type: foreign_data_wrapper - role: reader - -# Available since version 2.10 -- name: GRANT ALL PRIVILEGES ON TYPE customtype TO reader - postgresql_privs: - db: test - objs: customtype - privs: ALL - type: type - role: reader - -# Available since version 2.8 -- name: GRANT ALL PRIVILEGES ON FOREIGN SERVER fdw_server TO reader - postgresql_privs: - db: test - objs: fdw_server - privs: ALL - type: foreign_server - role: reader - -# Available since version 2.8 -# Grant 'execute' permissions on all functions in schema 'common' to role 'caller' -- name: GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA common TO caller - postgresql_privs: - type: function - state: present - privs: EXECUTE - roles: caller - objs: ALL_IN_SCHEMA - schema: common - -# Available since version 2.8 -# ALTER DEFAULT PRIVILEGES FOR ROLE librarian IN SCHEMA library GRANT SELECT ON TABLES TO reader -# GRANT SELECT privileges for new TABLES objects created by librarian as -# default to the role reader. -# For specific -- name: ALTER privs - postgresql_privs: - db: library - schema: library - objs: TABLES - privs: SELECT - type: default_privs - role: reader - target_roles: librarian - -# Available since version 2.8 -# ALTER DEFAULT PRIVILEGES FOR ROLE librarian IN SCHEMA library REVOKE SELECT ON TABLES FROM reader -# REVOKE SELECT privileges for new TABLES objects created by librarian as -# default from the role reader. -# For specific -- name: ALTER privs - postgresql_privs: - db: library - state: absent - schema: library - objs: TABLES - privs: SELECT - type: default_privs - role: reader - target_roles: librarian - -# Available since version 2.10 -- name: Grant type privileges for pg_catalog.numeric type to alice - postgresql_privs: - type: type - roles: alice - privs: ALL - objs: numeric - schema: pg_catalog - db: acme -''' - -RETURN = r''' -queries: - description: List of executed queries. - returned: always - type: list - sample: ['REVOKE GRANT OPTION FOR INSERT ON TABLE "books" FROM "reader";'] - version_added: '2.8' -''' - -import traceback - -PSYCOPG2_IMP_ERR = None -try: - import psycopg2 - import psycopg2.extensions -except ImportError: - PSYCOPG2_IMP_ERR = traceback.format_exc() - psycopg2 = None - -# import module snippets -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.database import pg_quote_identifier -from ansible.module_utils.postgres import postgres_common_argument_spec -from ansible.module_utils._text import to_native - -VALID_PRIVS = frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', - 'REFERENCES', 'TRIGGER', 'CREATE', 'CONNECT', - 'TEMPORARY', 'TEMP', 'EXECUTE', 'USAGE', 'ALL', 'USAGE')) -VALID_DEFAULT_OBJS = {'TABLES': ('ALL', 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER'), - 'SEQUENCES': ('ALL', 'SELECT', 'UPDATE', 'USAGE'), - 'FUNCTIONS': ('ALL', 'EXECUTE'), - 'TYPES': ('ALL', 'USAGE')} - -executed_queries = [] - - -class Error(Exception): - pass - - -def role_exists(module, cursor, rolname): - """Check user exists or not""" - query = "SELECT 1 FROM pg_roles WHERE rolname = '%s'" % rolname - try: - cursor.execute(query) - return cursor.rowcount > 0 - - except Exception as e: - module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e))) - - return False - - -# We don't have functools.partial in Python < 2.5 -def partial(f, *args, **kwargs): - """Partial function application""" - - def g(*g_args, **g_kwargs): - new_kwargs = kwargs.copy() - new_kwargs.update(g_kwargs) - return f(*(args + g_args), **g_kwargs) - - g.f = f - g.args = args - g.kwargs = kwargs - return g - - -class Connection(object): - """Wrapper around a psycopg2 connection with some convenience methods""" - - def __init__(self, params, module): - self.database = params.database - self.module = module - # To use defaults values, keyword arguments must be absent, so - # check which values are empty and don't include in the **kw - # dictionary - params_map = { - "host": "host", - "login": "user", - "password": "password", - "port": "port", - "database": "database", - "ssl_mode": "sslmode", - "ca_cert": "sslrootcert" - } - - kw = dict((params_map[k], getattr(params, k)) for k in params_map - if getattr(params, k) != '' and getattr(params, k) is not None) - - # If a unix_socket is specified, incorporate it here. - is_localhost = "host" not in kw or kw["host"] == "" or kw["host"] == "localhost" - if is_localhost and params.unix_socket != "": - kw["host"] = params.unix_socket - - sslrootcert = params.ca_cert - if psycopg2.__version__ < '2.4.3' and sslrootcert is not None: - raise ValueError('psycopg2 must be at least 2.4.3 in order to user the ca_cert parameter') - - self.connection = psycopg2.connect(**kw) - self.cursor = self.connection.cursor() - - def commit(self): - self.connection.commit() - - def rollback(self): - self.connection.rollback() - - @property - def encoding(self): - """Connection encoding in Python-compatible form""" - return psycopg2.extensions.encodings[self.connection.encoding] - - # Methods for querying database objects - - # PostgreSQL < 9.0 doesn't support "ALL TABLES IN SCHEMA schema"-like - # phrases in GRANT or REVOKE statements, therefore alternative methods are - # provided here. - - def schema_exists(self, schema): - query = """SELECT count(*) - FROM pg_catalog.pg_namespace WHERE nspname = %s""" - self.cursor.execute(query, (schema,)) - return self.cursor.fetchone()[0] > 0 - - def get_all_tables_in_schema(self, schema): - if not self.schema_exists(schema): - raise Error('Schema "%s" does not exist.' % schema) - query = """SELECT relname - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind in ('r', 'v', 'm', 'p')""" - self.cursor.execute(query, (schema,)) - return [t[0] for t in self.cursor.fetchall()] - - def get_all_sequences_in_schema(self, schema): - if not self.schema_exists(schema): - raise Error('Schema "%s" does not exist.' % schema) - query = """SELECT relname - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind = 'S'""" - self.cursor.execute(query, (schema,)) - return [t[0] for t in self.cursor.fetchall()] - - def get_all_functions_in_schema(self, schema): - if not self.schema_exists(schema): - raise Error('Schema "%s" does not exist.' % schema) - query = """SELECT p.proname, oidvectortypes(p.proargtypes) - FROM pg_catalog.pg_proc p - JOIN pg_namespace n ON n.oid = p.pronamespace - WHERE nspname = %s""" - self.cursor.execute(query, (schema,)) - return ["%s(%s)" % (t[0], t[1]) for t in self.cursor.fetchall()] - - # Methods for getting access control lists and group membership info - - # To determine whether anything has changed after granting/revoking - # privileges, we compare the access control lists of the specified database - # objects before and afterwards. Python's list/string comparison should - # suffice for change detection, we should not actually have to parse ACLs. - # The same should apply to group membership information. - - def get_table_acls(self, schema, tables): - query = """SELECT relacl - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind in ('r','p','v','m') AND relname = ANY (%s) - ORDER BY relname""" - self.cursor.execute(query, (schema, tables)) - return [t[0] for t in self.cursor.fetchall()] - - def get_sequence_acls(self, schema, sequences): - query = """SELECT relacl - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE nspname = %s AND relkind = 'S' AND relname = ANY (%s) - ORDER BY relname""" - self.cursor.execute(query, (schema, sequences)) - return [t[0] for t in self.cursor.fetchall()] - - def get_function_acls(self, schema, function_signatures): - funcnames = [f.split('(', 1)[0] for f in function_signatures] - query = """SELECT proacl - FROM pg_catalog.pg_proc p - JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace - WHERE nspname = %s AND proname = ANY (%s) - ORDER BY proname, proargtypes""" - self.cursor.execute(query, (schema, funcnames)) - return [t[0] for t in self.cursor.fetchall()] - - def get_schema_acls(self, schemas): - query = """SELECT nspacl FROM pg_catalog.pg_namespace - WHERE nspname = ANY (%s) ORDER BY nspname""" - self.cursor.execute(query, (schemas,)) - return [t[0] for t in self.cursor.fetchall()] - - def get_language_acls(self, languages): - query = """SELECT lanacl FROM pg_catalog.pg_language - WHERE lanname = ANY (%s) ORDER BY lanname""" - self.cursor.execute(query, (languages,)) - return [t[0] for t in self.cursor.fetchall()] - - def get_tablespace_acls(self, tablespaces): - query = """SELECT spcacl FROM pg_catalog.pg_tablespace - WHERE spcname = ANY (%s) ORDER BY spcname""" - self.cursor.execute(query, (tablespaces,)) - return [t[0] for t in self.cursor.fetchall()] - - def get_database_acls(self, databases): - query = """SELECT datacl FROM pg_catalog.pg_database - WHERE datname = ANY (%s) ORDER BY datname""" - self.cursor.execute(query, (databases,)) - return [t[0] for t in self.cursor.fetchall()] - - def get_group_memberships(self, groups): - query = """SELECT roleid, grantor, member, admin_option - FROM pg_catalog.pg_auth_members am - JOIN pg_catalog.pg_roles r ON r.oid = am.roleid - WHERE r.rolname = ANY(%s) - ORDER BY roleid, grantor, member""" - self.cursor.execute(query, (groups,)) - return self.cursor.fetchall() - - def get_default_privs(self, schema, *args): - query = """SELECT defaclacl - FROM pg_default_acl a - JOIN pg_namespace b ON a.defaclnamespace=b.oid - WHERE b.nspname = %s;""" - self.cursor.execute(query, (schema,)) - return [t[0] for t in self.cursor.fetchall()] - - def get_foreign_data_wrapper_acls(self, fdws): - query = """SELECT fdwacl FROM pg_catalog.pg_foreign_data_wrapper - WHERE fdwname = ANY (%s) ORDER BY fdwname""" - self.cursor.execute(query, (fdws,)) - return [t[0] for t in self.cursor.fetchall()] - - def get_foreign_server_acls(self, fs): - query = """SELECT srvacl FROM pg_catalog.pg_foreign_server - WHERE srvname = ANY (%s) ORDER BY srvname""" - self.cursor.execute(query, (fs,)) - return [t[0] for t in self.cursor.fetchall()] - - def get_type_acls(self, schema, types): - query = """SELECT t.typacl FROM pg_catalog.pg_type t - JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace - WHERE n.nspname = %s AND t.typname = ANY (%s) ORDER BY typname""" - self.cursor.execute(query, (schema, types)) - return [t[0] for t in self.cursor.fetchall()] - - # Manipulating privileges - - def manipulate_privs(self, obj_type, privs, objs, roles, target_roles, - state, grant_option, schema_qualifier=None, fail_on_role=True): - """Manipulate database object privileges. - - :param obj_type: Type of database object to grant/revoke - privileges for. - :param privs: Either a list of privileges to grant/revoke - or None if type is "group". - :param objs: List of database objects to grant/revoke - privileges for. - :param roles: Either a list of role names or "PUBLIC" - for the implicitly defined "PUBLIC" group - :param target_roles: List of role names to grant/revoke - default privileges as. - :param state: "present" to grant privileges, "absent" to revoke. - :param grant_option: Only for state "present": If True, set - grant/admin option. If False, revoke it. - If None, don't change grant option. - :param schema_qualifier: Some object types ("TABLE", "SEQUENCE", - "FUNCTION") must be qualified by schema. - Ignored for other Types. - """ - # get_status: function to get current status - if obj_type == 'table': - get_status = partial(self.get_table_acls, schema_qualifier) - elif obj_type == 'sequence': - get_status = partial(self.get_sequence_acls, schema_qualifier) - elif obj_type == 'function': - get_status = partial(self.get_function_acls, schema_qualifier) - elif obj_type == 'schema': - get_status = self.get_schema_acls - elif obj_type == 'language': - get_status = self.get_language_acls - elif obj_type == 'tablespace': - get_status = self.get_tablespace_acls - elif obj_type == 'database': - get_status = self.get_database_acls - elif obj_type == 'group': - get_status = self.get_group_memberships - elif obj_type == 'default_privs': - get_status = partial(self.get_default_privs, schema_qualifier) - elif obj_type == 'foreign_data_wrapper': - get_status = self.get_foreign_data_wrapper_acls - elif obj_type == 'foreign_server': - get_status = self.get_foreign_server_acls - elif obj_type == 'type': - get_status = partial(self.get_type_acls, schema_qualifier) - else: - raise Error('Unsupported database object type "%s".' % obj_type) - - # Return False (nothing has changed) if there are no objs to work on. - if not objs: - return False - - # obj_ids: quoted db object identifiers (sometimes schema-qualified) - if obj_type == 'function': - obj_ids = [] - for obj in objs: - try: - f, args = obj.split('(', 1) - except Exception: - raise Error('Illegal function signature: "%s".' % obj) - obj_ids.append('"%s"."%s"(%s' % (schema_qualifier, f, args)) - elif obj_type in ['table', 'sequence', 'type']: - obj_ids = ['"%s"."%s"' % (schema_qualifier, o) for o in objs] - else: - obj_ids = ['"%s"' % o for o in objs] - - # set_what: SQL-fragment specifying what to set for the target roles: - # Either group membership or privileges on objects of a certain type - if obj_type == 'group': - set_what = ','.join('"%s"' % i for i in obj_ids) - elif obj_type == 'default_privs': - # We don't want privs to be quoted here - set_what = ','.join(privs) - else: - # function types are already quoted above - if obj_type != 'function': - obj_ids = [pg_quote_identifier(i, 'table') for i in obj_ids] - # Note: obj_type has been checked against a set of string literals - # and privs was escaped when it was parsed - # Note: Underscores are replaced with spaces to support multi-word obj_type - set_what = '%s ON %s %s' % (','.join(privs), obj_type.replace('_', ' '), - ','.join(obj_ids)) - - # for_whom: SQL-fragment specifying for whom to set the above - if roles == 'PUBLIC': - for_whom = 'PUBLIC' - else: - for_whom = [] - for r in roles: - if not role_exists(self.module, self.cursor, r): - if fail_on_role: - self.module.fail_json(msg="Role '%s' does not exist" % r.strip()) - - else: - self.module.warn("Role '%s' does not exist, pass it" % r.strip()) - else: - for_whom.append('"%s"' % r) - - if not for_whom: - return False - - for_whom = ','.join(for_whom) - - # as_who: - as_who = None - if target_roles: - as_who = ','.join('"%s"' % r for r in target_roles) - - status_before = get_status(objs) - - query = QueryBuilder(state) \ - .for_objtype(obj_type) \ - .with_grant_option(grant_option) \ - .for_whom(for_whom) \ - .as_who(as_who) \ - .for_schema(schema_qualifier) \ - .set_what(set_what) \ - .for_objs(objs) \ - .build() - - executed_queries.append(query) - self.cursor.execute(query) - status_after = get_status(objs) - - def nonesorted(e): - # For python 3+ that can fail trying - # to compare NoneType elements by sort method. - if e is None: - return '' - return e - - status_before.sort(key=nonesorted) - status_after.sort(key=nonesorted) - return status_before != status_after - - -class QueryBuilder(object): - def __init__(self, state): - self._grant_option = None - self._for_whom = None - self._as_who = None - self._set_what = None - self._obj_type = None - self._state = state - self._schema = None - self._objs = None - self.query = [] - - def for_objs(self, objs): - self._objs = objs - return self - - def for_schema(self, schema): - self._schema = schema - return self - - def with_grant_option(self, option): - self._grant_option = option - return self - - def for_whom(self, who): - self._for_whom = who - return self - - def as_who(self, target_roles): - self._as_who = target_roles - return self - - def set_what(self, what): - self._set_what = what - return self - - def for_objtype(self, objtype): - self._obj_type = objtype - return self - - def build(self): - if self._state == 'present': - self.build_present() - elif self._state == 'absent': - self.build_absent() - else: - self.build_absent() - return '\n'.join(self.query) - - def add_default_revoke(self): - for obj in self._objs: - if self._as_who: - self.query.append( - 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} REVOKE ALL ON {2} FROM {3};'.format(self._as_who, - self._schema, obj, - self._for_whom)) - else: - self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, - self._for_whom)) - - def add_grant_option(self): - if self._grant_option: - if self._obj_type == 'group': - self.query[-1] += ' WITH ADMIN OPTION;' - else: - self.query[-1] += ' WITH GRANT OPTION;' - else: - self.query[-1] += ';' - if self._obj_type == 'group': - self.query.append('REVOKE ADMIN OPTION FOR {0} FROM {1};'.format(self._set_what, self._for_whom)) - elif not self._obj_type == 'default_privs': - self.query.append('REVOKE GRANT OPTION FOR {0} FROM {1};'.format(self._set_what, self._for_whom)) - - def add_default_priv(self): - for obj in self._objs: - if self._as_who: - self.query.append( - 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} GRANT {2} ON {3} TO {4}'.format(self._as_who, - self._schema, - self._set_what, - obj, - self._for_whom)) - else: - self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT {1} ON {2} TO {3}'.format(self._schema, - self._set_what, - obj, - self._for_whom)) - self.add_grant_option() - if self._as_who: - self.query.append( - 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} GRANT USAGE ON TYPES TO {2}'.format(self._as_who, - self._schema, - self._for_whom)) - else: - self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} GRANT USAGE ON TYPES TO {1}'.format(self._schema, self._for_whom)) - self.add_grant_option() - - def build_present(self): - if self._obj_type == 'default_privs': - self.add_default_revoke() - self.add_default_priv() - else: - self.query.append('GRANT {0} TO {1}'.format(self._set_what, self._for_whom)) - self.add_grant_option() - - def build_absent(self): - if self._obj_type == 'default_privs': - self.query = [] - for obj in ['TABLES', 'SEQUENCES', 'TYPES']: - if self._as_who: - self.query.append( - 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} IN SCHEMA {1} REVOKE ALL ON {2} FROM {3};'.format(self._as_who, - self._schema, obj, - self._for_whom)) - else: - self.query.append( - 'ALTER DEFAULT PRIVILEGES IN SCHEMA {0} REVOKE ALL ON {1} FROM {2};'.format(self._schema, obj, - self._for_whom)) - else: - self.query.append('REVOKE {0} FROM {1};'.format(self._set_what, self._for_whom)) - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - database=dict(required=True, aliases=['db', 'login_db']), - state=dict(default='present', choices=['present', 'absent']), - privs=dict(required=False, aliases=['priv']), - type=dict(default='table', - choices=['table', - 'sequence', - 'function', - 'database', - 'schema', - 'language', - 'tablespace', - 'group', - 'default_privs', - 'foreign_data_wrapper', - 'foreign_server', - 'type', ]), - objs=dict(required=False, aliases=['obj']), - schema=dict(required=False), - roles=dict(required=True, aliases=['role']), - session_role=dict(required=False), - target_roles=dict(required=False), - grant_option=dict(required=False, type='bool', - aliases=['admin_option']), - host=dict(default='', aliases=['login_host']), - unix_socket=dict(default='', aliases=['login_unix_socket']), - login=dict(default='postgres', aliases=['login_user']), - password=dict(default='', aliases=['login_password'], no_log=True), - fail_on_role=dict(type='bool', default=True), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - fail_on_role = module.params['fail_on_role'] - - # Create type object as namespace for module params - p = type('Params', (), module.params) - # param "schema": default, allowed depends on param "type" - if p.type in ['table', 'sequence', 'function', 'type', 'default_privs']: - p.schema = p.schema or 'public' - elif p.schema: - module.fail_json(msg='Argument "schema" is not allowed ' - 'for type "%s".' % p.type) - - # param "objs": default, required depends on param "type" - if p.type == 'database': - p.objs = p.objs or p.database - elif not p.objs: - module.fail_json(msg='Argument "objs" is required ' - 'for type "%s".' % p.type) - - # param "privs": allowed, required depends on param "type" - if p.type == 'group': - if p.privs: - module.fail_json(msg='Argument "privs" is not allowed ' - 'for type "group".') - elif not p.privs: - module.fail_json(msg='Argument "privs" is required ' - 'for type "%s".' % p.type) - - # Connect to Database - if not psycopg2: - module.fail_json(msg=missing_required_lib('psycopg2'), exception=PSYCOPG2_IMP_ERR) - try: - conn = Connection(p, module) - except psycopg2.Error as e: - module.fail_json(msg='Could not connect to database: %s' % to_native(e), exception=traceback.format_exc()) - except TypeError as e: - if 'sslrootcert' in e.args[0]: - module.fail_json(msg='Postgresql server must be at least version 8.4 to support sslrootcert') - module.fail_json(msg="unable to connect to database: %s" % to_native(e), exception=traceback.format_exc()) - except ValueError as e: - # We raise this when the psycopg library is too old - module.fail_json(msg=to_native(e)) - - if p.session_role: - try: - conn.cursor.execute('SET ROLE "%s"' % p.session_role) - except Exception as e: - module.fail_json(msg="Could not switch to role %s: %s" % (p.session_role, to_native(e)), exception=traceback.format_exc()) - - try: - # privs - if p.privs: - privs = frozenset(pr.upper() for pr in p.privs.split(',')) - if not privs.issubset(VALID_PRIVS): - module.fail_json(msg='Invalid privileges specified: %s' % privs.difference(VALID_PRIVS)) - else: - privs = None - # objs: - if p.type == 'table' and p.objs == 'ALL_IN_SCHEMA': - objs = conn.get_all_tables_in_schema(p.schema) - elif p.type == 'sequence' and p.objs == 'ALL_IN_SCHEMA': - objs = conn.get_all_sequences_in_schema(p.schema) - elif p.type == 'function' and p.objs == 'ALL_IN_SCHEMA': - objs = conn.get_all_functions_in_schema(p.schema) - elif p.type == 'default_privs': - if p.objs == 'ALL_DEFAULT': - objs = frozenset(VALID_DEFAULT_OBJS.keys()) - else: - objs = frozenset(obj.upper() for obj in p.objs.split(',')) - if not objs.issubset(VALID_DEFAULT_OBJS): - module.fail_json( - msg='Invalid Object set specified: %s' % objs.difference(VALID_DEFAULT_OBJS.keys())) - # Again, do we have valid privs specified for object type: - valid_objects_for_priv = frozenset(obj for obj in objs if privs.issubset(VALID_DEFAULT_OBJS[obj])) - if not valid_objects_for_priv == objs: - module.fail_json( - msg='Invalid priv specified. Valid object for priv: {0}. Objects: {1}'.format( - valid_objects_for_priv, objs)) - else: - objs = p.objs.split(',') - - # function signatures are encoded using ':' to separate args - if p.type == 'function': - objs = [obj.replace(':', ',') for obj in objs] - - # roles - if p.roles == 'PUBLIC': - roles = 'PUBLIC' - else: - roles = p.roles.split(',') - - if len(roles) == 1 and not role_exists(module, conn.cursor, roles[0]): - module.exit_json(changed=False) - - if fail_on_role: - module.fail_json(msg="Role '%s' does not exist" % roles[0].strip()) - - else: - module.warn("Role '%s' does not exist, nothing to do" % roles[0].strip()) - - # check if target_roles is set with type: default_privs - if p.target_roles and not p.type == 'default_privs': - module.warn('"target_roles" will be ignored ' - 'Argument "type: default_privs" is required for usage of "target_roles".') - - # target roles - if p.target_roles: - target_roles = p.target_roles.split(',') - else: - target_roles = None - - changed = conn.manipulate_privs( - obj_type=p.type, - privs=privs, - objs=objs, - roles=roles, - target_roles=target_roles, - state=p.state, - grant_option=p.grant_option, - schema_qualifier=p.schema, - fail_on_role=fail_on_role, - ) - - except Error as e: - conn.rollback() - module.fail_json(msg=e.message, exception=traceback.format_exc()) - - except psycopg2.Error as e: - conn.rollback() - module.fail_json(msg=to_native(e.message)) - - if module.check_mode: - conn.rollback() - else: - conn.commit() - module.exit_json(changed=changed, queries=executed_queries) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_publication.py b/lib/ansible/modules/database/postgresql/postgresql_publication.py deleted file mode 100644 index 863c6fc66d..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_publication.py +++ /dev/null @@ -1,655 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Loic Blot (@nerzhul) <loic.blot@unix-experience.fr> -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: postgresql_publication -short_description: Add, update, or remove PostgreSQL publication -description: -- Add, update, or remove PostgreSQL publication. -version_added: "2.9" -options: - name: - description: - - Name of the publication to add, update, or remove. - required: true - type: str - db: - description: - - Name of the database to connect to and where - the publication state will be changed. - aliases: [ login_db ] - type: str - tables: - description: - - List of tables to add to the publication. - - If no value is set all tables are targeted. - - If the publication already exists for specific tables and I(tables) is not passed, - nothing will be changed. If you need to add all tables to the publication with the same name, - drop existent and create new without passing I(tables). - type: list - elements: str - state: - description: - - The publication state. - default: present - choices: [ absent, present ] - type: str - parameters: - description: - - Dictionary with optional publication parameters. - - Available parameters depend on PostgreSQL version. - type: dict - owner: - description: - - Publication owner. - - If I(owner) is not defined, the owner will be set as I(login_user) or I(session_role). - type: str - cascade: - description: - - Drop publication dependencies. Has effect with I(state=absent) only. - type: bool - default: false -notes: -- PostgreSQL version must be 10 or greater. -seealso: -- name: CREATE PUBLICATION reference - description: Complete reference of the CREATE PUBLICATION command documentation. - link: https://www.postgresql.org/docs/current/sql-createpublication.html -- name: ALTER PUBLICATION reference - description: Complete reference of the ALTER PUBLICATION command documentation. - link: https://www.postgresql.org/docs/current/sql-alterpublication.html -- name: DROP PUBLICATION reference - description: Complete reference of the DROP PUBLICATION command documentation. - link: https://www.postgresql.org/docs/current/sql-droppublication.html -author: -- Loic Blot (@nerzhul) <loic.blot@unix-experience.fr> -- Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -extends_documentation_fragment: -- postgres -''' - -EXAMPLES = r''' -- name: Create a new publication with name "acme" targeting all tables in database "test". - postgresql_publication: - db: test - name: acme - -- name: Create publication "acme" publishing only prices and vehicles tables. - postgresql_publication: - name: acme - tables: - - prices - - vehicles - -- name: > - Create publication "acme", set user alice as an owner, targeting all tables. - Allowable DML operations are INSERT and UPDATE only - postgresql_publication: - name: acme - owner: alice - parameters: - publish: 'insert,update' - -- name: > - Assuming publication "acme" exists and there are targeted - tables "prices" and "vehicles", add table "stores" to the publication. - postgresql_publication: - name: acme - tables: - - prices - - vehicles - - stores - -- name: Remove publication "acme" if exists in database "test". - postgresql_publication: - db: test - name: acme - state: absent -''' - -RETURN = r''' -exists: - description: - - Flag indicates the publication exists or not at the end of runtime. - returned: always - type: bool - sample: true -queries: - description: List of executed queries. - returned: always - type: str - sample: [ 'DROP PUBLICATION "acme" CASCADE' ] -owner: - description: Owner of the publication at the end of runtime. - returned: if publication exists - type: str - sample: "alice" -tables: - description: - - List of tables in the publication at the end of runtime. - - If all tables are published, returns empty list. - returned: if publication exists - type: list - sample: ["\"public\".\"prices\"", "\"public\".\"vehicles\""] -alltables: - description: - - Flag indicates that all tables are published. - returned: if publication exists - type: bool - sample: false -parameters: - description: Publication parameters at the end of runtime. - returned: if publication exists - type: dict - sample: {'publish': {'insert': false, 'delete': false, 'update': true}} -''' - - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import pg_quote_identifier -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) -from ansible.module_utils.six import iteritems - -SUPPORTED_PG_VERSION = 10000 - - -################################ -# Module functions and classes # -################################ - -def transform_tables_representation(tbl_list): - """Add 'public.' to names of tables where a schema identifier is absent - and add quotes to each element. - - Args: - tbl_list (list): List of table names. - - Returns: - tbl_list (list): Changed list. - """ - for i, table in enumerate(tbl_list): - if '.' not in table: - tbl_list[i] = pg_quote_identifier('public.%s' % table.strip(), 'table') - else: - tbl_list[i] = pg_quote_identifier(table.strip(), 'table') - - return tbl_list - - -class PgPublication(): - """Class to work with PostgreSQL publication. - - Args: - module (AnsibleModule): Object of AnsibleModule class. - cursor (cursor): Cursor object of psycopg2 library to work with PostgreSQL. - name (str): The name of the publication. - - Attributes: - module (AnsibleModule): Object of AnsibleModule class. - cursor (cursor): Cursor object of psycopg2 library to work with PostgreSQL. - name (str): Name of the publication. - executed_queries (list): List of executed queries. - attrs (dict): Dict with publication attributes. - exists (bool): Flag indicates the publication exists or not. - """ - - def __init__(self, module, cursor, name): - self.module = module - self.cursor = cursor - self.name = name - self.executed_queries = [] - self.attrs = { - 'alltables': False, - 'tables': [], - 'parameters': {}, - 'owner': '', - } - self.exists = self.check_pub() - - def get_info(self): - """Refresh the publication information. - - Returns: - ``self.attrs``. - """ - self.exists = self.check_pub() - return self.attrs - - def check_pub(self): - """Check the publication and refresh ``self.attrs`` publication attribute. - - Returns: - True if the publication with ``self.name`` exists, False otherwise. - """ - - pub_info = self.__get_general_pub_info() - - if not pub_info: - # Publication does not exist: - return False - - self.attrs['owner'] = pub_info.get('pubowner') - - # Publication DML operations: - self.attrs['parameters']['publish'] = {} - self.attrs['parameters']['publish']['insert'] = pub_info.get('pubinsert', False) - self.attrs['parameters']['publish']['update'] = pub_info.get('pubupdate', False) - self.attrs['parameters']['publish']['delete'] = pub_info.get('pubdelete', False) - if pub_info.get('pubtruncate'): - self.attrs['parameters']['publish']['truncate'] = pub_info.get('pubtruncate') - - # If alltables flag is False, get the list of targeted tables: - if not pub_info.get('puballtables'): - table_info = self.__get_tables_pub_info() - # Join sublists [['schema', 'table'], ...] to ['schema.table', ...] - # for better representation: - for i, schema_and_table in enumerate(table_info): - table_info[i] = pg_quote_identifier('.'.join(schema_and_table), 'table') - - self.attrs['tables'] = table_info - else: - self.attrs['alltables'] = True - - # Publication exists: - return True - - def create(self, tables, params, owner, check_mode=True): - """Create the publication. - - Args: - tables (list): List with names of the tables that need to be added to the publication. - params (dict): Dict contains optional publication parameters and their values. - owner (str): Name of the publication owner. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - changed (bool): True if publication has been created, otherwise False. - """ - changed = True - - query_fragments = ["CREATE PUBLICATION %s" % pg_quote_identifier(self.name, 'publication')] - - if tables: - query_fragments.append("FOR TABLE %s" % ', '.join(tables)) - else: - query_fragments.append("FOR ALL TABLES") - - if params: - params_list = [] - # Make list ["param = 'value'", ...] from params dict: - for (key, val) in iteritems(params): - params_list.append("%s = '%s'" % (key, val)) - - # Add the list to query_fragments: - query_fragments.append("WITH (%s)" % ', '.join(params_list)) - - changed = self.__exec_sql(' '.join(query_fragments), check_mode=check_mode) - - if owner: - # If check_mode, just add possible SQL to - # executed_queries and return: - self.__pub_set_owner(owner, check_mode=check_mode) - - return changed - - def update(self, tables, params, owner, check_mode=True): - """Update the publication. - - Args: - tables (list): List with names of the tables that need to be presented in the publication. - params (dict): Dict contains optional publication parameters and their values. - owner (str): Name of the publication owner. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - changed (bool): True if publication has been updated, otherwise False. - """ - changed = False - - # Add or drop tables from published tables suit: - if tables and not self.attrs['alltables']: - - # 1. If needs to add table to the publication: - for tbl in tables: - if tbl not in self.attrs['tables']: - # If needs to add table to the publication: - changed = self.__pub_add_table(tbl, check_mode=check_mode) - - # 2. if there is a table in targeted tables - # that's not presented in the passed tables: - for tbl in self.attrs['tables']: - if tbl not in tables: - changed = self.__pub_drop_table(tbl, check_mode=check_mode) - - elif tables and self.attrs['alltables']: - changed = self.__pub_set_tables(tables, check_mode=check_mode) - - # Update pub parameters: - if params: - for key, val in iteritems(params): - if self.attrs['parameters'].get(key): - - # In PostgreSQL 10/11 only 'publish' optional parameter is presented. - if key == 'publish': - # 'publish' value can be only a string with comma-separated items - # of allowed DML operations like 'insert,update' or - # 'insert,update,delete', etc. - # Make dictionary to compare with current attrs later: - val_dict = self.attrs['parameters']['publish'].copy() - val_list = val.split(',') - for v in val_dict: - if v in val_list: - val_dict[v] = True - else: - val_dict[v] = False - - # Compare val_dict and the dict with current 'publish' parameters, - # if they're different, set new values: - if val_dict != self.attrs['parameters']['publish']: - changed = self.__pub_set_param(key, val, check_mode=check_mode) - - # Default behavior for other cases: - elif self.attrs['parameters'][key] != val: - changed = self.__pub_set_param(key, val, check_mode=check_mode) - - else: - # If the parameter was not set before: - changed = self.__pub_set_param(key, val, check_mode=check_mode) - - # Update pub owner: - if owner: - if owner != self.attrs['owner']: - changed = self.__pub_set_owner(owner, check_mode=check_mode) - - return changed - - def drop(self, cascade=False, check_mode=True): - """Drop the publication. - - Kwargs: - cascade (bool): Flag indicates that publication needs to be deleted - with its dependencies. - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - changed (bool): True if publication has been updated, otherwise False. - """ - if self.exists: - query_fragments = [] - query_fragments.append("DROP PUBLICATION %s" % pg_quote_identifier(self.name, 'publication')) - if cascade: - query_fragments.append("CASCADE") - - return self.__exec_sql(' '.join(query_fragments), check_mode=check_mode) - - def __get_general_pub_info(self): - """Get and return general publication information. - - Returns: - Dict with publication information if successful, False otherwise. - """ - # Check pg_publication.pubtruncate exists (supported from PostgreSQL 11): - pgtrunc_sup = exec_sql(self, ("SELECT 1 FROM information_schema.columns " - "WHERE table_name = 'pg_publication' " - "AND column_name = 'pubtruncate'"), add_to_executed=False) - - if pgtrunc_sup: - query = ("SELECT r.rolname AS pubowner, p.puballtables, p.pubinsert, " - "p.pubupdate , p.pubdelete, p.pubtruncate FROM pg_publication AS p " - "JOIN pg_catalog.pg_roles AS r " - "ON p.pubowner = r.oid " - "WHERE p.pubname = %(pname)s") - else: - query = ("SELECT r.rolname AS pubowner, p.puballtables, p.pubinsert, " - "p.pubupdate , p.pubdelete FROM pg_publication AS p " - "JOIN pg_catalog.pg_roles AS r " - "ON p.pubowner = r.oid " - "WHERE p.pubname = %(pname)s") - - result = exec_sql(self, query, query_params={'pname': self.name}, add_to_executed=False) - if result: - return result[0] - else: - return False - - def __get_tables_pub_info(self): - """Get and return tables that are published by the publication. - - Returns: - List of dicts with published tables. - """ - query = ("SELECT schemaname, tablename " - "FROM pg_publication_tables WHERE pubname = %(pname)s") - return exec_sql(self, query, query_params={'pname': self.name}, add_to_executed=False) - - def __pub_add_table(self, table, check_mode=False): - """Add a table to the publication. - - Args: - table (str): Table name. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - query = ("ALTER PUBLICATION %s ADD TABLE %s" % (pg_quote_identifier(self.name, 'publication'), - pg_quote_identifier(table, 'table'))) - return self.__exec_sql(query, check_mode=check_mode) - - def __pub_drop_table(self, table, check_mode=False): - """Drop a table from the publication. - - Args: - table (str): Table name. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - query = ("ALTER PUBLICATION %s DROP TABLE %s" % (pg_quote_identifier(self.name, 'publication'), - pg_quote_identifier(table, 'table'))) - return self.__exec_sql(query, check_mode=check_mode) - - def __pub_set_tables(self, tables, check_mode=False): - """Set a table suit that need to be published by the publication. - - Args: - tables (list): List of tables. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - quoted_tables = [pg_quote_identifier(t, 'table') for t in tables] - query = ("ALTER PUBLICATION %s SET TABLE %s" % (pg_quote_identifier(self.name, 'publication'), - ', '.join(quoted_tables))) - return self.__exec_sql(query, check_mode=check_mode) - - def __pub_set_param(self, param, value, check_mode=False): - """Set an optional publication parameter. - - Args: - param (str): Name of the parameter. - value (str): Parameter value. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - query = ("ALTER PUBLICATION %s SET (%s = '%s')" % (pg_quote_identifier(self.name, 'publication'), - param, value)) - return self.__exec_sql(query, check_mode=check_mode) - - def __pub_set_owner(self, role, check_mode=False): - """Set a publication owner. - - Args: - role (str): Role (user) name that needs to be set as a publication owner. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - query = ("ALTER PUBLICATION %s OWNER TO %s" % (pg_quote_identifier(self.name, 'publication'), - pg_quote_identifier(role, 'role'))) - return self.__exec_sql(query, check_mode=check_mode) - - def __exec_sql(self, query, check_mode=False): - """Execute SQL query. - - Note: If we need just to get information from the database, - we use ``exec_sql`` function directly. - - Args: - query (str): Query that needs to be executed. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just add ``query`` to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - if check_mode: - self.executed_queries.append(query) - return True - else: - return exec_sql(self, query, ddl=True) - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - name=dict(required=True), - db=dict(type='str', aliases=['login_db']), - state=dict(type='str', default='present', choices=['absent', 'present']), - tables=dict(type='list', elements='str'), - parameters=dict(type='dict'), - owner=dict(type='str'), - cascade=dict(type='bool', default=False), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - # Parameters handling: - name = module.params['name'] - state = module.params['state'] - tables = module.params['tables'] - params = module.params['parameters'] - owner = module.params['owner'] - cascade = module.params['cascade'] - - if state == 'absent': - if tables: - module.warn('parameter "tables" is ignored when "state=absent"') - if params: - module.warn('parameter "parameters" is ignored when "state=absent"') - if owner: - module.warn('parameter "owner" is ignored when "state=absent"') - - if state == 'present' and cascade: - module.warn('parameter "cascade" is ignored when "state=present"') - - # Connect to DB and make cursor object: - conn_params = get_conn_params(module, module.params) - # We check publication state without DML queries execution, so set autocommit: - db_connection = connect_to_db(module, conn_params, autocommit=True) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - # Check version: - if cursor.connection.server_version < SUPPORTED_PG_VERSION: - module.fail_json(msg="PostgreSQL server version should be 10.0 or greater") - - # Nothing was changed by default: - changed = False - - ################################### - # Create object and do rock'n'roll: - publication = PgPublication(module, cursor, name) - - if tables: - tables = transform_tables_representation(tables) - - # If module.check_mode=True, nothing will be changed: - if state == 'present': - if not publication.exists: - changed = publication.create(tables, params, owner, check_mode=module.check_mode) - - else: - changed = publication.update(tables, params, owner, check_mode=module.check_mode) - - elif state == 'absent': - changed = publication.drop(cascade=cascade, check_mode=module.check_mode) - - # Get final publication info: - pub_fin_info = {} - if state == 'present' or (state == 'absent' and module.check_mode): - pub_fin_info = publication.get_info() - elif state == 'absent' and not module.check_mode: - publication.exists = False - - # Connection is not needed any more: - cursor.close() - db_connection.close() - - # Update publication info and return ret values: - module.exit_json(changed=changed, queries=publication.executed_queries, exists=publication.exists, **pub_fin_info) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_query.py b/lib/ansible/modules/database/postgresql/postgresql_query.py deleted file mode 100644 index 18d63e332a..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_query.py +++ /dev/null @@ -1,364 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2017, Felix Archambault -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'supported_by': 'community', - 'status': ['preview'] -} - -DOCUMENTATION = r''' ---- -module: postgresql_query -short_description: Run PostgreSQL queries -description: -- Runs arbitrary PostgreSQL queries. -- Can run queries from SQL script files. -- Does not run against backup files. Use M(postgresql_db) with I(state=restore) - to run queries on files made by pg_dump/pg_dumpall utilities. -version_added: '2.8' -options: - query: - description: - - SQL query to run. Variables can be escaped with psycopg2 syntax - U(http://initd.org/psycopg/docs/usage.html). - type: str - positional_args: - description: - - List of values to be passed as positional arguments to the query. - When the value is a list, it will be converted to PostgreSQL array. - - Mutually exclusive with I(named_args). - type: list - elements: raw - named_args: - description: - - Dictionary of key-value arguments to pass to the query. - When the value is a list, it will be converted to PostgreSQL array. - - Mutually exclusive with I(positional_args). - type: dict - path_to_script: - description: - - Path to SQL script on the remote host. - - Returns result of the last query in the script. - - Mutually exclusive with I(query). - type: path - session_role: - description: - - Switch to session_role after connecting. The specified session_role must - be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str - db: - description: - - Name of database to connect to and run queries against. - type: str - aliases: - - login_db - autocommit: - description: - - Execute in autocommit mode when the query can't be run inside a transaction block - (e.g., VACUUM). - - Mutually exclusive with I(check_mode). - type: bool - default: no - version_added: '2.9' - encoding: - description: - - Set the client encoding for the current session (e.g. C(UTF-8)). - - The default is the encoding defined by the database. - type: str - version_added: '2.10' -seealso: -- module: postgresql_db -author: -- Felix Archambault (@archf) -- Andrew Klychkov (@Andersson007) -- Will Rouesnel (@wrouesnel) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Simple select query to acme db - postgresql_query: - db: acme - query: SELECT version() - -- name: Select query to db acme with positional arguments and non-default credentials - postgresql_query: - db: acme - login_user: django - login_password: mysecretpass - query: SELECT * FROM acme WHERE id = %s AND story = %s - positional_args: - - 1 - - test - -- name: Select query to test_db with named_args - postgresql_query: - db: test_db - query: SELECT * FROM test WHERE id = %(id_val)s AND story = %(story_val)s - named_args: - id_val: 1 - story_val: test - -- name: Insert query to test_table in db test_db - postgresql_query: - db: test_db - query: INSERT INTO test_table (id, story) VALUES (2, 'my_long_story') - -- name: Run queries from SQL script using UTF-8 client encoding for session - postgresql_query: - db: test_db - path_to_script: /var/lib/pgsql/test.sql - positional_args: - - 1 - encoding: UTF-8 - -- name: Example of using autocommit parameter - postgresql_query: - db: test_db - query: VACUUM - autocommit: yes - -- name: > - Insert data to the column of array type using positional_args. - Note that we use quotes here, the same as for passing JSON, etc. - postgresql_query: - query: INSERT INTO test_table (array_column) VALUES (%s) - positional_args: - - '{1,2,3}' - -# Pass list and string vars as positional_args -- name: Set vars - set_fact: - my_list: - - 1 - - 2 - - 3 - my_arr: '{1, 2, 3}' - -- name: Select from test table by passing positional_args as arrays - postgresql_query: - query: SELECT * FROM test_array_table WHERE arr_col1 = %s AND arr_col2 = %s - positional_args: - - '{{ my_list }}' - - '{{ my_arr|string }}' -''' - -RETURN = r''' -query: - description: Query that was tried to be executed. - returned: always - type: str - sample: 'SELECT * FROM bar' -statusmessage: - description: Attribute containing the message returned by the command. - returned: always - type: str - sample: 'INSERT 0 1' -query_result: - description: - - List of dictionaries in column:value form representing returned rows. - returned: changed - type: list - sample: [{"Column": "Value1"},{"Column": "Value2"}] -rowcount: - description: Number of affected rows. - returned: changed - type: int - sample: 5 -''' - -try: - from psycopg2 import ProgrammingError as Psycopg2ProgrammingError - from psycopg2.extras import DictCursor -except ImportError: - # it is needed for checking 'no result to fetch' in main(), - # psycopg2 availability will be checked by connect_to_db() into - # ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - get_conn_params, - postgres_common_argument_spec, -) -from ansible.module_utils._text import to_native -from ansible.module_utils.six import iteritems - - -# =========================================== -# Module execution. -# - -def list_to_pg_array(elem): - """Convert the passed list to PostgreSQL array - represented as a string. - - Args: - elem (list): List that needs to be converted. - - Returns: - elem (str): String representation of PostgreSQL array. - """ - elem = str(elem).strip('[]') - elem = '{' + elem + '}' - return elem - - -def convert_elements_to_pg_arrays(obj): - """Convert list elements of the passed object - to PostgreSQL arrays represented as strings. - - Args: - obj (dict or list): Object whose elements need to be converted. - - Returns: - obj (dict or list): Object with converted elements. - """ - if isinstance(obj, dict): - for (key, elem) in iteritems(obj): - if isinstance(elem, list): - obj[key] = list_to_pg_array(elem) - - elif isinstance(obj, list): - for i, elem in enumerate(obj): - if isinstance(elem, list): - obj[i] = list_to_pg_array(elem) - - return obj - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - query=dict(type='str'), - db=dict(type='str', aliases=['login_db']), - positional_args=dict(type='list', elements='raw'), - named_args=dict(type='dict'), - session_role=dict(type='str'), - path_to_script=dict(type='path'), - autocommit=dict(type='bool', default=False), - encoding=dict(type='str'), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=(('positional_args', 'named_args'),), - supports_check_mode=True, - ) - - query = module.params["query"] - positional_args = module.params["positional_args"] - named_args = module.params["named_args"] - path_to_script = module.params["path_to_script"] - autocommit = module.params["autocommit"] - encoding = module.params["encoding"] - - if autocommit and module.check_mode: - module.fail_json(msg="Using autocommit is mutually exclusive with check_mode") - - if path_to_script and query: - module.fail_json(msg="path_to_script is mutually exclusive with query") - - if positional_args: - positional_args = convert_elements_to_pg_arrays(positional_args) - - elif named_args: - named_args = convert_elements_to_pg_arrays(named_args) - - if path_to_script: - try: - with open(path_to_script, 'rb') as f: - query = to_native(f.read()) - except Exception as e: - module.fail_json(msg="Cannot read file '%s' : %s" % (path_to_script, to_native(e))) - - conn_params = get_conn_params(module, module.params) - db_connection = connect_to_db(module, conn_params, autocommit=autocommit) - if encoding is not None: - db_connection.set_client_encoding(encoding) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - # Prepare args: - if module.params.get("positional_args"): - arguments = module.params["positional_args"] - elif module.params.get("named_args"): - arguments = module.params["named_args"] - else: - arguments = None - - # Set defaults: - changed = False - - # Execute query: - try: - cursor.execute(query, arguments) - except Exception as e: - if not autocommit: - db_connection.rollback() - - cursor.close() - db_connection.close() - module.fail_json(msg="Cannot execute SQL '%s' %s: %s" % (query, arguments, to_native(e))) - - statusmessage = cursor.statusmessage - rowcount = cursor.rowcount - - try: - query_result = [dict(row) for row in cursor.fetchall()] - except Psycopg2ProgrammingError as e: - if to_native(e) == 'no results to fetch': - query_result = {} - - except Exception as e: - module.fail_json(msg="Cannot fetch rows from cursor: %s" % to_native(e)) - - if 'SELECT' not in statusmessage: - if 'UPDATE' in statusmessage or 'INSERT' in statusmessage or 'DELETE' in statusmessage: - s = statusmessage.split() - if len(s) == 3: - if statusmessage.split()[2] != '0': - changed = True - - elif len(s) == 2: - if statusmessage.split()[1] != '0': - changed = True - - else: - changed = True - - else: - changed = True - - if module.check_mode: - db_connection.rollback() - else: - if not autocommit: - db_connection.commit() - - kw = dict( - changed=changed, - query=cursor.query, - statusmessage=statusmessage, - query_result=query_result, - rowcount=rowcount if rowcount >= 0 else 0, - ) - - cursor.close() - db_connection.close() - - module.exit_json(**kw) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_schema.py b/lib/ansible/modules/database/postgresql/postgresql_schema.py deleted file mode 100644 index 531b92a70d..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_schema.py +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2016, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: postgresql_schema -short_description: Add or remove PostgreSQL schema -description: -- Add or remove PostgreSQL schema. -version_added: '2.3' -options: - name: - description: - - Name of the schema to add or remove. - required: true - type: str - aliases: - - schema - database: - description: - - Name of the database to connect to and add or remove the schema. - type: str - default: postgres - aliases: - - db - - login_db - owner: - description: - - Name of the role to set as owner of the schema. - type: str - session_role: - version_added: '2.8' - description: - - Switch to session_role after connecting. - - The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though the session_role - were the one that had logged in originally. - type: str - state: - description: - - The schema state. - type: str - default: present - choices: [ absent, present ] - cascade_drop: - description: - - Drop schema with CASCADE to remove child objects. - type: bool - default: false - version_added: '2.8' - ssl_mode: - description: - - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. - - See U(https://www.postgresql.org/docs/current/static/libpq-ssl.html) for more information on the modes. - - Default of C(prefer) matches libpq default. - type: str - default: prefer - choices: [ allow, disable, prefer, require, verify-ca, verify-full ] - version_added: '2.8' - ca_cert: - description: - - Specifies the name of a file containing SSL certificate authority (CA) certificate(s). - - If the file exists, the server's certificate will be verified to be signed by one of these authorities. - type: str - aliases: [ ssl_rootcert ] - version_added: '2.8' -seealso: -- name: PostgreSQL schemas - description: General information about PostgreSQL schemas. - link: https://www.postgresql.org/docs/current/ddl-schemas.html -- name: CREATE SCHEMA reference - description: Complete reference of the CREATE SCHEMA command documentation. - link: https://www.postgresql.org/docs/current/sql-createschema.html -- name: ALTER SCHEMA reference - description: Complete reference of the ALTER SCHEMA command documentation. - link: https://www.postgresql.org/docs/current/sql-alterschema.html -- name: DROP SCHEMA reference - description: Complete reference of the DROP SCHEMA command documentation. - link: https://www.postgresql.org/docs/current/sql-dropschema.html -author: -- Flavien Chantelot (@Dorn-) <contact@flavien.io> -- Thomas O'Donnell (@andytom) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Create a new schema with name acme in test database - postgresql_schema: - db: test - name: acme - -- name: Create a new schema acme with a user bob who will own it - postgresql_schema: - name: acme - owner: bob - -- name: Drop schema "acme" with cascade - postgresql_schema: - name: acme - state: absent - cascade_drop: yes -''' - -RETURN = r''' -schema: - description: Name of the schema. - returned: success, changed - type: str - sample: "acme" -queries: - description: List of executed queries. - returned: always - type: list - sample: ["CREATE SCHEMA \"acme\""] -''' - -import traceback - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - get_conn_params, - postgres_common_argument_spec, -) -from ansible.module_utils.database import SQLParseError, pg_quote_identifier -from ansible.module_utils._text import to_native - -executed_queries = [] - - -class NotSupportedError(Exception): - pass - - -# =========================================== -# PostgreSQL module specific support methods. -# - -def set_owner(cursor, schema, owner): - query = "ALTER SCHEMA %s OWNER TO %s" % ( - pg_quote_identifier(schema, 'schema'), - pg_quote_identifier(owner, 'role')) - cursor.execute(query) - executed_queries.append(query) - return True - - -def get_schema_info(cursor, schema): - query = ("SELECT schema_owner AS owner " - "FROM information_schema.schemata " - "WHERE schema_name = %(schema)s") - cursor.execute(query, {'schema': schema}) - return cursor.fetchone() - - -def schema_exists(cursor, schema): - query = ("SELECT schema_name FROM information_schema.schemata " - "WHERE schema_name = %(schema)s") - cursor.execute(query, {'schema': schema}) - return cursor.rowcount == 1 - - -def schema_delete(cursor, schema, cascade): - if schema_exists(cursor, schema): - query = "DROP SCHEMA %s" % pg_quote_identifier(schema, 'schema') - if cascade: - query += " CASCADE" - cursor.execute(query) - executed_queries.append(query) - return True - else: - return False - - -def schema_create(cursor, schema, owner): - if not schema_exists(cursor, schema): - query_fragments = ['CREATE SCHEMA %s' % pg_quote_identifier(schema, 'schema')] - if owner: - query_fragments.append('AUTHORIZATION %s' % pg_quote_identifier(owner, 'role')) - query = ' '.join(query_fragments) - cursor.execute(query) - executed_queries.append(query) - return True - else: - schema_info = get_schema_info(cursor, schema) - if owner and owner != schema_info['owner']: - return set_owner(cursor, schema, owner) - else: - return False - - -def schema_matches(cursor, schema, owner): - if not schema_exists(cursor, schema): - return False - else: - schema_info = get_schema_info(cursor, schema) - if owner and owner != schema_info['owner']: - return False - else: - return True - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - schema=dict(type="str", required=True, aliases=['name']), - owner=dict(type="str", default=""), - database=dict(type="str", default="postgres", aliases=["db", "login_db"]), - cascade_drop=dict(type="bool", default=False), - state=dict(type="str", default="present", choices=["absent", "present"]), - session_role=dict(type="str"), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - schema = module.params["schema"] - owner = module.params["owner"] - state = module.params["state"] - cascade_drop = module.params["cascade_drop"] - changed = False - - conn_params = get_conn_params(module, module.params) - db_connection = connect_to_db(module, conn_params, autocommit=True) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - try: - if module.check_mode: - if state == "absent": - changed = not schema_exists(cursor, schema) - elif state == "present": - changed = not schema_matches(cursor, schema, owner) - module.exit_json(changed=changed, schema=schema) - - if state == "absent": - try: - changed = schema_delete(cursor, schema, cascade_drop) - except SQLParseError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - elif state == "present": - try: - changed = schema_create(cursor, schema, owner) - except SQLParseError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - except NotSupportedError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - except SystemExit: - # Avoid catching this on Python 2.4 - raise - except Exception as e: - module.fail_json(msg="Database query failed: %s" % to_native(e), exception=traceback.format_exc()) - - db_connection.close() - module.exit_json(changed=changed, schema=schema, queries=executed_queries) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_sequence.py b/lib/ansible/modules/database/postgresql/postgresql_sequence.py deleted file mode 100644 index 1ca0c0c1a8..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_sequence.py +++ /dev/null @@ -1,610 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Tobias Birkefeld (@tcraxs) <t@craxs.de> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = r''' ---- -module: postgresql_sequence -short_description: Create, drop, or alter a PostgreSQL sequence -description: -- Allows to create, drop or change the definition of a sequence generator. -version_added: '2.9' -options: - sequence: - description: - - The name of the sequence. - required: true - type: str - aliases: - - name - state: - description: - - The sequence state. - - If I(state=absent) other options will be ignored except of I(name) and - I(schema). - default: present - choices: [ absent, present ] - type: str - data_type: - description: - - Specifies the data type of the sequence. Valid types are bigint, integer, - and smallint. bigint is the default. The data type determines the default - minimum and maximum values of the sequence. For more info see the - documentation - U(https://www.postgresql.org/docs/current/sql-createsequence.html). - - Supported from PostgreSQL 10. - choices: [ bigint, integer, smallint ] - type: str - increment: - description: - - Increment specifies which value is added to the current sequence value - to create a new value. - - A positive value will make an ascending sequence, a negative one a - descending sequence. The default value is 1. - type: int - minvalue: - description: - - Minvalue determines the minimum value a sequence can generate. The - default for an ascending sequence is 1. The default for a descending - sequence is the minimum value of the data type. - type: int - aliases: - - min - maxvalue: - description: - - Maxvalue determines the maximum value for the sequence. The default for - an ascending sequence is the maximum - value of the data type. The default for a descending sequence is -1. - type: int - aliases: - - max - start: - description: - - Start allows the sequence to begin anywhere. The default starting value - is I(minvalue) for ascending sequences and I(maxvalue) for descending - ones. - type: int - cache: - description: - - Cache specifies how many sequence numbers are to be preallocated and - stored in memory for faster access. The minimum value is 1 (only one - value can be generated at a time, i.e., no cache), and this is also - the default. - type: int - cycle: - description: - - The cycle option allows the sequence to wrap around when the I(maxvalue) - or I(minvalue) has been reached by an ascending or descending sequence - respectively. If the limit is reached, the next number generated will be - the minvalue or maxvalue, respectively. - - If C(false) (NO CYCLE) is specified, any calls to nextval after the sequence - has reached its maximum value will return an error. False (NO CYCLE) is - the default. - type: bool - default: no - cascade: - description: - - Automatically drop objects that depend on the sequence, and in turn all - objects that depend on those objects. - - Ignored if I(state=present). - - Only used with I(state=absent). - type: bool - default: no - rename_to: - description: - - The new name for the I(sequence). - - Works only for existing sequences. - type: str - owner: - description: - - Set the owner for the I(sequence). - type: str - schema: - description: - - The schema of the I(sequence). This is be used to create and relocate - a I(sequence) in the given schema. - default: public - type: str - newschema: - description: - - The new schema for the I(sequence). Will be used for moving a - I(sequence) to another I(schema). - - Works only for existing sequences. - type: str - session_role: - description: - - Switch to session_role after connecting. The specified I(session_role) - must be a role that the current I(login_user) is a member of. - - Permissions checking for SQL commands is carried out as though - the I(session_role) were the one that had logged in originally. - type: str - db: - description: - - Name of database to connect to and run queries against. - type: str - aliases: - - database - - login_db -notes: -- If you do not pass db parameter, sequence will be created in the database - named postgres. -seealso: -- module: postgresql_table -- module: postgresql_owner -- module: postgresql_privs -- module: postgresql_tablespace -- name: CREATE SEQUENCE reference - description: Complete reference of the CREATE SEQUENCE command documentation. - link: https://www.postgresql.org/docs/current/sql-createsequence.html -- name: ALTER SEQUENCE reference - description: Complete reference of the ALTER SEQUENCE command documentation. - link: https://www.postgresql.org/docs/current/sql-altersequence.html -- name: DROP SEQUENCE reference - description: Complete reference of the DROP SEQUENCE command documentation. - link: https://www.postgresql.org/docs/current/sql-dropsequence.html -author: -- Tobias Birkefeld (@tcraxs) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Create an ascending bigint sequence called foobar in the default - database - postgresql_sequence: - name: foobar - -- name: Create an ascending integer sequence called foobar, starting at 101 - postgresql_sequence: - name: foobar - data_type: integer - start: 101 - -- name: Create an descending sequence called foobar, starting at 101 and - preallocated 10 sequence numbers in cache - postgresql_sequence: - name: foobar - increment: -1 - cache: 10 - start: 101 - -- name: Create an ascending sequence called foobar, which cycle between 1 to 10 - postgresql_sequence: - name: foobar - cycle: yes - min: 1 - max: 10 - -- name: Create an ascending bigint sequence called foobar in the default - database with owner foobar - postgresql_sequence: - name: foobar - owner: foobar - -- name: Rename an existing sequence named foo to bar - postgresql_sequence: - name: foo - rename_to: bar - -- name: Change the schema of an existing sequence to foobar - postgresql_sequence: - name: foobar - newschema: foobar - -- name: Change the owner of an existing sequence to foobar - postgresql_sequence: - name: foobar - owner: foobar - -- name: Drop a sequence called foobar - postgresql_sequence: - name: foobar - state: absent - -- name: Drop a sequence called foobar with cascade - postgresql_sequence: - name: foobar - cascade: yes - state: absent -''' - -RETURN = r''' -state: - description: Sequence state at the end of execution. - returned: always - type: str - sample: 'present' -sequence: - description: Sequence name. - returned: always - type: str - sample: 'foobar' -queries: - description: List of queries that was tried to be executed. - returned: always - type: str - sample: [ "CREATE SEQUENCE \"foo\"" ] -schema: - description: Name of the schema of the sequence - returned: always - type: str - sample: 'foo' -data_type: - description: Shows the current data type of the sequence. - returned: always - type: str - sample: 'bigint' -increment: - description: The value of increment of the sequence. A positive value will - make an ascending sequence, a negative one a descending - sequence. - returned: always - type: int - sample: '-1' -minvalue: - description: The value of minvalue of the sequence. - returned: always - type: int - sample: '1' -maxvalue: - description: The value of maxvalue of the sequence. - returned: always - type: int - sample: '9223372036854775807' -start: - description: The value of start of the sequence. - returned: always - type: int - sample: '12' -cycle: - description: Shows if the sequence cycle or not. - returned: always - type: str - sample: 'NO' -owner: - description: Shows the current owner of the sequence - after the successful run of the task. - returned: always - type: str - sample: 'postgres' -newname: - description: Shows the new sequence name after rename. - returned: on success - type: str - sample: 'barfoo' -newschema: - description: Shows the new schema of the sequence after schema change. - returned: on success - type: str - sample: 'foobar' -''' - - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import pg_quote_identifier -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) - - -class Sequence(object): - """Implements behavior of CREATE, ALTER or DROP SEQUENCE PostgreSQL command. - - Arguments: - module (AnsibleModule) -- object of AnsibleModule class - cursor (cursor) -- cursor object of psycopg2 library - - Attributes: - module (AnsibleModule) -- object of AnsibleModule class - cursor (cursor) -- cursor object of psycopg2 library - changed (bool) -- something was changed after execution or not - executed_queries (list) -- executed queries - name (str) -- name of the sequence - owner (str) -- name of the owner of the sequence - schema (str) -- name of the schema (default: public) - data_type (str) -- data type of the sequence - start_value (int) -- value of the sequence start - minvalue (int) -- minimum value of the sequence - maxvalue (int) -- maximum value of the sequence - increment (int) -- increment value of the sequence - cycle (bool) -- sequence can cycle or not - new_name (str) -- name of the renamed sequence - new_schema (str) -- name of the new schema - exists (bool) -- sequence exists or not - """ - - def __init__(self, module, cursor): - self.module = module - self.cursor = cursor - self.executed_queries = [] - self.name = self.module.params['sequence'] - self.owner = '' - self.schema = self.module.params['schema'] - self.data_type = '' - self.start_value = '' - self.minvalue = '' - self.maxvalue = '' - self.increment = '' - self.cycle = '' - self.new_name = '' - self.new_schema = '' - self.exists = False - # Collect info - self.get_info() - - def get_info(self): - """Getter to refresh and get sequence info""" - query = ("SELECT " - "s.sequence_schema AS schemaname, " - "s.sequence_name AS sequencename, " - "pg_get_userbyid(c.relowner) AS sequenceowner, " - "s.data_type::regtype AS data_type, " - "s.start_value AS start_value, " - "s.minimum_value AS min_value, " - "s.maximum_value AS max_value, " - "s.increment AS increment_by, " - "s.cycle_option AS cycle " - "FROM information_schema.sequences s " - "JOIN pg_class c ON c.relname = s.sequence_name " - "LEFT JOIN pg_namespace n ON n.oid = c.relnamespace " - "WHERE NOT pg_is_other_temp_schema(n.oid) " - "AND c.relkind = 'S'::\"char\" " - "AND sequence_name = %(name)s " - "AND sequence_schema = %(schema)s") - - res = exec_sql(self, query, - query_params={'name': self.name, 'schema': self.schema}, - add_to_executed=False) - - if not res: - self.exists = False - return False - - if res: - self.exists = True - self.schema = res[0]['schemaname'] - self.name = res[0]['sequencename'] - self.owner = res[0]['sequenceowner'] - self.data_type = res[0]['data_type'] - self.start_value = res[0]['start_value'] - self.minvalue = res[0]['min_value'] - self.maxvalue = res[0]['max_value'] - self.increment = res[0]['increment_by'] - self.cycle = res[0]['cycle'] - - def create(self): - """Implements CREATE SEQUENCE command behavior.""" - query = ['CREATE SEQUENCE'] - query.append(self.__add_schema()) - - if self.module.params.get('data_type'): - query.append('AS %s' % self.module.params['data_type']) - - if self.module.params.get('increment'): - query.append('INCREMENT BY %s' % self.module.params['increment']) - - if self.module.params.get('minvalue'): - query.append('MINVALUE %s' % self.module.params['minvalue']) - - if self.module.params.get('maxvalue'): - query.append('MAXVALUE %s' % self.module.params['maxvalue']) - - if self.module.params.get('start'): - query.append('START WITH %s' % self.module.params['start']) - - if self.module.params.get('cache'): - query.append('CACHE %s' % self.module.params['cache']) - - if self.module.params.get('cycle'): - query.append('CYCLE') - - return exec_sql(self, ' '.join(query), ddl=True) - - def drop(self): - """Implements DROP SEQUENCE command behavior.""" - query = ['DROP SEQUENCE'] - query.append(self.__add_schema()) - - if self.module.params.get('cascade'): - query.append('CASCADE') - - return exec_sql(self, ' '.join(query), ddl=True) - - def rename(self): - """Implements ALTER SEQUENCE RENAME TO command behavior.""" - query = ['ALTER SEQUENCE'] - query.append(self.__add_schema()) - query.append('RENAME TO %s' % pg_quote_identifier(self.module.params['rename_to'], 'sequence')) - - return exec_sql(self, ' '.join(query), ddl=True) - - def set_owner(self): - """Implements ALTER SEQUENCE OWNER TO command behavior.""" - query = ['ALTER SEQUENCE'] - query.append(self.__add_schema()) - query.append('OWNER TO %s' % pg_quote_identifier(self.module.params['owner'], 'role')) - - return exec_sql(self, ' '.join(query), ddl=True) - - def set_schema(self): - """Implements ALTER SEQUENCE SET SCHEMA command behavior.""" - query = ['ALTER SEQUENCE'] - query.append(self.__add_schema()) - query.append('SET SCHEMA %s' % pg_quote_identifier(self.module.params['newschema'], 'schema')) - - return exec_sql(self, ' '.join(query), ddl=True) - - def __add_schema(self): - return '.'.join([pg_quote_identifier(self.schema, 'schema'), - pg_quote_identifier(self.name, 'sequence')]) - - -# =========================================== -# Module execution. -# - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - sequence=dict(type='str', required=True, aliases=['name']), - state=dict(type='str', default='present', choices=['absent', 'present']), - data_type=dict(type='str', choices=['bigint', 'integer', 'smallint']), - increment=dict(type='int'), - minvalue=dict(type='int', aliases=['min']), - maxvalue=dict(type='int', aliases=['max']), - start=dict(type='int'), - cache=dict(type='int'), - cycle=dict(type='bool', default=False), - schema=dict(type='str', default='public'), - cascade=dict(type='bool', default=False), - rename_to=dict(type='str'), - owner=dict(type='str'), - newschema=dict(type='str'), - db=dict(type='str', default='', aliases=['login_db', 'database']), - session_role=dict(type='str'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - mutually_exclusive=[ - ['rename_to', 'data_type'], - ['rename_to', 'increment'], - ['rename_to', 'minvalue'], - ['rename_to', 'maxvalue'], - ['rename_to', 'start'], - ['rename_to', 'cache'], - ['rename_to', 'cycle'], - ['rename_to', 'cascade'], - ['rename_to', 'owner'], - ['rename_to', 'newschema'], - ['cascade', 'data_type'], - ['cascade', 'increment'], - ['cascade', 'minvalue'], - ['cascade', 'maxvalue'], - ['cascade', 'start'], - ['cascade', 'cache'], - ['cascade', 'cycle'], - ['cascade', 'owner'], - ['cascade', 'newschema'], - ] - ) - - # Note: we don't need to check mutually exclusive params here, because they are - # checked automatically by AnsibleModule (mutually_exclusive=[] list above). - - # Change autocommit to False if check_mode: - autocommit = not module.check_mode - # Connect to DB and make cursor object: - conn_params = get_conn_params(module, module.params) - db_connection = connect_to_db(module, conn_params, autocommit=autocommit) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - ############## - # Create the object and do main job: - data = Sequence(module, cursor) - - # Set defaults: - changed = False - - # Create new sequence - if not data.exists and module.params['state'] == 'present': - if module.params.get('rename_to'): - module.fail_json(msg="Sequence '%s' does not exist, nothing to rename" % module.params['sequence']) - if module.params.get('newschema'): - module.fail_json(msg="Sequence '%s' does not exist, change of schema not possible" % module.params['sequence']) - - changed = data.create() - - # Drop non-existing sequence - elif not data.exists and module.params['state'] == 'absent': - # Nothing to do - changed = False - - # Drop existing sequence - elif data.exists and module.params['state'] == 'absent': - changed = data.drop() - - # Rename sequence - if data.exists and module.params.get('rename_to'): - if data.name != module.params['rename_to']: - changed = data.rename() - if changed: - data.new_name = module.params['rename_to'] - - # Refresh information - if module.params['state'] == 'present': - data.get_info() - - # Change owner, schema and settings - if module.params['state'] == 'present' and data.exists: - # change owner - if module.params.get('owner'): - if data.owner != module.params['owner']: - changed = data.set_owner() - - # Set schema - if module.params.get('newschema'): - if data.schema != module.params['newschema']: - changed = data.set_schema() - if changed: - data.new_schema = module.params['newschema'] - - # Rollback if it's possible and check_mode: - if module.check_mode: - db_connection.rollback() - else: - db_connection.commit() - - cursor.close() - db_connection.close() - - # Make return values: - kw = dict( - changed=changed, - state='present', - sequence=data.name, - queries=data.executed_queries, - schema=data.schema, - data_type=data.data_type, - increment=data.increment, - minvalue=data.minvalue, - maxvalue=data.maxvalue, - start=data.start_value, - cycle=data.cycle, - owner=data.owner, - ) - - if module.params['state'] == 'present': - if data.new_name: - kw['newname'] = data.new_name - if data.new_schema: - kw['newschema'] = data.new_schema - - elif module.params['state'] == 'absent': - kw['state'] = 'absent' - - module.exit_json(**kw) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_set.py b/lib/ansible/modules/database/postgresql/postgresql_set.py deleted file mode 100644 index 5593b4312f..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_set.py +++ /dev/null @@ -1,434 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2018, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = r''' ---- -module: postgresql_set -short_description: Change a PostgreSQL server configuration parameter -description: - - Allows to change a PostgreSQL server configuration parameter. - - The module uses ALTER SYSTEM command and applies changes by reload server configuration. - - ALTER SYSTEM is used for changing server configuration parameters across the entire database cluster. - - It can be more convenient and safe than the traditional method of manually editing the postgresql.conf file. - - ALTER SYSTEM writes the given parameter setting to the $PGDATA/postgresql.auto.conf file, - which is read in addition to postgresql.conf. - - The module allows to reset parameter to boot_val (cluster initial value) by I(reset=yes) or remove parameter - string from postgresql.auto.conf and reload I(value=default) (for settings with postmaster context restart is required). - - After change you can see in the ansible output the previous and - the new parameter value and other information using returned values and M(debug) module. -version_added: '2.8' -options: - name: - description: - - Name of PostgreSQL server parameter. - type: str - required: true - value: - description: - - Parameter value to set. - - To remove parameter string from postgresql.auto.conf and - reload the server configuration you must pass I(value=default). - With I(value=default) the playbook always returns changed is true. - type: str - reset: - description: - - Restore parameter to initial state (boot_val). Mutually exclusive with I(value). - type: bool - default: false - session_role: - description: - - Switch to session_role after connecting. The specified session_role must - be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str - db: - description: - - Name of database to connect. - type: str - aliases: - - login_db -notes: -- Supported version of PostgreSQL is 9.4 and later. -- Pay attention, change setting with 'postmaster' context can return changed is true - when actually nothing changes because the same value may be presented in - several different form, for example, 1024MB, 1GB, etc. However in pg_settings - system view it can be defined like 131072 number of 8kB pages. - The final check of the parameter value cannot compare it because the server was - not restarted and the value in pg_settings is not updated yet. -- For some parameters restart of PostgreSQL server is required. - See official documentation U(https://www.postgresql.org/docs/current/view-pg-settings.html). -seealso: -- module: postgresql_info -- name: PostgreSQL server configuration - description: General information about PostgreSQL server configuration. - link: https://www.postgresql.org/docs/current/runtime-config.html -- name: PostgreSQL view pg_settings reference - description: Complete reference of the pg_settings view documentation. - link: https://www.postgresql.org/docs/current/view-pg-settings.html -- name: PostgreSQL ALTER SYSTEM command reference - description: Complete reference of the ALTER SYSTEM command documentation. - link: https://www.postgresql.org/docs/current/sql-altersystem.html -author: -- Andrew Klychkov (@Andersson007) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Restore wal_keep_segments parameter to initial state - postgresql_set: - name: wal_keep_segments - reset: yes - -# Set work_mem parameter to 32MB and show what's been changed and restart is required or not -# (output example: "msg": "work_mem 4MB >> 64MB restart_req: False") -- name: Set work mem parameter - postgresql_set: - name: work_mem - value: 32mb - register: set - -- debug: - msg: "{{ set.name }} {{ set.prev_val_pretty }} >> {{ set.value_pretty }} restart_req: {{ set.restart_required }}" - when: set.changed -# Ensure that the restart of PostgreSQL server must be required for some parameters. -# In this situation you see the same parameter in prev_val_pretty and value_pretty, but 'changed=True' -# (If you passed the value that was different from the current server setting). - -- name: Set log_min_duration_statement parameter to 1 second - postgresql_set: - name: log_min_duration_statement - value: 1s - -- name: Set wal_log_hints parameter to default value (remove parameter from postgresql.auto.conf) - postgresql_set: - name: wal_log_hints - value: default -''' - -RETURN = r''' -name: - description: Name of PostgreSQL server parameter. - returned: always - type: str - sample: 'shared_buffers' -restart_required: - description: Information about parameter current state. - returned: always - type: bool - sample: true -prev_val_pretty: - description: Information about previous state of the parameter. - returned: always - type: str - sample: '4MB' -value_pretty: - description: Information about current state of the parameter. - returned: always - type: str - sample: '64MB' -value: - description: - - Dictionary that contains the current parameter value (at the time of playbook finish). - - Pay attention that for real change some parameters restart of PostgreSQL server is required. - - Returns the current value in the check mode. - returned: always - type: dict - sample: { "value": 67108864, "unit": "b" } -context: - description: - - PostgreSQL setting context. - returned: always - type: str - sample: user -''' - -try: - from psycopg2.extras import DictCursor -except Exception: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from copy import deepcopy - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - get_conn_params, - postgres_common_argument_spec, -) -from ansible.module_utils._text import to_native - -PG_REQ_VER = 90400 - -# To allow to set value like 1mb instead of 1MB, etc: -POSSIBLE_SIZE_UNITS = ("mb", "gb", "tb") - -# =========================================== -# PostgreSQL module specific support methods. -# - - -def param_get(cursor, module, name): - query = ("SELECT name, setting, unit, context, boot_val " - "FROM pg_settings WHERE name = %(name)s") - try: - cursor.execute(query, {'name': name}) - info = cursor.fetchall() - cursor.execute("SHOW %s" % name) - val = cursor.fetchone() - - except Exception as e: - module.fail_json(msg="Unable to get %s value due to : %s" % (name, to_native(e))) - - raw_val = info[0][1] - unit = info[0][2] - context = info[0][3] - boot_val = info[0][4] - - if val[0] == 'True': - val[0] = 'on' - elif val[0] == 'False': - val[0] = 'off' - - if unit == 'kB': - if int(raw_val) > 0: - raw_val = int(raw_val) * 1024 - if int(boot_val) > 0: - boot_val = int(boot_val) * 1024 - - unit = 'b' - - elif unit == 'MB': - if int(raw_val) > 0: - raw_val = int(raw_val) * 1024 * 1024 - if int(boot_val) > 0: - boot_val = int(boot_val) * 1024 * 1024 - - unit = 'b' - - return (val[0], raw_val, unit, boot_val, context) - - -def pretty_to_bytes(pretty_val): - # The function returns a value in bytes - # if the value contains 'B', 'kB', 'MB', 'GB', 'TB'. - # Otherwise it returns the passed argument. - - val_in_bytes = None - - if 'kB' in pretty_val: - num_part = int(''.join(d for d in pretty_val if d.isdigit())) - val_in_bytes = num_part * 1024 - - elif 'MB' in pretty_val.upper(): - num_part = int(''.join(d for d in pretty_val if d.isdigit())) - val_in_bytes = num_part * 1024 * 1024 - - elif 'GB' in pretty_val.upper(): - num_part = int(''.join(d for d in pretty_val if d.isdigit())) - val_in_bytes = num_part * 1024 * 1024 * 1024 - - elif 'TB' in pretty_val.upper(): - num_part = int(''.join(d for d in pretty_val if d.isdigit())) - val_in_bytes = num_part * 1024 * 1024 * 1024 * 1024 - - elif 'B' in pretty_val.upper(): - num_part = int(''.join(d for d in pretty_val if d.isdigit())) - val_in_bytes = num_part - - else: - return pretty_val - - return val_in_bytes - - -def param_set(cursor, module, name, value, context): - try: - if str(value).lower() == 'default': - query = "ALTER SYSTEM SET %s = DEFAULT" % name - else: - query = "ALTER SYSTEM SET %s = '%s'" % (name, value) - cursor.execute(query) - - if context != 'postmaster': - cursor.execute("SELECT pg_reload_conf()") - - except Exception as e: - module.fail_json(msg="Unable to get %s value due to : %s" % (name, to_native(e))) - - return True - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - name=dict(type='str', required=True), - db=dict(type='str', aliases=['login_db']), - value=dict(type='str'), - reset=dict(type='bool'), - session_role=dict(type='str'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - name = module.params["name"] - value = module.params["value"] - reset = module.params["reset"] - - # Allow to pass values like 1mb instead of 1MB, etc: - if value: - for unit in POSSIBLE_SIZE_UNITS: - if value[:-2].isdigit() and unit in value[-2:]: - value = value.upper() - - if value and reset: - module.fail_json(msg="%s: value and reset params are mutually exclusive" % name) - - if not value and not reset: - module.fail_json(msg="%s: at least one of value or reset param must be specified" % name) - - conn_params = get_conn_params(module, module.params, warn_db_default=False) - db_connection = connect_to_db(module, conn_params, autocommit=True) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - kw = {} - # Check server version (needs 9.4 or later): - ver = db_connection.server_version - if ver < PG_REQ_VER: - module.warn("PostgreSQL is %s version but %s or later is required" % (ver, PG_REQ_VER)) - kw = dict( - changed=False, - restart_required=False, - value_pretty="", - prev_val_pretty="", - value={"value": "", "unit": ""}, - ) - kw['name'] = name - db_connection.close() - module.exit_json(**kw) - - # Set default returned values: - restart_required = False - changed = False - kw['name'] = name - kw['restart_required'] = False - - # Get info about param state: - res = param_get(cursor, module, name) - current_value = res[0] - raw_val = res[1] - unit = res[2] - boot_val = res[3] - context = res[4] - - if value == 'True': - value = 'on' - elif value == 'False': - value = 'off' - - kw['prev_val_pretty'] = current_value - kw['value_pretty'] = deepcopy(kw['prev_val_pretty']) - kw['context'] = context - - # Do job - if context == "internal": - module.fail_json(msg="%s: cannot be changed (internal context). See " - "https://www.postgresql.org/docs/current/runtime-config-preset.html" % name) - - if context == "postmaster": - restart_required = True - - # If check_mode, just compare and exit: - if module.check_mode: - if pretty_to_bytes(value) == pretty_to_bytes(current_value): - kw['changed'] = False - - else: - kw['value_pretty'] = value - kw['changed'] = True - - # Anyway returns current raw value in the check_mode: - kw['value'] = dict( - value=raw_val, - unit=unit, - ) - kw['restart_required'] = restart_required - module.exit_json(**kw) - - # Set param: - if value and value != current_value: - changed = param_set(cursor, module, name, value, context) - - kw['value_pretty'] = value - - # Reset param: - elif reset: - if raw_val == boot_val: - # nothing to change, exit: - kw['value'] = dict( - value=raw_val, - unit=unit, - ) - module.exit_json(**kw) - - changed = param_set(cursor, module, name, boot_val, context) - - if restart_required: - module.warn("Restart of PostgreSQL is required for setting %s" % name) - - cursor.close() - db_connection.close() - - # Reconnect and recheck current value: - if context in ('sighup', 'superuser-backend', 'backend', 'superuser', 'user'): - db_connection = connect_to_db(module, conn_params, autocommit=True) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - res = param_get(cursor, module, name) - # f_ means 'final' - f_value = res[0] - f_raw_val = res[1] - - if raw_val == f_raw_val: - changed = False - - else: - changed = True - - kw['value_pretty'] = f_value - kw['value'] = dict( - value=f_raw_val, - unit=unit, - ) - - cursor.close() - db_connection.close() - - kw['changed'] = changed - kw['restart_required'] = restart_required - module.exit_json(**kw) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_slot.py b/lib/ansible/modules/database/postgresql/postgresql_slot.py deleted file mode 100644 index 30aaf39458..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_slot.py +++ /dev/null @@ -1,292 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, John Scalia (@jscalia), Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: postgresql_slot -short_description: Add or remove replication slots from a PostgreSQL database -description: -- Add or remove physical or logical replication slots from a PostgreSQL database. -version_added: '2.8' - -options: - name: - description: - - Name of the replication slot to add or remove. - type: str - required: yes - aliases: - - slot_name - slot_type: - description: - - Slot type. - type: str - default: physical - choices: [ logical, physical ] - state: - description: - - The slot state. - - I(state=present) implies the slot must be present in the system. - - I(state=absent) implies the I(groups) must be revoked from I(target_roles). - type: str - default: present - choices: [ absent, present ] - immediately_reserve: - description: - - Optional parameter that when C(yes) specifies that the LSN for this replication slot be reserved - immediately, otherwise the default, C(no), specifies that the LSN is reserved on the first connection - from a streaming replication client. - - Is available from PostgreSQL version 9.6. - - Uses only with I(slot_type=physical). - - Mutually exclusive with I(slot_type=logical). - type: bool - default: no - output_plugin: - description: - - All logical slots must indicate which output plugin decoder they're using. - - This parameter does not apply to physical slots. - - It will be ignored with I(slot_type=physical). - type: str - default: "test_decoding" - db: - description: - - Name of database to connect to. - type: str - aliases: - - login_db - session_role: - description: - - Switch to session_role after connecting. - The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str - -notes: -- Physical replication slots were introduced to PostgreSQL with version 9.4, - while logical replication slots were added beginning with version 10.0. - -seealso: -- name: PostgreSQL pg_replication_slots view reference - description: Complete reference of the PostgreSQL pg_replication_slots view. - link: https://www.postgresql.org/docs/current/view-pg-replication-slots.html -- name: PostgreSQL streaming replication protocol reference - description: Complete reference of the PostgreSQL streaming replication protocol documentation. - link: https://www.postgresql.org/docs/current/protocol-replication.html -- name: PostgreSQL logical replication protocol reference - description: Complete reference of the PostgreSQL logical replication protocol documentation. - link: https://www.postgresql.org/docs/current/protocol-logical-replication.html - -author: -- John Scalia (@jscalia) -- Andrew Klychkov (@Andersson007) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Create physical_one physical slot if doesn't exist - become_user: postgres - postgresql_slot: - slot_name: physical_one - db: ansible - -- name: Remove physical_one slot if exists - become_user: postgres - postgresql_slot: - slot_name: physical_one - db: ansible - state: absent - -- name: Create logical_one logical slot to the database acme if doesn't exist - postgresql_slot: - name: logical_slot_one - slot_type: logical - state: present - output_plugin: custom_decoder_one - db: "acme" - -- name: Remove logical_one slot if exists from the cluster running on another host and non-standard port - postgresql_slot: - name: logical_one - login_host: mydatabase.example.org - port: 5433 - login_user: ourSuperuser - login_password: thePassword - state: absent -''' - -RETURN = r''' -name: - description: Name of the slot - returned: always - type: str - sample: "physical_one" -queries: - description: List of executed queries. - returned: always - type: str - sample: [ "SELECT pg_create_physical_replication_slot('physical_one', False, False)" ] -''' - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) - - -# =========================================== -# PostgreSQL module specific support methods. -# - -class PgSlot(object): - def __init__(self, module, cursor, name): - self.module = module - self.cursor = cursor - self.name = name - self.exists = False - self.kind = '' - self.__slot_exists() - self.changed = False - self.executed_queries = [] - - def create(self, kind='physical', immediately_reserve=False, output_plugin=False, just_check=False): - if self.exists: - if self.kind == kind: - return False - else: - self.module.warn("slot with name '%s' already exists " - "but has another type '%s'" % (self.name, self.kind)) - return False - - if just_check: - return None - - if kind == 'physical': - # Check server version (needs for immedately_reserverd needs 9.6+): - if self.cursor.connection.server_version < 96000: - query = "SELECT pg_create_physical_replication_slot(%(name)s)" - - else: - query = "SELECT pg_create_physical_replication_slot(%(name)s, %(i_reserve)s)" - - self.changed = exec_sql(self, query, - query_params={'name': self.name, 'i_reserve': immediately_reserve}, - ddl=True) - - elif kind == 'logical': - query = "SELECT pg_create_logical_replication_slot(%(name)s, %(o_plugin)s)" - self.changed = exec_sql(self, query, - query_params={'name': self.name, 'o_plugin': output_plugin}, ddl=True) - - def drop(self): - if not self.exists: - return False - - query = "SELECT pg_drop_replication_slot(%(name)s)" - self.changed = exec_sql(self, query, query_params={'name': self.name}, ddl=True) - - def __slot_exists(self): - query = "SELECT slot_type FROM pg_replication_slots WHERE slot_name = %(name)s" - res = exec_sql(self, query, query_params={'name': self.name}, add_to_executed=False) - if res: - self.exists = True - self.kind = res[0][0] - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - db=dict(type="str", aliases=["login_db"]), - name=dict(type="str", required=True, aliases=["slot_name"]), - slot_type=dict(type="str", default="physical", choices=["logical", "physical"]), - immediately_reserve=dict(type="bool", default=False), - session_role=dict(type="str"), - output_plugin=dict(type="str", default="test_decoding"), - state=dict(type="str", default="present", choices=["absent", "present"]), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - name = module.params["name"] - slot_type = module.params["slot_type"] - immediately_reserve = module.params["immediately_reserve"] - state = module.params["state"] - output_plugin = module.params["output_plugin"] - - if immediately_reserve and slot_type == 'logical': - module.fail_json(msg="Module parameters immediately_reserve and slot_type=logical are mutually exclusive") - - # When slot_type is logical and parameter db is not passed, - # the default database will be used to create the slot and - # the user should know about this. - # When the slot type is physical, - # it doesn't matter which database will be used - # because physical slots are global objects. - if slot_type == 'logical': - warn_db_default = True - else: - warn_db_default = False - - conn_params = get_conn_params(module, module.params, warn_db_default=warn_db_default) - db_connection = connect_to_db(module, conn_params, autocommit=True) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - ################################## - # Create an object and do main job - pg_slot = PgSlot(module, cursor, name) - - changed = False - - if module.check_mode: - if state == "present": - if not pg_slot.exists: - changed = True - - pg_slot.create(slot_type, immediately_reserve, output_plugin, just_check=True) - - elif state == "absent": - if pg_slot.exists: - changed = True - else: - if state == "absent": - pg_slot.drop() - - elif state == "present": - pg_slot.create(slot_type, immediately_reserve, output_plugin) - - changed = pg_slot.changed - - db_connection.close() - module.exit_json(changed=changed, name=name, queries=pg_slot.executed_queries) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_subscription.py b/lib/ansible/modules/database/postgresql/postgresql_subscription.py deleted file mode 100644 index ba32e1c0a8..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_subscription.py +++ /dev/null @@ -1,685 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: postgresql_subscription -short_description: Add, update, or remove PostgreSQL subscription -description: -- Add, update, or remove PostgreSQL subscription. -version_added: '2.10' - -options: - name: - description: - - Name of the subscription to add, update, or remove. - type: str - required: yes - db: - description: - - Name of the database to connect to and where - the subscription state will be changed. - aliases: [ login_db ] - type: str - required: yes - state: - description: - - The subscription state. - - C(present) implies that if I(name) subscription doesn't exist, it will be created. - - C(absent) implies that if I(name) subscription exists, it will be removed. - - C(refresh) implies that if I(name) subscription exists, it will be refreshed. - Fetch missing table information from publisher. Always returns ``changed`` is ``True``. - This will start replication of tables that were added to the subscribed-to publications - since the last invocation of REFRESH PUBLICATION or since CREATE SUBSCRIPTION. - The existing data in the publications that are being subscribed to - should be copied once the replication starts. - - For more information about C(refresh) see U(https://www.postgresql.org/docs/current/sql-altersubscription.html). - type: str - choices: [ absent, present, refresh ] - default: present - owner: - description: - - Subscription owner. - - If I(owner) is not defined, the owner will be set as I(login_user) or I(session_role). - - Ignored when I(state) is not C(present). - type: str - publications: - description: - - The publication names on the publisher to use for the subscription. - - Ignored when I(state) is not C(present). - type: list - elements: str - connparams: - description: - - The connection dict param-value to connect to the publisher. - - For more information see U(https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). - - Ignored when I(state) is not C(present). - type: dict - cascade: - description: - - Drop subscription dependencies. Has effect with I(state=absent) only. - - Ignored when I(state) is not C(absent). - type: bool - default: false - subsparams: - description: - - Dictionary of optional parameters for a subscription, e.g. copy_data, enabled, create_slot, etc. - - For update the subscription allowed keys are C(enabled), C(slot_name), C(synchronous_commit), C(publication_name). - - See available parameters to create a new subscription - on U(https://www.postgresql.org/docs/current/sql-createsubscription.html). - - Ignored when I(state) is not C(present). - type: dict - -notes: -- PostgreSQL version must be 10 or greater. - -seealso: -- module: postgresql_publication -- module: postgresql_info -- name: CREATE SUBSCRIPTION reference - description: Complete reference of the CREATE SUBSCRIPTION command documentation. - link: https://www.postgresql.org/docs/current/sql-createsubscription.html -- name: ALTER SUBSCRIPTION reference - description: Complete reference of the ALTER SUBSCRIPTION command documentation. - link: https://www.postgresql.org/docs/current/sql-altersubscription.html -- name: DROP SUBSCRIPTION reference - description: Complete reference of the DROP SUBSCRIPTION command documentation. - link: https://www.postgresql.org/docs/current/sql-dropsubscription.html - -author: -- Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> - -extends_documentation_fragment: -- postgres -''' - -EXAMPLES = r''' -- name: > - Create acme subscription in mydb database using acme_publication and - the following connection parameters to connect to the publisher. - Set the subscription owner as alice. - postgresql_subscription: - db: mydb - name: acme - state: present - publications: acme_publication - owner: alice - connparams: - host: 127.0.0.1 - port: 5432 - user: repl - password: replpass - dbname: mydb - -- name: Assuming that acme subscription exists, try to change conn parameters - postgresql_subscription: - db: mydb - name: acme - connparams: - host: 127.0.0.1 - port: 5432 - user: repl - password: replpass - connect_timeout: 100 - -- name: Refresh acme publication - postgresql_subscription: - db: mydb - name: acme - state: refresh - -- name: Drop acme subscription from mydb with dependencies (cascade=yes) - postgresql_subscription: - db: mydb - name: acme - state: absent - cascade: yes - -- name: Assuming that acme subscription exists and enabled, disable the subscription - postgresql_subscription: - db: mydb - name: acme - state: present - subsparams: - enabled: no -''' - -RETURN = r''' -name: - description: - - Name of the subscription. - returned: always - type: str - sample: acme -exists: - description: - - Flag indicates the subscription exists or not at the end of runtime. - returned: always - type: bool - sample: true -queries: - description: List of executed queries. - returned: always - type: str - sample: [ 'DROP SUBSCRIPTION "mysubscription"' ] -initial_state: - description: Subscription configuration at the beginning of runtime. - returned: always - type: dict - sample: {"conninfo": {}, "enabled": true, "owner": "postgres", "slotname": "test", "synccommit": true} -final_state: - description: Subscription configuration at the end of runtime. - returned: always - type: dict - sample: {"conninfo": {}, "enabled": true, "owner": "postgres", "slotname": "test", "synccommit": true} -''' - -from copy import deepcopy - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) -from ansible.module_utils.six import iteritems - -SUPPORTED_PG_VERSION = 10000 - -SUBSPARAMS_KEYS_FOR_UPDATE = ('enabled', 'synchronous_commit', 'slot_name') - - -################################ -# Module functions and classes # -################################ - -def convert_conn_params(conn_dict): - """Converts the passed connection dictionary to string. - - Args: - conn_dict (list): Dictionary which needs to be converted. - - Returns: - Connection string. - """ - conn_list = [] - for (param, val) in iteritems(conn_dict): - conn_list.append('%s=%s' % (param, val)) - - return ' '.join(conn_list) - - -def convert_subscr_params(params_dict): - """Converts the passed params dictionary to string. - - Args: - params_dict (list): Dictionary which needs to be converted. - - Returns: - Parameters string. - """ - params_list = [] - for (param, val) in iteritems(params_dict): - if val is False: - val = 'false' - elif val is True: - val = 'true' - - params_list.append('%s = %s' % (param, val)) - - return ', '.join(params_list) - - -class PgSubscription(): - """Class to work with PostgreSQL subscription. - - Args: - module (AnsibleModule): Object of AnsibleModule class. - cursor (cursor): Cursor object of psycopg2 library to work with PostgreSQL. - name (str): The name of the subscription. - db (str): The database name the subscription will be associated with. - - Attributes: - module (AnsibleModule): Object of AnsibleModule class. - cursor (cursor): Cursor object of psycopg2 library to work with PostgreSQL. - name (str): Name of subscription. - executed_queries (list): List of executed queries. - attrs (dict): Dict with subscription attributes. - exists (bool): Flag indicates the subscription exists or not. - """ - - def __init__(self, module, cursor, name, db): - self.module = module - self.cursor = cursor - self.name = name - self.db = db - self.executed_queries = [] - self.attrs = { - 'owner': None, - 'enabled': None, - 'synccommit': None, - 'conninfo': {}, - 'slotname': None, - 'publications': [], - } - self.empty_attrs = deepcopy(self.attrs) - self.exists = self.check_subscr() - - def get_info(self): - """Refresh the subscription information. - - Returns: - ``self.attrs``. - """ - self.exists = self.check_subscr() - return self.attrs - - def check_subscr(self): - """Check the subscription and refresh ``self.attrs`` subscription attribute. - - Returns: - True if the subscription with ``self.name`` exists, False otherwise. - """ - - subscr_info = self.__get_general_subscr_info() - - if not subscr_info: - # The subscription does not exist: - self.attrs = deepcopy(self.empty_attrs) - return False - - self.attrs['owner'] = subscr_info.get('rolname') - self.attrs['enabled'] = subscr_info.get('subenabled') - self.attrs['synccommit'] = subscr_info.get('subenabled') - self.attrs['slotname'] = subscr_info.get('subslotname') - self.attrs['publications'] = subscr_info.get('subpublications') - if subscr_info.get('subconninfo'): - for param in subscr_info['subconninfo'].split(' '): - tmp = param.split('=') - try: - self.attrs['conninfo'][tmp[0]] = int(tmp[1]) - except ValueError: - self.attrs['conninfo'][tmp[0]] = tmp[1] - - return True - - def create(self, connparams, publications, subsparams, check_mode=True): - """Create the subscription. - - Args: - connparams (str): Connection string in libpq style. - publications (list): Publications on the master to use. - subsparams (str): Parameters string in WITH () clause style. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - changed (bool): True if the subscription has been created, otherwise False. - """ - query_fragments = [] - query_fragments.append("CREATE SUBSCRIPTION %s CONNECTION '%s' " - "PUBLICATION %s" % (self.name, connparams, ', '.join(publications))) - - if subsparams: - query_fragments.append("WITH (%s)" % subsparams) - - changed = self.__exec_sql(' '.join(query_fragments), check_mode=check_mode) - - return changed - - def update(self, connparams, publications, subsparams, check_mode=True): - """Update the subscription. - - Args: - connparams (str): Connection string in libpq style. - publications (list): Publications on the master to use. - subsparams (dict): Dictionary of optional parameters. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - changed (bool): True if subscription has been updated, otherwise False. - """ - changed = False - - if connparams: - if connparams != self.attrs['conninfo']: - changed = self.__set_conn_params(convert_conn_params(connparams), - check_mode=check_mode) - - if publications: - if sorted(self.attrs['publications']) != sorted(publications): - changed = self.__set_publications(publications, check_mode=check_mode) - - if subsparams: - params_to_update = [] - - for (param, value) in iteritems(subsparams): - if param == 'enabled': - if self.attrs['enabled'] and value is False: - changed = self.enable(enabled=False, check_mode=check_mode) - elif not self.attrs['enabled'] and value is True: - changed = self.enable(enabled=True, check_mode=check_mode) - - elif param == 'synchronous_commit': - if self.attrs['synccommit'] is True and value is False: - params_to_update.append("%s = false" % param) - elif self.attrs['synccommit'] is False and value is True: - params_to_update.append("%s = true" % param) - - elif param == 'slot_name': - if self.attrs['slotname'] and self.attrs['slotname'] != value: - params_to_update.append("%s = %s" % (param, value)) - - else: - self.module.warn("Parameter '%s' is not in params supported " - "for update '%s', ignored..." % (param, SUBSPARAMS_KEYS_FOR_UPDATE)) - - if params_to_update: - changed = self.__set_params(params_to_update, check_mode=check_mode) - - return changed - - def drop(self, cascade=False, check_mode=True): - """Drop the subscription. - - Kwargs: - cascade (bool): Flag indicates that the subscription needs to be deleted - with its dependencies. - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - changed (bool): True if the subscription has been removed, otherwise False. - """ - if self.exists: - query_fragments = ["DROP SUBSCRIPTION %s" % self.name] - if cascade: - query_fragments.append("CASCADE") - - return self.__exec_sql(' '.join(query_fragments), check_mode=check_mode) - - def set_owner(self, role, check_mode=True): - """Set a subscription owner. - - Args: - role (str): Role (user) name that needs to be set as a subscription owner. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - query = 'ALTER SUBSCRIPTION %s OWNER TO "%s"' % (self.name, role) - return self.__exec_sql(query, check_mode=check_mode) - - def refresh(self, check_mode=True): - """Refresh publication. - - Fetches missing table info from publisher. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - query = 'ALTER SUBSCRIPTION %s REFRESH PUBLICATION' % self.name - return self.__exec_sql(query, check_mode=check_mode) - - def __set_params(self, params_to_update, check_mode=True): - """Update optional subscription parameters. - - Args: - params_to_update (list): Parameters with values to update. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - query = 'ALTER SUBSCRIPTION %s SET (%s)' % (self.name, ', '.join(params_to_update)) - return self.__exec_sql(query, check_mode=check_mode) - - def __set_conn_params(self, connparams, check_mode=True): - """Update connection parameters. - - Args: - connparams (str): Connection string in libpq style. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - query = "ALTER SUBSCRIPTION %s CONNECTION '%s'" % (self.name, connparams) - return self.__exec_sql(query, check_mode=check_mode) - - def __set_publications(self, publications, check_mode=True): - """Update publications. - - Args: - publications (list): Publications on the master to use. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - query = 'ALTER SUBSCRIPTION %s SET PUBLICATION %s' % (self.name, ', '.join(publications)) - return self.__exec_sql(query, check_mode=check_mode) - - def enable(self, enabled=True, check_mode=True): - """Enable or disable the subscription. - - Kwargs: - enable (bool): Flag indicates that the subscription needs - to be enabled or disabled. - check_mode (bool): If True, don't actually change anything, - just make SQL, add it to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - if enabled: - query = 'ALTER SUBSCRIPTION %s ENABLE' % self.name - else: - query = 'ALTER SUBSCRIPTION %s DISABLE' % self.name - - return self.__exec_sql(query, check_mode=check_mode) - - def __get_general_subscr_info(self): - """Get and return general subscription information. - - Returns: - Dict with subscription information if successful, False otherwise. - """ - query = ("SELECT d.datname, r.rolname, s.subenabled, " - "s.subconninfo, s.subslotname, s.subsynccommit, " - "s.subpublications FROM pg_catalog.pg_subscription s " - "JOIN pg_catalog.pg_database d " - "ON s.subdbid = d.oid " - "JOIN pg_catalog.pg_roles AS r " - "ON s.subowner = r.oid " - "WHERE s.subname = %(name)s AND d.datname = %(db)s") - - result = exec_sql(self, query, query_params={'name': self.name, 'db': self.db}, add_to_executed=False) - if result: - return result[0] - else: - return False - - def __exec_sql(self, query, check_mode=False): - """Execute SQL query. - - Note: If we need just to get information from the database, - we use ``exec_sql`` function directly. - - Args: - query (str): Query that needs to be executed. - - Kwargs: - check_mode (bool): If True, don't actually change anything, - just add ``query`` to ``self.executed_queries`` and return True. - - Returns: - True if successful, False otherwise. - """ - if check_mode: - self.executed_queries.append(query) - return True - else: - return exec_sql(self, query, ddl=True) - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - name=dict(type='str', required=True), - db=dict(type='str', required=True, aliases=['login_db']), - state=dict(type='str', default='present', choices=['absent', 'present', 'refresh']), - publications=dict(type='list', elements='str'), - connparams=dict(type='dict'), - cascade=dict(type='bool', default=False), - owner=dict(type='str'), - subsparams=dict(type='dict'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - # Parameters handling: - db = module.params['db'] - name = module.params['name'] - state = module.params['state'] - publications = module.params['publications'] - cascade = module.params['cascade'] - owner = module.params['owner'] - subsparams = module.params['subsparams'] - connparams = module.params['connparams'] - - if state == 'present' and cascade: - module.warn('parameter "cascade" is ignored when state is not absent') - - if state != 'present': - if owner: - module.warn("parameter 'owner' is ignored when state is not 'present'") - if publications: - module.warn("parameter 'publications' is ignored when state is not 'present'") - if connparams: - module.warn("parameter 'connparams' is ignored when state is not 'present'") - if subsparams: - module.warn("parameter 'subsparams' is ignored when state is not 'present'") - - # Connect to DB and make cursor object: - pg_conn_params = get_conn_params(module, module.params) - # We check subscription state without DML queries execution, so set autocommit: - db_connection = connect_to_db(module, pg_conn_params, autocommit=True) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - # Check version: - if cursor.connection.server_version < SUPPORTED_PG_VERSION: - module.fail_json(msg="PostgreSQL server version should be 10.0 or greater") - - # Set defaults: - changed = False - initial_state = {} - final_state = {} - - ################################### - # Create object and do rock'n'roll: - subscription = PgSubscription(module, cursor, name, db) - - if subscription.exists: - initial_state = deepcopy(subscription.attrs) - final_state = deepcopy(initial_state) - - if state == 'present': - if not subscription.exists: - if subsparams: - subsparams = convert_subscr_params(subsparams) - - if connparams: - connparams = convert_conn_params(connparams) - - changed = subscription.create(connparams, - publications, - subsparams, - check_mode=module.check_mode) - - else: - changed = subscription.update(connparams, - publications, - subsparams, - check_mode=module.check_mode) - - if owner and subscription.attrs['owner'] != owner: - changed = subscription.set_owner(owner, check_mode=module.check_mode) or changed - - elif state == 'absent': - changed = subscription.drop(cascade, check_mode=module.check_mode) - - elif state == 'refresh': - if not subscription.exists: - module.fail_json(msg="Refresh failed: subscription '%s' does not exist" % name) - - # Always returns True: - changed = subscription.refresh(check_mode=module.check_mode) - - # Get final subscription info: - final_state = subscription.get_info() - - # Connection is not needed any more: - cursor.close() - db_connection.close() - - # Return ret values and exit: - module.exit_json(changed=changed, - name=name, - exists=subscription.exists, - queries=subscription.executed_queries, - initial_state=initial_state, - final_state=final_state) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_table.py b/lib/ansible/modules/database/postgresql/postgresql_table.py deleted file mode 100644 index 3bef03b08f..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_table.py +++ /dev/null @@ -1,601 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = r''' ---- -module: postgresql_table -short_description: Create, drop, or modify a PostgreSQL table -description: -- Allows to create, drop, rename, truncate a table, or change some table attributes. -version_added: '2.8' -options: - table: - description: - - Table name. - required: true - aliases: - - name - type: str - state: - description: - - The table state. I(state=absent) is mutually exclusive with I(tablespace), I(owner), I(unlogged), - I(like), I(including), I(columns), I(truncate), I(storage_params) and, I(rename). - type: str - default: present - choices: [ absent, present ] - tablespace: - description: - - Set a tablespace for the table. - required: false - type: str - owner: - description: - - Set a table owner. - type: str - unlogged: - description: - - Create an unlogged table. - type: bool - default: no - like: - description: - - Create a table like another table (with similar DDL). - Mutually exclusive with I(columns), I(rename), and I(truncate). - type: str - including: - description: - - Keywords that are used with like parameter, may be DEFAULTS, CONSTRAINTS, INDEXES, STORAGE, COMMENTS or ALL. - Needs I(like) specified. Mutually exclusive with I(columns), I(rename), and I(truncate). - type: str - columns: - description: - - Columns that are needed. - type: list - elements: str - rename: - description: - - New table name. Mutually exclusive with I(tablespace), I(owner), - I(unlogged), I(like), I(including), I(columns), I(truncate), and I(storage_params). - type: str - truncate: - description: - - Truncate a table. Mutually exclusive with I(tablespace), I(owner), I(unlogged), - I(like), I(including), I(columns), I(rename), and I(storage_params). - type: bool - default: no - storage_params: - description: - - Storage parameters like fillfactor, autovacuum_vacuum_treshold, etc. - Mutually exclusive with I(rename) and I(truncate). - type: list - elements: str - db: - description: - - Name of database to connect and where the table will be created. - type: str - aliases: - - login_db - session_role: - description: - - Switch to session_role after connecting. - The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str - cascade: - description: - - Automatically drop objects that depend on the table (such as views). - Used with I(state=absent) only. - type: bool - default: no - version_added: '2.9' -notes: -- If you do not pass db parameter, tables will be created in the database - named postgres. -- PostgreSQL allows to create columnless table, so columns param is optional. -- Unlogged tables are available from PostgreSQL server version 9.1. -seealso: -- module: postgresql_sequence -- module: postgresql_idx -- module: postgresql_info -- module: postgresql_tablespace -- module: postgresql_owner -- module: postgresql_privs -- module: postgresql_copy -- name: CREATE TABLE reference - description: Complete reference of the CREATE TABLE command documentation. - link: https://www.postgresql.org/docs/current/sql-createtable.html -- name: ALTER TABLE reference - description: Complete reference of the ALTER TABLE command documentation. - link: https://www.postgresql.org/docs/current/sql-altertable.html -- name: DROP TABLE reference - description: Complete reference of the DROP TABLE command documentation. - link: https://www.postgresql.org/docs/current/sql-droptable.html -- name: PostgreSQL data types - description: Complete reference of the PostgreSQL data types documentation. - link: https://www.postgresql.org/docs/current/datatype.html -author: -- Andrei Klychkov (@Andersson007) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Create tbl2 in the acme database with the DDL like tbl1 with testuser as an owner - postgresql_table: - db: acme - name: tbl2 - like: tbl1 - owner: testuser - -- name: Create tbl2 in the acme database and tablespace ssd with the DDL like tbl1 including comments and indexes - postgresql_table: - db: acme - table: tbl2 - like: tbl1 - including: comments, indexes - tablespace: ssd - -- name: Create test_table with several columns in ssd tablespace with fillfactor=10 and autovacuum_analyze_threshold=1 - postgresql_table: - name: test_table - columns: - - id bigserial primary key - - num bigint - - stories text - tablespace: ssd - storage_params: - - fillfactor=10 - - autovacuum_analyze_threshold=1 - -- name: Create an unlogged table in schema acme - postgresql_table: - name: acme.useless_data - columns: waste_id int - unlogged: true - -- name: Rename table foo to bar - postgresql_table: - table: foo - rename: bar - -- name: Rename table foo from schema acme to bar - postgresql_table: - name: acme.foo - rename: bar - -- name: Set owner to someuser - postgresql_table: - name: foo - owner: someuser - -- name: Change tablespace of foo table to new_tablespace and set owner to new_user - postgresql_table: - name: foo - tablespace: new_tablespace - owner: new_user - -- name: Truncate table foo - postgresql_table: - name: foo - truncate: yes - -- name: Drop table foo from schema acme - postgresql_table: - name: acme.foo - state: absent - -- name: Drop table bar cascade - postgresql_table: - name: bar - state: absent - cascade: yes -''' - -RETURN = r''' -table: - description: Name of a table. - returned: always - type: str - sample: 'foo' -state: - description: Table state. - returned: always - type: str - sample: 'present' -owner: - description: Table owner. - returned: always - type: str - sample: 'postgres' -tablespace: - description: Tablespace. - returned: always - type: str - sample: 'ssd_tablespace' -queries: - description: List of executed queries. - returned: always - type: str - sample: [ 'CREATE TABLE "test_table" (id bigint)' ] -storage_params: - description: Storage parameters. - returned: always - type: list - sample: [ "fillfactor=100", "autovacuum_analyze_threshold=1" ] -''' - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import pg_quote_identifier -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) - - -# =========================================== -# PostgreSQL module specific support methods. -# - -class Table(object): - def __init__(self, name, module, cursor): - self.name = name - self.module = module - self.cursor = cursor - self.info = { - 'owner': '', - 'tblspace': '', - 'storage_params': [], - } - self.exists = False - self.__exists_in_db() - self.executed_queries = [] - - def get_info(self): - """Getter to refresh and get table info""" - self.__exists_in_db() - - def __exists_in_db(self): - """Check table exists and refresh info""" - if "." in self.name: - schema = self.name.split('.')[-2] - tblname = self.name.split('.')[-1] - else: - schema = 'public' - tblname = self.name - - query = ("SELECT t.tableowner, t.tablespace, c.reloptions " - "FROM pg_tables AS t " - "INNER JOIN pg_class AS c ON c.relname = t.tablename " - "INNER JOIN pg_namespace AS n ON c.relnamespace = n.oid " - "WHERE t.tablename = %(tblname)s " - "AND n.nspname = %(schema)s") - res = exec_sql(self, query, query_params={'tblname': tblname, 'schema': schema}, - add_to_executed=False) - if res: - self.exists = True - self.info = dict( - owner=res[0][0], - tblspace=res[0][1] if res[0][1] else '', - storage_params=res[0][2] if res[0][2] else [], - ) - - return True - else: - self.exists = False - return False - - def create(self, columns='', params='', tblspace='', - unlogged=False, owner=''): - """ - Create table. - If table exists, check passed args (params, tblspace, owner) and, - if they're different from current, change them. - Arguments: - params - storage params (passed by "WITH (...)" in SQL), - comma separated. - tblspace - tablespace. - owner - table owner. - unlogged - create unlogged table. - columns - column string (comma separated). - """ - name = pg_quote_identifier(self.name, 'table') - - changed = False - - if self.exists: - if tblspace == 'pg_default' and self.info['tblspace'] is None: - pass # Because they have the same meaning - elif tblspace and self.info['tblspace'] != tblspace: - self.set_tblspace(tblspace) - changed = True - - if owner and self.info['owner'] != owner: - self.set_owner(owner) - changed = True - - if params: - param_list = [p.strip(' ') for p in params.split(',')] - - new_param = False - for p in param_list: - if p not in self.info['storage_params']: - new_param = True - - if new_param: - self.set_stor_params(params) - changed = True - - if changed: - return True - return False - - query = "CREATE" - if unlogged: - query += " UNLOGGED TABLE %s" % name - else: - query += " TABLE %s" % name - - if columns: - query += " (%s)" % columns - else: - query += " ()" - - if params: - query += " WITH (%s)" % params - - if tblspace: - query += " TABLESPACE %s" % pg_quote_identifier(tblspace, 'database') - - if exec_sql(self, query, ddl=True): - changed = True - - if owner: - changed = self.set_owner(owner) - - return changed - - def create_like(self, src_table, including='', tblspace='', - unlogged=False, params='', owner=''): - """ - Create table like another table (with similar DDL). - Arguments: - src_table - source table. - including - corresponds to optional INCLUDING expression - in CREATE TABLE ... LIKE statement. - params - storage params (passed by "WITH (...)" in SQL), - comma separated. - tblspace - tablespace. - owner - table owner. - unlogged - create unlogged table. - """ - changed = False - - name = pg_quote_identifier(self.name, 'table') - - query = "CREATE" - if unlogged: - query += " UNLOGGED TABLE %s" % name - else: - query += " TABLE %s" % name - - query += " (LIKE %s" % pg_quote_identifier(src_table, 'table') - - if including: - including = including.split(',') - for i in including: - query += " INCLUDING %s" % i - - query += ')' - - if params: - query += " WITH (%s)" % params - - if tblspace: - query += " TABLESPACE %s" % pg_quote_identifier(tblspace, 'database') - - if exec_sql(self, query, ddl=True): - changed = True - - if owner: - changed = self.set_owner(owner) - - return changed - - def truncate(self): - query = "TRUNCATE TABLE %s" % pg_quote_identifier(self.name, 'table') - return exec_sql(self, query, ddl=True) - - def rename(self, newname): - query = "ALTER TABLE %s RENAME TO %s" % (pg_quote_identifier(self.name, 'table'), - pg_quote_identifier(newname, 'table')) - return exec_sql(self, query, ddl=True) - - def set_owner(self, username): - query = "ALTER TABLE %s OWNER TO %s" % (pg_quote_identifier(self.name, 'table'), - pg_quote_identifier(username, 'role')) - return exec_sql(self, query, ddl=True) - - def drop(self, cascade=False): - if not self.exists: - return False - - query = "DROP TABLE %s" % pg_quote_identifier(self.name, 'table') - if cascade: - query += " CASCADE" - return exec_sql(self, query, ddl=True) - - def set_tblspace(self, tblspace): - query = "ALTER TABLE %s SET TABLESPACE %s" % (pg_quote_identifier(self.name, 'table'), - pg_quote_identifier(tblspace, 'database')) - return exec_sql(self, query, ddl=True) - - def set_stor_params(self, params): - query = "ALTER TABLE %s SET (%s)" % (pg_quote_identifier(self.name, 'table'), params) - return exec_sql(self, query, ddl=True) - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - table=dict(type='str', required=True, aliases=['name']), - state=dict(type='str', default="present", choices=["absent", "present"]), - db=dict(type='str', default='', aliases=['login_db']), - tablespace=dict(type='str'), - owner=dict(type='str'), - unlogged=dict(type='bool', default=False), - like=dict(type='str'), - including=dict(type='str'), - rename=dict(type='str'), - truncate=dict(type='bool', default=False), - columns=dict(type='list', elements='str'), - storage_params=dict(type='list', elements='str'), - session_role=dict(type='str'), - cascade=dict(type='bool', default=False), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - table = module.params["table"] - state = module.params["state"] - tablespace = module.params["tablespace"] - owner = module.params["owner"] - unlogged = module.params["unlogged"] - like = module.params["like"] - including = module.params["including"] - newname = module.params["rename"] - storage_params = module.params["storage_params"] - truncate = module.params["truncate"] - columns = module.params["columns"] - cascade = module.params["cascade"] - - if state == 'present' and cascade: - module.warn("cascade=true is ignored when state=present") - - # Check mutual exclusive parameters: - if state == 'absent' and (truncate or newname or columns or tablespace or like or storage_params or unlogged or owner or including): - module.fail_json(msg="%s: state=absent is mutually exclusive with: " - "truncate, rename, columns, tablespace, " - "including, like, storage_params, unlogged, owner" % table) - - if truncate and (newname or columns or like or unlogged or storage_params or owner or tablespace or including): - module.fail_json(msg="%s: truncate is mutually exclusive with: " - "rename, columns, like, unlogged, including, " - "storage_params, owner, tablespace" % table) - - if newname and (columns or like or unlogged or storage_params or owner or tablespace or including): - module.fail_json(msg="%s: rename is mutually exclusive with: " - "columns, like, unlogged, including, " - "storage_params, owner, tablespace" % table) - - if like and columns: - module.fail_json(msg="%s: like and columns params are mutually exclusive" % table) - if including and not like: - module.fail_json(msg="%s: including param needs like param specified" % table) - - conn_params = get_conn_params(module, module.params) - db_connection = connect_to_db(module, conn_params, autocommit=False) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - if storage_params: - storage_params = ','.join(storage_params) - - if columns: - columns = ','.join(columns) - - ############## - # Do main job: - table_obj = Table(table, module, cursor) - - # Set default returned values: - changed = False - kw = {} - kw['table'] = table - kw['state'] = '' - if table_obj.exists: - kw = dict( - table=table, - state='present', - owner=table_obj.info['owner'], - tablespace=table_obj.info['tblspace'], - storage_params=table_obj.info['storage_params'], - ) - - if state == 'absent': - changed = table_obj.drop(cascade=cascade) - - elif truncate: - changed = table_obj.truncate() - - elif newname: - changed = table_obj.rename(newname) - q = table_obj.executed_queries - table_obj = Table(newname, module, cursor) - table_obj.executed_queries = q - - elif state == 'present' and not like: - changed = table_obj.create(columns, storage_params, - tablespace, unlogged, owner) - - elif state == 'present' and like: - changed = table_obj.create_like(like, including, tablespace, - unlogged, storage_params) - - if changed: - if module.check_mode: - db_connection.rollback() - else: - db_connection.commit() - - # Refresh table info for RETURN. - # Note, if table has been renamed, it gets info by newname: - table_obj.get_info() - db_connection.commit() - if table_obj.exists: - kw = dict( - table=table, - state='present', - owner=table_obj.info['owner'], - tablespace=table_obj.info['tblspace'], - storage_params=table_obj.info['storage_params'], - ) - else: - # We just change the table state here - # to keep other information about the dropped table: - kw['state'] = 'absent' - - kw['queries'] = table_obj.executed_queries - kw['changed'] = changed - db_connection.close() - module.exit_json(**kw) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_tablespace.py b/lib/ansible/modules/database/postgresql/postgresql_tablespace.py deleted file mode 100644 index aceced98af..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_tablespace.py +++ /dev/null @@ -1,519 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2017, Flavien Chantelot (@Dorn-) -# Copyright: (c) 2018, Antoine Levy-Lambert (@antoinell) -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'supported_by': 'community', - 'status': ['preview'] -} - -DOCUMENTATION = r''' ---- -module: postgresql_tablespace -short_description: Add or remove PostgreSQL tablespaces from remote hosts -description: -- Adds or removes PostgreSQL tablespaces from remote hosts. -version_added: '2.8' -options: - tablespace: - description: - - Name of the tablespace to add or remove. - required: true - type: str - aliases: - - name - location: - description: - - Path to the tablespace directory in the file system. - - Ensure that the location exists and has right privileges. - type: path - aliases: - - path - state: - description: - - Tablespace state. - - I(state=present) implies the tablespace must be created if it doesn't exist. - - I(state=absent) implies the tablespace must be removed if present. - I(state=absent) is mutually exclusive with I(location), I(owner), i(set). - - See the Notes section for information about check mode restrictions. - type: str - default: present - choices: [ absent, present ] - owner: - description: - - Name of the role to set as an owner of the tablespace. - - If this option is not specified, the tablespace owner is a role that creates the tablespace. - type: str - set: - description: - - Dict of tablespace options to set. Supported from PostgreSQL 9.0. - - For more information see U(https://www.postgresql.org/docs/current/sql-createtablespace.html). - - When reset is passed as an option's value, if the option was set previously, it will be removed. - type: dict - rename_to: - description: - - New name of the tablespace. - - The new name cannot begin with pg_, as such names are reserved for system tablespaces. - type: str - session_role: - description: - - Switch to session_role after connecting. The specified session_role must - be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str - db: - description: - - Name of database to connect to and run queries against. - type: str - aliases: - - login_db - -notes: -- I(state=absent) and I(state=present) (the second one if the tablespace doesn't exist) do not - support check mode because the corresponding PostgreSQL DROP and CREATE TABLESPACE commands - can not be run inside the transaction block. - -seealso: -- name: PostgreSQL tablespaces - description: General information about PostgreSQL tablespaces. - link: https://www.postgresql.org/docs/current/manage-ag-tablespaces.html -- name: CREATE TABLESPACE reference - description: Complete reference of the CREATE TABLESPACE command documentation. - link: https://www.postgresql.org/docs/current/sql-createtablespace.html -- name: ALTER TABLESPACE reference - description: Complete reference of the ALTER TABLESPACE command documentation. - link: https://www.postgresql.org/docs/current/sql-altertablespace.html -- name: DROP TABLESPACE reference - description: Complete reference of the DROP TABLESPACE command documentation. - link: https://www.postgresql.org/docs/current/sql-droptablespace.html - -author: -- Flavien Chantelot (@Dorn-) -- Antoine Levy-Lambert (@antoinell) -- Andrew Klychkov (@Andersson007) - -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Create a new tablespace called acme and set bob as an its owner - postgresql_tablespace: - name: acme - owner: bob - location: /data/foo - -- name: Create a new tablespace called bar with tablespace options - postgresql_tablespace: - name: bar - set: - random_page_cost: 1 - seq_page_cost: 1 - -- name: Reset random_page_cost option - postgresql_tablespace: - name: bar - set: - random_page_cost: reset - -- name: Rename the tablespace from bar to pcie_ssd - postgresql_tablespace: - name: bar - rename_to: pcie_ssd - -- name: Drop tablespace called bloat - postgresql_tablespace: - name: bloat - state: absent -''' - -RETURN = r''' -queries: - description: List of queries that was tried to be executed. - returned: always - type: str - sample: [ "CREATE TABLESPACE bar LOCATION '/incredible/ssd'" ] -tablespace: - description: Tablespace name. - returned: always - type: str - sample: 'ssd' -owner: - description: Tablespace owner. - returned: always - type: str - sample: 'Bob' -options: - description: Tablespace options. - returned: always - type: dict - sample: { 'random_page_cost': 1, 'seq_page_cost': 1 } -location: - description: Path to the tablespace in the file system. - returned: always - type: str - sample: '/incredible/fast/ssd' -newname: - description: New tablespace name - returned: if existent - type: str - sample: new_ssd -state: - description: Tablespace state at the end of execution. - returned: always - type: str - sample: 'present' -''' - -try: - from psycopg2 import __version__ as PSYCOPG2_VERSION - from psycopg2.extras import DictCursor - from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT as AUTOCOMMIT - from psycopg2.extensions import ISOLATION_LEVEL_READ_COMMITTED as READ_COMMITTED -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import pg_quote_identifier -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) - - -class PgTablespace(object): - - """Class for working with PostgreSQL tablespaces. - - Args: - module (AnsibleModule) -- object of AnsibleModule class - cursor (cursor) -- cursor object of psycopg2 library - name (str) -- name of the tablespace - - Attrs: - module (AnsibleModule) -- object of AnsibleModule class - cursor (cursor) -- cursor object of psycopg2 library - name (str) -- name of the tablespace - exists (bool) -- flag the tablespace exists in the DB or not - owner (str) -- tablespace owner - location (str) -- path to the tablespace directory in the file system - executed_queries (list) -- list of executed queries - new_name (str) -- new name for the tablespace - opt_not_supported (bool) -- flag indicates a tablespace option is supported or not - """ - - def __init__(self, module, cursor, name): - self.module = module - self.cursor = cursor - self.name = name - self.exists = False - self.owner = '' - self.settings = {} - self.location = '' - self.executed_queries = [] - self.new_name = '' - self.opt_not_supported = False - # Collect info: - self.get_info() - - def get_info(self): - """Get tablespace information.""" - # Check that spcoptions exists: - opt = exec_sql(self, "SELECT 1 FROM information_schema.columns " - "WHERE table_name = 'pg_tablespace' " - "AND column_name = 'spcoptions'", add_to_executed=False) - - # For 9.1 version and earlier: - location = exec_sql(self, "SELECT 1 FROM information_schema.columns " - "WHERE table_name = 'pg_tablespace' " - "AND column_name = 'spclocation'", add_to_executed=False) - if location: - location = 'spclocation' - else: - location = 'pg_tablespace_location(t.oid)' - - if not opt: - self.opt_not_supported = True - query = ("SELECT r.rolname, (SELECT Null), %s " - "FROM pg_catalog.pg_tablespace AS t " - "JOIN pg_catalog.pg_roles AS r " - "ON t.spcowner = r.oid " % location) - else: - query = ("SELECT r.rolname, t.spcoptions, %s " - "FROM pg_catalog.pg_tablespace AS t " - "JOIN pg_catalog.pg_roles AS r " - "ON t.spcowner = r.oid " % location) - - res = exec_sql(self, query + "WHERE t.spcname = %(name)s", - query_params={'name': self.name}, add_to_executed=False) - - if not res: - self.exists = False - return False - - if res[0][0]: - self.exists = True - self.owner = res[0][0] - - if res[0][1]: - # Options exist: - for i in res[0][1]: - i = i.split('=') - self.settings[i[0]] = i[1] - - if res[0][2]: - # Location exists: - self.location = res[0][2] - - def create(self, location): - """Create tablespace. - - Return True if success, otherwise, return False. - - args: - location (str) -- tablespace directory path in the FS - """ - query = ("CREATE TABLESPACE %s LOCATION '%s'" % (pg_quote_identifier(self.name, 'database'), location)) - return exec_sql(self, query, ddl=True) - - def drop(self): - """Drop tablespace. - - Return True if success, otherwise, return False. - """ - return exec_sql(self, "DROP TABLESPACE %s" % pg_quote_identifier(self.name, 'database'), ddl=True) - - def set_owner(self, new_owner): - """Set tablespace owner. - - Return True if success, otherwise, return False. - - args: - new_owner (str) -- name of a new owner for the tablespace" - """ - if new_owner == self.owner: - return False - - query = "ALTER TABLESPACE %s OWNER TO %s" % (pg_quote_identifier(self.name, 'database'), new_owner) - return exec_sql(self, query, ddl=True) - - def rename(self, newname): - """Rename tablespace. - - Return True if success, otherwise, return False. - - args: - newname (str) -- new name for the tablespace" - """ - query = "ALTER TABLESPACE %s RENAME TO %s" % (pg_quote_identifier(self.name, 'database'), newname) - self.new_name = newname - return exec_sql(self, query, ddl=True) - - def set_settings(self, new_settings): - """Set tablespace settings (options). - - If some setting has been changed, set changed = True. - After all settings list is handling, return changed. - - args: - new_settings (list) -- list of new settings - """ - # settings must be a dict {'key': 'value'} - if self.opt_not_supported: - return False - - changed = False - - # Apply new settings: - for i in new_settings: - if new_settings[i] == 'reset': - if i in self.settings: - changed = self.__reset_setting(i) - self.settings[i] = None - - elif (i not in self.settings) or (str(new_settings[i]) != self.settings[i]): - changed = self.__set_setting("%s = '%s'" % (i, new_settings[i])) - - return changed - - def __reset_setting(self, setting): - """Reset tablespace setting. - - Return True if success, otherwise, return False. - - args: - setting (str) -- string in format "setting_name = 'setting_value'" - """ - query = "ALTER TABLESPACE %s RESET (%s)" % (pg_quote_identifier(self.name, 'database'), setting) - return exec_sql(self, query, ddl=True) - - def __set_setting(self, setting): - """Set tablespace setting. - - Return True if success, otherwise, return False. - - args: - setting (str) -- string in format "setting_name = 'setting_value'" - """ - query = "ALTER TABLESPACE %s SET (%s)" % (pg_quote_identifier(self.name, 'database'), setting) - return exec_sql(self, query, ddl=True) - - -# =========================================== -# Module execution. -# - - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - tablespace=dict(type='str', required=True, aliases=['name']), - state=dict(type='str', default="present", choices=["absent", "present"]), - location=dict(type='path', aliases=['path']), - owner=dict(type='str'), - set=dict(type='dict'), - rename_to=dict(type='str'), - db=dict(type='str', aliases=['login_db']), - session_role=dict(type='str'), - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=(('positional_args', 'named_args'),), - supports_check_mode=True, - ) - - tablespace = module.params["tablespace"] - state = module.params["state"] - location = module.params["location"] - owner = module.params["owner"] - rename_to = module.params["rename_to"] - settings = module.params["set"] - - if state == 'absent' and (location or owner or rename_to or settings): - module.fail_json(msg="state=absent is mutually exclusive location, " - "owner, rename_to, and set") - - conn_params = get_conn_params(module, module.params, warn_db_default=False) - db_connection = connect_to_db(module, conn_params, autocommit=True) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - # Change autocommit to False if check_mode: - if module.check_mode: - if PSYCOPG2_VERSION >= '2.4.2': - db_connection.set_session(autocommit=False) - else: - db_connection.set_isolation_level(READ_COMMITTED) - - # Set defaults: - autocommit = False - changed = False - - ############## - # Create PgTablespace object and do main job: - tblspace = PgTablespace(module, cursor, tablespace) - - # If tablespace exists with different location, exit: - if tblspace.exists and location and location != tblspace.location: - module.fail_json(msg="Tablespace '%s' exists with different location '%s'" % (tblspace.name, tblspace.location)) - - # Create new tablespace: - if not tblspace.exists and state == 'present': - if rename_to: - module.fail_json(msg="Tablespace %s does not exist, nothing to rename" % tablespace) - - if not location: - module.fail_json(msg="'location' parameter must be passed with " - "state=present if the tablespace doesn't exist") - - # Because CREATE TABLESPACE can not be run inside the transaction block: - autocommit = True - if PSYCOPG2_VERSION >= '2.4.2': - db_connection.set_session(autocommit=True) - else: - db_connection.set_isolation_level(AUTOCOMMIT) - - changed = tblspace.create(location) - - # Drop non-existing tablespace: - elif not tblspace.exists and state == 'absent': - # Nothing to do: - module.fail_json(msg="Tries to drop nonexistent tablespace '%s'" % tblspace.name) - - # Drop existing tablespace: - elif tblspace.exists and state == 'absent': - # Because DROP TABLESPACE can not be run inside the transaction block: - autocommit = True - if PSYCOPG2_VERSION >= '2.4.2': - db_connection.set_session(autocommit=True) - else: - db_connection.set_isolation_level(AUTOCOMMIT) - - changed = tblspace.drop() - - # Rename tablespace: - elif tblspace.exists and rename_to: - if tblspace.name != rename_to: - changed = tblspace.rename(rename_to) - - if state == 'present': - # Refresh information: - tblspace.get_info() - - # Change owner and settings: - if state == 'present' and tblspace.exists: - if owner: - changed = tblspace.set_owner(owner) - - if settings: - changed = tblspace.set_settings(settings) - - tblspace.get_info() - - # Rollback if it's possible and check_mode: - if not autocommit: - if module.check_mode: - db_connection.rollback() - else: - db_connection.commit() - - cursor.close() - db_connection.close() - - # Make return values: - kw = dict( - changed=changed, - state='present', - tablespace=tblspace.name, - owner=tblspace.owner, - queries=tblspace.executed_queries, - options=tblspace.settings, - location=tblspace.location, - ) - - if state == 'present': - kw['state'] = 'present' - - if tblspace.new_name: - kw['newname'] = tblspace.new_name - - elif state == 'absent': - kw['state'] = 'absent' - - module.exit_json(**kw) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_user.py b/lib/ansible/modules/database/postgresql/postgresql_user.py deleted file mode 100644 index dc1b6a17b2..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_user.py +++ /dev/null @@ -1,927 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community' -} - -DOCUMENTATION = r''' ---- -module: postgresql_user -short_description: Add or remove a user (role) from a PostgreSQL server instance -description: -- Adds or removes a user (role) from a PostgreSQL server instance - ("cluster" in PostgreSQL terminology) and, optionally, - grants the user access to an existing database or tables. -- A user is a role with login privilege. -- The fundamental function of the module is to create, or delete, users from - a PostgreSQL instances. Privilege assignment, or removal, is an optional - step, which works on one database at a time. This allows for the module to - be called several times in the same module to modify the permissions on - different databases, or to grant permissions to already existing users. -- A user cannot be removed until all the privileges have been stripped from - the user. In such situation, if the module tries to remove the user it - will fail. To avoid this from happening the fail_on_user option signals - the module to try to remove the user, but if not possible keep going; the - module will report if changes happened and separately if the user was - removed or not. -version_added: '0.6' -options: - name: - description: - - Name of the user (role) to add or remove. - type: str - required: true - aliases: - - user - password: - description: - - Set the user's password, before 1.4 this was required. - - Password can be passed unhashed or hashed (MD5-hashed). - - Unhashed password will automatically be hashed when saved into the - database if C(encrypted) parameter is set, otherwise it will be save in - plain text format. - - When passing a hashed password it must be generated with the format - C('str["md5"] + md5[ password + username ]'), resulting in a total of - 35 characters. An easy way to do this is C(echo "md5$(echo -n - 'verysecretpasswordJOE' | md5sum | awk '{print $1}')"). - - Note that if the provided password string is already in MD5-hashed - format, then it is used as-is, regardless of C(encrypted) parameter. - type: str - db: - description: - - Name of database to connect to and where user's permissions will be granted. - type: str - aliases: - - login_db - fail_on_user: - description: - - If C(yes), fail when user (role) can't be removed. Otherwise just log and continue. - default: 'yes' - type: bool - aliases: - - fail_on_role - priv: - description: - - "Slash-separated PostgreSQL privileges string: C(priv1/priv2), where - privileges can be defined for database ( allowed options - 'CREATE', - 'CONNECT', 'TEMPORARY', 'TEMP', 'ALL'. For example C(CONNECT) ) or - for table ( allowed options - 'SELECT', 'INSERT', 'UPDATE', 'DELETE', - 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'ALL'. For example - C(table:SELECT) ). Mixed example of this string: - C(CONNECT/CREATE/table1:SELECT/table2:INSERT)." - type: str - role_attr_flags: - description: - - "PostgreSQL user attributes string in the format: CREATEDB,CREATEROLE,SUPERUSER." - - Note that '[NO]CREATEUSER' is deprecated. - - To create a simple role for using it like a group, use C(NOLOGIN) flag. - type: str - choices: [ '[NO]SUPERUSER', '[NO]CREATEROLE', '[NO]CREATEDB', - '[NO]INHERIT', '[NO]LOGIN', '[NO]REPLICATION', '[NO]BYPASSRLS' ] - session_role: - version_added: '2.8' - description: - - Switch to session_role after connecting. - - The specified session_role must be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though the session_role were the one that had logged in originally. - type: str - state: - description: - - The user (role) state. - type: str - default: present - choices: [ absent, present ] - encrypted: - description: - - Whether the password is stored hashed in the database. - - Passwords can be passed already hashed or unhashed, and postgresql - ensures the stored password is hashed when C(encrypted) is set. - - "Note: Postgresql 10 and newer doesn't support unhashed passwords." - - Previous to Ansible 2.6, this was C(no) by default. - default: 'yes' - type: bool - version_added: '1.4' - expires: - description: - - The date at which the user's password is to expire. - - If set to C('infinity'), user's password never expire. - - Note that this value should be a valid SQL date and time type. - type: str - version_added: '1.4' - no_password_changes: - description: - - If C(yes), don't inspect database for password changes. Effective when - C(pg_authid) is not accessible (such as AWS RDS). Otherwise, make - password changes as necessary. - default: 'no' - type: bool - version_added: '2.0' - conn_limit: - description: - - Specifies the user (role) connection limit. - type: int - version_added: '2.4' - ssl_mode: - description: - - Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. - - See U(https://www.postgresql.org/docs/current/static/libpq-ssl.html) for more information on the modes. - - Default of C(prefer) matches libpq default. - type: str - default: prefer - choices: [ allow, disable, prefer, require, verify-ca, verify-full ] - version_added: '2.3' - ca_cert: - description: - - Specifies the name of a file containing SSL certificate authority (CA) certificate(s). - - If the file exists, the server's certificate will be verified to be signed by one of these authorities. - type: str - aliases: [ ssl_rootcert ] - version_added: '2.3' - groups: - description: - - The list of groups (roles) that need to be granted to the user. - type: list - elements: str - version_added: '2.9' - comment: - description: - - Add a comment on the user (equal to the COMMENT ON ROLE statement result). - type: str - version_added: '2.10' -notes: -- The module creates a user (role) with login privilege by default. - Use NOLOGIN role_attr_flags to change this behaviour. -- If you specify PUBLIC as the user (role), then the privilege changes will apply to all users (roles). - You may not specify password or role_attr_flags when the PUBLIC user is specified. -seealso: -- module: postgresql_privs -- module: postgresql_membership -- module: postgresql_owner -- name: PostgreSQL database roles - description: Complete reference of the PostgreSQL database roles documentation. - link: https://www.postgresql.org/docs/current/user-manag.html -author: -- Ansible Core Team -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Connect to acme database, create django user, and grant access to database and products table - postgresql_user: - db: acme - name: django - password: ceec4eif7ya - priv: "CONNECT/products:ALL" - expires: "Jan 31 2020" - -- name: Add a comment on django user - postgresql_user: - db: acme - name: django - comment: This is a test user - -# Connect to default database, create rails user, set its password (MD5-hashed), -# and grant privilege to create other databases and demote rails from super user status if user exists -- name: Create rails user, set MD5-hashed password, grant privs - postgresql_user: - name: rails - password: md59543f1d82624df2b31672ec0f7050460 - role_attr_flags: CREATEDB,NOSUPERUSER - -- name: Connect to acme database and remove test user privileges from there - postgresql_user: - db: acme - name: test - priv: "ALL/products:ALL" - state: absent - fail_on_user: no - -- name: Connect to test database, remove test user from cluster - postgresql_user: - db: test - name: test - priv: ALL - state: absent - -- name: Connect to acme database and set user's password with no expire date - postgresql_user: - db: acme - name: django - password: mysupersecretword - priv: "CONNECT/products:ALL" - expires: infinity - -# Example privileges string format -# INSERT,UPDATE/table:SELECT/anothertable:ALL - -- name: Connect to test database and remove an existing user's password - postgresql_user: - db: test - user: test - password: "" - -- name: Create user test and grant group user_ro and user_rw to it - postgresql_user: - name: test - groups: - - user_ro - - user_rw -''' - -RETURN = r''' -queries: - description: List of executed queries. - returned: always - type: list - sample: ['CREATE USER "alice"', 'GRANT CONNECT ON DATABASE "acme" TO "alice"'] - version_added: '2.8' -''' - -import itertools -import re -import traceback -from hashlib import md5 - -try: - import psycopg2 - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.database import pg_quote_identifier, SQLParseError -from ansible.module_utils.postgres import ( - connect_to_db, - get_conn_params, - PgMembership, - postgres_common_argument_spec, -) -from ansible.module_utils._text import to_bytes, to_native -from ansible.module_utils.six import iteritems - - -FLAGS = ('SUPERUSER', 'CREATEROLE', 'CREATEDB', 'INHERIT', 'LOGIN', 'REPLICATION') -FLAGS_BY_VERSION = {'BYPASSRLS': 90500} - -VALID_PRIVS = dict(table=frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'ALL')), - database=frozenset( - ('CREATE', 'CONNECT', 'TEMPORARY', 'TEMP', 'ALL')), - ) - -# map to cope with idiosyncracies of SUPERUSER and LOGIN -PRIV_TO_AUTHID_COLUMN = dict(SUPERUSER='rolsuper', CREATEROLE='rolcreaterole', - CREATEDB='rolcreatedb', INHERIT='rolinherit', LOGIN='rolcanlogin', - REPLICATION='rolreplication', BYPASSRLS='rolbypassrls') - -executed_queries = [] - - -class InvalidFlagsError(Exception): - pass - - -class InvalidPrivsError(Exception): - pass - -# =========================================== -# PostgreSQL module specific support methods. -# - - -def user_exists(cursor, user): - # The PUBLIC user is a special case that is always there - if user == 'PUBLIC': - return True - query = "SELECT rolname FROM pg_roles WHERE rolname=%(user)s" - cursor.execute(query, {'user': user}) - return cursor.rowcount > 0 - - -def user_add(cursor, user, password, role_attr_flags, encrypted, expires, conn_limit): - """Create a new database user (role).""" - # Note: role_attr_flags escaped by parse_role_attrs and encrypted is a - # literal - query_password_data = dict(password=password, expires=expires) - query = ['CREATE USER "%(user)s"' % - {"user": user}] - if password is not None and password != '': - query.append("WITH %(crypt)s" % {"crypt": encrypted}) - query.append("PASSWORD %(password)s") - if expires is not None: - query.append("VALID UNTIL %(expires)s") - if conn_limit is not None: - query.append("CONNECTION LIMIT %(conn_limit)s" % {"conn_limit": conn_limit}) - query.append(role_attr_flags) - query = ' '.join(query) - executed_queries.append(query) - cursor.execute(query, query_password_data) - return True - - -def user_should_we_change_password(current_role_attrs, user, password, encrypted): - """Check if we should change the user's password. - - Compare the proposed password with the existing one, comparing - hashes if encrypted. If we can't access it assume yes. - """ - - if current_role_attrs is None: - # on some databases, E.g. AWS RDS instances, there is no access to - # the pg_authid relation to check the pre-existing password, so we - # just assume password is different - return True - - # Do we actually need to do anything? - pwchanging = False - if password is not None: - # Empty password means that the role shouldn't have a password, which - # means we need to check if the current password is None. - if password == '': - if current_role_attrs['rolpassword'] is not None: - pwchanging = True - # 32: MD5 hashes are represented as a sequence of 32 hexadecimal digits - # 3: The size of the 'md5' prefix - # When the provided password looks like a MD5-hash, value of - # 'encrypted' is ignored. - elif (password.startswith('md5') and len(password) == 32 + 3) or encrypted == 'UNENCRYPTED': - if password != current_role_attrs['rolpassword']: - pwchanging = True - elif encrypted == 'ENCRYPTED': - hashed_password = 'md5{0}'.format(md5(to_bytes(password) + to_bytes(user)).hexdigest()) - if hashed_password != current_role_attrs['rolpassword']: - pwchanging = True - - return pwchanging - - -def user_alter(db_connection, module, user, password, role_attr_flags, encrypted, expires, no_password_changes, conn_limit): - """Change user password and/or attributes. Return True if changed, False otherwise.""" - changed = False - - cursor = db_connection.cursor(cursor_factory=DictCursor) - # Note: role_attr_flags escaped by parse_role_attrs and encrypted is a - # literal - if user == 'PUBLIC': - if password is not None: - module.fail_json(msg="cannot change the password for PUBLIC user") - elif role_attr_flags != '': - module.fail_json(msg="cannot change the role_attr_flags for PUBLIC user") - else: - return False - - # Handle passwords. - if not no_password_changes and (password is not None or role_attr_flags != '' or expires is not None or conn_limit is not None): - # Select password and all flag-like columns in order to verify changes. - try: - select = "SELECT * FROM pg_authid where rolname=%(user)s" - cursor.execute(select, {"user": user}) - # Grab current role attributes. - current_role_attrs = cursor.fetchone() - except psycopg2.ProgrammingError: - current_role_attrs = None - db_connection.rollback() - - pwchanging = user_should_we_change_password(current_role_attrs, user, password, encrypted) - - if current_role_attrs is None: - try: - # AWS RDS instances does not allow user to access pg_authid - # so try to get current_role_attrs from pg_roles tables - select = "SELECT * FROM pg_roles where rolname=%(user)s" - cursor.execute(select, {"user": user}) - # Grab current role attributes from pg_roles - current_role_attrs = cursor.fetchone() - except psycopg2.ProgrammingError as e: - db_connection.rollback() - module.fail_json(msg="Failed to get role details for current user %s: %s" % (user, e)) - - role_attr_flags_changing = False - if role_attr_flags: - role_attr_flags_dict = {} - for r in role_attr_flags.split(' '): - if r.startswith('NO'): - role_attr_flags_dict[r.replace('NO', '', 1)] = False - else: - role_attr_flags_dict[r] = True - - for role_attr_name, role_attr_value in role_attr_flags_dict.items(): - if current_role_attrs[PRIV_TO_AUTHID_COLUMN[role_attr_name]] != role_attr_value: - role_attr_flags_changing = True - - if expires is not None: - cursor.execute("SELECT %s::timestamptz;", (expires,)) - expires_with_tz = cursor.fetchone()[0] - expires_changing = expires_with_tz != current_role_attrs.get('rolvaliduntil') - else: - expires_changing = False - - conn_limit_changing = (conn_limit is not None and conn_limit != current_role_attrs['rolconnlimit']) - - if not pwchanging and not role_attr_flags_changing and not expires_changing and not conn_limit_changing: - return False - - alter = ['ALTER USER "%(user)s"' % {"user": user}] - if pwchanging: - if password != '': - alter.append("WITH %(crypt)s" % {"crypt": encrypted}) - alter.append("PASSWORD %(password)s") - else: - alter.append("WITH PASSWORD NULL") - alter.append(role_attr_flags) - elif role_attr_flags: - alter.append('WITH %s' % role_attr_flags) - if expires is not None: - alter.append("VALID UNTIL %(expires)s") - if conn_limit is not None: - alter.append("CONNECTION LIMIT %(conn_limit)s" % {"conn_limit": conn_limit}) - - query_password_data = dict(password=password, expires=expires) - try: - cursor.execute(' '.join(alter), query_password_data) - changed = True - except psycopg2.InternalError as e: - if e.pgcode == '25006': - # Handle errors due to read-only transactions indicated by pgcode 25006 - # ERROR: cannot execute ALTER ROLE in a read-only transaction - changed = False - module.fail_json(msg=e.pgerror, exception=traceback.format_exc()) - return changed - else: - raise psycopg2.InternalError(e) - except psycopg2.NotSupportedError as e: - module.fail_json(msg=e.pgerror, exception=traceback.format_exc()) - - elif no_password_changes and role_attr_flags != '': - # Grab role information from pg_roles instead of pg_authid - select = "SELECT * FROM pg_roles where rolname=%(user)s" - cursor.execute(select, {"user": user}) - # Grab current role attributes. - current_role_attrs = cursor.fetchone() - - role_attr_flags_changing = False - - if role_attr_flags: - role_attr_flags_dict = {} - for r in role_attr_flags.split(' '): - if r.startswith('NO'): - role_attr_flags_dict[r.replace('NO', '', 1)] = False - else: - role_attr_flags_dict[r] = True - - for role_attr_name, role_attr_value in role_attr_flags_dict.items(): - if current_role_attrs[PRIV_TO_AUTHID_COLUMN[role_attr_name]] != role_attr_value: - role_attr_flags_changing = True - - if not role_attr_flags_changing: - return False - - alter = ['ALTER USER "%(user)s"' % - {"user": user}] - if role_attr_flags: - alter.append('WITH %s' % role_attr_flags) - - try: - cursor.execute(' '.join(alter)) - except psycopg2.InternalError as e: - if e.pgcode == '25006': - # Handle errors due to read-only transactions indicated by pgcode 25006 - # ERROR: cannot execute ALTER ROLE in a read-only transaction - changed = False - module.fail_json(msg=e.pgerror, exception=traceback.format_exc()) - return changed - else: - raise psycopg2.InternalError(e) - - # Grab new role attributes. - cursor.execute(select, {"user": user}) - new_role_attrs = cursor.fetchone() - - # Detect any differences between current_ and new_role_attrs. - changed = current_role_attrs != new_role_attrs - - return changed - - -def user_delete(cursor, user): - """Try to remove a user. Returns True if successful otherwise False""" - cursor.execute("SAVEPOINT ansible_pgsql_user_delete") - try: - query = 'DROP USER "%s"' % user - executed_queries.append(query) - cursor.execute(query) - except Exception: - cursor.execute("ROLLBACK TO SAVEPOINT ansible_pgsql_user_delete") - cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete") - return False - - cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete") - return True - - -def has_table_privileges(cursor, user, table, privs): - """ - Return the difference between the privileges that a user already has and - the privileges that they desire to have. - - :returns: tuple of: - * privileges that they have and were requested - * privileges they currently hold but were not requested - * privileges requested that they do not hold - """ - cur_privs = get_table_privileges(cursor, user, table) - have_currently = cur_privs.intersection(privs) - other_current = cur_privs.difference(privs) - desired = privs.difference(cur_privs) - return (have_currently, other_current, desired) - - -def get_table_privileges(cursor, user, table): - if '.' in table: - schema, table = table.split('.', 1) - else: - schema = 'public' - query = ("SELECT privilege_type FROM information_schema.role_table_grants " - "WHERE grantee=%(user)s AND table_name=%(table)s AND table_schema=%(schema)s") - cursor.execute(query, {'user': user, 'table': table, 'schema': schema}) - return frozenset([x[0] for x in cursor.fetchall()]) - - -def grant_table_privileges(cursor, user, table, privs): - # Note: priv escaped by parse_privs - privs = ', '.join(privs) - query = 'GRANT %s ON TABLE %s TO "%s"' % ( - privs, pg_quote_identifier(table, 'table'), user) - executed_queries.append(query) - cursor.execute(query) - - -def revoke_table_privileges(cursor, user, table, privs): - # Note: priv escaped by parse_privs - privs = ', '.join(privs) - query = 'REVOKE %s ON TABLE %s FROM "%s"' % ( - privs, pg_quote_identifier(table, 'table'), user) - executed_queries.append(query) - cursor.execute(query) - - -def get_database_privileges(cursor, user, db): - priv_map = { - 'C': 'CREATE', - 'T': 'TEMPORARY', - 'c': 'CONNECT', - } - query = 'SELECT datacl FROM pg_database WHERE datname = %s' - cursor.execute(query, (db,)) - datacl = cursor.fetchone()[0] - if datacl is None: - return set() - r = re.search(r'%s\\?"?=(C?T?c?)/[^,]+,?' % user, datacl) - if r is None: - return set() - o = set() - for v in r.group(1): - o.add(priv_map[v]) - return normalize_privileges(o, 'database') - - -def has_database_privileges(cursor, user, db, privs): - """ - Return the difference between the privileges that a user already has and - the privileges that they desire to have. - - :returns: tuple of: - * privileges that they have and were requested - * privileges they currently hold but were not requested - * privileges requested that they do not hold - """ - cur_privs = get_database_privileges(cursor, user, db) - have_currently = cur_privs.intersection(privs) - other_current = cur_privs.difference(privs) - desired = privs.difference(cur_privs) - return (have_currently, other_current, desired) - - -def grant_database_privileges(cursor, user, db, privs): - # Note: priv escaped by parse_privs - privs = ', '.join(privs) - if user == "PUBLIC": - query = 'GRANT %s ON DATABASE %s TO PUBLIC' % ( - privs, pg_quote_identifier(db, 'database')) - else: - query = 'GRANT %s ON DATABASE %s TO "%s"' % ( - privs, pg_quote_identifier(db, 'database'), user) - - executed_queries.append(query) - cursor.execute(query) - - -def revoke_database_privileges(cursor, user, db, privs): - # Note: priv escaped by parse_privs - privs = ', '.join(privs) - if user == "PUBLIC": - query = 'REVOKE %s ON DATABASE %s FROM PUBLIC' % ( - privs, pg_quote_identifier(db, 'database')) - else: - query = 'REVOKE %s ON DATABASE %s FROM "%s"' % ( - privs, pg_quote_identifier(db, 'database'), user) - - executed_queries.append(query) - cursor.execute(query) - - -def revoke_privileges(cursor, user, privs): - if privs is None: - return False - - revoke_funcs = dict(table=revoke_table_privileges, - database=revoke_database_privileges) - check_funcs = dict(table=has_table_privileges, - database=has_database_privileges) - - changed = False - for type_ in privs: - for name, privileges in iteritems(privs[type_]): - # Check that any of the privileges requested to be removed are - # currently granted to the user - differences = check_funcs[type_](cursor, user, name, privileges) - if differences[0]: - revoke_funcs[type_](cursor, user, name, privileges) - changed = True - return changed - - -def grant_privileges(cursor, user, privs): - if privs is None: - return False - - grant_funcs = dict(table=grant_table_privileges, - database=grant_database_privileges) - check_funcs = dict(table=has_table_privileges, - database=has_database_privileges) - - changed = False - for type_ in privs: - for name, privileges in iteritems(privs[type_]): - # Check that any of the privileges requested for the user are - # currently missing - differences = check_funcs[type_](cursor, user, name, privileges) - if differences[2]: - grant_funcs[type_](cursor, user, name, privileges) - changed = True - return changed - - -def parse_role_attrs(cursor, role_attr_flags): - """ - Parse role attributes string for user creation. - Format: - - attributes[,attributes,...] - - Where: - - attributes := CREATEDB,CREATEROLE,NOSUPERUSER,... - [ "[NO]SUPERUSER","[NO]CREATEROLE", "[NO]CREATEDB", - "[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION", - "[NO]BYPASSRLS" ] - - Note: "[NO]BYPASSRLS" role attribute introduced in 9.5 - Note: "[NO]CREATEUSER" role attribute is deprecated. - - """ - flags = frozenset(role.upper() for role in role_attr_flags.split(',') if role) - - valid_flags = frozenset(itertools.chain(FLAGS, get_valid_flags_by_version(cursor))) - valid_flags = frozenset(itertools.chain(valid_flags, ('NO%s' % flag for flag in valid_flags))) - - if not flags.issubset(valid_flags): - raise InvalidFlagsError('Invalid role_attr_flags specified: %s' % - ' '.join(flags.difference(valid_flags))) - - return ' '.join(flags) - - -def normalize_privileges(privs, type_): - new_privs = set(privs) - if 'ALL' in new_privs: - new_privs.update(VALID_PRIVS[type_]) - new_privs.remove('ALL') - if 'TEMP' in new_privs: - new_privs.add('TEMPORARY') - new_privs.remove('TEMP') - - return new_privs - - -def parse_privs(privs, db): - """ - Parse privilege string to determine permissions for database db. - Format: - - privileges[/privileges/...] - - Where: - - privileges := DATABASE_PRIVILEGES[,DATABASE_PRIVILEGES,...] | - TABLE_NAME:TABLE_PRIVILEGES[,TABLE_PRIVILEGES,...] - """ - if privs is None: - return privs - - o_privs = { - 'database': {}, - 'table': {} - } - for token in privs.split('/'): - if ':' not in token: - type_ = 'database' - name = db - priv_set = frozenset(x.strip().upper() - for x in token.split(',') if x.strip()) - else: - type_ = 'table' - name, privileges = token.split(':', 1) - priv_set = frozenset(x.strip().upper() - for x in privileges.split(',') if x.strip()) - - if not priv_set.issubset(VALID_PRIVS[type_]): - raise InvalidPrivsError('Invalid privs specified for %s: %s' % - (type_, ' '.join(priv_set.difference(VALID_PRIVS[type_])))) - - priv_set = normalize_privileges(priv_set, type_) - o_privs[type_][name] = priv_set - - return o_privs - - -def get_valid_flags_by_version(cursor): - """ - Some role attributes were introduced after certain versions. We want to - compile a list of valid flags against the current Postgres version. - """ - current_version = cursor.connection.server_version - - return [ - flag - for flag, version_introduced in FLAGS_BY_VERSION.items() - if current_version >= version_introduced - ] - - -def get_comment(cursor, user): - """Get user's comment.""" - query = ("SELECT pg_catalog.shobj_description(r.oid, 'pg_authid') " - "FROM pg_catalog.pg_roles r " - "WHERE r.rolname = %(user)s") - cursor.execute(query, {'user': user}) - return cursor.fetchone()[0] - - -def add_comment(cursor, user, comment): - """Add comment on user.""" - if comment != get_comment(cursor, user): - query = 'COMMENT ON ROLE "%s" IS ' % user - cursor.execute(query + '%(comment)s', {'comment': comment}) - executed_queries.append(cursor.mogrify(query + '%(comment)s', {'comment': comment})) - return True - else: - return False - - -# =========================================== -# Module execution. -# - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - user=dict(type='str', required=True, aliases=['name']), - password=dict(type='str', default=None, no_log=True), - state=dict(type='str', default='present', choices=['absent', 'present']), - priv=dict(type='str', default=None), - db=dict(type='str', default='', aliases=['login_db']), - fail_on_user=dict(type='bool', default='yes', aliases=['fail_on_role']), - role_attr_flags=dict(type='str', default=''), - encrypted=dict(type='bool', default='yes'), - no_password_changes=dict(type='bool', default='no'), - expires=dict(type='str', default=None), - conn_limit=dict(type='int', default=None), - session_role=dict(type='str'), - groups=dict(type='list', elements='str'), - comment=dict(type='str', default=None), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - user = module.params["user"] - password = module.params["password"] - state = module.params["state"] - fail_on_user = module.params["fail_on_user"] - if module.params['db'] == '' and module.params["priv"] is not None: - module.fail_json(msg="privileges require a database to be specified") - privs = parse_privs(module.params["priv"], module.params["db"]) - no_password_changes = module.params["no_password_changes"] - if module.params["encrypted"]: - encrypted = "ENCRYPTED" - else: - encrypted = "UNENCRYPTED" - expires = module.params["expires"] - conn_limit = module.params["conn_limit"] - role_attr_flags = module.params["role_attr_flags"] - groups = module.params["groups"] - if groups: - groups = [e.strip() for e in groups] - comment = module.params["comment"] - - conn_params = get_conn_params(module, module.params, warn_db_default=False) - db_connection = connect_to_db(module, conn_params) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - try: - role_attr_flags = parse_role_attrs(cursor, role_attr_flags) - except InvalidFlagsError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - kw = dict(user=user) - changed = False - user_removed = False - - if state == "present": - if user_exists(cursor, user): - try: - changed = user_alter(db_connection, module, user, password, - role_attr_flags, encrypted, expires, no_password_changes, conn_limit) - except SQLParseError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - else: - try: - changed = user_add(cursor, user, password, - role_attr_flags, encrypted, expires, conn_limit) - except psycopg2.ProgrammingError as e: - module.fail_json(msg="Unable to add user with given requirement " - "due to : %s" % to_native(e), - exception=traceback.format_exc()) - except SQLParseError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - try: - changed = grant_privileges(cursor, user, privs) or changed - except SQLParseError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - if groups: - target_roles = [] - target_roles.append(user) - pg_membership = PgMembership(module, cursor, groups, target_roles) - changed = pg_membership.grant() or changed - executed_queries.extend(pg_membership.executed_queries) - - if comment is not None: - try: - changed = add_comment(cursor, user, comment) or changed - except Exception as e: - module.fail_json(msg='Unable to add comment on role: %s' % to_native(e), - exception=traceback.format_exc()) - - else: - if user_exists(cursor, user): - if module.check_mode: - changed = True - kw['user_removed'] = True - else: - try: - changed = revoke_privileges(cursor, user, privs) - user_removed = user_delete(cursor, user) - except SQLParseError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - changed = changed or user_removed - if fail_on_user and not user_removed: - msg = "Unable to remove user" - module.fail_json(msg=msg) - kw['user_removed'] = user_removed - - if changed: - if module.check_mode: - db_connection.rollback() - else: - db_connection.commit() - - kw['changed'] = changed - kw['queries'] = executed_queries - module.exit_json(**kw) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/postgresql/postgresql_user_obj_stat_info.py b/lib/ansible/modules/database/postgresql/postgresql_user_obj_stat_info.py deleted file mode 100644 index bb1aed02bd..0000000000 --- a/lib/ansible/modules/database/postgresql/postgresql_user_obj_stat_info.py +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = r''' ---- -module: postgresql_user_obj_stat_info -short_description: Gather statistics about PostgreSQL user objects -description: -- Gathers statistics about PostgreSQL user objects. -version_added: '2.10' -options: - filter: - description: - - Limit the collected information by comma separated string or YAML list. - - Allowable values are C(functions), C(indexes), C(tables). - - By default, collects all subsets. - - Unsupported values are ignored. - type: list - elements: str - schema: - description: - - Restrict the output by certain schema. - type: str - db: - description: - - Name of database to connect. - type: str - aliases: - - login_db - session_role: - description: - - Switch to session_role after connecting. The specified session_role must - be a role that the current login_user is a member of. - - Permissions checking for SQL commands is carried out as though - the session_role were the one that had logged in originally. - type: str -notes: -- C(size) and C(total_size) returned values are presented in bytes. -- For tracking function statistics the PostgreSQL C(track_functions) parameter must be enabled. - See U(https://www.postgresql.org/docs/current/runtime-config-statistics.html) for more information. -seealso: -- module: postgresql_info -- module: postgresql_ping -- name: PostgreSQL statistics collector reference - description: Complete reference of the PostgreSQL statistics collector documentation. - link: https://www.postgresql.org/docs/current/monitoring-stats.html -author: -- Andrew Klychkov (@Andersson007) -extends_documentation_fragment: postgres -''' - -EXAMPLES = r''' -- name: Collect information about all supported user objects of the acme database - postgresql_user_obj_stat_info: - db: acme - -- name: Collect information about all supported user objects in the custom schema of the acme database - postgresql_user_obj_stat_info: - db: acme - schema: custom - -- name: Collect information about user tables and indexes in the acme database - postgresql_user_obj_stat_info: - db: acme - filter: tables, indexes -''' - -RETURN = r''' -indexes: - description: User index statistics - returned: always - type: dict - sample: {"public": {"test_id_idx": {"idx_scan": 0, "idx_tup_fetch": 0, "idx_tup_read": 0, "relname": "test", "size": 8192, ...}}} -tables: - description: User table statistics. - returned: always - type: dict - sample: {"public": {"test": {"analyze_count": 3, "n_dead_tup": 0, "n_live_tup": 0, "seq_scan": 2, "size": 0, "total_size": 8192, ...}}} -functions: - description: User function statistics. - returned: always - type: dict - sample: {"public": {"inc": {"calls": 1, "funcid": 26722, "self_time": 0.23, "total_time": 0.23}}} -''' - -try: - from psycopg2.extras import DictCursor -except ImportError: - # psycopg2 is checked by connect_to_db() - # from ansible.module_utils.postgres - pass - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.postgres import ( - connect_to_db, - exec_sql, - get_conn_params, - postgres_common_argument_spec, -) -from ansible.module_utils.six import iteritems - - -# =========================================== -# PostgreSQL module specific support methods. -# - - -class PgUserObjStatInfo(): - """Class to collect information about PostgreSQL user objects. - - Args: - module (AnsibleModule): Object of AnsibleModule class. - cursor (cursor): Cursor object of psycopg2 library to work with PostgreSQL. - - Attributes: - module (AnsibleModule): Object of AnsibleModule class. - cursor (cursor): Cursor object of psycopg2 library to work with PostgreSQL. - executed_queries (list): List of executed queries. - info (dict): Statistics dictionary. - obj_func_mapping (dict): Mapping of object types to corresponding functions. - schema (str): Name of a schema to restrict stat collecting. - """ - - def __init__(self, module, cursor): - self.module = module - self.cursor = cursor - self.info = { - 'functions': {}, - 'indexes': {}, - 'tables': {}, - } - self.obj_func_mapping = { - 'functions': self.get_func_stat, - 'indexes': self.get_idx_stat, - 'tables': self.get_tbl_stat, - } - self.schema = None - - def collect(self, filter_=None, schema=None): - """Collect statistics information of user objects. - - Kwargs: - filter_ (list): List of subsets which need to be collected. - schema (str): Restrict stat collecting by certain schema. - - Returns: - ``self.info``. - """ - if schema: - self.set_schema(schema) - - if filter_: - for obj_type in filter_: - obj_type = obj_type.strip() - obj_func = self.obj_func_mapping.get(obj_type) - - if obj_func is not None: - obj_func() - else: - self.module.warn("Unknown filter option '%s'" % obj_type) - - else: - for obj_func in self.obj_func_mapping.values(): - obj_func() - - return self.info - - def get_func_stat(self): - """Get function statistics and fill out self.info dictionary.""" - if not self.schema: - query = "SELECT * FROM pg_stat_user_functions" - result = exec_sql(self, query, add_to_executed=False) - else: - query = "SELECT * FROM pg_stat_user_functions WHERE schemaname = %s" - result = exec_sql(self, query, query_params=(self.schema,), - add_to_executed=False) - - if not result: - return - - self.__fill_out_info(result, - info_key='functions', - schema_key='schemaname', - name_key='funcname') - - def get_idx_stat(self): - """Get index statistics and fill out self.info dictionary.""" - if not self.schema: - query = "SELECT * FROM pg_stat_user_indexes" - result = exec_sql(self, query, add_to_executed=False) - else: - query = "SELECT * FROM pg_stat_user_indexes WHERE schemaname = %s" - result = exec_sql(self, query, query_params=(self.schema,), - add_to_executed=False) - - if not result: - return - - self.__fill_out_info(result, - info_key='indexes', - schema_key='schemaname', - name_key='indexrelname') - - def get_tbl_stat(self): - """Get table statistics and fill out self.info dictionary.""" - if not self.schema: - query = "SELECT * FROM pg_stat_user_tables" - result = exec_sql(self, query, add_to_executed=False) - else: - query = "SELECT * FROM pg_stat_user_tables WHERE schemaname = %s" - result = exec_sql(self, query, query_params=(self.schema,), - add_to_executed=False) - - if not result: - return - - self.__fill_out_info(result, - info_key='tables', - schema_key='schemaname', - name_key='relname') - - def __fill_out_info(self, result, info_key=None, schema_key=None, name_key=None): - # Convert result to list of dicts to handle it easier: - result = [dict(row) for row in result] - - for elem in result: - # Add schema name as a key if not presented: - if not self.info[info_key].get(elem[schema_key]): - self.info[info_key][elem[schema_key]] = {} - - # Add object name key as a subkey - # (they must be uniq over a schema, so no need additional checks): - self.info[info_key][elem[schema_key]][elem[name_key]] = {} - - # Add other other attributes to a certain index: - for key, val in iteritems(elem): - if key not in (schema_key, name_key): - self.info[info_key][elem[schema_key]][elem[name_key]][key] = val - - if info_key in ('tables', 'indexes'): - relname = elem[name_key] - schemaname = elem[schema_key] - if not self.schema: - result = exec_sql(self, "SELECT pg_relation_size ('%s.%s')" % (schemaname, relname), - add_to_executed=False) - else: - relname = '%s.%s' % (self.schema, relname) - result = exec_sql(self, "SELECT pg_relation_size (%s)", - query_params=(relname,), - add_to_executed=False) - - self.info[info_key][elem[schema_key]][elem[name_key]]['size'] = result[0][0] - - if info_key == 'tables': - relname = elem[name_key] - schemaname = elem[schema_key] - if not self.schema: - result = exec_sql(self, "SELECT pg_total_relation_size ('%s.%s')" % (schemaname, relname), - add_to_executed=False) - else: - relname = '%s.%s' % (self.schema, relname) - result = exec_sql(self, "SELECT pg_total_relation_size (%s)", - query_params=(relname,), - add_to_executed=False) - - self.info[info_key][elem[schema_key]][elem[name_key]]['total_size'] = result[0][0] - - def set_schema(self, schema): - """If schema exists, sets self.schema, otherwise fails.""" - query = ("SELECT 1 FROM information_schema.schemata " - "WHERE schema_name = %s") - result = exec_sql(self, query, query_params=(schema,), - add_to_executed=False) - - if result and result[0][0]: - self.schema = schema - else: - self.module.fail_json(msg="Schema '%s' does not exist" % (schema)) - - -# =========================================== -# Module execution. -# - -def main(): - argument_spec = postgres_common_argument_spec() - argument_spec.update( - db=dict(type='str', aliases=['login_db']), - filter=dict(type='list', elements='str'), - session_role=dict(type='str'), - schema=dict(type='str'), - ) - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - filter_ = module.params["filter"] - schema = module.params["schema"] - - # Connect to DB and make cursor object: - pg_conn_params = get_conn_params(module, module.params) - # We don't need to commit anything, so, set it to False: - db_connection = connect_to_db(module, pg_conn_params, autocommit=False) - cursor = db_connection.cursor(cursor_factory=DictCursor) - - ############################ - # Create object and do work: - pg_obj_info = PgUserObjStatInfo(module, cursor) - - info_dict = pg_obj_info.collect(filter_, schema) - - # Clean up: - cursor.close() - db_connection.close() - - # Return information: - module.exit_json(**info_dict) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_backend_servers.py b/lib/ansible/modules/database/proxysql/proxysql_backend_servers.py deleted file mode 100644 index 04a8de2962..0000000000 --- a/lib/ansible/modules/database/proxysql/proxysql_backend_servers.py +++ /dev/null @@ -1,509 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: proxysql_backend_servers -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Adds or removes mysql hosts from proxysql admin interface. -description: - - The M(proxysql_backend_servers) module adds or removes mysql hosts using - the proxysql admin interface. -options: - hostgroup_id: - description: - - The hostgroup in which this mysqld instance is included. An instance - can be part of one or more hostgroups. - default: 0 - hostname: - description: - - The ip address at which the mysqld instance can be contacted. - required: True - port: - description: - - The port at which the mysqld instance can be contacted. - default: 3306 - status: - description: - - ONLINE - Backend server is fully operational. - OFFLINE_SOFT - When a server is put into C(OFFLINE_SOFT) mode, - connections are kept in use until the current - transaction is completed. This allows to gracefully - detach a backend. - OFFLINE_HARD - When a server is put into C(OFFLINE_HARD) mode, the - existing connections are dropped, while new incoming - connections aren't accepted either. - - If omitted the proxysql database default for I(status) is C(ONLINE). - choices: [ "ONLINE", "OFFLINE_SOFT", "OFFLINE_HARD"] - weight: - description: - - The bigger the weight of a server relative to other weights, the higher - the probability of the server being chosen from the hostgroup. If - omitted the proxysql database default for I(weight) is 1. - compression: - description: - - If the value of I(compression) is greater than 0, new connections to - that server will use compression. If omitted the proxysql database - default for I(compression) is 0. - max_connections: - description: - - The maximum number of connections ProxySQL will open to this backend - server. If omitted the proxysql database default for I(max_connections) - is 1000. - max_replication_lag: - description: - - If greater than 0, ProxySQL will regularly monitor replication lag. If - replication lag goes above I(max_replication_lag), proxysql will - temporarily shun the server until replication catches up. If omitted - the proxysql database default for I(max_replication_lag) is 0. - use_ssl: - description: - - If I(use_ssl) is set to C(True), connections to this server will be - made using SSL connections. If omitted the proxysql database default - for I(use_ssl) is C(False). - type: bool - max_latency_ms: - description: - - Ping time is monitored regularly. If a host has a ping time greater - than I(max_latency_ms) it is excluded from the connection pool - (although the server stays ONLINE). If omitted the proxysql database - default for I(max_latency_ms) is 0. - comment: - description: - - Text field that can be used for any purposed defined by the user. - Could be a description of what the host stores, a reminder of when the - host was added or disabled, or a JSON processed by some checker script. - default: '' - state: - description: - - When C(present) - adds the host, when C(absent) - removes the host. - choices: [ "present", "absent" ] - default: present -extends_documentation_fragment: - - proxysql.managing_config - - proxysql.connectivity -''' - -EXAMPLES = ''' ---- -# This example adds a server, it saves the mysql server config to disk, but -# avoids loading the mysql server config to runtime (this might be because -# several servers are being added and the user wants to push the config to -# runtime in a single batch using the M(proxysql_manage_config) module). It -# uses supplied credentials to connect to the proxysql admin interface. - -- proxysql_backend_servers: - login_user: 'admin' - login_password: 'admin' - hostname: 'mysql01' - state: present - load_to_runtime: False - -# This example removes a server, saves the mysql server config to disk, and -# dynamically loads the mysql server config to runtime. It uses credentials -# in a supplied config file to connect to the proxysql admin interface. - -- proxysql_backend_servers: - config_file: '~/proxysql.cnf' - hostname: 'mysql02' - state: absent -''' - -RETURN = ''' -stdout: - description: The mysql host modified or removed from proxysql - returned: On create/update will return the newly modified host, on delete - it will return the deleted record. - type: dict - "sample": { - "changed": true, - "hostname": "192.168.52.1", - "msg": "Added server to mysql_hosts", - "server": { - "comment": "", - "compression": "0", - "hostgroup_id": "1", - "hostname": "192.168.52.1", - "max_connections": "1000", - "max_latency_ms": "0", - "max_replication_lag": "0", - "port": "3306", - "status": "ONLINE", - "use_ssl": "0", - "weight": "1" - }, - "state": "present" - } -''' - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils.six import iteritems -from ansible.module_utils._text import to_native - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if module.params["port"] < 0 \ - or module.params["port"] > 65535: - module.fail_json( - msg="port must be a valid unix port number (0-65535)" - ) - - if module.params["compression"]: - if module.params["compression"] < 0 \ - or module.params["compression"] > 102400: - module.fail_json( - msg="compression must be set between 0 and 102400" - ) - - if module.params["max_replication_lag"]: - if module.params["max_replication_lag"] < 0 \ - or module.params["max_replication_lag"] > 126144000: - module.fail_json( - msg="max_replication_lag must be set between 0 and 102400" - ) - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - -def save_config_to_disk(cursor): - cursor.execute("SAVE MYSQL SERVERS TO DISK") - return True - - -def load_config_to_runtime(cursor): - cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") - return True - - -class ProxySQLServer(object): - - def __init__(self, module): - self.state = module.params["state"] - self.save_to_disk = module.params["save_to_disk"] - self.load_to_runtime = module.params["load_to_runtime"] - - self.hostgroup_id = module.params["hostgroup_id"] - self.hostname = module.params["hostname"] - self.port = module.params["port"] - - config_data_keys = ["status", - "weight", - "compression", - "max_connections", - "max_replication_lag", - "use_ssl", - "max_latency_ms", - "comment"] - - self.config_data = dict((k, module.params[k]) - for k in config_data_keys) - - def check_server_config_exists(self, cursor): - query_string = \ - """SELECT count(*) AS `host_count` - FROM mysql_servers - WHERE hostgroup_id = %s - AND hostname = %s - AND port = %s""" - - query_data = \ - [self.hostgroup_id, - self.hostname, - self.port] - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['host_count']) > 0) - - def check_server_config(self, cursor): - query_string = \ - """SELECT count(*) AS `host_count` - FROM mysql_servers - WHERE hostgroup_id = %s - AND hostname = %s - AND port = %s""" - - query_data = \ - [self.hostgroup_id, - self.hostname, - self.port] - - for col, val in iteritems(self.config_data): - if val is not None: - query_data.append(val) - query_string += "\n AND " + col + " = %s" - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - - if isinstance(check_count, tuple): - return int(check_count[0]) > 0 - - return (int(check_count['host_count']) > 0) - - def get_server_config(self, cursor): - query_string = \ - """SELECT * - FROM mysql_servers - WHERE hostgroup_id = %s - AND hostname = %s - AND port = %s""" - - query_data = \ - [self.hostgroup_id, - self.hostname, - self.port] - - cursor.execute(query_string, query_data) - server = cursor.fetchone() - return server - - def create_server_config(self, cursor): - query_string = \ - """INSERT INTO mysql_servers ( - hostgroup_id, - hostname, - port""" - - cols = 3 - query_data = \ - [self.hostgroup_id, - self.hostname, - self.port] - - for col, val in iteritems(self.config_data): - if val is not None: - cols += 1 - query_data.append(val) - query_string += ",\n" + col - - query_string += \ - (")\n" + - "VALUES (" + - "%s ," * cols) - - query_string = query_string[:-2] - query_string += ")" - - cursor.execute(query_string, query_data) - return True - - def update_server_config(self, cursor): - query_string = """UPDATE mysql_servers""" - - cols = 0 - query_data = [] - - for col, val in iteritems(self.config_data): - if val is not None: - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\nSET " + col + "= %s," - else: - query_string += "\n " + col + " = %s," - - query_string = query_string[:-1] - query_string += ("\nWHERE hostgroup_id = %s\n AND hostname = %s" + - "\n AND port = %s") - - query_data.append(self.hostgroup_id) - query_data.append(self.hostname) - query_data.append(self.port) - - cursor.execute(query_string, query_data) - return True - - def delete_server_config(self, cursor): - query_string = \ - """DELETE FROM mysql_servers - WHERE hostgroup_id = %s - AND hostname = %s - AND port = %s""" - - query_data = \ - [self.hostgroup_id, - self.hostname, - self.port] - - cursor.execute(query_string, query_data) - return True - - def manage_config(self, cursor, state): - if state: - if self.save_to_disk: - save_config_to_disk(cursor) - if self.load_to_runtime: - load_config_to_runtime(cursor) - - def create_server(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.create_server_config(cursor) - result['msg'] = "Added server to mysql_hosts" - result['server'] = \ - self.get_server_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Server would have been added to" + - " mysql_hosts, however check_mode" + - " is enabled.") - - def update_server(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.update_server_config(cursor) - result['msg'] = "Updated server in mysql_hosts" - result['server'] = \ - self.get_server_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Server would have been updated in" + - " mysql_hosts, however check_mode" + - " is enabled.") - - def delete_server(self, check_mode, result, cursor): - if not check_mode: - result['server'] = \ - self.get_server_config(cursor) - result['changed'] = \ - self.delete_server_config(cursor) - result['msg'] = "Deleted server from mysql_hosts" - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Server would have been deleted from" + - " mysql_hosts, however check_mode is" + - " enabled.") - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default='127.0.0.1'), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default='', type='path'), - hostgroup_id=dict(default=0, type='int'), - hostname=dict(required=True, type='str'), - port=dict(default=3306, type='int'), - status=dict(choices=['ONLINE', - 'OFFLINE_SOFT', - 'OFFLINE_HARD']), - weight=dict(type='int'), - compression=dict(type='int'), - max_connections=dict(type='int'), - max_replication_lag=dict(type='int'), - use_ssl=dict(type='bool'), - max_latency_ms=dict(type='int'), - comment=dict(default='', type='str'), - state=dict(default='present', choices=['present', - 'absent']), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - - cursor = None - try: - cursor, db_conn = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class='DictCursor') - except mysql_driver.Error as e: - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % to_native(e) - ) - - proxysql_server = ProxySQLServer(module) - result = {} - - result['state'] = proxysql_server.state - if proxysql_server.hostname: - result['hostname'] = proxysql_server.hostname - - if proxysql_server.state == "present": - try: - if not proxysql_server.check_server_config(cursor): - if not proxysql_server.check_server_config_exists(cursor): - proxysql_server.create_server(module.check_mode, - result, - cursor) - else: - proxysql_server.update_server(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The server already exists in mysql_hosts" + - " and doesn't need to be updated.") - result['server'] = \ - proxysql_server.get_server_config(cursor) - except mysql_driver.Error as e: - module.fail_json( - msg="unable to modify server.. %s" % to_native(e) - ) - - elif proxysql_server.state == "absent": - try: - if proxysql_server.check_server_config_exists(cursor): - proxysql_server.delete_server(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The server is already absent from the" + - " mysql_hosts memory configuration") - except mysql_driver.Error as e: - module.fail_json( - msg="unable to remove server.. %s" % to_native(e) - ) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_global_variables.py b/lib/ansible/modules/database/proxysql/proxysql_global_variables.py deleted file mode 100644 index 8436a76d62..0000000000 --- a/lib/ansible/modules/database/proxysql/proxysql_global_variables.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: proxysql_global_variables -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Gets or sets the proxysql global variables. -description: - - The M(proxysql_global_variables) module gets or sets the proxysql global - variables. -options: - variable: - description: - - Defines which variable should be returned, or if I(value) is specified - which variable should be updated. - required: True - value: - description: - - Defines a value the variable specified using I(variable) should be set - to. -extends_documentation_fragment: - - proxysql.managing_config - - proxysql.connectivity -''' - -EXAMPLES = ''' ---- -# This example sets the value of a variable, saves the mysql admin variables -# config to disk, and dynamically loads the mysql admin variables config to -# runtime. It uses supplied credentials to connect to the proxysql admin -# interface. - -- proxysql_global_variables: - login_user: 'admin' - login_password: 'admin' - variable: 'mysql-max_connections' - value: 4096 - -# This example gets the value of a variable. It uses credentials in a -# supplied config file to connect to the proxysql admin interface. - -- proxysql_global_variables: - config_file: '~/proxysql.cnf' - variable: 'mysql-default_query_delay' -''' - -RETURN = ''' -stdout: - description: Returns the mysql variable supplied with it's associated value. - returned: Returns the current variable and value, or the newly set value - for the variable supplied.. - type: dict - "sample": { - "changed": false, - "msg": "The variable is already been set to the supplied value", - "var": { - "variable_name": "mysql-poll_timeout", - "variable_value": "3000" - } - } -''' - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils._text import to_native - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - -def save_config_to_disk(variable, cursor): - if variable.startswith("admin"): - cursor.execute("SAVE ADMIN VARIABLES TO DISK") - else: - cursor.execute("SAVE MYSQL VARIABLES TO DISK") - return True - - -def load_config_to_runtime(variable, cursor): - if variable.startswith("admin"): - cursor.execute("LOAD ADMIN VARIABLES TO RUNTIME") - else: - cursor.execute("LOAD MYSQL VARIABLES TO RUNTIME") - return True - - -def check_config(variable, value, cursor): - query_string = \ - """SELECT count(*) AS `variable_count` - FROM global_variables - WHERE variable_name = %s and variable_value = %s""" - - query_data = \ - [variable, value] - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - - if isinstance(check_count, tuple): - return int(check_count[0]) > 0 - - return (int(check_count['variable_count']) > 0) - - -def get_config(variable, cursor): - - query_string = \ - """SELECT * - FROM global_variables - WHERE variable_name = %s""" - - query_data = \ - [variable, ] - - cursor.execute(query_string, query_data) - row_count = cursor.rowcount - resultset = cursor.fetchone() - - if row_count > 0: - return resultset - else: - return False - - -def set_config(variable, value, cursor): - - query_string = \ - """UPDATE global_variables - SET variable_value = %s - WHERE variable_name = %s""" - - query_data = \ - [value, variable] - - cursor.execute(query_string, query_data) - return True - - -def manage_config(variable, save_to_disk, load_to_runtime, cursor, state): - if state: - if save_to_disk: - save_config_to_disk(variable, cursor) - if load_to_runtime: - load_config_to_runtime(variable, cursor) - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default="", type='path'), - variable=dict(required=True, type='str'), - value=dict(), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - variable = module.params["variable"] - value = module.params["value"] - save_to_disk = module.params["save_to_disk"] - load_to_runtime = module.params["load_to_runtime"] - - cursor = None - try: - cursor, db_conn = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class='DictCursor') - except mysql_driver.Error as e: - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % to_native(e) - ) - - result = {} - - if not value: - try: - if get_config(variable, cursor): - result['changed'] = False - result['msg'] = \ - "Returned the variable and it's current value" - result['var'] = get_config(variable, cursor) - else: - module.fail_json( - msg="The variable \"%s\" was not found" % variable - ) - - except mysql_driver.Error as e: - module.fail_json( - msg="unable to get config.. %s" % to_native(e) - ) - else: - try: - if get_config(variable, cursor): - if not check_config(variable, value, cursor): - if not module.check_mode: - result['changed'] = set_config(variable, value, cursor) - result['msg'] = \ - "Set the variable to the supplied value" - result['var'] = get_config(variable, cursor) - manage_config(variable, - save_to_disk, - load_to_runtime, - cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Variable would have been set to" + - " the supplied value, however" + - " check_mode is enabled.") - else: - result['changed'] = False - result['msg'] = ("The variable is already been set to" + - " the supplied value") - result['var'] = get_config(variable, cursor) - else: - module.fail_json( - msg="The variable \"%s\" was not found" % variable - ) - - except mysql_driver.Error as e: - module.fail_json( - msg="unable to set config.. %s" % to_native(e) - ) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_manage_config.py b/lib/ansible/modules/database/proxysql/proxysql_manage_config.py deleted file mode 100644 index b1d471dbf4..0000000000 --- a/lib/ansible/modules/database/proxysql/proxysql_manage_config.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: proxysql_manage_config -version_added: "2.3" - -author: "Ben Mildren (@bmildren)" -short_description: Writes the proxysql configuration settings between layers. -description: - - The M(proxysql_global_variables) module writes the proxysql configuration - settings between layers. Currently this module will always report a - changed state, so should typically be used with WHEN however this will - change in a future version when the CHECKSUM table commands are available - for all tables in proxysql. -options: - action: - description: - - The supplied I(action) combines with the supplied I(direction) to - provide the semantics of how we want to move the I(config_settings) - between the I(config_layers). - choices: [ "LOAD", "SAVE" ] - required: True - config_settings: - description: - - The I(config_settings) specifies which configuration we're writing. - choices: [ "MYSQL USERS", "MYSQL SERVERS", "MYSQL QUERY RULES", - "MYSQL VARIABLES", "ADMIN VARIABLES", "SCHEDULER" ] - required: True - direction: - description: - - FROM - denotes we're reading values FROM the supplied I(config_layer) - and writing to the next layer. - TO - denotes we're reading from the previous layer and writing TO the - supplied I(config_layer)." - choices: [ "FROM", "TO" ] - required: True - config_layer: - description: - - RUNTIME - represents the in-memory data structures of ProxySQL used by - the threads that are handling the requests. - MEMORY - (sometimes also referred as main) represents the in-memory - SQLite3 database. - DISK - represents the on-disk SQLite3 database. - CONFIG - is the classical config file. You can only LOAD FROM the - config file. - choices: [ "MEMORY", "DISK", "RUNTIME", "CONFIG" ] - required: True -extends_documentation_fragment: - - proxysql.connectivity -''' - -EXAMPLES = ''' ---- -# This example saves the mysql users config from memory to disk. It uses -# supplied credentials to connect to the proxysql admin interface. - -- proxysql_manage_config: - login_user: 'admin' - login_password: 'admin' - action: "SAVE" - config_settings: "MYSQL USERS" - direction: "FROM" - config_layer: "MEMORY" - -# This example loads the mysql query rules config from memory to to runtime. It -# uses supplied credentials to connect to the proxysql admin interface. - -- proxysql_manage_config: - config_file: '~/proxysql.cnf' - action: "LOAD" - config_settings: "MYSQL QUERY RULES" - direction: "TO" - config_layer: "RUNTIME" -''' - -RETURN = ''' -stdout: - description: Simply reports whether the action reported a change. - returned: Currently the returned value with always be changed=True. - type: dict - "sample": { - "changed": true - } -''' - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils._text import to_native - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if module.params["config_layer"] == 'CONFIG' and \ - (module.params["action"] != 'LOAD' or - module.params["direction"] != 'FROM'): - - if (module.params["action"] != 'LOAD' and - module.params["direction"] != 'FROM'): - msg_string = ("Neither the action \"%s\" nor the direction" + - " \"%s\" are valid combination with the CONFIG" + - " config_layer") - module.fail_json(msg=msg_string % (module.params["action"], - module.params["direction"])) - - elif module.params["action"] != 'LOAD': - msg_string = ("The action \"%s\" is not a valid combination" + - " with the CONFIG config_layer") - module.fail_json(msg=msg_string % module.params["action"]) - - else: - msg_string = ("The direction \"%s\" is not a valid combination" + - " with the CONFIG config_layer") - module.fail_json(msg=msg_string % module.params["direction"]) - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - -def manage_config(manage_config_settings, cursor): - - query_string = "%s" % ' '.join(manage_config_settings) - - cursor.execute(query_string) - return True - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default="", type='path'), - action=dict(required=True, choices=['LOAD', - 'SAVE']), - config_settings=dict(required=True, choices=['MYSQL USERS', - 'MYSQL SERVERS', - 'MYSQL QUERY RULES', - 'MYSQL VARIABLES', - 'ADMIN VARIABLES', - 'SCHEDULER']), - direction=dict(required=True, choices=['FROM', - 'TO']), - config_layer=dict(required=True, choices=['MEMORY', - 'DISK', - 'RUNTIME', - 'CONFIG']) - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - action = module.params["action"] - config_settings = module.params["config_settings"] - direction = module.params["direction"] - config_layer = module.params["config_layer"] - - cursor = None - try: - cursor, db_conn = mysql_connect(module, - login_user, - login_password, - config_file) - except mysql_driver.Error as e: - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % to_native(e) - ) - - result = {} - - manage_config_settings = \ - [action, config_settings, direction, config_layer] - - try: - result['changed'] = manage_config(manage_config_settings, - cursor) - except mysql_driver.Error as e: - module.fail_json( - msg="unable to manage config.. %s" % to_native(e) - ) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_mysql_users.py b/lib/ansible/modules/database/proxysql/proxysql_mysql_users.py deleted file mode 100644 index e0c7f5ff9a..0000000000 --- a/lib/ansible/modules/database/proxysql/proxysql_mysql_users.py +++ /dev/null @@ -1,477 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: proxysql_mysql_users -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Adds or removes mysql users from proxysql admin interface. -description: - - The M(proxysql_mysql_users) module adds or removes mysql users using the - proxysql admin interface. -options: - username: - description: - - Name of the user connecting to the mysqld or ProxySQL instance. - required: True - password: - description: - - Password of the user connecting to the mysqld or ProxySQL instance. - active: - description: - - A user with I(active) set to C(False) will be tracked in the database, - but will be never loaded in the in-memory data structures. If omitted - the proxysql database default for I(active) is C(True). - type: bool - use_ssl: - description: - - If I(use_ssl) is set to C(True), connections by this user will be made - using SSL connections. If omitted the proxysql database default for - I(use_ssl) is C(False). - type: bool - default_hostgroup: - description: - - If there is no matching rule for the queries sent by this user, the - traffic it generates is sent to the specified hostgroup. - If omitted the proxysql database default for I(use_ssl) is 0. - default_schema: - description: - - The schema to which the connection should change to by default. - transaction_persistent: - description: - - If this is set for the user with which the MySQL client is connecting - to ProxySQL (thus a "frontend" user), transactions started within a - hostgroup will remain within that hostgroup regardless of any other - rules. - If omitted the proxysql database default for I(transaction_persistent) - is C(False). - type: bool - fast_forward: - description: - - If I(fast_forward) is set to C(True), I(fast_forward) will bypass the - query processing layer (rewriting, caching) and pass through the query - directly as is to the backend server. If omitted the proxysql database - default for I(fast_forward) is C(False). - type: bool - backend: - description: - - If I(backend) is set to C(True), this (username, password) pair is - used for authenticating to the ProxySQL instance. - default: True - type: bool - frontend: - description: - - If I(frontend) is set to C(True), this (username, password) pair is - used for authenticating to the mysqld servers against any hostgroup. - default: True - type: bool - max_connections: - description: - - The maximum number of connections ProxySQL will open to the backend for - this user. If omitted the proxysql database default for - I(max_connections) is 10000. - state: - description: - - When C(present) - adds the user, when C(absent) - removes the user. - choices: [ "present", "absent" ] - default: present -extends_documentation_fragment: - - proxysql.managing_config - - proxysql.connectivity -''' - -EXAMPLES = ''' ---- -# This example adds a user, it saves the mysql user config to disk, but -# avoids loading the mysql user config to runtime (this might be because -# several users are being added and the user wants to push the config to -# runtime in a single batch using the M(proxysql_manage_config) module). It -# uses supplied credentials to connect to the proxysql admin interface. - -- proxysql_mysql_users: - login_user: 'admin' - login_password: 'admin' - username: 'productiondba' - state: present - load_to_runtime: False - -# This example removes a user, saves the mysql user config to disk, and -# dynamically loads the mysql user config to runtime. It uses credentials -# in a supplied config file to connect to the proxysql admin interface. - -- proxysql_mysql_users: - config_file: '~/proxysql.cnf' - username: 'mysqlboy' - state: absent -''' - -RETURN = ''' -stdout: - description: The mysql user modified or removed from proxysql - returned: On create/update will return the newly modified user, on delete - it will return the deleted record. - type: dict - sample: - changed: true - msg: Added user to mysql_users - state: present - user: - active: 1 - backend: 1 - default_hostgroup: 1 - default_schema: null - fast_forward: 0 - frontend: 1 - max_connections: 10000 - password: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER - schema_locked: 0 - transaction_persistent: 0 - use_ssl: 0 - username: guest_ro - username: guest_ro -''' - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils.six import iteritems -from ansible.module_utils._text import to_native - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - -def save_config_to_disk(cursor): - cursor.execute("SAVE MYSQL USERS TO DISK") - return True - - -def load_config_to_runtime(cursor): - cursor.execute("LOAD MYSQL USERS TO RUNTIME") - return True - - -class ProxySQLUser(object): - - def __init__(self, module): - self.state = module.params["state"] - self.save_to_disk = module.params["save_to_disk"] - self.load_to_runtime = module.params["load_to_runtime"] - - self.username = module.params["username"] - self.backend = module.params["backend"] - self.frontend = module.params["frontend"] - - config_data_keys = ["password", - "active", - "use_ssl", - "default_hostgroup", - "default_schema", - "transaction_persistent", - "fast_forward", - "max_connections"] - - self.config_data = dict((k, module.params[k]) - for k in config_data_keys) - - def check_user_config_exists(self, cursor): - query_string = \ - """SELECT count(*) AS `user_count` - FROM mysql_users - WHERE username = %s - AND backend = %s - AND frontend = %s""" - - query_data = \ - [self.username, - self.backend, - self.frontend] - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['user_count']) > 0) - - def check_user_privs(self, cursor): - query_string = \ - """SELECT count(*) AS `user_count` - FROM mysql_users - WHERE username = %s - AND backend = %s - AND frontend = %s""" - - query_data = \ - [self.username, - self.backend, - self.frontend] - - for col, val in iteritems(self.config_data): - if val is not None: - query_data.append(val) - query_string += "\n AND " + col + " = %s" - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['user_count']) > 0) - - def get_user_config(self, cursor): - query_string = \ - """SELECT * - FROM mysql_users - WHERE username = %s - AND backend = %s - AND frontend = %s""" - - query_data = \ - [self.username, - self.backend, - self.frontend] - - cursor.execute(query_string, query_data) - user = cursor.fetchone() - return user - - def create_user_config(self, cursor): - query_string = \ - """INSERT INTO mysql_users ( - username, - backend, - frontend""" - - cols = 3 - query_data = \ - [self.username, - self.backend, - self.frontend] - - for col, val in iteritems(self.config_data): - if val is not None: - cols += 1 - query_data.append(val) - query_string += ",\n" + col - - query_string += \ - (")\n" + - "VALUES (" + - "%s ," * cols) - - query_string = query_string[:-2] - query_string += ")" - - cursor.execute(query_string, query_data) - return True - - def update_user_config(self, cursor): - query_string = """UPDATE mysql_users""" - - cols = 0 - query_data = [] - - for col, val in iteritems(self.config_data): - if val is not None: - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\nSET " + col + "= %s," - else: - query_string += "\n " + col + " = %s," - - query_string = query_string[:-1] - query_string += ("\nWHERE username = %s\n AND backend = %s" + - "\n AND frontend = %s") - - query_data.append(self.username) - query_data.append(self.backend) - query_data.append(self.frontend) - - cursor.execute(query_string, query_data) - return True - - def delete_user_config(self, cursor): - query_string = \ - """DELETE FROM mysql_users - WHERE username = %s - AND backend = %s - AND frontend = %s""" - - query_data = \ - [self.username, - self.backend, - self.frontend] - - cursor.execute(query_string, query_data) - return True - - def manage_config(self, cursor, state): - if state: - if self.save_to_disk: - save_config_to_disk(cursor) - if self.load_to_runtime: - load_config_to_runtime(cursor) - - def create_user(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.create_user_config(cursor) - result['msg'] = "Added user to mysql_users" - result['user'] = \ - self.get_user_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("User would have been added to" + - " mysql_users, however check_mode" + - " is enabled.") - - def update_user(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.update_user_config(cursor) - result['msg'] = "Updated user in mysql_users" - result['user'] = \ - self.get_user_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("User would have been updated in" + - " mysql_users, however check_mode" + - " is enabled.") - - def delete_user(self, check_mode, result, cursor): - if not check_mode: - result['user'] = \ - self.get_user_config(cursor) - result['changed'] = \ - self.delete_user_config(cursor) - result['msg'] = "Deleted user from mysql_users" - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("User would have been deleted from" + - " mysql_users, however check_mode is" + - " enabled.") - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default='', type='path'), - username=dict(required=True, type='str'), - password=dict(no_log=True, type='str'), - active=dict(type='bool'), - use_ssl=dict(type='bool'), - default_hostgroup=dict(type='int'), - default_schema=dict(type='str'), - transaction_persistent=dict(type='bool'), - fast_forward=dict(type='bool'), - backend=dict(default=True, type='bool'), - frontend=dict(default=True, type='bool'), - max_connections=dict(type='int'), - state=dict(default='present', choices=['present', - 'absent']), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - - cursor = None - try: - cursor, db_conn = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class='DictCursor') - except mysql_driver.Error as e: - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % to_native(e) - ) - - proxysql_user = ProxySQLUser(module) - result = {} - - result['state'] = proxysql_user.state - if proxysql_user.username: - result['username'] = proxysql_user.username - - if proxysql_user.state == "present": - try: - if not proxysql_user.check_user_privs(cursor): - if not proxysql_user.check_user_config_exists(cursor): - proxysql_user.create_user(module.check_mode, - result, - cursor) - else: - proxysql_user.update_user(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The user already exists in mysql_users" + - " and doesn't need to be updated.") - result['user'] = \ - proxysql_user.get_user_config(cursor) - except mysql_driver.Error as e: - module.fail_json( - msg="unable to modify user.. %s" % to_native(e) - ) - - elif proxysql_user.state == "absent": - try: - if proxysql_user.check_user_config_exists(cursor): - proxysql_user.delete_user(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The user is already absent from the" + - " mysql_users memory configuration") - except mysql_driver.Error as e: - module.fail_json( - msg="unable to remove user.. %s" % to_native(e) - ) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_query_rules.py b/lib/ansible/modules/database/proxysql/proxysql_query_rules.py deleted file mode 100644 index f40617ad63..0000000000 --- a/lib/ansible/modules/database/proxysql/proxysql_query_rules.py +++ /dev/null @@ -1,613 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: proxysql_query_rules -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Modifies query rules using the proxysql admin interface. -description: - - The M(proxysql_query_rules) module modifies query rules using the - proxysql admin interface. -options: - rule_id: - description: - - The unique id of the rule. Rules are processed in rule_id order. - active: - description: - - A rule with I(active) set to C(False) will be tracked in the database, - but will be never loaded in the in-memory data structures. - type: bool - username: - description: - - Filtering criteria matching username. If I(username) is non-NULL, a - query will match only if the connection is made with the correct - username. - schemaname: - description: - - Filtering criteria matching schemaname. If I(schemaname) is non-NULL, a - query will match only if the connection uses schemaname as its default - schema. - flagIN: - description: - - Used in combination with I(flagOUT) and I(apply) to create chains of - rules. - client_addr: - description: - - Match traffic from a specific source. - proxy_addr: - description: - - Match incoming traffic on a specific local IP. - proxy_port: - description: - - Match incoming traffic on a specific local port. - digest: - description: - - Match queries with a specific digest, as returned by - stats_mysql_query_digest.digest. - match_digest: - description: - - Regular expression that matches the query digest. The dialect of - regular expressions used is that of re2 - https://github.com/google/re2 - match_pattern: - description: - - Regular expression that matches the query text. The dialect of regular - expressions used is that of re2 - https://github.com/google/re2 - negate_match_pattern: - description: - - If I(negate_match_pattern) is set to C(True), only queries not matching - the query text will be considered as a match. This acts as a NOT - operator in front of the regular expression matching against - match_pattern. - type: bool - flagOUT: - description: - - Used in combination with I(flagIN) and apply to create chains of rules. - When set, I(flagOUT) signifies the I(flagIN) to be used in the next - chain of rules. - replace_pattern: - description: - - This is the pattern with which to replace the matched pattern. Note - that this is optional, and when omitted, the query processor will only - cache, route, or set other parameters without rewriting. - destination_hostgroup: - description: - - Route matched queries to this hostgroup. This happens unless there is a - started transaction and the logged in user has - I(transaction_persistent) set to C(True) (see M(proxysql_mysql_users)). - cache_ttl: - description: - - The number of milliseconds for which to cache the result of the query. - Note in ProxySQL 1.1 I(cache_ttl) was in seconds. - timeout: - description: - - The maximum timeout in milliseconds with which the matched or rewritten - query should be executed. If a query run for longer than the specific - threshold, the query is automatically killed. If timeout is not - specified, the global variable mysql-default_query_timeout applies. - retries: - description: - - The maximum number of times a query needs to be re-executed in case of - detected failure during the execution of the query. If retries is not - specified, the global variable mysql-query_retries_on_failure applies. - delay: - description: - - Number of milliseconds to delay the execution of the query. This is - essentially a throttling mechanism and QoS, and allows a way to give - priority to queries over others. This value is added to the - mysql-default_query_delay global variable that applies to all queries. - mirror_flagOUT: - description: - - Enables query mirroring. If set I(mirror_flagOUT) can be used to - evaluates the mirrored query against the specified chain of rules. - mirror_hostgroup: - description: - - Enables query mirroring. If set I(mirror_hostgroup) can be used to - mirror queries to the same or different hostgroup. - error_msg: - description: - - Query will be blocked, and the specified error_msg will be returned to - the client. - log: - description: - - Query will be logged. - type: bool - apply: - description: - - Used in combination with I(flagIN) and I(flagOUT) to create chains of - rules. Setting apply to True signifies the last rule to be applied. - type: bool - comment: - description: - - Free form text field, usable for a descriptive comment of the query - rule. - state: - description: - - When C(present) - adds the rule, when C(absent) - removes the rule. - choices: [ "present", "absent" ] - default: present - force_delete: - description: - - By default we avoid deleting more than one schedule in a single batch, - however if you need this behaviour and you're not concerned about the - schedules deleted, you can set I(force_delete) to C(True). - default: False - type: bool -extends_documentation_fragment: - - proxysql.managing_config - - proxysql.connectivity -''' - -EXAMPLES = ''' ---- -# This example adds a rule to redirect queries from a specific user to another -# hostgroup, it saves the mysql query rule config to disk, but avoids loading -# the mysql query config config to runtime (this might be because several -# rules are being added and the user wants to push the config to runtime in a -# single batch using the M(proxysql_manage_config) module). It uses supplied -# credentials to connect to the proxysql admin interface. - -- proxysql_query_rules: - login_user: admin - login_password: admin - username: 'guest_ro' - match_pattern: "^SELECT.*" - destination_hostgroup: 1 - active: 1 - retries: 3 - state: present - load_to_runtime: False - -# This example removes all rules that use the username 'guest_ro', saves the -# mysql query rule config to disk, and dynamically loads the mysql query rule -# config to runtime. It uses credentials in a supplied config file to connect -# to the proxysql admin interface. - -- proxysql_query_rules: - config_file: '~/proxysql.cnf' - username: 'guest_ro' - state: absent - force_delete: true -''' - -RETURN = ''' -stdout: - description: The mysql user modified or removed from proxysql - returned: On create/update will return the newly modified rule, in all - other cases will return a list of rules that match the supplied - criteria. - type: dict - "sample": { - "changed": true, - "msg": "Added rule to mysql_query_rules", - "rules": [ - { - "active": "0", - "apply": "0", - "cache_ttl": null, - "client_addr": null, - "comment": null, - "delay": null, - "destination_hostgroup": 1, - "digest": null, - "error_msg": null, - "flagIN": "0", - "flagOUT": null, - "log": null, - "match_digest": null, - "match_pattern": null, - "mirror_flagOUT": null, - "mirror_hostgroup": null, - "negate_match_pattern": "0", - "proxy_addr": null, - "proxy_port": null, - "reconnect": null, - "replace_pattern": null, - "retries": null, - "rule_id": "1", - "schemaname": null, - "timeout": null, - "username": "guest_ro" - } - ], - "state": "present" - } -''' - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils.six import iteritems -from ansible.module_utils._text import to_native - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - -def save_config_to_disk(cursor): - cursor.execute("SAVE MYSQL QUERY RULES TO DISK") - return True - - -def load_config_to_runtime(cursor): - cursor.execute("LOAD MYSQL QUERY RULES TO RUNTIME") - return True - - -class ProxyQueryRule(object): - - def __init__(self, module): - self.state = module.params["state"] - self.force_delete = module.params["force_delete"] - self.save_to_disk = module.params["save_to_disk"] - self.load_to_runtime = module.params["load_to_runtime"] - - config_data_keys = ["rule_id", - "active", - "username", - "schemaname", - "flagIN", - "client_addr", - "proxy_addr", - "proxy_port", - "digest", - "match_digest", - "match_pattern", - "negate_match_pattern", - "flagOUT", - "replace_pattern", - "destination_hostgroup", - "cache_ttl", - "timeout", - "retries", - "delay", - "mirror_flagOUT", - "mirror_hostgroup", - "error_msg", - "log", - "apply", - "comment"] - - self.config_data = dict((k, module.params[k]) - for k in config_data_keys) - - def check_rule_pk_exists(self, cursor): - query_string = \ - """SELECT count(*) AS `rule_count` - FROM mysql_query_rules - WHERE rule_id = %s""" - - query_data = \ - [self.config_data["rule_id"]] - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['rule_count']) > 0) - - def check_rule_cfg_exists(self, cursor): - query_string = \ - """SELECT count(*) AS `rule_count` - FROM mysql_query_rules""" - - cols = 0 - query_data = [] - - for col, val in iteritems(self.config_data): - if val is not None: - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\n WHERE " + col + " = %s" - else: - query_string += "\n AND " + col + " = %s" - - if cols > 0: - cursor.execute(query_string, query_data) - else: - cursor.execute(query_string) - check_count = cursor.fetchone() - return int(check_count['rule_count']) - - def get_rule_config(self, cursor, created_rule_id=None): - query_string = \ - """SELECT * - FROM mysql_query_rules""" - - if created_rule_id: - query_data = [created_rule_id, ] - query_string += "\nWHERE rule_id = %s" - - cursor.execute(query_string, query_data) - rule = cursor.fetchone() - else: - cols = 0 - query_data = [] - - for col, val in iteritems(self.config_data): - if val is not None: - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\n WHERE " + col + " = %s" - else: - query_string += "\n AND " + col + " = %s" - - if cols > 0: - cursor.execute(query_string, query_data) - else: - cursor.execute(query_string) - rule = cursor.fetchall() - - return rule - - def create_rule_config(self, cursor): - query_string = \ - """INSERT INTO mysql_query_rules (""" - - cols = 0 - query_data = [] - - for col, val in iteritems(self.config_data): - if val is not None: - cols += 1 - query_data.append(val) - query_string += "\n" + col + "," - - query_string = query_string[:-1] - - query_string += \ - (")\n" + - "VALUES (" + - "%s ," * cols) - - query_string = query_string[:-2] - query_string += ")" - - cursor.execute(query_string, query_data) - new_rule_id = cursor.lastrowid - return True, new_rule_id - - def update_rule_config(self, cursor): - query_string = """UPDATE mysql_query_rules""" - - cols = 0 - query_data = [] - - for col, val in iteritems(self.config_data): - if val is not None and col != "rule_id": - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\nSET " + col + "= %s," - else: - query_string += "\n " + col + " = %s," - - query_string = query_string[:-1] - query_string += "\nWHERE rule_id = %s" - - query_data.append(self.config_data["rule_id"]) - - cursor.execute(query_string, query_data) - return True - - def delete_rule_config(self, cursor): - query_string = \ - """DELETE FROM mysql_query_rules""" - - cols = 0 - query_data = [] - - for col, val in iteritems(self.config_data): - if val is not None: - cols += 1 - query_data.append(val) - if cols == 1: - query_string += "\n WHERE " + col + " = %s" - else: - query_string += "\n AND " + col + " = %s" - - if cols > 0: - cursor.execute(query_string, query_data) - else: - cursor.execute(query_string) - check_count = cursor.rowcount - return True, int(check_count) - - def manage_config(self, cursor, state): - if state: - if self.save_to_disk: - save_config_to_disk(cursor) - if self.load_to_runtime: - load_config_to_runtime(cursor) - - def create_rule(self, check_mode, result, cursor): - if not check_mode: - result['changed'], new_rule_id = \ - self.create_rule_config(cursor) - result['msg'] = "Added rule to mysql_query_rules" - self.manage_config(cursor, - result['changed']) - result['rules'] = \ - self.get_rule_config(cursor, new_rule_id) - else: - result['changed'] = True - result['msg'] = ("Rule would have been added to" + - " mysql_query_rules, however" + - " check_mode is enabled.") - - def update_rule(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.update_rule_config(cursor) - result['msg'] = "Updated rule in mysql_query_rules" - self.manage_config(cursor, - result['changed']) - result['rules'] = \ - self.get_rule_config(cursor) - else: - result['changed'] = True - result['msg'] = ("Rule would have been updated in" + - " mysql_query_rules, however" + - " check_mode is enabled.") - - def delete_rule(self, check_mode, result, cursor): - if not check_mode: - result['rules'] = \ - self.get_rule_config(cursor) - result['changed'], result['rows_affected'] = \ - self.delete_rule_config(cursor) - result['msg'] = "Deleted rule from mysql_query_rules" - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Rule would have been deleted from" + - " mysql_query_rules, however" + - " check_mode is enabled.") - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default="", type='path'), - rule_id=dict(type='int'), - active=dict(type='bool'), - username=dict(type='str'), - schemaname=dict(type='str'), - flagIN=dict(type='int'), - client_addr=dict(type='str'), - proxy_addr=dict(type='str'), - proxy_port=dict(type='int'), - digest=dict(type='str'), - match_digest=dict(type='str'), - match_pattern=dict(type='str'), - negate_match_pattern=dict(type='bool'), - flagOUT=dict(type='int'), - replace_pattern=dict(type='str'), - destination_hostgroup=dict(type='int'), - cache_ttl=dict(type='int'), - timeout=dict(type='int'), - retries=dict(type='int'), - delay=dict(type='int'), - mirror_flagOUT=dict(type='int'), - mirror_hostgroup=dict(type='int'), - error_msg=dict(type='str'), - log=dict(type='bool'), - apply=dict(type='bool'), - comment=dict(type='str'), - state=dict(default='present', choices=['present', - 'absent']), - force_delete=dict(default=False, type='bool'), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - - cursor = None - try: - cursor, db_conn = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class='DictCursor') - except mysql_driver.Error as e: - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % to_native(e) - ) - - proxysql_query_rule = ProxyQueryRule(module) - result = {} - - result['state'] = proxysql_query_rule.state - - if proxysql_query_rule.state == "present": - try: - if not proxysql_query_rule.check_rule_cfg_exists(cursor): - if proxysql_query_rule.config_data["rule_id"] and \ - proxysql_query_rule.check_rule_pk_exists(cursor): - proxysql_query_rule.update_rule(module.check_mode, - result, - cursor) - else: - proxysql_query_rule.create_rule(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The rule already exists in" + - " mysql_query_rules and doesn't need to be" + - " updated.") - result['rules'] = \ - proxysql_query_rule.get_rule_config(cursor) - - except mysql_driver.Error as e: - module.fail_json( - msg="unable to modify rule.. %s" % to_native(e) - ) - - elif proxysql_query_rule.state == "absent": - try: - existing_rules = proxysql_query_rule.check_rule_cfg_exists(cursor) - if existing_rules > 0: - if existing_rules == 1 or \ - proxysql_query_rule.force_delete: - proxysql_query_rule.delete_rule(module.check_mode, - result, - cursor) - else: - module.fail_json( - msg=("Operation would delete multiple rules" + - " use force_delete to override this") - ) - else: - result['changed'] = False - result['msg'] = ("The rule is already absent from the" + - " mysql_query_rules memory configuration") - except mysql_driver.Error as e: - module.fail_json( - msg="unable to remove rule.. %s" % to_native(e) - ) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_replication_hostgroups.py b/lib/ansible/modules/database/proxysql/proxysql_replication_hostgroups.py deleted file mode 100644 index c685f0863a..0000000000 --- a/lib/ansible/modules/database/proxysql/proxysql_replication_hostgroups.py +++ /dev/null @@ -1,380 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: proxysql_replication_hostgroups -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Manages replication hostgroups using the proxysql admin - interface. -description: - - Each row in mysql_replication_hostgroups represent a pair of - writer_hostgroup and reader_hostgroup. ProxySQL will monitor the value of - read_only for all the servers in specified hostgroups, and based on the - value of read_only will assign the server to the writer or reader - hostgroups. -options: - writer_hostgroup: - description: - - Id of the writer hostgroup. - required: True - reader_hostgroup: - description: - - Id of the reader hostgroup. - required: True - comment: - description: - - Text field that can be used for any purposes defined by the user. - state: - description: - - When C(present) - adds the replication hostgroup, when C(absent) - - removes the replication hostgroup. - choices: [ "present", "absent" ] - default: present -extends_documentation_fragment: - - proxysql.managing_config - - proxysql.connectivity -''' - -EXAMPLES = ''' ---- -# This example adds a replication hostgroup, it saves the mysql server config -# to disk, but avoids loading the mysql server config to runtime (this might be -# because several replication hostgroup are being added and the user wants to -# push the config to runtime in a single batch using the -# M(proxysql_manage_config) module). It uses supplied credentials to connect -# to the proxysql admin interface. - -- proxysql_replication_hostgroups: - login_user: 'admin' - login_password: 'admin' - writer_hostgroup: 1 - reader_hostgroup: 2 - state: present - load_to_runtime: False - -# This example removes a replication hostgroup, saves the mysql server config -# to disk, and dynamically loads the mysql server config to runtime. It uses -# credentials in a supplied config file to connect to the proxysql admin -# interface. - -- proxysql_replication_hostgroups: - config_file: '~/proxysql.cnf' - writer_hostgroup: 3 - reader_hostgroup: 4 - state: absent -''' - -RETURN = ''' -stdout: - description: The replication hostgroup modified or removed from proxysql - returned: On create/update will return the newly modified group, on delete - it will return the deleted record. - type: dict - "sample": { - "changed": true, - "msg": "Added server to mysql_hosts", - "repl_group": { - "comment": "", - "reader_hostgroup": "1", - "writer_hostgroup": "2" - }, - "state": "present" - } -''' - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils._text import to_native - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if not module.params["writer_hostgroup"] >= 0: - module.fail_json( - msg="writer_hostgroup must be a integer greater than or equal to 0" - ) - - if not module.params["reader_hostgroup"] == \ - module.params["writer_hostgroup"]: - if not module.params["reader_hostgroup"] > 0: - module.fail_json( - msg=("writer_hostgroup must be a integer greater than" + - " or equal to 0") - ) - else: - module.fail_json( - msg="reader_hostgroup cannot equal writer_hostgroup" - ) - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - -def save_config_to_disk(cursor): - cursor.execute("SAVE MYSQL SERVERS TO DISK") - return True - - -def load_config_to_runtime(cursor): - cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") - return True - - -class ProxySQLReplicationHostgroup(object): - - def __init__(self, module): - self.state = module.params["state"] - self.save_to_disk = module.params["save_to_disk"] - self.load_to_runtime = module.params["load_to_runtime"] - self.writer_hostgroup = module.params["writer_hostgroup"] - self.reader_hostgroup = module.params["reader_hostgroup"] - self.comment = module.params["comment"] - - def check_repl_group_config(self, cursor, keys): - query_string = \ - """SELECT count(*) AS `repl_groups` - FROM mysql_replication_hostgroups - WHERE writer_hostgroup = %s - AND reader_hostgroup = %s""" - - query_data = \ - [self.writer_hostgroup, - self.reader_hostgroup] - - if self.comment and not keys: - query_string += "\n AND comment = %s" - query_data.append(self.comment) - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return (int(check_count['repl_groups']) > 0) - - def get_repl_group_config(self, cursor): - query_string = \ - """SELECT * - FROM mysql_replication_hostgroups - WHERE writer_hostgroup = %s - AND reader_hostgroup = %s""" - - query_data = \ - [self.writer_hostgroup, - self.reader_hostgroup] - - cursor.execute(query_string, query_data) - repl_group = cursor.fetchone() - return repl_group - - def create_repl_group_config(self, cursor): - query_string = \ - """INSERT INTO mysql_replication_hostgroups ( - writer_hostgroup, - reader_hostgroup, - comment) - VALUES (%s, %s, %s)""" - - query_data = \ - [self.writer_hostgroup, - self.reader_hostgroup, - self.comment or ''] - - cursor.execute(query_string, query_data) - return True - - def update_repl_group_config(self, cursor): - query_string = \ - """UPDATE mysql_replication_hostgroups - SET comment = %s - WHERE writer_hostgroup = %s - AND reader_hostgroup = %s""" - - query_data = \ - [self.comment, - self.writer_hostgroup, - self.reader_hostgroup] - - cursor.execute(query_string, query_data) - return True - - def delete_repl_group_config(self, cursor): - query_string = \ - """DELETE FROM mysql_replication_hostgroups - WHERE writer_hostgroup = %s - AND reader_hostgroup = %s""" - - query_data = \ - [self.writer_hostgroup, - self.reader_hostgroup] - - cursor.execute(query_string, query_data) - return True - - def manage_config(self, cursor, state): - if state: - if self.save_to_disk: - save_config_to_disk(cursor) - if self.load_to_runtime: - load_config_to_runtime(cursor) - - def create_repl_group(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.create_repl_group_config(cursor) - result['msg'] = "Added server to mysql_hosts" - result['repl_group'] = \ - self.get_repl_group_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Repl group would have been added to" + - " mysql_replication_hostgroups, however" + - " check_mode is enabled.") - - def update_repl_group(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.update_repl_group_config(cursor) - result['msg'] = "Updated server in mysql_hosts" - result['repl_group'] = \ - self.get_repl_group_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Repl group would have been updated in" + - " mysql_replication_hostgroups, however" + - " check_mode is enabled.") - - def delete_repl_group(self, check_mode, result, cursor): - if not check_mode: - result['repl_group'] = \ - self.get_repl_group_config(cursor) - result['changed'] = \ - self.delete_repl_group_config(cursor) - result['msg'] = "Deleted server from mysql_hosts" - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Repl group would have been deleted from" + - " mysql_replication_hostgroups, however" + - " check_mode is enabled.") - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default="", type='path'), - writer_hostgroup=dict(required=True, type='int'), - reader_hostgroup=dict(required=True, type='int'), - comment=dict(type='str'), - state=dict(default='present', choices=['present', - 'absent']), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - - cursor = None - try: - cursor, db_conn = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class='DictCursor') - except mysql_driver.Error as e: - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % to_native(e) - ) - - proxysql_repl_group = ProxySQLReplicationHostgroup(module) - result = {} - - result['state'] = proxysql_repl_group.state - - if proxysql_repl_group.state == "present": - try: - if not proxysql_repl_group.check_repl_group_config(cursor, - keys=True): - proxysql_repl_group.create_repl_group(module.check_mode, - result, - cursor) - else: - if not proxysql_repl_group.check_repl_group_config(cursor, - keys=False): - proxysql_repl_group.update_repl_group(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The repl group already exists in" + - " mysql_replication_hostgroups and" + - " doesn't need to be updated.") - result['repl_group'] = \ - proxysql_repl_group.get_repl_group_config(cursor) - - except mysql_driver.Error as e: - module.fail_json( - msg="unable to modify replication hostgroup.. %s" % to_native(e) - ) - - elif proxysql_repl_group.state == "absent": - try: - if proxysql_repl_group.check_repl_group_config(cursor, - keys=True): - proxysql_repl_group.delete_repl_group(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The repl group is already absent from the" + - " mysql_replication_hostgroups memory" + - " configuration") - - except mysql_driver.Error as e: - module.fail_json( - msg="unable to delete replication hostgroup.. %s" % to_native(e) - ) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_scheduler.py b/lib/ansible/modules/database/proxysql/proxysql_scheduler.py deleted file mode 100644 index 3ba7b67864..0000000000 --- a/lib/ansible/modules/database/proxysql/proxysql_scheduler.py +++ /dev/null @@ -1,417 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -DOCUMENTATION = ''' ---- -module: proxysql_scheduler -version_added: "2.3" -author: "Ben Mildren (@bmildren)" -short_description: Adds or removes schedules from proxysql admin interface. -description: - - The M(proxysql_scheduler) module adds or removes schedules using the - proxysql admin interface. -options: - active: - description: - - A schedule with I(active) set to C(False) will be tracked in the - database, but will be never loaded in the in-memory data structures. - default: True - type: bool - interval_ms: - description: - - How often (in millisecond) the job will be started. The minimum value - for I(interval_ms) is 100 milliseconds. - default: 10000 - filename: - description: - - Full path of the executable to be executed. - required: True - arg1: - description: - - Argument that can be passed to the job. - arg2: - description: - - Argument that can be passed to the job. - arg3: - description: - - Argument that can be passed to the job. - arg4: - description: - - Argument that can be passed to the job. - arg5: - description: - - Argument that can be passed to the job. - comment: - description: - - Text field that can be used for any purposed defined by the user. - state: - description: - - When C(present) - adds the schedule, when C(absent) - removes the - schedule. - choices: [ "present", "absent" ] - default: present - force_delete: - description: - - By default we avoid deleting more than one schedule in a single batch, - however if you need this behaviour and you're not concerned about the - schedules deleted, you can set I(force_delete) to C(True). - default: False - type: bool -extends_documentation_fragment: - - proxysql.managing_config - - proxysql.connectivity -''' - -EXAMPLES = ''' ---- -# This example adds a schedule, it saves the scheduler config to disk, but -# avoids loading the scheduler config to runtime (this might be because -# several servers are being added and the user wants to push the config to -# runtime in a single batch using the M(proxysql_manage_config) module). It -# uses supplied credentials to connect to the proxysql admin interface. - -- proxysql_scheduler: - login_user: 'admin' - login_password: 'admin' - interval_ms: 1000 - filename: "/opt/maintenance.py" - state: present - load_to_runtime: False - -# This example removes a schedule, saves the scheduler config to disk, and -# dynamically loads the scheduler config to runtime. It uses credentials -# in a supplied config file to connect to the proxysql admin interface. - -- proxysql_scheduler: - config_file: '~/proxysql.cnf' - filename: "/opt/old_script.py" - state: absent -''' - -RETURN = ''' -stdout: - description: The schedule modified or removed from proxysql - returned: On create/update will return the newly modified schedule, on - delete it will return the deleted record. - type: dict - "sample": { - "changed": true, - "filename": "/opt/test.py", - "msg": "Added schedule to scheduler", - "schedules": [ - { - "active": "1", - "arg1": null, - "arg2": null, - "arg3": null, - "arg4": null, - "arg5": null, - "comment": "", - "filename": "/opt/test.py", - "id": "1", - "interval_ms": "10000" - } - ], - "state": "present" - } -''' - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg -from ansible.module_utils.six import iteritems -from ansible.module_utils._text import to_native - -# =========================================== -# proxysql module specific support methods. -# - - -def perform_checks(module): - if module.params["login_port"] < 0 \ - or module.params["login_port"] > 65535: - module.fail_json( - msg="login_port must be a valid unix port number (0-65535)" - ) - - if module.params["interval_ms"] < 100 \ - or module.params["interval_ms"] > 100000000: - module.fail_json( - msg="interval_ms must between 100ms & 100000000ms" - ) - - if mysql_driver is None: - module.fail_json(msg=mysql_driver_fail_msg) - - -def save_config_to_disk(cursor): - cursor.execute("SAVE SCHEDULER TO DISK") - return True - - -def load_config_to_runtime(cursor): - cursor.execute("LOAD SCHEDULER TO RUNTIME") - return True - - -class ProxySQLSchedule(object): - - def __init__(self, module): - self.state = module.params["state"] - self.force_delete = module.params["force_delete"] - self.save_to_disk = module.params["save_to_disk"] - self.load_to_runtime = module.params["load_to_runtime"] - self.active = module.params["active"] - self.interval_ms = module.params["interval_ms"] - self.filename = module.params["filename"] - - config_data_keys = ["arg1", - "arg2", - "arg3", - "arg4", - "arg5", - "comment"] - - self.config_data = dict((k, module.params[k]) - for k in config_data_keys) - - def check_schedule_config(self, cursor): - query_string = \ - """SELECT count(*) AS `schedule_count` - FROM scheduler - WHERE active = %s - AND interval_ms = %s - AND filename = %s""" - - query_data = \ - [self.active, - self.interval_ms, - self.filename] - - for col, val in iteritems(self.config_data): - if val is not None: - query_data.append(val) - query_string += "\n AND " + col + " = %s" - - cursor.execute(query_string, query_data) - check_count = cursor.fetchone() - return int(check_count['schedule_count']) - - def get_schedule_config(self, cursor): - query_string = \ - """SELECT * - FROM scheduler - WHERE active = %s - AND interval_ms = %s - AND filename = %s""" - - query_data = \ - [self.active, - self.interval_ms, - self.filename] - - for col, val in iteritems(self.config_data): - if val is not None: - query_data.append(val) - query_string += "\n AND " + col + " = %s" - - cursor.execute(query_string, query_data) - schedule = cursor.fetchall() - return schedule - - def create_schedule_config(self, cursor): - query_string = \ - """INSERT INTO scheduler ( - active, - interval_ms, - filename""" - - cols = 0 - query_data = \ - [self.active, - self.interval_ms, - self.filename] - - for col, val in iteritems(self.config_data): - if val is not None: - cols += 1 - query_data.append(val) - query_string += ",\n" + col - - query_string += \ - (")\n" + - "VALUES (%s, %s, %s" + - ", %s" * cols + - ")") - - cursor.execute(query_string, query_data) - return True - - def delete_schedule_config(self, cursor): - query_string = \ - """DELETE FROM scheduler - WHERE active = %s - AND interval_ms = %s - AND filename = %s""" - - query_data = \ - [self.active, - self.interval_ms, - self.filename] - - for col, val in iteritems(self.config_data): - if val is not None: - query_data.append(val) - query_string += "\n AND " + col + " = %s" - - cursor.execute(query_string, query_data) - check_count = cursor.rowcount - return True, int(check_count) - - def manage_config(self, cursor, state): - if state: - if self.save_to_disk: - save_config_to_disk(cursor) - if self.load_to_runtime: - load_config_to_runtime(cursor) - - def create_schedule(self, check_mode, result, cursor): - if not check_mode: - result['changed'] = \ - self.create_schedule_config(cursor) - result['msg'] = "Added schedule to scheduler" - result['schedules'] = \ - self.get_schedule_config(cursor) - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Schedule would have been added to" + - " scheduler, however check_mode" + - " is enabled.") - - def delete_schedule(self, check_mode, result, cursor): - if not check_mode: - result['schedules'] = \ - self.get_schedule_config(cursor) - result['changed'] = \ - self.delete_schedule_config(cursor) - result['msg'] = "Deleted schedule from scheduler" - self.manage_config(cursor, - result['changed']) - else: - result['changed'] = True - result['msg'] = ("Schedule would have been deleted from" + - " scheduler, however check_mode is" + - " enabled.") - -# =========================================== -# Module execution. -# - - -def main(): - module = AnsibleModule( - argument_spec=dict( - login_user=dict(default=None, type='str'), - login_password=dict(default=None, no_log=True, type='str'), - login_host=dict(default="127.0.0.1"), - login_unix_socket=dict(default=None), - login_port=dict(default=6032, type='int'), - config_file=dict(default="", type='path'), - active=dict(default=True, type='bool'), - interval_ms=dict(default=10000, type='int'), - filename=dict(required=True, type='str'), - arg1=dict(type='str'), - arg2=dict(type='str'), - arg3=dict(type='str'), - arg4=dict(type='str'), - arg5=dict(type='str'), - comment=dict(type='str'), - state=dict(default='present', choices=['present', - 'absent']), - force_delete=dict(default=False, type='bool'), - save_to_disk=dict(default=True, type='bool'), - load_to_runtime=dict(default=True, type='bool') - ), - supports_check_mode=True - ) - - perform_checks(module) - - login_user = module.params["login_user"] - login_password = module.params["login_password"] - config_file = module.params["config_file"] - - cursor = None - try: - cursor, db_conn = mysql_connect(module, - login_user, - login_password, - config_file, - cursor_class='DictCursor') - except mysql_driver.Error as e: - module.fail_json( - msg="unable to connect to ProxySQL Admin Module.. %s" % to_native(e) - ) - - proxysql_schedule = ProxySQLSchedule(module) - result = {} - - result['state'] = proxysql_schedule.state - result['filename'] = proxysql_schedule.filename - - if proxysql_schedule.state == "present": - try: - if proxysql_schedule.check_schedule_config(cursor) <= 0: - proxysql_schedule.create_schedule(module.check_mode, - result, - cursor) - else: - result['changed'] = False - result['msg'] = ("The schedule already exists and doesn't" + - " need to be updated.") - result['schedules'] = \ - proxysql_schedule.get_schedule_config(cursor) - except mysql_driver.Error as e: - module.fail_json( - msg="unable to modify schedule.. %s" % to_native(e) - ) - - elif proxysql_schedule.state == "absent": - try: - existing_schedules = \ - proxysql_schedule.check_schedule_config(cursor) - if existing_schedules > 0: - if existing_schedules == 1 or proxysql_schedule.force_delete: - proxysql_schedule.delete_schedule(module.check_mode, - result, - cursor) - else: - module.fail_json( - msg=("Operation would delete multiple records" + - " use force_delete to override this") - ) - else: - result['changed'] = False - result['msg'] = ("The schedule is already absent from the" + - " memory configuration") - except mysql_driver.Error as e: - module.fail_json( - msg="unable to remove schedule.. %s" % to_native(e) - ) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/vertica/_vertica_facts.py b/lib/ansible/modules/database/vertica/_vertica_facts.py deleted file mode 120000 index bf964af0ae..0000000000 --- a/lib/ansible/modules/database/vertica/_vertica_facts.py +++ /dev/null @@ -1 +0,0 @@ -vertica_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/database/vertica/vertica_configuration.py b/lib/ansible/modules/database/vertica/vertica_configuration.py deleted file mode 100644 index 1e4958b6d3..0000000000 --- a/lib/ansible/modules/database/vertica/vertica_configuration.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: vertica_configuration -version_added: '2.0' -short_description: Updates Vertica configuration parameters. -description: - - Updates Vertica configuration parameters. -options: - name: - description: - - Name of the parameter to update. - required: true - aliases: [parameter] - value: - description: - - Value of the parameter to be set. - required: true - db: - description: - - Name of the Vertica database. - cluster: - description: - - Name of the Vertica cluster. - default: localhost - port: - description: - - Vertica cluster port to connect to. - default: 5433 - login_user: - description: - - The username used to authenticate with. - default: dbadmin - login_password: - description: - - The password used to authenticate with. -notes: - - The default authentication assumes that you are either logging in as or sudo'ing - to the C(dbadmin) account on the host. - - This module uses C(pyodbc), a Python ODBC database adapter. You must ensure - that C(unixODBC) and C(pyodbc) is installed on the host and properly configured. - - Configuring C(unixODBC) for Vertica requires C(Driver = /opt/vertica/lib64/libverticaodbc.so) - to be added to the C(Vertica) section of either C(/etc/odbcinst.ini) or C($HOME/.odbcinst.ini) - and both C(ErrorMessagesPath = /opt/vertica/lib64) and C(DriverManagerEncoding = UTF-16) - to be added to the C(Driver) section of either C(/etc/vertica.ini) or C($HOME/.vertica.ini). -requirements: [ 'unixODBC', 'pyodbc' ] -author: "Dariusz Owczarek (@dareko)" -""" - -EXAMPLES = """ -- name: updating load_balance_policy - vertica_configuration: name=failovertostandbyafter value='8 hours' -""" -import traceback - -PYODBC_IMP_ERR = None -try: - import pyodbc -except ImportError: - PYODBC_IMP_ERR = traceback.format_exc() - pyodbc_found = False -else: - pyodbc_found = True - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native - - -class NotSupportedError(Exception): - pass - - -class CannotDropError(Exception): - pass - -# module specific functions - - -def get_configuration_facts(cursor, parameter_name=''): - facts = {} - cursor.execute(""" - select c.parameter_name, c.current_value, c.default_value - from configuration_parameters c - where c.node_name = 'ALL' - and (? = '' or c.parameter_name ilike ?) - """, parameter_name, parameter_name) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - facts[row.parameter_name.lower()] = { - 'parameter_name': row.parameter_name, - 'current_value': row.current_value, - 'default_value': row.default_value} - return facts - - -def check(configuration_facts, parameter_name, current_value): - parameter_key = parameter_name.lower() - if current_value and current_value.lower() != configuration_facts[parameter_key]['current_value'].lower(): - return False - return True - - -def present(configuration_facts, cursor, parameter_name, current_value): - parameter_key = parameter_name.lower() - changed = False - if current_value and current_value.lower() != configuration_facts[parameter_key]['current_value'].lower(): - cursor.execute("select set_config_parameter('{0}', '{1}')".format(parameter_name, current_value)) - changed = True - if changed: - configuration_facts.update(get_configuration_facts(cursor, parameter_name)) - return changed - -# module logic - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - parameter=dict(required=True, aliases=['name']), - value=dict(default=None), - db=dict(default=None), - cluster=dict(default='localhost'), - port=dict(default='5433'), - login_user=dict(default='dbadmin'), - login_password=dict(default=None, no_log=True), - ), supports_check_mode=True) - - if not pyodbc_found: - module.fail_json(msg=missing_required_lib('pyodbc'), exception=PYODBC_IMP_ERR) - - parameter_name = module.params['parameter'] - current_value = module.params['value'] - db = '' - if module.params['db']: - db = module.params['db'] - - changed = False - - try: - dsn = ( - "Driver=Vertica;" - "Server={0};" - "Port={1};" - "Database={2};" - "User={3};" - "Password={4};" - "ConnectionLoadBalance={5}" - ).format(module.params['cluster'], module.params['port'], db, - module.params['login_user'], module.params['login_password'], 'true') - db_conn = pyodbc.connect(dsn, autocommit=True) - cursor = db_conn.cursor() - except Exception as e: - module.fail_json(msg="Unable to connect to database: {0}.".format(to_native(e)), - exception=traceback.format_exc()) - - try: - configuration_facts = get_configuration_facts(cursor) - if module.check_mode: - changed = not check(configuration_facts, parameter_name, current_value) - else: - try: - changed = present(configuration_facts, cursor, parameter_name, current_value) - except pyodbc.Error as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - except NotSupportedError as e: - module.fail_json(msg=to_native(e), ansible_facts={'vertica_configuration': configuration_facts}) - except CannotDropError as e: - module.fail_json(msg=to_native(e), ansible_facts={'vertica_configuration': configuration_facts}) - except SystemExit: - # avoid catching this on python 2.4 - raise - except Exception as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - module.exit_json(changed=changed, parameter=parameter_name, ansible_facts={'vertica_configuration': configuration_facts}) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/vertica/vertica_info.py b/lib/ansible/modules/database/vertica/vertica_info.py deleted file mode 100644 index 18cc99c72d..0000000000 --- a/lib/ansible/modules/database/vertica/vertica_info.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: vertica_info -version_added: '2.0' -short_description: Gathers Vertica database facts. -description: - - Gathers Vertica database information. - - This module was called C(vertica_facts) before Ansible 2.9, returning C(ansible_facts). - Note that the M(vertica_info) module no longer returns C(ansible_facts)! -options: - cluster: - description: - - Name of the cluster running the schema. - default: localhost - port: - description: - Database port to connect to. - default: 5433 - db: - description: - - Name of the database running the schema. - login_user: - description: - - The username used to authenticate with. - default: dbadmin - login_password: - description: - - The password used to authenticate with. -notes: - - The default authentication assumes that you are either logging in as or sudo'ing - to the C(dbadmin) account on the host. - - This module uses C(pyodbc), a Python ODBC database adapter. You must ensure - that C(unixODBC) and C(pyodbc) is installed on the host and properly configured. - - Configuring C(unixODBC) for Vertica requires C(Driver = /opt/vertica/lib64/libverticaodbc.so) - to be added to the C(Vertica) section of either C(/etc/odbcinst.ini) or C($HOME/.odbcinst.ini) - and both C(ErrorMessagesPath = /opt/vertica/lib64) and C(DriverManagerEncoding = UTF-16) - to be added to the C(Driver) section of either C(/etc/vertica.ini) or C($HOME/.vertica.ini). -requirements: [ 'unixODBC', 'pyodbc' ] -author: "Dariusz Owczarek (@dareko)" -""" - -EXAMPLES = """ -- name: gathering vertica facts - vertica_info: db=db_name - register: result - -- name: Print schemas - debug: - msg: "{{ result.vertica_schemas }}" -""" -import traceback - -PYODBC_IMP_ERR = None -try: - import pyodbc -except ImportError: - PYODBC_IMP_ERR = traceback.format_exc() - pyodbc_found = False -else: - pyodbc_found = True - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native - - -class NotSupportedError(Exception): - pass - -# module specific functions - - -def get_schema_facts(cursor, schema=''): - facts = {} - cursor.execute(""" - select schema_name, schema_owner, create_time - from schemata - where not is_system_schema and schema_name not in ('public') - and (? = '' or schema_name ilike ?) - """, schema, schema) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - facts[row.schema_name.lower()] = { - 'name': row.schema_name, - 'owner': row.schema_owner, - 'create_time': str(row.create_time), - 'usage_roles': [], - 'create_roles': []} - cursor.execute(""" - select g.object_name as schema_name, r.name as role_name, - lower(g.privileges_description) privileges_description - from roles r join grants g - on g.grantee = r.name and g.object_type='SCHEMA' - and g.privileges_description like '%USAGE%' - and g.grantee not in ('public', 'dbadmin') - and (? = '' or g.object_name ilike ?) - """, schema, schema) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - schema_key = row.schema_name.lower() - if 'create' in row.privileges_description: - facts[schema_key]['create_roles'].append(row.role_name) - else: - facts[schema_key]['usage_roles'].append(row.role_name) - return facts - - -def get_user_facts(cursor, user=''): - facts = {} - cursor.execute(""" - select u.user_name, u.is_locked, u.lock_time, - p.password, p.acctexpired as is_expired, - u.profile_name, u.resource_pool, - u.all_roles, u.default_roles - from users u join password_auditor p on p.user_id = u.user_id - where not u.is_super_user - and (? = '' or u.user_name ilike ?) - """, user, user) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - user_key = row.user_name.lower() - facts[user_key] = { - 'name': row.user_name, - 'locked': str(row.is_locked), - 'password': row.password, - 'expired': str(row.is_expired), - 'profile': row.profile_name, - 'resource_pool': row.resource_pool, - 'roles': [], - 'default_roles': []} - if row.is_locked: - facts[user_key]['locked_time'] = str(row.lock_time) - if row.all_roles: - facts[user_key]['roles'] = row.all_roles.replace(' ', '').split(',') - if row.default_roles: - facts[user_key]['default_roles'] = row.default_roles.replace(' ', '').split(',') - return facts - - -def get_role_facts(cursor, role=''): - facts = {} - cursor.execute(""" - select r.name, r.assigned_roles - from roles r - where (? = '' or r.name ilike ?) - """, role, role) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - role_key = row.name.lower() - facts[role_key] = { - 'name': row.name, - 'assigned_roles': []} - if row.assigned_roles: - facts[role_key]['assigned_roles'] = row.assigned_roles.replace(' ', '').split(',') - return facts - - -def get_configuration_facts(cursor, parameter=''): - facts = {} - cursor.execute(""" - select c.parameter_name, c.current_value, c.default_value - from configuration_parameters c - where c.node_name = 'ALL' - and (? = '' or c.parameter_name ilike ?) - """, parameter, parameter) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - facts[row.parameter_name.lower()] = { - 'parameter_name': row.parameter_name, - 'current_value': row.current_value, - 'default_value': row.default_value} - return facts - - -def get_node_facts(cursor, schema=''): - facts = {} - cursor.execute(""" - select node_name, node_address, export_address, node_state, node_type, - catalog_path - from nodes - """) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - facts[row.node_address] = { - 'node_name': row.node_name, - 'export_address': row.export_address, - 'node_state': row.node_state, - 'node_type': row.node_type, - 'catalog_path': row.catalog_path} - return facts - -# module logic - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - cluster=dict(default='localhost'), - port=dict(default='5433'), - db=dict(default=None), - login_user=dict(default='dbadmin'), - login_password=dict(default=None, no_log=True), - ), supports_check_mode=True) - is_old_facts = module._name == 'vertica_facts' - if is_old_facts: - module.deprecate("The 'vertica_facts' module has been renamed to 'vertica_info', " - "and the renamed one no longer returns ansible_facts", version='2.13') - - if not pyodbc_found: - module.fail_json(msg=missing_required_lib('pyodbc'), exception=PYODBC_IMP_ERR) - - db = '' - if module.params['db']: - db = module.params['db'] - - try: - dsn = ( - "Driver=Vertica;" - "Server=%s;" - "Port=%s;" - "Database=%s;" - "User=%s;" - "Password=%s;" - "ConnectionLoadBalance=%s" - ) % (module.params['cluster'], module.params['port'], db, - module.params['login_user'], module.params['login_password'], 'true') - db_conn = pyodbc.connect(dsn, autocommit=True) - cursor = db_conn.cursor() - except Exception as e: - module.fail_json(msg="Unable to connect to database: %s." % to_native(e), exception=traceback.format_exc()) - - try: - schema_facts = get_schema_facts(cursor) - user_facts = get_user_facts(cursor) - role_facts = get_role_facts(cursor) - configuration_facts = get_configuration_facts(cursor) - node_facts = get_node_facts(cursor) - - if is_old_facts: - module.exit_json(changed=False, - ansible_facts={'vertica_schemas': schema_facts, - 'vertica_users': user_facts, - 'vertica_roles': role_facts, - 'vertica_configuration': configuration_facts, - 'vertica_nodes': node_facts}) - else: - module.exit_json(changed=False, - vertica_schemas=schema_facts, - vertica_users=user_facts, - vertica_roles=role_facts, - vertica_configuration=configuration_facts, - vertica_nodes=node_facts) - except NotSupportedError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - except SystemExit: - # avoid catching this on python 2.4 - raise - except Exception as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/vertica/vertica_role.py b/lib/ansible/modules/database/vertica/vertica_role.py deleted file mode 100644 index ec419a0ebd..0000000000 --- a/lib/ansible/modules/database/vertica/vertica_role.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: vertica_role -version_added: '2.0' -short_description: Adds or removes Vertica database roles and assigns roles to them. -description: - - Adds or removes Vertica database role and, optionally, assign other roles. -options: - name: - description: - - Name of the role to add or remove. - required: true - assigned_roles: - description: - - Comma separated list of roles to assign to the role. - aliases: ['assigned_role'] - state: - description: - - Whether to create C(present), drop C(absent) or lock C(locked) a role. - choices: ['present', 'absent'] - default: present - db: - description: - - Name of the Vertica database. - cluster: - description: - - Name of the Vertica cluster. - default: localhost - port: - description: - - Vertica cluster port to connect to. - default: 5433 - login_user: - description: - - The username used to authenticate with. - default: dbadmin - login_password: - description: - - The password used to authenticate with. -notes: - - The default authentication assumes that you are either logging in as or sudo'ing - to the C(dbadmin) account on the host. - - This module uses C(pyodbc), a Python ODBC database adapter. You must ensure - that C(unixODBC) and C(pyodbc) is installed on the host and properly configured. - - Configuring C(unixODBC) for Vertica requires C(Driver = /opt/vertica/lib64/libverticaodbc.so) - to be added to the C(Vertica) section of either C(/etc/odbcinst.ini) or C($HOME/.odbcinst.ini) - and both C(ErrorMessagesPath = /opt/vertica/lib64) and C(DriverManagerEncoding = UTF-16) - to be added to the C(Driver) section of either C(/etc/vertica.ini) or C($HOME/.vertica.ini). -requirements: [ 'unixODBC', 'pyodbc' ] -author: "Dariusz Owczarek (@dareko)" -""" - -EXAMPLES = """ -- name: creating a new vertica role - vertica_role: name=role_name db=db_name state=present - -- name: creating a new vertica role with other role assigned - vertica_role: name=role_name assigned_role=other_role_name state=present -""" -import traceback - -PYODBC_IMP_ERR = None -try: - import pyodbc -except ImportError: - PYODBC_IMP_ERR = traceback.format_exc() - pyodbc_found = False -else: - pyodbc_found = True - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native - - -class NotSupportedError(Exception): - pass - - -class CannotDropError(Exception): - pass - -# module specific functions - - -def get_role_facts(cursor, role=''): - facts = {} - cursor.execute(""" - select r.name, r.assigned_roles - from roles r - where (? = '' or r.name ilike ?) - """, role, role) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - role_key = row.name.lower() - facts[role_key] = { - 'name': row.name, - 'assigned_roles': []} - if row.assigned_roles: - facts[role_key]['assigned_roles'] = row.assigned_roles.replace(' ', '').split(',') - return facts - - -def update_roles(role_facts, cursor, role, - existing, required): - for assigned_role in set(existing) - set(required): - cursor.execute("revoke {0} from {1}".format(assigned_role, role)) - for assigned_role in set(required) - set(existing): - cursor.execute("grant {0} to {1}".format(assigned_role, role)) - - -def check(role_facts, role, assigned_roles): - role_key = role.lower() - if role_key not in role_facts: - return False - if assigned_roles and sorted(assigned_roles) != sorted(role_facts[role_key]['assigned_roles']): - return False - return True - - -def present(role_facts, cursor, role, assigned_roles): - role_key = role.lower() - if role_key not in role_facts: - cursor.execute("create role {0}".format(role)) - update_roles(role_facts, cursor, role, [], assigned_roles) - role_facts.update(get_role_facts(cursor, role)) - return True - else: - changed = False - if assigned_roles and (sorted(assigned_roles) != sorted(role_facts[role_key]['assigned_roles'])): - update_roles(role_facts, cursor, role, - role_facts[role_key]['assigned_roles'], assigned_roles) - changed = True - if changed: - role_facts.update(get_role_facts(cursor, role)) - return changed - - -def absent(role_facts, cursor, role, assigned_roles): - role_key = role.lower() - if role_key in role_facts: - update_roles(role_facts, cursor, role, - role_facts[role_key]['assigned_roles'], []) - cursor.execute("drop role {0} cascade".format(role_facts[role_key]['name'])) - del role_facts[role_key] - return True - else: - return False - -# module logic - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - role=dict(required=True, aliases=['name']), - assigned_roles=dict(default=None, aliases=['assigned_role']), - state=dict(default='present', choices=['absent', 'present']), - db=dict(default=None), - cluster=dict(default='localhost'), - port=dict(default='5433'), - login_user=dict(default='dbadmin'), - login_password=dict(default=None, no_log=True), - ), supports_check_mode=True) - - if not pyodbc_found: - module.fail_json(msg=missing_required_lib('pyodbc'), exception=PYODBC_IMP_ERR) - - role = module.params['role'] - assigned_roles = [] - if module.params['assigned_roles']: - assigned_roles = module.params['assigned_roles'].split(',') - assigned_roles = filter(None, assigned_roles) - state = module.params['state'] - db = '' - if module.params['db']: - db = module.params['db'] - - changed = False - - try: - dsn = ( - "Driver=Vertica;" - "Server={0};" - "Port={1};" - "Database={2};" - "User={3};" - "Password={4};" - "ConnectionLoadBalance={5}" - ).format(module.params['cluster'], module.params['port'], db, - module.params['login_user'], module.params['login_password'], 'true') - db_conn = pyodbc.connect(dsn, autocommit=True) - cursor = db_conn.cursor() - except Exception as e: - module.fail_json(msg="Unable to connect to database: {0}.".format(to_native(e))) - - try: - role_facts = get_role_facts(cursor) - if module.check_mode: - changed = not check(role_facts, role, assigned_roles) - elif state == 'absent': - try: - changed = absent(role_facts, cursor, role, assigned_roles) - except pyodbc.Error as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - elif state == 'present': - try: - changed = present(role_facts, cursor, role, assigned_roles) - except pyodbc.Error as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - except NotSupportedError as e: - module.fail_json(msg=to_native(e), ansible_facts={'vertica_roles': role_facts}) - except CannotDropError as e: - module.fail_json(msg=to_native(e), ansible_facts={'vertica_roles': role_facts}) - except SystemExit: - # avoid catching this on python 2.4 - raise - except Exception as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - module.exit_json(changed=changed, role=role, ansible_facts={'vertica_roles': role_facts}) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/vertica/vertica_schema.py b/lib/ansible/modules/database/vertica/vertica_schema.py deleted file mode 100644 index 3465c4d95c..0000000000 --- a/lib/ansible/modules/database/vertica/vertica_schema.py +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: vertica_schema -version_added: '2.0' -short_description: Adds or removes Vertica database schema and roles. -description: - - Adds or removes Vertica database schema and, optionally, roles - with schema access privileges. - - A schema will not be removed until all the objects have been dropped. - - In such a situation, if the module tries to remove the schema it - will fail and only remove roles created for the schema if they have - no dependencies. -options: - name: - description: - - Name of the schema to add or remove. - required: true - usage_roles: - description: - - Comma separated list of roles to create and grant usage access to the schema. - aliases: ['usage_role'] - create_roles: - description: - - Comma separated list of roles to create and grant usage and create access to the schema. - aliases: ['create_role'] - owner: - description: - - Name of the user to set as owner of the schema. - state: - description: - - Whether to create C(present), or drop C(absent) a schema. - default: present - choices: ['present', 'absent'] - db: - description: - - Name of the Vertica database. - cluster: - description: - - Name of the Vertica cluster. - default: localhost - port: - description: - - Vertica cluster port to connect to. - default: 5433 - login_user: - description: - - The username used to authenticate with. - default: dbadmin - login_password: - description: - - The password used to authenticate with. -notes: - - The default authentication assumes that you are either logging in as or sudo'ing - to the C(dbadmin) account on the host. - - This module uses C(pyodbc), a Python ODBC database adapter. You must ensure - that C(unixODBC) and C(pyodbc) is installed on the host and properly configured. - - Configuring C(unixODBC) for Vertica requires C(Driver = /opt/vertica/lib64/libverticaodbc.so) - to be added to the C(Vertica) section of either C(/etc/odbcinst.ini) or C($HOME/.odbcinst.ini) - and both C(ErrorMessagesPath = /opt/vertica/lib64) and C(DriverManagerEncoding = UTF-16) - to be added to the C(Driver) section of either C(/etc/vertica.ini) or C($HOME/.vertica.ini). -requirements: [ 'unixODBC', 'pyodbc' ] -author: "Dariusz Owczarek (@dareko)" -""" - -EXAMPLES = """ -- name: creating a new vertica schema - vertica_schema: name=schema_name db=db_name state=present - -- name: creating a new schema with specific schema owner - vertica_schema: name=schema_name owner=dbowner db=db_name state=present - -- name: creating a new schema with roles - vertica_schema: - name=schema_name - create_roles=schema_name_all - usage_roles=schema_name_ro,schema_name_rw - db=db_name - state=present -""" -import traceback - -PYODBC_IMP_ERR = None -try: - import pyodbc -except ImportError: - PYODBC_IMP_ERR = traceback.format_exc() - pyodbc_found = False -else: - pyodbc_found = True - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native - - -class NotSupportedError(Exception): - pass - - -class CannotDropError(Exception): - pass - -# module specific functions - - -def get_schema_facts(cursor, schema=''): - facts = {} - cursor.execute(""" - select schema_name, schema_owner, create_time - from schemata - where not is_system_schema and schema_name not in ('public', 'TxtIndex') - and (? = '' or schema_name ilike ?) - """, schema, schema) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - facts[row.schema_name.lower()] = { - 'name': row.schema_name, - 'owner': row.schema_owner, - 'create_time': str(row.create_time), - 'usage_roles': [], - 'create_roles': []} - cursor.execute(""" - select g.object_name as schema_name, r.name as role_name, - lower(g.privileges_description) privileges_description - from roles r join grants g - on g.grantee_id = r.role_id and g.object_type='SCHEMA' - and g.privileges_description like '%USAGE%' - and g.grantee not in ('public', 'dbadmin') - and (? = '' or g.object_name ilike ?) - """, schema, schema) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - schema_key = row.schema_name.lower() - if 'create' in row.privileges_description: - facts[schema_key]['create_roles'].append(row.role_name) - else: - facts[schema_key]['usage_roles'].append(row.role_name) - return facts - - -def update_roles(schema_facts, cursor, schema, - existing, required, - create_existing, create_required): - for role in set(existing + create_existing) - set(required + create_required): - cursor.execute("drop role {0} cascade".format(role)) - for role in set(create_existing) - set(create_required): - cursor.execute("revoke create on schema {0} from {1}".format(schema, role)) - for role in set(required + create_required) - set(existing + create_existing): - cursor.execute("create role {0}".format(role)) - cursor.execute("grant usage on schema {0} to {1}".format(schema, role)) - for role in set(create_required) - set(create_existing): - cursor.execute("grant create on schema {0} to {1}".format(schema, role)) - - -def check(schema_facts, schema, usage_roles, create_roles, owner): - schema_key = schema.lower() - if schema_key not in schema_facts: - return False - if owner and owner.lower() == schema_facts[schema_key]['owner'].lower(): - return False - if sorted(usage_roles) != sorted(schema_facts[schema_key]['usage_roles']): - return False - if sorted(create_roles) != sorted(schema_facts[schema_key]['create_roles']): - return False - return True - - -def present(schema_facts, cursor, schema, usage_roles, create_roles, owner): - schema_key = schema.lower() - if schema_key not in schema_facts: - query_fragments = ["create schema {0}".format(schema)] - if owner: - query_fragments.append("authorization {0}".format(owner)) - cursor.execute(' '.join(query_fragments)) - update_roles(schema_facts, cursor, schema, [], usage_roles, [], create_roles) - schema_facts.update(get_schema_facts(cursor, schema)) - return True - else: - changed = False - if owner and owner.lower() != schema_facts[schema_key]['owner'].lower(): - raise NotSupportedError(( - "Changing schema owner is not supported. " - "Current owner: {0}." - ).format(schema_facts[schema_key]['owner'])) - if sorted(usage_roles) != sorted(schema_facts[schema_key]['usage_roles']) or \ - sorted(create_roles) != sorted(schema_facts[schema_key]['create_roles']): - - update_roles(schema_facts, cursor, schema, - schema_facts[schema_key]['usage_roles'], usage_roles, - schema_facts[schema_key]['create_roles'], create_roles) - changed = True - if changed: - schema_facts.update(get_schema_facts(cursor, schema)) - return changed - - -def absent(schema_facts, cursor, schema, usage_roles, create_roles): - schema_key = schema.lower() - if schema_key in schema_facts: - update_roles(schema_facts, cursor, schema, - schema_facts[schema_key]['usage_roles'], [], schema_facts[schema_key]['create_roles'], []) - try: - cursor.execute("drop schema {0} restrict".format(schema_facts[schema_key]['name'])) - except pyodbc.Error: - raise CannotDropError("Dropping schema failed due to dependencies.") - del schema_facts[schema_key] - return True - else: - return False - -# module logic - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - schema=dict(required=True, aliases=['name']), - usage_roles=dict(default=None, aliases=['usage_role']), - create_roles=dict(default=None, aliases=['create_role']), - owner=dict(default=None), - state=dict(default='present', choices=['absent', 'present']), - db=dict(default=None), - cluster=dict(default='localhost'), - port=dict(default='5433'), - login_user=dict(default='dbadmin'), - login_password=dict(default=None, no_log=True), - ), supports_check_mode=True) - - if not pyodbc_found: - module.fail_json(msg=missing_required_lib('pyodbc'), exception=PYODBC_IMP_ERR) - - schema = module.params['schema'] - usage_roles = [] - if module.params['usage_roles']: - usage_roles = module.params['usage_roles'].split(',') - usage_roles = filter(None, usage_roles) - create_roles = [] - if module.params['create_roles']: - create_roles = module.params['create_roles'].split(',') - create_roles = filter(None, create_roles) - owner = module.params['owner'] - state = module.params['state'] - db = '' - if module.params['db']: - db = module.params['db'] - - changed = False - - try: - dsn = ( - "Driver=Vertica;" - "Server={0};" - "Port={1};" - "Database={2};" - "User={3};" - "Password={4};" - "ConnectionLoadBalance={5}" - ).format(module.params['cluster'], module.params['port'], db, - module.params['login_user'], module.params['login_password'], 'true') - db_conn = pyodbc.connect(dsn, autocommit=True) - cursor = db_conn.cursor() - except Exception as e: - module.fail_json(msg="Unable to connect to database: {0}.".format(to_native(e))) - - try: - schema_facts = get_schema_facts(cursor) - if module.check_mode: - changed = not check(schema_facts, schema, usage_roles, create_roles, owner) - elif state == 'absent': - try: - changed = absent(schema_facts, cursor, schema, usage_roles, create_roles) - except pyodbc.Error as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - elif state == 'present': - try: - changed = present(schema_facts, cursor, schema, usage_roles, create_roles, owner) - except pyodbc.Error as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - except NotSupportedError as e: - module.fail_json(msg=to_native(e), ansible_facts={'vertica_schemas': schema_facts}) - except CannotDropError as e: - module.fail_json(msg=to_native(e), ansible_facts={'vertica_schemas': schema_facts}) - except SystemExit: - # avoid catching this on python 2.4 - raise - except Exception as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - module.exit_json(changed=changed, schema=schema, ansible_facts={'vertica_schemas': schema_facts}) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/database/vertica/vertica_user.py b/lib/ansible/modules/database/vertica/vertica_user.py deleted file mode 100644 index 53202537ce..0000000000 --- a/lib/ansible/modules/database/vertica/vertica_user.py +++ /dev/null @@ -1,379 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: vertica_user -version_added: '2.0' -short_description: Adds or removes Vertica database users and assigns roles. -description: - - Adds or removes Vertica database user and, optionally, assigns roles. - - A user will not be removed until all the dependencies have been dropped. - - In such a situation, if the module tries to remove the user it - will fail and only remove roles granted to the user. -options: - name: - description: - - Name of the user to add or remove. - required: true - profile: - description: - - Sets the user's profile. - resource_pool: - description: - - Sets the user's resource pool. - password: - description: - - The user's password encrypted by the MD5 algorithm. - - The password must be generated with the format C("md5" + md5[password + username]), - resulting in a total of 35 characters. An easy way to do this is by querying - the Vertica database with select 'md5'||md5('<user_password><user_name>'). - expired: - description: - - Sets the user's password expiration. - type: bool - ldap: - description: - - Set to true if users are authenticated via LDAP. - - The user will be created with password expired and set to I($ldap$). - type: bool - roles: - description: - - Comma separated list of roles to assign to the user. - aliases: ['role'] - state: - description: - - Whether to create C(present), drop C(absent) or lock C(locked) a user. - choices: ['present', 'absent', 'locked'] - default: present - db: - description: - - Name of the Vertica database. - cluster: - description: - - Name of the Vertica cluster. - default: localhost - port: - description: - - Vertica cluster port to connect to. - default: 5433 - login_user: - description: - - The username used to authenticate with. - default: dbadmin - login_password: - description: - - The password used to authenticate with. -notes: - - The default authentication assumes that you are either logging in as or sudo'ing - to the C(dbadmin) account on the host. - - This module uses C(pyodbc), a Python ODBC database adapter. You must ensure - that C(unixODBC) and C(pyodbc) is installed on the host and properly configured. - - Configuring C(unixODBC) for Vertica requires C(Driver = /opt/vertica/lib64/libverticaodbc.so) - to be added to the C(Vertica) section of either C(/etc/odbcinst.ini) or C($HOME/.odbcinst.ini) - and both C(ErrorMessagesPath = /opt/vertica/lib64) and C(DriverManagerEncoding = UTF-16) - to be added to the C(Driver) section of either C(/etc/vertica.ini) or C($HOME/.vertica.ini). -requirements: [ 'unixODBC', 'pyodbc' ] -author: "Dariusz Owczarek (@dareko)" -""" - -EXAMPLES = """ -- name: creating a new vertica user with password - vertica_user: name=user_name password=md5<encrypted_password> db=db_name state=present - -- name: creating a new vertica user authenticated via ldap with roles assigned - vertica_user: - name=user_name - ldap=true - db=db_name - roles=schema_name_ro - state=present -""" -import traceback - -PYODBC_IMP_ERR = None -try: - import pyodbc -except ImportError: - PYODBC_IMP_ERR = traceback.format_exc() - pyodbc_found = False -else: - pyodbc_found = True - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native - - -class NotSupportedError(Exception): - pass - - -class CannotDropError(Exception): - pass - -# module specific functions - - -def get_user_facts(cursor, user=''): - facts = {} - cursor.execute(""" - select u.user_name, u.is_locked, u.lock_time, - p.password, p.acctexpired as is_expired, - u.profile_name, u.resource_pool, - u.all_roles, u.default_roles - from users u join password_auditor p on p.user_id = u.user_id - where not u.is_super_user - and (? = '' or u.user_name ilike ?) - """, user, user) - while True: - rows = cursor.fetchmany(100) - if not rows: - break - for row in rows: - user_key = row.user_name.lower() - facts[user_key] = { - 'name': row.user_name, - 'locked': str(row.is_locked), - 'password': row.password, - 'expired': str(row.is_expired), - 'profile': row.profile_name, - 'resource_pool': row.resource_pool, - 'roles': [], - 'default_roles': []} - if row.is_locked: - facts[user_key]['locked_time'] = str(row.lock_time) - if row.all_roles: - facts[user_key]['roles'] = row.all_roles.replace(' ', '').split(',') - if row.default_roles: - facts[user_key]['default_roles'] = row.default_roles.replace(' ', '').split(',') - return facts - - -def update_roles(user_facts, cursor, user, - existing_all, existing_default, required): - del_roles = list(set(existing_all) - set(required)) - if del_roles: - cursor.execute("revoke {0} from {1}".format(','.join(del_roles), user)) - new_roles = list(set(required) - set(existing_all)) - if new_roles: - cursor.execute("grant {0} to {1}".format(','.join(new_roles), user)) - if required: - cursor.execute("alter user {0} default role {1}".format(user, ','.join(required))) - - -def check(user_facts, user, profile, resource_pool, - locked, password, expired, ldap, roles): - user_key = user.lower() - if user_key not in user_facts: - return False - if profile and profile != user_facts[user_key]['profile']: - return False - if resource_pool and resource_pool != user_facts[user_key]['resource_pool']: - return False - if locked != (user_facts[user_key]['locked'] == 'True'): - return False - if password and password != user_facts[user_key]['password']: - return False - if (expired is not None and expired != (user_facts[user_key]['expired'] == 'True') or - ldap is not None and ldap != (user_facts[user_key]['expired'] == 'True')): - return False - if roles and (sorted(roles) != sorted(user_facts[user_key]['roles']) or - sorted(roles) != sorted(user_facts[user_key]['default_roles'])): - return False - return True - - -def present(user_facts, cursor, user, profile, resource_pool, - locked, password, expired, ldap, roles): - user_key = user.lower() - if user_key not in user_facts: - query_fragments = ["create user {0}".format(user)] - if locked: - query_fragments.append("account lock") - if password or ldap: - if password: - query_fragments.append("identified by '{0}'".format(password)) - else: - query_fragments.append("identified by '$ldap$'") - if expired or ldap: - query_fragments.append("password expire") - if profile: - query_fragments.append("profile {0}".format(profile)) - if resource_pool: - query_fragments.append("resource pool {0}".format(resource_pool)) - cursor.execute(' '.join(query_fragments)) - if resource_pool and resource_pool != 'general': - cursor.execute("grant usage on resource pool {0} to {1}".format( - resource_pool, user)) - update_roles(user_facts, cursor, user, [], [], roles) - user_facts.update(get_user_facts(cursor, user)) - return True - else: - changed = False - query_fragments = ["alter user {0}".format(user)] - if locked is not None and locked != (user_facts[user_key]['locked'] == 'True'): - if locked: - state = 'lock' - else: - state = 'unlock' - query_fragments.append("account {0}".format(state)) - changed = True - if password and password != user_facts[user_key]['password']: - query_fragments.append("identified by '{0}'".format(password)) - changed = True - if ldap: - if ldap != (user_facts[user_key]['expired'] == 'True'): - query_fragments.append("password expire") - changed = True - elif expired is not None and expired != (user_facts[user_key]['expired'] == 'True'): - if expired: - query_fragments.append("password expire") - changed = True - else: - raise NotSupportedError("Unexpiring user password is not supported.") - if profile and profile != user_facts[user_key]['profile']: - query_fragments.append("profile {0}".format(profile)) - changed = True - if resource_pool and resource_pool != user_facts[user_key]['resource_pool']: - query_fragments.append("resource pool {0}".format(resource_pool)) - if user_facts[user_key]['resource_pool'] != 'general': - cursor.execute("revoke usage on resource pool {0} from {1}".format( - user_facts[user_key]['resource_pool'], user)) - if resource_pool != 'general': - cursor.execute("grant usage on resource pool {0} to {1}".format( - resource_pool, user)) - changed = True - if changed: - cursor.execute(' '.join(query_fragments)) - if roles and (sorted(roles) != sorted(user_facts[user_key]['roles']) or - sorted(roles) != sorted(user_facts[user_key]['default_roles'])): - update_roles(user_facts, cursor, user, - user_facts[user_key]['roles'], user_facts[user_key]['default_roles'], roles) - changed = True - if changed: - user_facts.update(get_user_facts(cursor, user)) - return changed - - -def absent(user_facts, cursor, user, roles): - user_key = user.lower() - if user_key in user_facts: - update_roles(user_facts, cursor, user, - user_facts[user_key]['roles'], user_facts[user_key]['default_roles'], []) - try: - cursor.execute("drop user {0}".format(user_facts[user_key]['name'])) - except pyodbc.Error: - raise CannotDropError("Dropping user failed due to dependencies.") - del user_facts[user_key] - return True - else: - return False - -# module logic - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - user=dict(required=True, aliases=['name']), - profile=dict(default=None), - resource_pool=dict(default=None), - password=dict(default=None, no_log=True), - expired=dict(type='bool', default=None), - ldap=dict(type='bool', default=None), - roles=dict(default=None, aliases=['role']), - state=dict(default='present', choices=['absent', 'present', 'locked']), - db=dict(default=None), - cluster=dict(default='localhost'), - port=dict(default='5433'), - login_user=dict(default='dbadmin'), - login_password=dict(default=None, no_log=True), - ), supports_check_mode=True) - - if not pyodbc_found: - module.fail_json(msg=missing_required_lib('pyodbc'), exception=PYODBC_IMP_ERR) - - user = module.params['user'] - profile = module.params['profile'] - if profile: - profile = profile.lower() - resource_pool = module.params['resource_pool'] - if resource_pool: - resource_pool = resource_pool.lower() - password = module.params['password'] - expired = module.params['expired'] - ldap = module.params['ldap'] - roles = [] - if module.params['roles']: - roles = module.params['roles'].split(',') - roles = filter(None, roles) - state = module.params['state'] - if state == 'locked': - locked = True - else: - locked = False - db = '' - if module.params['db']: - db = module.params['db'] - - changed = False - - try: - dsn = ( - "Driver=Vertica;" - "Server={0};" - "Port={1};" - "Database={2};" - "User={3};" - "Password={4};" - "ConnectionLoadBalance={5}" - ).format(module.params['cluster'], module.params['port'], db, - module.params['login_user'], module.params['login_password'], 'true') - db_conn = pyodbc.connect(dsn, autocommit=True) - cursor = db_conn.cursor() - except Exception as e: - module.fail_json(msg="Unable to connect to database: {0}.".format(e)) - - try: - user_facts = get_user_facts(cursor) - if module.check_mode: - changed = not check(user_facts, user, profile, resource_pool, - locked, password, expired, ldap, roles) - elif state == 'absent': - try: - changed = absent(user_facts, cursor, user, roles) - except pyodbc.Error as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - elif state in ['present', 'locked']: - try: - changed = present(user_facts, cursor, user, profile, resource_pool, - locked, password, expired, ldap, roles) - except pyodbc.Error as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - except NotSupportedError as e: - module.fail_json(msg=to_native(e), ansible_facts={'vertica_users': user_facts}) - except CannotDropError as e: - module.fail_json(msg=to_native(e), ansible_facts={'vertica_users': user_facts}) - except SystemExit: - # avoid catching this on python 2.4 - raise - except Exception as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - module.exit_json(changed=changed, user=user, ansible_facts={'vertica_users': user_facts}) - - -if __name__ == '__main__': - main() |