diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/proxysql/plugins | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/proxysql/plugins')
12 files changed, 3839 insertions, 0 deletions
diff --git a/ansible_collections/community/proxysql/plugins/README.md b/ansible_collections/community/proxysql/plugins/README.md new file mode 100644 index 000000000..6541cf7cf --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/README.md @@ -0,0 +1,31 @@ +# Collections Plugins Directory + +This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that +is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that +would contain module utils and modules respectively. + +Here is an example directory of the majority of plugins currently supported by Ansible: + +``` +└── plugins + ├── action + ├── become + ├── cache + ├── callback + ├── cliconf + ├── connection + ├── filter + ├── httpapi + ├── inventory + ├── lookup + ├── module_utils + ├── modules + ├── netconf + ├── shell + ├── strategy + ├── terminal + ├── test + └── vars +``` + +A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible/2.9/plugins/plugins.html).
\ No newline at end of file diff --git a/ansible_collections/community/proxysql/plugins/doc_fragments/proxysql.py b/ansible_collections/community/proxysql/plugins/doc_fragments/proxysql.py new file mode 100644 index 000000000..81c22815b --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/doc_fragments/proxysql.py @@ -0,0 +1,61 @@ +# -*- 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 + + +class ModuleDocFragment(object): + + # Documentation fragment for ProxySQL connectivity + CONNECTIVITY = r''' +options: + login_user: + description: + - The username used to authenticate to ProxySQL admin interface. + type: str + login_password: + description: + - The password used to authenticate to ProxySQL admin interface. + type: str + login_host: + description: + - The host used to connect to ProxySQL admin interface. + type: str + default: '127.0.0.1' + login_unix_socket: + description: + - The socket used to connect to ProxySQL admin interface. + type: str + login_port: + description: + - The port used to connect to ProxySQL admin interface. + type: int + default: 6032 + config_file: + description: + - Specify a config file from which I(login_user) and I(login_password) + are to be read. + type: path + default: '' +requirements: + - PyMySQL + - mysqlclient +''' + + # Documentation fragment for managing ProxySQL configuration + MANAGING_CONFIG = r''' +options: + save_to_disk: + description: + - Save config to sqlite db on disk to persist the configuration. + type: bool + default: true + load_to_runtime: + description: + - Dynamically load config to runtime memory. + type: bool + default: true +''' diff --git a/ansible_collections/community/proxysql/plugins/module_utils/mysql.py b/ansible_collections/community/proxysql/plugins/module_utils/mysql.py new file mode 100644 index 000000000..ce581dd2f --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/module_utils/mysql.py @@ -0,0 +1,170 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c), Jonathan Mainguy <jon@soh.re>, 2015 +# Most of this was originally added by Sven Schliesing @muffl0n in the mysql_user.py module +# +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os + +from ansible.module_utils.six.moves import configparser +from ansible.module_utils.basic import missing_required_lib + +MYSQL_IMP_ERR = None +try: + import pymysql as mysql_driver + _mysql_cursor_param = 'cursor' + HAS_MYSQL_PACKAGE = True +except ImportError: + try: + import MySQLdb as mysql_driver + import MySQLdb.cursors + _mysql_cursor_param = 'cursorclass' + HAS_MYSQL_PACKAGE = True + except ImportError: + MYSQL_IMP_ERR = 'Cannot find PyMySQL or mysqlclient library.' + HAS_MYSQL_PACKAGE = False + mysql_driver = None + + +def parse_from_mysql_config_file(cnf): + cp = configparser.ConfigParser() + cp.read(cnf) + return cp + + +def _version(cursor): + cursor.execute("select version();") + res = cursor.fetchone() + + # 2.2.0-72-ge14accd + # 2.3.2-percona-1.1 + raw_version = res.get('version()').split('-', 1) + _version = raw_version[0].split('.') + + version = dict() + version['full'] = res.get('version()') + version['major'] = int(_version[0]) + version['minor'] = int(_version[1]) + version['release'] = int(_version[2]) + version['suffix'] = raw_version[1] + + return version + + +def mysql_connect(module, login_user=None, login_password=None, config_file='', ssl_cert=None, + ssl_key=None, ssl_ca=None, db=None, cursor_class=None, + connect_timeout=30, autocommit=False, config_overrides_defaults=False): + config = {} + + if not HAS_MYSQL_PACKAGE: + module.fail_json(msg=missing_required_lib("pymysql or MySQLdb"), exception=MYSQL_IMP_ERR) + + 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 config_file and os.path.exists(config_file): + config['read_default_file'] = config_file + cp = parse_from_mysql_config_file(config_file) + # Override some commond defaults with values from config file if needed + if cp and cp.has_section('client') and config_overrides_defaults: + try: + module.params['login_host'] = cp.get('client', 'host', fallback=module.params['login_host']) + module.params['login_port'] = cp.getint('client', 'port', fallback=module.params['login_port']) + except Exception as e: + if "got an unexpected keyword argument 'fallback'" in e.message: + module.fail_json('To use config_overrides_defaults, ' + 'it needs Python 3.5+ as the default interpreter on a target host') + + if ssl_ca is not None or ssl_key is not None or ssl_cert is not None: + config['ssl'] = {} + + if module.params['login_unix_socket']: + config['unix_socket'] = module.params['login_unix_socket'] + else: + config['host'] = module.params['login_host'] + config['port'] = module.params['login_port'] + + # If login_user or login_password are given, they should override the + # config file + if login_user is not None: + config['user'] = login_user + if login_password is not None: + config['passwd'] = login_password + if ssl_cert is not None: + config['ssl']['cert'] = ssl_cert + if ssl_key is not None: + config['ssl']['key'] = ssl_key + if ssl_ca is not None: + config['ssl']['ca'] = ssl_ca + if db is not None: + config['db'] = db + if connect_timeout is not None: + config['connect_timeout'] = connect_timeout + + if _mysql_cursor_param == 'cursor': + # In case of PyMySQL driver: + db_connection = mysql_driver.connect(autocommit=autocommit, **config) + else: + # In case of MySQLdb driver + db_connection = mysql_driver.connect(**config) + if autocommit: + db_connection.autocommit(True) + + version = _version(db_connection.cursor(**{_mysql_cursor_param: mysql_driver.cursors.DictCursor})) + + if cursor_class == 'DictCursor': + return (db_connection.cursor(**{_mysql_cursor_param: mysql_driver.cursors.DictCursor}), + db_connection, + version) + else: + return (db_connection.cursor(), + db_connection, + version) + + +def proxysql_common_argument_spec(): + return dict( + login_user=dict(type='str', default=None), + login_password=dict(type='str', no_log=True), + login_host=dict(type='str', default='127.0.0.1'), + login_port=dict(type='int', default=6032), + login_unix_socket=dict(type='str'), + config_file=dict(type='path', default=''), + ) + + +def save_config_to_disk(cursor, save_what, variable=None): + if variable and variable.startswith("admin"): + config_type = "ADMIN" + elif save_what == "SCHEDULER": + config_type = "" + else: + config_type = "MYSQL" + + cursor.execute("SAVE {0} {1} TO DISK".format(config_type, save_what)) + + return True + + +def load_config_to_runtime(cursor, save_what, variable=None): + if variable and variable.startswith("admin"): + config_type = "ADMIN" + elif save_what == "SCHEDULER": + config_type = "" + else: + config_type = "MYSQL" + + cursor.execute("LOAD {0} {1} TO RUNTIME".format(config_type, save_what)) + + return True diff --git a/ansible_collections/community/proxysql/plugins/modules/proxysql_backend_servers.py b/ansible_collections/community/proxysql/plugins/modules/proxysql_backend_servers.py new file mode 100644 index 000000000..89be2edff --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/modules/proxysql_backend_servers.py @@ -0,0 +1,503 @@ +#!/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 +author: "Ben Mildren (@bmildren)" +short_description: Adds or removes mysql hosts from proxysql admin interface +description: + - The M(community.proxysql.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. + type: int + default: 0 + hostname: + description: + - The ip address at which the mysqld instance can be contacted. + type: str + required: true + port: + description: + - The port at which the mysqld instance can be contacted. + type: int + 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). + type: str + 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. + type: int + 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. + type: int + 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. + type: int + 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. + type: int + 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. + type: int + 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. + type: str + default: '' + state: + description: + - When C(present) - adds the host, when C(absent) - removes the host. + type: str + choices: [ "present", "absent" ] + default: present +extends_documentation_fragment: +- community.proxysql.proxysql.managing_config +- community.proxysql.proxysql.connectivity +notes: +- Supports C(check_mode). +''' + +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 community.general.proxysql_manage_config +# module). It uses supplied credentials to connect to the proxysql admin +# interface. + +- name: Add a server + community.proxysql.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. + +- name: Remove a server + community.proxysql.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" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.proxysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + proxysql_common_argument_spec, + save_config_to_disk, + load_config_to_runtime, +) +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["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" + ) + + +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, "SERVERS") + if self.load_to_runtime: + load_config_to_runtime(cursor, "SERVERS") + + 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(): + argument_spec = proxysql_common_argument_spec() + argument_spec.update( + 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') + ) + + module = AnsibleModule( + supports_check_mode=True, + argument_spec=argument_spec + ) + + 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, version = 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/ansible_collections/community/proxysql/plugins/modules/proxysql_global_variables.py b/ansible_collections/community/proxysql/plugins/modules/proxysql_global_variables.py new file mode 100644 index 000000000..9996041ed --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/modules/proxysql_global_variables.py @@ -0,0 +1,244 @@ +#!/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 +author: "Ben Mildren (@bmildren)" +short_description: Gets or sets the proxysql global variables +description: + - The M(community.proxysql.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. + type: str + required: true + value: + description: + - Defines a value the variable specified using I(variable) should be set + to. + type: str +extends_documentation_fragment: +- community.proxysql.proxysql.managing_config +- community.proxysql.proxysql.connectivity +notes: +- Supports C(check_mode). +''' + +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. + +- name: Set the value of a variable + community.proxysql.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. + +- name: Get the value of a variable + community.proxysql.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" + } + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.proxysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + proxysql_common_argument_spec, + save_config_to_disk, + load_config_to_runtime, +) +from ansible.module_utils._text import to_native + +# =========================================== +# proxysql module specific support methods. +# + + +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(cursor, "VARIABLES", variable) + if load_to_runtime: + load_config_to_runtime(cursor, "VARIABLES", variable) + +# =========================================== +# Module execution. +# + + +def main(): + argument_spec = proxysql_common_argument_spec() + argument_spec.update( + variable=dict(required=True, type='str'), + value=dict(), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ) + + module = AnsibleModule( + supports_check_mode=True, + argument_spec=argument_spec + ) + + 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, version = 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/ansible_collections/community/proxysql/plugins/modules/proxysql_info.py b/ansible_collections/community/proxysql/plugins/modules/proxysql_info.py new file mode 100644 index 000000000..a6088b151 --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/modules/proxysql_info.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, 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_info +author: "Markus Bergholz (@markuman)" +short_description: Gathers information about proxysql server +description: + - Gathers information about proxysql server. + - Caution. The number of tables that returns, depends on the underlying proyxsql server version. +version_added: '1.2.0' +extends_documentation_fragment: + - community.proxysql.proxysql.connectivity +notes: + - Supports C(check_mode). +''' + +EXAMPLES = ''' +- name: Receive information about proxysql setup + community.proxysql.proxysql_info: + login_user: admin + login_password: admin +''' + +RETURN = ''' +stdout: + description: The number of tables that returns, depends on the underlying proyxsql server version. + returned: Always + type: dict + sample: + changed: false + failed: false + version: + description: Version of proxysql. + sample: + full: 2.1.1-40-g1c2b7e4 + major: 2 + minor: 1 + release: 1 + suffix: 40 + type: dict + returned: Always + tables: + description: List of tables that exist in the requested proxysql version. + sample: + - global_variables + - mysql_aws_aurora_hostgroups + - mysql_collations + - mysql_firewall_whitelist_rules + - mysql_firewall_whitelist_sqli_fingerprints + - mysql_firewall_whitelist_users + - mysql_galera_hostgroups + - mysql_group_replication_hostgroups + - mysql_query_rules + - mysql_query_rules_fast_routing + - mysql_replication_hostgroups + - mysql_servers + - mysql_users + - proxysql_servers + - restapi_routes + - runtime_checksums_values + - runtime_global_variables + - runtime_mysql_aws_aurora_hostgroups + - runtime_mysql_firewall_whitelist_rules + - runtime_mysql_firewall_whitelist_sqli_fingerprints + - runtime_mysql_firewall_whitelist_users + - runtime_mysql_galera_hostgroups + - runtime_mysql_group_replication_hostgroups + - runtime_mysql_query_rules + - runtime_mysql_query_rules_fast_routing + - runtime_mysql_replication_hostgroups + - runtime_mysql_servers + - runtime_mysql_users + - runtime_proxysql_servers + - runtime_restapi_routes + - runtime_scheduler + - scheduler + type: list + returned: Always + global_variables: + description: Global variables of requested proxysql. + type: dict + returned: Always +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.proxysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + proxysql_common_argument_spec, +) +from ansible.module_utils._text import to_native + +# =========================================== +# proxysql module specific support methods. +# + + +def get_tables(cursor): + result = dict() + tables = list() + + cursor.execute("show tables") + + for table in cursor.fetchall(): + tables.append(table.get('tables')) + result['tables'] = tables + + for table in result.get('tables'): + cursor.execute("select * from {table}".format(table=table)) + + if 'global_variables' in table: + result[table] = dict() + for item in cursor.fetchall(): + result[table][item.get('variable_name')] = item.get('variable_value') + + else: + result[table] = cursor.fetchall() + + return result + + +def main(): + module = AnsibleModule( + supports_check_mode=True, + argument_spec=proxysql_common_argument_spec() + ) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor, db_conn, version = 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 = get_tables(cursor) + + result['version'] = version + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/proxysql/plugins/modules/proxysql_manage_config.py b/ansible_collections/community/proxysql/plugins/modules/proxysql_manage_config.py new file mode 100644 index 000000000..ead810e3c --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/modules/proxysql_manage_config.py @@ -0,0 +1,212 @@ +#!/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 + +author: "Ben Mildren (@bmildren)" +short_description: Writes the proxysql configuration settings between layers +description: + - The M(community.proxysql.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). + type: str + choices: [ "LOAD", "SAVE" ] + required: true + config_settings: + description: + - The I(config_settings) specifies which configuration we're writing. + type: str + 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)." + type: str + 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. + type: str + choices: [ "MEMORY", "DISK", "RUNTIME", "CONFIG" ] + required: true +extends_documentation_fragment: +- community.proxysql.proxysql.connectivity +notes: +- Supports C(check_mode). +''' + +EXAMPLES = ''' +--- +# This example saves the mysql users config from memory to disk. It uses +# supplied credentials to connect to the proxysql admin interface. + +- name: Save the mysql users config from memory to disk + community.proxysql.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 runtime. It +# uses supplied credentials to connect to the proxysql admin interface. + +- name: Load the mysql query rules config from memory to runtime + community.proxysql.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 + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.proxysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + proxysql_common_argument_spec +) +from ansible.module_utils._text import to_native + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + 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"]) + + +def manage_config(manage_config_settings, cursor, check_mode): + + if not check_mode: + query_string = "%s" % ' '.join(manage_config_settings) + cursor.execute(query_string) + + return True + +# =========================================== +# Module execution. +# + + +def main(): + argument_spec = proxysql_common_argument_spec() + argument_spec.update( + 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']) + ) + + module = AnsibleModule( + supports_check_mode=True, + argument_spec=argument_spec + ) + + 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, version = 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, module.check_mode) + 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/ansible_collections/community/proxysql/plugins/modules/proxysql_mysql_users.py b/ansible_collections/community/proxysql/plugins/modules/proxysql_mysql_users.py new file mode 100644 index 000000000..417172d2d --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/modules/proxysql_mysql_users.py @@ -0,0 +1,497 @@ +#!/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 +author: "Ben Mildren (@bmildren)" +short_description: Adds or removes mysql users from proxysql admin interface +description: + - The M(community.proxysql.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. + type: str + required: true + password: + description: + - Password of the user connecting to the mysqld or ProxySQL instance. + type: str + encrypt_password: + description: + - Encrypt a cleartext password passed in the I(password) option, using + the method defined in I(encryption_method). + default: false + type: bool + encryption_method: + description: + - Encryption method used when I(encrypt_password) is set to C(True). + type: str + choices: [ "mysql_native_password" ] + default: mysql_native_password + 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. + type: int + default_schema: + description: + - The schema to which the connection should change to by default. + type: str + 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. + type: int + state: + description: + - When C(present) - adds the user, when C(absent) - removes the user. + type: str + choices: [ "present", "absent" ] + default: present +extends_documentation_fragment: +- community.proxysql.proxysql.managing_config +- community.proxysql.proxysql.connectivity +notes: +- Supports C(check_mode). +''' + +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 community.general.proxysql_manage_config +# module). It uses supplied credentials to connect to the proxysql admin +# interface. + +- name: Add a user + community.proxysql.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. + +- name: Remove a user + community.proxysql.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 +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.proxysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + proxysql_common_argument_spec, + save_config_to_disk, + load_config_to_runtime, +) +from ansible.module_utils.six import iteritems +from ansible.module_utils._text import to_native, to_bytes +from hashlib import sha1 + +# =========================================== +# proxysql module specific support methods. +# + + +def _mysql_native_password(cleartext_password): + mysql_native_encrypted_password = "*" + sha1(sha1(to_bytes(cleartext_password)).digest()).hexdigest().upper() + return mysql_native_encrypted_password + + +def encrypt_cleartext_password(password_to_encrypt, encryption_method): + encrypted_password = encryption_method(password_to_encrypt) + return encrypted_password + + +encryption_method_map = { + 'mysql_native_password': _mysql_native_password +} + + +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) + + if module.params["password"] is not None and module.params["encrypt_password"]: + encryption_method = encryption_method_map[module.params["encryption_method"]] + encrypted_password = encrypt_cleartext_password(module.params["password"], encryption_method) + self.config_data["password"] = encrypted_password + + 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, "USERS") + if self.load_to_runtime: + load_config_to_runtime(cursor, "USERS") + + 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(): + argument_spec = proxysql_common_argument_spec() + argument_spec.update( + username=dict(required=True, type='str'), + password=dict(no_log=True, type='str'), + encrypt_password=dict(default=False, type='bool', no_log=False), + encryption_method=dict(default='mysql_native_password', choices=list(encryption_method_map.keys())), + 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') + ) + + module = AnsibleModule( + supports_check_mode=True, + argument_spec=argument_spec + ) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor, db_conn, version = 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/ansible_collections/community/proxysql/plugins/modules/proxysql_query_rules.py b/ansible_collections/community/proxysql/plugins/modules/proxysql_query_rules.py new file mode 100644 index 000000000..f03456f98 --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/modules/proxysql_query_rules.py @@ -0,0 +1,735 @@ +#!/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 +author: "Ben Mildren (@bmildren)" +short_description: Modifies query rules using the proxysql admin interface +description: + - The M(community.proxysql.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. + type: int + 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. + type: str + 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. + type: str + flagIN: + description: + - Used in combination with I(flagOUT) and I(apply) to create chains of + rules. + type: int + client_addr: + description: + - Match traffic from a specific source. + type: str + proxy_addr: + description: + - Match incoming traffic on a specific local IP. + type: str + proxy_port: + description: + - Match incoming traffic on a specific local port. + type: int + digest: + description: + - Match queries with a specific digest, as returned by + stats_mysql_query_digest.digest. + type: str + match_digest: + description: + - Regular expression that matches the query digest. The dialect of + regular expressions used is that of re2 - U(https://github.com/google/re2). + type: str + match_pattern: + description: + - Regular expression that matches the query text. The dialect of regular + expressions used is that of re2 - U(https://github.com/google/re2). + type: str + 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 + re_modifiers: + description: + - Comma separated list of options to modify the behavior of the RE engine. + With C(CASELESS) the match is case insensitive. With C(GLOBAL) the replace + is global (replaces all matches and not just the first). + For backward compatibility, only C(CASELESS) is the enabled by default. + type: str + version_added: "1.3.0" + 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. + type: int + 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. + type: str + 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(community.proxysql.proxysql_mysql_users)). + type: int + 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. + type: int + cache_empty_result: + description: + - Controls if resultset without rows will be cached or not. + type: bool + version_added: '1.1.0' + multiplex: + description: + - If C(0), multiplex will be disabled. + - If C(1), try to enable multiplex. There can be other conditions preventing this (for example, user variables or transactions). + - If C(2), multiplexing is not disabled for just the current query. + - By default, does not change multiplexing policies. + type: int + choices: [0, 1, 2] + version_added: '1.1.0' + 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. + type: int + 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. + type: int + 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. + type: int + next_query_flagIN: + description: + - When is set, its value will become the I(flagIN) value for the next queries. + type: int + version_added: "1.3.0" + 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. + type: int + mirror_hostgroup: + description: + - Enables query mirroring. If set I(mirror_hostgroup) can be used to + mirror queries to the same or different hostgroup. + type: int + OK_msg: + description: + - The specified message will be returned for a query that uses the + defined rule. + type: str + version_added: '1.1.0' + error_msg: + description: + - Query will be blocked, and the specified error_msg will be returned to + the client. + type: str + 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. + type: str + state: + description: + - When C(present) - adds the rule, when C(absent) - removes the rule. + type: str + 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 are not concerned about the + schedules deleted, you can set I(force_delete) to C(True). + type: bool + default: false +extends_documentation_fragment: +- community.proxysql.proxysql.managing_config +- community.proxysql.proxysql.connectivity +notes: +- Supports C(check_mode). +''' + +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 community.general.proxysql_manage_config module). It +# uses supplied credentials to connect to the proxysql admin interface. + +- name: Add a rule + community.proxysql.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 demonstrates the situation, if your application tries to set a +# variable that will disable multiplexing, and you think it can be filtered out, +# you can create a filter that returns OK without executing the request. + +- name: Add a filter rule + community.proxysql.proxysql_query_rules: + login_user: admin + login_password: admin + match_digest: '^SET @@wait_timeout = ?' + active: 1 + OK_msg: 'The wait_timeout variable is ignored' + +# This example adds a caching rule for a query that matches the digest. +# The query digest can be obtained from the `stats_mysql_query_digest` +# table. `cache_ttl` is specified in milliseconds. Empty responses are +# not cached. + +- name: Add a cache rule + community.proxysql.proxysql_query_rules: + login_user: admin + login_password: admin + rule_id: 1 + digest: 0xECA450EA500A9A55 + cache_ttl: 30000 + cache_empty_result: false + destination_hostgroup: 1 + active: true + state: present + save_to_disk: true + load_to_runtime: true + +# This example demonstrates how to prevent disabling multiplexing for +# situations where a request contains @. + +- name: Add a rule with multiplex + community.proxysql.proxysql_query_rules: + login_user: admin + login_password: admin + rule_id: 1 + active: 1 + match_digest: '^SELECT @@max_allowed_packet' + multiplex: 2 + +# This example demonstrates how to use next_query_flagIN argument. It allows +# ProxySQL query rules to be chained. The examples shows how you can have SELECTS +# immediately follow INSERT/UPDATE/DELETE statements to query the primary hostgroup +# and avoid replication lag + +- name: Add insert query rule + proxysql_query_rules: + match_digest: "^INSERT" + destination_hostgroup: 1, + next_query_flagIN: 1 + +- name: Add update query rule + proxysql_query_rules: + match_digest: "^UPDATE" + destination_hostgroup: 1, + next_query_flagIN: 1 + +- name: Add delete query rules + proxysql_query_rules: + match_digest: "^DELETE" + destination_hostgroup: 1, + next_query_flagIN: 1 + +- name: Add insert query rules + proxysql_query_rules: + match_digest: ".*" + destination_hostgroup: 1, + next_query_flagIN: 1 + comment: Match every queries after an INSERT/UPDATE/DELETE query + +# 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. + +- name: Remove rules + community.proxysql.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, + "cache_empty_result": null, + "multiplex": null, + "client_addr": null, + "comment": null, + "delay": null, + "destination_hostgroup": 1, + "digest": null, + "OK_msg": 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" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.proxysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + proxysql_common_argument_spec, + save_config_to_disk, + load_config_to_runtime, +) +from ansible.module_utils.six import iteritems +from ansible.module_utils._text import to_native + +# =========================================== +# proxysql module specific support methods. +# + + +class ProxyQueryRule(object): + + def __init__(self, module, version): + 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", + "re_modifiers", + "flagOUT", + "replace_pattern", + "destination_hostgroup", + "cache_ttl", + "multiplex", + "timeout", + "retries", + "delay", + "next_query_flagIN", + "mirror_flagOUT", + "mirror_hostgroup", + "error_msg", + "OK_msg", + "multiplex", + "log", + "apply", + "comment"] + + if version.get('major') >= 2: + config_data_keys.append("cache_empty_result") + + 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, "QUERY RULES") + if self.load_to_runtime: + load_config_to_runtime(cursor, "QUERY RULES") + + 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(): + argument_spec = proxysql_common_argument_spec() + argument_spec.update( + 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'), + re_modifiers=dict(type='str'), + flagOUT=dict(type='int'), + replace_pattern=dict(type='str'), + destination_hostgroup=dict(type='int'), + cache_ttl=dict(type='int'), + cache_empty_result=dict(type='bool'), + multiplex=dict(type='int', choices=[0, 1, 2]), + timeout=dict(type='int'), + retries=dict(type='int'), + delay=dict(type='int'), + next_query_flagIN=dict(type='int'), + mirror_flagOUT=dict(type='int'), + mirror_hostgroup=dict(type='int'), + OK_msg=dict(type='str'), + 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') + ) + + module = AnsibleModule( + supports_check_mode=True, + argument_spec=argument_spec + ) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor, db_conn, version = 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, version) + 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/ansible_collections/community/proxysql/plugins/modules/proxysql_query_rules_fast_routing.py b/ansible_collections/community/proxysql/plugins/modules/proxysql_query_rules_fast_routing.py new file mode 100644 index 000000000..a02f9be7c --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/modules/proxysql_query_rules_fast_routing.py @@ -0,0 +1,420 @@ +#!/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_fast_routing +author: "Akim Lindberg (@akimrx)" +short_description: Modifies query rules for fast routing policies using the proxysql admin interface +description: + - The M(community.proxysql.proxysql_query_rules_fast_routing) module modifies query rules for fast + routing policies and attributes using the proxysql admin interface. +version_added: '1.1.0' +options: + username: + description: + - Filtering criteria matching username, a query will match only if the connection is made with + the correct username. + type: str + required: true + schemaname: + description: + - Filtering criteria matching schemaname, a query will match only if the connection uses + schemaname as its default schema. + type: str + required: true + flagIN: + description: + - Evaluated in the same way as I(flagIN) is in B(mysql_query_rules) and correlates to the + I(flagOUT/apply) specified in the B(mysql_query_rules) table. + (see M(community.proxysql.proxysql_query_rules)). + type: int + default: 0 + 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) (refer to M(community.proxysql.proxysql_mysql_users)). + type: int + required: true + comment: + description: + - Free form text field, usable for a descriptive comment of the query rule. + type: str + default: '' + state: + description: + - When C(present), adds the rule. When C(absent), removes the rule. + type: str + 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 are not concerned about the + schedules deleted, you can set I(force_delete) to C(True). + type: bool + default: false +extends_documentation_fragment: +- community.proxysql.proxysql.managing_config +- community.proxysql.proxysql.connectivity +notes: +- Supports C(check_mode). +''' + +EXAMPLES = ''' +--- +# This example adds a rule for fast routing +- name: Add a rule + community.proxysql.proxysql_query_rules_fast_routing: + login_user: admin + login_password: admin + username: 'user_ro' + schemaname: 'default' + destination_hostgroup: 1 + comment: 'fast route user_ro to default schema' + state: present + save_to_disk: true + load_to_runtime: 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_fast_routing", + "rules": [ + { + "username": "user_ro", + "schemaname": "default", + "destination_hostgroup": 1, + "flagIN": "0", + "comment": "" + } + ], + "state": "present" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.proxysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + proxysql_common_argument_spec, + save_config_to_disk, + load_config_to_runtime, +) +from ansible.module_utils.six import iteritems +from ansible.module_utils._text import to_native + +# =========================================== +# proxysql module specific support methods. +# + + +class ProxyQueryRuleFastRouting(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 = [ + "username", + "schemaname", + "flagIN", + "destination_hostgroup", + "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_fast_routing " + "WHERE username = %s " + "AND schemaname = %s " + "AND flagIN = %s" + ) + + query_data = [ + self.config_data["username"], + self.config_data["schemaname"], + self.config_data["flagIN"], + ] + + 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_fast_routing" + + 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 += " WHERE " + col + " = %s" + else: + query_string += " 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): + query_string = ( + "SELECT * " + "FROM mysql_query_rules_fast_routing " + "WHERE username = %s " + "AND schemaname = %s " + "AND flagIN = %s" + ) + + query_data = [ + self.config_data["username"], + self.config_data["schemaname"], + self.config_data["flagIN"] + ] + + for col, val in iteritems(self.config_data): + if val is not None: + query_data.append(val) + query_string += " AND " + col + " = %s" + + cursor.execute(query_string, query_data) + rule = cursor.fetchall() + return rule + + def create_rule_config(self, cursor): + query_string = "INSERT INTO mysql_query_rules_fast_routing (" + + 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 += col + "," + + query_string = query_string[:-1] + query_string += ") VALUES (" + "%s, " * cols + query_string = query_string[:-2] + query_string += ")" + + cursor.execute(query_string, query_data) + return True + + def update_rule_config(self, cursor): + query_string = "UPDATE mysql_query_rules_fast_routing" + + cols = 0 + query_data = [ + self.config_data["username"], + self.config_data["schemaname"], + self.config_data["flagIN"] + ] + + for col, val in iteritems(self.config_data): + if val is not None and col not in ("username", "schemaname", "flagIN"): + query_data.insert(cols, val) + cols += 1 + if cols == 1: + query_string += " SET " + col + "= %s," + else: + query_string += " " + col + " = %s," + + query_string = query_string[:-1] + query_string += ( + "WHERE username = %s " + "AND schemaname = %s " + "AND flagIN = %s" + ) + + cursor.execute(query_string, query_data) + return True + + def delete_rule_config(self, cursor): + query_string = "DELETE FROM mysql_query_rules_fast_routing" + + 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 += " WHERE " + col + " = %s" + else: + query_string += " 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, changed): + if not changed: + return + + if self.save_to_disk: + save_config_to_disk(cursor, "QUERY RULES") + if self.load_to_runtime: + load_config_to_runtime(cursor, "QUERY RULES") + + def create_rule(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = self.create_rule_config(cursor) + result['msg'] = "Added rule to mysql_query_rules_fast_routing." + self.manage_config(cursor, result['changed']) + result['rules'] = self.get_rule_config(cursor) + else: + result['changed'] = True + result['msg'] = ( + "Rule would have been added to " + "mysql_query_rules_fast_routing, " + "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_fast_routing." + 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_fast_routing, " + "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_fast_routing." + self.manage_config(cursor, result['changed']) + else: + result['changed'] = True + result['msg'] = ( + "Rule would have been deleted from " + "mysql_query_rules_fast_routing, " + "however check_mode is enabled." + ) + +# =========================================== +# Module execution. +# + + +def main(): + argument_spec = proxysql_common_argument_spec() + argument_spec.update( + username=dict(required=True, type='str'), + schemaname=dict(required=True, type='str'), + destination_hostgroup=dict(required=True, type='int'), + flagIN=dict(default=0, type='int'), + comment=dict(default='', 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') + ) + + module = AnsibleModule( + supports_check_mode=True, + argument_spec=argument_spec + ) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor, db_conn, version = 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)) + + query_rule = ProxyQueryRuleFastRouting(module) + result = {} + + result['state'] = query_rule.state + + if query_rule.state == "present": + try: + if not query_rule.check_rule_cfg_exists(cursor): + if query_rule.config_data["username"] and query_rule.config_data["schemaname"] and \ + query_rule.check_rule_pk_exists(cursor): + query_rule.update_rule(module.check_mode, result, cursor) + else: + query_rule.create_rule(module.check_mode, result, cursor) + else: + result['changed'] = False + result['msg'] = ( + "The rule already exists in " + "mysql_query_rules_fast_routing " + "and doesn't need to be updated." + ) + result['rules'] = 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 query_rule.state == "absent": + try: + existing_rules = query_rule.check_rule_cfg_exists(cursor) + if existing_rules > 0: + if existing_rules == 1 or query_rule.force_delete: + 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_fast_routing 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/ansible_collections/community/proxysql/plugins/modules/proxysql_replication_hostgroups.py b/ansible_collections/community/proxysql/plugins/modules/proxysql_replication_hostgroups.py new file mode 100644 index 000000000..9ea04053f --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/modules/proxysql_replication_hostgroups.py @@ -0,0 +1,398 @@ +#!/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 +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. + type: int + required: true + reader_hostgroup: + description: + - Id of the reader hostgroup. + type: int + required: true + comment: + description: + - Text field that can be used for any purposes defined by the user. + type: str + default: "" + state: + description: + - When C(present) - adds the replication hostgroup, when C(absent) - + removes the replication hostgroup. + type: str + choices: [ "present", "absent" ] + default: present + check_type: + description: + - Which check type to use when detecting that the node is a standby. + - Requires proxysql >= 2.0.1. Otherwise it has no effect. + - C(read_only|innodb_read_only) and C(read_only&innodb_read_only) requires proxysql >= 2.0.8. + type: str + choices: [ "read_only", "innodb_read_only", "super_read_only", "read_only|innodb_read_only", "read_only&innodb_read_only" ] + default: read_only + version_added: 1.3.0 +extends_documentation_fragment: +- community.proxysql.proxysql.managing_config +- community.proxysql.proxysql.connectivity +notes: +- Supports C(check_mode). +''' + +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 +# community.general.proxysql_manage_config module). It uses supplied credentials +# to connect to the proxysql admin interface. + +- name: Add a replication hostgroup + community.proxysql.proxysql_replication_hostgroups: + login_user: 'admin' + login_password: 'admin' + writer_hostgroup: 1 + reader_hostgroup: 2 + state: present + load_to_runtime: false + +- name: Change check_type + community.proxysql.proxysql_replication_hostgroups: + login_user: 'admin' + login_password: 'admin' + writer_hostgroup: 1 + reader_hostgroup: 2 + check_type: innodb_read_only + 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. + +- name: Remove a replication hostgroup + community.proxysql.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", + "check_type": "read_only" + }, + "state": "present" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.proxysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + proxysql_common_argument_spec, + save_config_to_disk, + load_config_to_runtime, +) +from ansible.module_utils._text import to_native + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if not module.params["writer_hostgroup"] >= 0: + module.fail_json( + msg="writer_hostgroup must be a integer greater than or equal to 0" + ) + + if module.params["reader_hostgroup"] < 0: + module.fail_json( + msg="reader_hostgroup must be an integer greater than or equal to 0" + ) + + if module.params["reader_hostgroup"] == module.params["writer_hostgroup"]: + module.fail_json( + msg="reader_hostgroup and writer_hostgroup must be different integer values") + + +class ProxySQLReplicationHostgroup(object): + + def __init__(self, module, version): + 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"] + self.check_type = module.params["check_type"] + self.check_type_support = version.get('major') >= 2 + self.check_mode = module.check_mode + + def check_repl_group_config(self, cursor, keys): + query_string = \ + """SELECT count(*) AS `repl_groups` + FROM mysql_replication_hostgroups + WHERE writer_hostgroup = %s""" + + query_data = \ + [self.writer_hostgroup] + + 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""" + + query_data = \ + [self.writer_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) + + if self.check_type_support: + self.update_check_type(cursor) + + return True + + def delete_repl_group_config(self, cursor): + query_string = \ + """DELETE FROM mysql_replication_hostgroups + WHERE writer_hostgroup = %s""" + + query_data = \ + [self.writer_hostgroup] + + cursor.execute(query_string, query_data) + return True + + def manage_config(self, cursor, state): + if state and not self.check_mode: + if self.save_to_disk: + save_config_to_disk(cursor, "SERVERS") + if self.load_to_runtime: + load_config_to_runtime(cursor, "SERVERS") + + def create_repl_group(self, result, cursor): + if not self.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, result, cursor): + current = self.get_repl_group_config(cursor) + + if self.check_type_support and current.get('check_type') != self.check_type: + result['changed'] = True + if not self.check_mode: + result['msg'] = "Updated replication hostgroups" + self.update_check_type(cursor) + else: + result['msg'] = "Updated replication hostgroups in check_mode" + + if current.get('comment') != self.comment: + result['changed'] = True + result['msg'] = "Updated replication hostgroups in check_mode" + if not self.check_mode: + result['msg'] = "Updated replication hostgroups" + self.update_comment(cursor) + + if int(current.get('reader_hostgroup')) != self.reader_hostgroup: + result['changed'] = True + result['msg'] = "Updated replication hostgroups in check_mode" + if not self.check_mode: + result['msg'] = "Updated replication hostgroups" + self.update_reader_hostgroup(cursor) + + result['repl_group'] = self.get_repl_group_config(cursor) + + self.manage_config(cursor, + result['changed']) + + def delete_repl_group(self, result, cursor): + if not self.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.") + + def update_check_type(self, cursor): + try: + query_string = ("UPDATE mysql_replication_hostgroups " + "SET check_type = %s " + "WHERE writer_hostgroup = %s") + + cursor.execute(query_string, (self.check_type, self.writer_hostgroup)) + except Exception as e: + pass + + def update_reader_hostgroup(self, cursor): + query_string = ("UPDATE mysql_replication_hostgroups " + "SET reader_hostgroup = %s " + "WHERE writer_hostgroup = %s") + + cursor.execute(query_string, (self.reader_hostgroup, self.writer_hostgroup)) + + def update_comment(self, cursor): + query_string = ("UPDATE mysql_replication_hostgroups " + "SET comment = %s " + "WHERE writer_hostgroup = %s ") + + cursor.execute(query_string, (self.comment, self.writer_hostgroup)) + + +# =========================================== +# Module execution. +# +def main(): + argument_spec = proxysql_common_argument_spec() + argument_spec.update( + writer_hostgroup=dict(required=True, type='int'), + reader_hostgroup=dict(required=True, type='int'), + check_type=dict(type='str', default='read_only', choices=['read_only', + 'innodb_read_only', + 'super_read_only', + 'read_only|innodb_read_only', + 'read_only&innodb_read_only']), + comment=dict(type='str', default=''), + state=dict(default='present', choices=['present', + 'absent']), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ) + + module = AnsibleModule( + supports_check_mode=True, + argument_spec=argument_spec + ) + + 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, version = 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, version) + result = {} + + result['state'] = proxysql_repl_group.state + result['changed'] = False + + 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(result, + cursor) + else: + proxysql_repl_group.update_repl_group(result, cursor) + + 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(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/ansible_collections/community/proxysql/plugins/modules/proxysql_scheduler.py b/ansible_collections/community/proxysql/plugins/modules/proxysql_scheduler.py new file mode 100644 index 000000000..474ddc0a1 --- /dev/null +++ b/ansible_collections/community/proxysql/plugins/modules/proxysql_scheduler.py @@ -0,0 +1,409 @@ +#!/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 +author: "Ben Mildren (@bmildren)" +short_description: Adds or removes schedules from proxysql admin interface +description: + - The M(community.proxysql.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. + type: bool + default: true + interval_ms: + description: + - How often (in millisecond) the job will be started. The minimum value + for I(interval_ms) is 100 milliseconds. + type: int + default: 10000 + filename: + description: + - Full path of the executable to be executed. + type: str + required: true + arg1: + description: + - Argument that can be passed to the job. + type: str + arg2: + description: + - Argument that can be passed to the job. + type: str + arg3: + description: + - Argument that can be passed to the job. + type: str + arg4: + description: + - Argument that can be passed to the job. + type: str + arg5: + description: + - Argument that can be passed to the job. + type: str + comment: + description: + - Text field that can be used for any purposed defined by the user. + type: str + state: + description: + - When C(present) - adds the schedule, when C(absent) - removes the + schedule. + type: str + 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 are not concerned about the + schedules deleted, you can set I(force_delete) to C(True). + type: bool + default: false +extends_documentation_fragment: +- community.proxysql.proxysql.managing_config +- community.proxysql.proxysql.connectivity +notes: +- Supports C(check_mode). +''' + +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 community.general.proxysql_manage_config +# module). It uses supplied credentials to connect to the proxysql admin +# interface. + +- name: Add a schedule + community.proxysql.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. + +- name: Remove a schedule + community.proxysql.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" + } +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.proxysql.plugins.module_utils.mysql import ( + mysql_connect, + mysql_driver, + proxysql_common_argument_spec, + save_config_to_disk, + load_config_to_runtime, +) +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["interval_ms"] < 100 \ + or module.params["interval_ms"] > 100000000: + module.fail_json( + msg="interval_ms must between 100ms & 100000000ms" + ) + + +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, "SCHEDULER") + if self.load_to_runtime: + load_config_to_runtime(cursor, "SCHEDULER") + + 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(): + argument_spec = proxysql_common_argument_spec() + argument_spec.update( + 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') + ) + + module = AnsibleModule( + supports_check_mode=True, + argument_spec=argument_spec + ) + + 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, version = 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() |