From 21ab4343e35133bb34c80d1b965babbf250a13ab Mon Sep 17 00:00:00 2001 From: bmildren Date: Tue, 4 Apr 2017 21:47:19 +0100 Subject: [PATCH] Adding support for ProxySQL (#19872) * Adding support for ProxySQL --- .../modules/database/proxysql/__init__.py | 0 .../proxysql/proxysql_backend_servers.py | 554 +++++++++++++++ .../proxysql/proxysql_global_variables.py | 316 +++++++++ .../proxysql/proxysql_manage_config.py | 257 +++++++ .../database/proxysql/proxysql_mysql_users.py | 523 ++++++++++++++ .../database/proxysql/proxysql_query_rules.py | 657 ++++++++++++++++++ .../proxysql_replication_hostgroups.py | 430 ++++++++++++ .../database/proxysql/proxysql_scheduler.py | 465 +++++++++++++ 8 files changed, 3202 insertions(+) create mode 100644 lib/ansible/modules/database/proxysql/__init__.py create mode 100644 lib/ansible/modules/database/proxysql/proxysql_backend_servers.py create mode 100644 lib/ansible/modules/database/proxysql/proxysql_global_variables.py create mode 100644 lib/ansible/modules/database/proxysql/proxysql_manage_config.py create mode 100644 lib/ansible/modules/database/proxysql/proxysql_mysql_users.py create mode 100644 lib/ansible/modules/database/proxysql/proxysql_query_rules.py create mode 100644 lib/ansible/modules/database/proxysql/proxysql_replication_hostgroups.py create mode 100644 lib/ansible/modules/database/proxysql/proxysql_scheduler.py diff --git a/lib/ansible/modules/database/proxysql/__init__.py b/lib/ansible/modules/database/proxysql/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/database/proxysql/proxysql_backend_servers.py b/lib/ansible/modules/database/proxysql/proxysql_backend_servers.py new file mode 100644 index 00000000000..969a78cc40d --- /dev/null +++ b/lib/ansible/modules/database/proxysql/proxysql_backend_servers.py @@ -0,0 +1,554 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_backend_servers +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Adds or removes mysql hosts from proxysql admin interface. +description: + - The M(proxysql_backend_servers) module adds or removes mysql hosts using + the proxysql admin interface. +options: + hostgroup_id: + description: + - The hostgroup in which this mysqld instance is included. An instance + can be part of one or more hostgroups. + default: 0 + hostname: + description: + - The ip address at which the mysqld instance can be contacted. + required: True + port: + description: + - The port at which the mysqld instance can be contacted. + default: 3306 + status: + description: + - ONLINE - Backend server is fully operational. + OFFLINE_SOFT - When a server is put into C(OFFLINE_SOFT) mode, + connections are kept in use until the current + transaction is completed. This allows to gracefully + detach a backend. + OFFLINE_HARD - When a server is put into C(OFFLINE_HARD) mode, the + existing connections are dropped, while new incoming + connections aren't accepted either. + + If omitted the proxysql database default for I(status) is C(ONLINE). + choices: [ "ONLINE", "OFFLINE_SOFT", "OFFLINE_HARD"] + weight: + description: + - The bigger the weight of a server relative to other weights, the higher + the probability of the server being chosen from the hostgroup. If + omitted the proxysql database default for I(weight) is 1. + compression: + description: + - If the value of I(compression) is greater than 0, new connections to + that server will use compression. If omitted the proxysql database + default for I(compression) is 0. + max_connections: + description: + - The maximum number of connections ProxySQL will open to this backend + server. If omitted the proxysql database default for I(max_connections) + is 1000. + max_replication_lag: + description: + - If greater than 0, ProxySQL will reguarly monitor replication lag. If + replication lag goes above I(max_replication_lag), proxysql will + temporarily shun the server until replication catches up. If omitted + the proxysql database default for I(max_replication_lag) is 0. + use_ssl: + description: + - If I(use_ssl) is set to C(True), connections to this server will be + made using SSL connections. If omitted the proxysql database default + for I(use_ssl) is C(False). + max_latency_ms: + description: + - Ping time is monitored regularly. If a host has a ping time greater + than I(max_latency_ms) it is excluded from the connection pool + (although the server stays ONLINE). If omitted the proxysql database + default for I(max_latency_ms) is 0. + comment: + description: + - Text field that can be used for any purposed defined by the user. + Could be a description of what the host stores, a reminder of when the + host was added or disabled, or a JSON processed by some checker script. + default: '' + state: + description: + - When C(present) - adds the host, when C(absent) - removes the host. + choices: [ "present", "absent" ] + default: present + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface. + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface. + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface. + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface. + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read. + default: '' +''' + +EXAMPLES = ''' +--- +# This example adds a server, it saves the mysql server config to disk, but +# avoids loading the mysql server config to runtime (this might be because +# several servers are being added and the user wants to push the config to +# runtime in a single batch using the M(proxysql_manage_config) module). It +# uses supplied credentials to connect to the proxysql admin interface. + +- proxysql_backend_servers: + login_user: 'admin' + login_password: 'admin' + hostname: 'mysql01' + state: present + load_to_runtime: False + +# This example removes a server, saves the mysql server config to disk, and +# dynamically loads the mysql server config to runtime. It uses credentials +# in a supplied config file to connect to the proxysql admin interface. + +- proxysql_backend_servers: + config_file: '~/proxysql.cnf' + hostname: 'mysql02' + state: absent +''' + +RETURN = ''' +stdout: + description: The mysql host modified or removed from proxysql + returned: On create/update will return the newly modified host, on delete + it will return the deleted record. + type: dict + "sample": { + "changed": true, + "hostname": "192.168.52.1", + "msg": "Added server to mysql_hosts", + "server": { + "comment": "", + "compression": "0", + "hostgroup_id": "1", + "hostname": "192.168.52.1", + "max_connections": "1000", + "max_latency_ms": "0", + "max_replication_lag": "0", + "port": "3306", + "status": "ONLINE", + "use_ssl": "0", + "weight": "1" + }, + "state": "present" + } +''' + +ANSIBLE_METADATA = { + 'version': '1.0', + 'supported_by': 'community', + 'status': ['stableinterface'] +} + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.six import iteritems + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if module.params["port"] < 0 \ + or module.params["port"] > 65535: + module.fail_json( + msg="port must be a valid unix port number (0-65535)" + ) + + if module.params["compression"]: + if module.params["compression"] < 0 \ + or module.params["compression"] > 102400: + module.fail_json( + msg="compression must be set between 0 and 102400" + ) + + if module.params["max_replication_lag"]: + if module.params["max_replication_lag"] < 0 \ + or module.params["max_replication_lag"] > 126144000: + module.fail_json( + msg="max_replication_lag must be set between 0 and 102400" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(cursor): + cursor.execute("SAVE MYSQL SERVERS TO DISK") + return True + + +def load_config_to_runtime(cursor): + cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") + return True + + +class ProxySQLServer(object): + + def __init__(self, module): + self.state = module.params["state"] + self.save_to_disk = module.params["save_to_disk"] + self.load_to_runtime = module.params["load_to_runtime"] + + self.hostgroup_id = module.params["hostgroup_id"] + self.hostname = module.params["hostname"] + self.port = module.params["port"] + + config_data_keys = ["status", + "weight", + "compression", + "max_connections", + "max_replication_lag", + "use_ssl", + "max_latency_ms", + "comment"] + + self.config_data = dict((k, module.params[k]) + for k in config_data_keys) + + def check_server_config_exists(self, cursor): + query_string = \ + """SELECT count(*) AS `host_count` + FROM mysql_servers + WHERE hostgroup_id = %s + AND hostname = %s + AND port = %s""" + + query_data = \ + [self.hostgroup_id, + self.hostname, + self.port] + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['host_count']) > 0) + + def check_server_config(self, cursor): + query_string = \ + """SELECT count(*) AS `host_count` + FROM mysql_servers + WHERE hostgroup_id = %s + AND hostname = %s + AND port = %s""" + + query_data = \ + [self.hostgroup_id, + self.hostname, + self.port] + + for col, val in iteritems(self.config_data): + if val is not None: + query_data.append(val) + query_string += "\n AND " + col + " = %s" + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['host_count']) > 0) + + def get_server_config(self, cursor): + query_string = \ + """SELECT * + FROM mysql_servers + WHERE hostgroup_id = %s + AND hostname = %s + AND port = %s""" + + query_data = \ + [self.hostgroup_id, + self.hostname, + self.port] + + cursor.execute(query_string, query_data) + server = cursor.fetchone() + return server + + def create_server_config(self, cursor): + query_string = \ + """INSERT INTO mysql_servers ( + hostgroup_id, + hostname, + port""" + + cols = 3 + query_data = \ + [self.hostgroup_id, + self.hostname, + self.port] + + for col, val in iteritems(self.config_data): + if val is not None: + cols += 1 + query_data.append(val) + query_string += ",\n" + col + + query_string += \ + (")\n" + + "VALUES (" + + "%s ," * cols) + + query_string = query_string[:-2] + query_string += ")" + + cursor.execute(query_string, query_data) + return True + + def update_server_config(self, cursor): + query_string = """UPDATE mysql_servers""" + + cols = 0 + query_data = [] + + for col, val in iteritems(self.config_data): + if val is not None: + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\nSET " + col + "= %s," + else: + query_string += "\n " + col + " = %s," + + query_string = query_string[:-1] + query_string += ("\nWHERE hostgroup_id = %s\n AND hostname = %s" + + "\n AND port = %s") + + query_data.append(self.hostgroup_id) + query_data.append(self.hostname) + query_data.append(self.port) + + cursor.execute(query_string, query_data) + return True + + def delete_server_config(self, cursor): + query_string = \ + """DELETE FROM mysql_servers + WHERE hostgroup_id = %s + AND hostname = %s + AND port = %s""" + + query_data = \ + [self.hostgroup_id, + self.hostname, + self.port] + + cursor.execute(query_string, query_data) + return True + + def manage_config(self, cursor, state): + if state: + if self.save_to_disk: + save_config_to_disk(cursor) + if self.load_to_runtime: + load_config_to_runtime(cursor) + + def create_server(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.create_server_config(cursor) + result['msg'] = "Added server to mysql_hosts" + result['server'] = \ + self.get_server_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Server would have been added to" + + " mysql_hosts, however check_mode" + + " is enabled.") + + def update_server(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.update_server_config(cursor) + result['msg'] = "Updated server in mysql_hosts" + result['server'] = \ + self.get_server_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Server would have been updated in" + + " mysql_hosts, however check_mode" + + " is enabled.") + + def delete_server(self, check_mode, result, cursor): + if not check_mode: + result['server'] = \ + self.get_server_config(cursor) + result['changed'] = \ + self.delete_server_config(cursor) + result['msg'] = "Deleted server from mysql_hosts" + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Server would have been deleted from" + + " mysql_hosts, however check_mode is" + + " enabled.") + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default='127.0.0.1'), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default='', type='path'), + hostgroup_id=dict(default=0, type='int'), + hostname=dict(required=True, type='str'), + port=dict(default=3306, type='int'), + status=dict(choices=['ONLINE', + 'OFFLINE_SOFT', + 'OFFLINE_HARD']), + weight=dict(type='int'), + compression=dict(type='int'), + max_connections=dict(type='int'), + max_replication_lag=dict(type='int'), + use_ssl=dict(type='bool'), + max_latency_ms=dict(type='int'), + comment=dict(default='', type='str'), + state=dict(default='present', choices=['present', + 'absent']), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % 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 MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to modify server.. %s" % 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 MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to remove server.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_global_variables.py b/lib/ansible/modules/database/proxysql/proxysql_global_variables.py new file mode 100644 index 00000000000..617ec185613 --- /dev/null +++ b/lib/ansible/modules/database/proxysql/proxysql_global_variables.py @@ -0,0 +1,316 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_global_variables +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Gets or sets the proxysql global variables. +description: + - The M(proxysql_global_variables) module gets or sets the proxysql global + variables. +options: + variable: + description: + - Defines which variable should be returned, or if I(value) is specified + which variable should be updated. + required: True + value: + description: + - Defines a value the variable specified using I(variable) should be set + to. + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface. + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface. + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface. + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface. + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read. + default: '' +''' + +EXAMPLES = ''' +--- +# This example sets the value of a variable, saves the mysql admin variables +# config to disk, and dynamically loads the mysql admin variables config to +# runtime. It uses supplied credentials to connect to the proxysql admin +# interface. + +- proxysql_global_variables: + login_user: 'admin' + login_password: 'admin' + variable: 'mysql-max_connections' + value: 4096 + +# This example gets the value of a variable. It uses credentials in a +# supplied config file to connect to the proxysql admin interface. + +- proxysql_global_variables: + config_file: '~/proxysql.cnf' + variable: 'mysql-default_query_delay' +''' + +RETURN = ''' +stdout: + description: Returns the mysql variable supplied with it's associted value. + returned: Returns the current variable and value, or the newly set value + for the variable supplied.. + type: dict + "sample": { + "changed": false, + "msg": "The variable is already been set to the supplied value", + "var": { + "variable_name": "mysql-poll_timeout", + "variable_value": "3000" + } + } +''' + +ANSIBLE_METADATA = { + 'version': '1.0', + 'supported_by': 'community', + 'status': ['stableinterface'] +} + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(variable, cursor): + if variable.startswith("admin"): + cursor.execute("SAVE ADMIN VARIABLES TO DISK") + else: + cursor.execute("SAVE MYSQL VARIABLES TO DISK") + return True + + +def load_config_to_runtime(variable, cursor): + if variable.startswith("admin"): + cursor.execute("LOAD ADMIN VARIABLES TO RUNTIME") + else: + cursor.execute("LOAD MYSQL VARIABLES TO RUNTIME") + return True + + +def check_config(variable, value, cursor): + query_string = \ + """SELECT count(*) AS `variable_count` + FROM global_variables + WHERE variable_name = %s and variable_value = %s""" + + query_data = \ + [variable, value] + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['variable_count']) > 0) + + +def get_config(variable, cursor): + + query_string = \ + """SELECT * + FROM global_variables + WHERE variable_name = %s""" + + query_data = \ + [variable, ] + + cursor.execute(query_string, query_data) + row_count = cursor.rowcount + resultset = cursor.fetchone() + + if row_count > 0: + return resultset + else: + return False + + +def set_config(variable, value, cursor): + + query_string = \ + """UPDATE global_variables + SET variable_value = %s + WHERE variable_name = %s""" + + query_data = \ + [value, variable] + + cursor.execute(query_string, query_data) + return True + + +def manage_config(variable, save_to_disk, load_to_runtime, cursor, state): + if state: + if save_to_disk: + save_config_to_disk(variable, cursor) + if load_to_runtime: + load_config_to_runtime(variable, cursor) + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default="", type='path'), + variable=dict(required=True, type='str'), + value=dict(), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + variable = module.params["variable"] + value = module.params["value"] + save_to_disk = module.params["save_to_disk"] + load_to_runtime = module.params["load_to_runtime"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % 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 MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to get config.. %s" % 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 MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to set config.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_manage_config.py b/lib/ansible/modules/database/proxysql/proxysql_manage_config.py new file mode 100644 index 00000000000..c440d29f405 --- /dev/null +++ b/lib/ansible/modules/database/proxysql/proxysql_manage_config.py @@ -0,0 +1,257 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_manage_config +version_added: "2.3" + +author: "Ben Mildren (@bmildren)" +short_description: Writes the proxysql configuration settings between layers. +description: + - The M(proxysql_global_variables) module writes the proxysql configuration + settings between layers. Currently this module will always report a + changed state, so should typically be used with WHEN however this will + change in a future version when the CHECKSUM table commands are available + for all tables in proxysql. +options: + action: + description: + - The supplied I(action) combines with the supplied I(direction) to + provide the semantics of how we want to move the I(config_settings) + between the I(config_layers). + choices: [ "LOAD", "SAVE" ] + required: True + config_settings: + description: + - The I(config_settings) specifies which configuration we're writing. + choices: [ "MYSQL USERS", "MYSQL SERVERS", "MYSQL QUERY RULES", + "MYSQL VARIABLES", "ADMIN VARIABLES", "SCHEDULER" ] + required: True + direction: + description: + - FROM - denotes we're reading values FROM the supplied I(config_layer) + and writing to the next layer. + TO - denotes we're reading from the previous layer and writing TO the + supplied I(config_layer)." + choices: [ "FROM", "TO" ] + required: True + config_layer: + description: + - RUNTIME - represents the in-memory data structures of ProxySQL used by + the threads that are handling the requests. + MEMORY - (sometimes also referred as main) represents the in-memory + SQLite3 database. + DISK - represents the on-disk SQLite3 database. + CONFIG - is the classical config file. You can only LOAD FROM the + config file. + choices: [ "MEMORY", "DISK", "RUNTIME", "CONFIG" ] + required: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface. + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface. + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface. + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface. + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read. + default: '' +''' + +EXAMPLES = ''' +--- +# This example saves the mysql users config from memory to disk. It uses +# supplied credentials to connect to the proxysql admin interface. + +- proxysql_global_variables: + login_user: 'admin' + login_password: 'admin' + action: "SAVE" + config_settings: "MYSQL USERS" + direction: "FROM" + config_layer: "MEMORY" + +# This example loads the mysql query rules config from memory to to runtime. It +# uses supplied credentials to connect to the proxysql admin interface. + +- proxysql_global_variables: + config_file: '~/proxysql.cnf' + action: "LOAD" + config_settings: "MYSQL QUERY RULES" + direction: "TO" + config_layer: "RUNTIME" +''' + +RETURN = ''' +stdout: + description: Simply reports whether the action reported a change. + returned: Currently the returned value with always be changed=True. + type: dict + "sample": { + "changed": true + } +''' + +ANSIBLE_METADATA = { + 'version': '1.0', + 'supported_by': 'community', + 'status': ['stableinterface'] +} + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + +try: + import MySQLdb +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if module.params["config_layer"] == 'CONFIG' and \ + (module.params["action"] != 'LOAD' or + module.params["direction"] != 'FROM'): + + if (module.params["action"] != 'LOAD' and + module.params["direction"] != 'FROM'): + msg_string = ("Neither the action \"%s\" nor the direction" + + " \"%s\" are valid combination with the CONFIG" + + " config_layer") + module.fail_json(msg=msg_string % (module.params["action"], + module.params["direction"])) + + elif module.params["action"] != 'LOAD': + msg_string = ("The action \"%s\" is not a valid combination" + + " with the CONFIG config_layer") + module.fail_json(msg=msg_string % module.params["action"]) + + else: + msg_string = ("The direction \"%s\" is not a valid combination" + + " with the CONFIG config_layer") + module.fail_json(msg=msg_string % module.params["direction"]) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def manage_config(manage_config_settings, cursor): + + query_string = "%s" % ' '.join(manage_config_settings) + + cursor.execute(query_string) + return True + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default="", type='path'), + action=dict(required=True, choices=['LOAD', + 'SAVE']), + config_settings=dict(required=True, choices=['MYSQL USERS', + 'MYSQL SERVERS', + 'MYSQL QUERY RULES', + 'MYSQL VARIABLES', + 'ADMIN VARIABLES', + 'SCHEDULER']), + direction=dict(required=True, choices=['FROM', + 'TO']), + config_layer=dict(required=True, choices=['MEMORY', + 'DISK', + 'RUNTIME', + 'CONFIG']) + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + action = module.params["action"] + config_settings = module.params["config_settings"] + direction = module.params["direction"] + config_layer = module.params["config_layer"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + result = {} + + manage_config_settings = \ + [action, config_settings, direction, config_layer] + + try: + result['changed'] = manage_config(manage_config_settings, + cursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to manage config.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_mysql_users.py b/lib/ansible/modules/database/proxysql/proxysql_mysql_users.py new file mode 100644 index 00000000000..e68e516fce9 --- /dev/null +++ b/lib/ansible/modules/database/proxysql/proxysql_mysql_users.py @@ -0,0 +1,523 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_mysql_users +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Adds or removes mysql users from proxysql admin interface. +description: + - The M(proxysql_mysql_users) module adds or removes mysql users using the + proxysql admin interface. +options: + username: + description: + - Name of the user connecting to the mysqld or ProxySQL instance. + required: True + password: + description: + - Password of the user connecting to the mysqld or ProxySQL instance. + active: + description: + - A user with I(active) set to C(False) will be tracked in the database, + but will be never loaded in the in-memory data structures. If omitted + the proxysql database default for I(active) is C(True). + 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). + default_hostgroup: + description: + - If there is no matching rule for the queries sent by this user, the + traffic it generates is sent to the specified hostgroup. + If omitted the proxysql database default for I(use_ssl) is 0. + default_schema: + description: + - The schema to which the connection should change to by default. + transaction_persistent: + description: + - If this is set for the user with which the MySQL client is connecting + to ProxySQL (thus a "frontend" user), transactions started within a + hostgroup will remain within that hostgroup regardless of any other + rules. + If omitted the proxysql database default for I(transaction_persistent) is + C(False). + 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). + backend: + description: + - If I(backend) is set to C(True), this (username, password) pair is + used for authenticating to the ProxySQL instance. + default: True + 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 + max_connections: + description: + - The maximum number of connections ProxySQL will open to the backend for + this user. If omitted the proxysql database default for + I(max_connections) is 10000. + state: + description: + - When C(present) - adds the user, when C(absent) - removes the user. + choices: [ "present", "absent" ] + default: present + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface. + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface. + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface. + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface. + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read. + default: '' +''' + +EXAMPLES = ''' +--- +# This example adds a user, it saves the mysql user config to disk, but +# avoids loading the mysql user config to runtime (this might be because +# several users are being added and the user wants to push the config to +# runtime in a single batch using the M(proxysql_manage_config) module). It +# uses supplied credentials to connect to the proxysql admin interface. + +- proxysql_mysql_users: + login_user: 'admin' + login_password: 'admin' + username: 'productiondba' + state: present + load_to_runtime: False + +# This example removes a user, saves the mysql user config to disk, and +# dynamically loads the mysql user config to runtime. It uses credentials +# in a supplied config file to connect to the proxysql admin interface. + +- proxysql_mysql_users: + config_file: '~/proxysql.cnf' + username: 'mysqlboy' + state: absent +''' + +RETURN = ''' +stdout: + description: The mysql user modified or removed from proxysql + returned: On create/update will return the newly modified user, on delete + it will return the deleted record. + type: dict + sample": { + "changed": true, + "msg": "Added user to mysql_users", + "state": "present", + "user": { + "active": "1", + "backend": "1", + "default_hostgroup": "1", + "default_schema": null, + "fast_forward": "0", + "frontend": "1", + "max_connections": "10000", + "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", + "schema_locked": "0", + "transaction_persistent": "0", + "use_ssl": "0", + "username": "guest_ro" + }, + "username": "guest_ro" + } +''' + +ANSIBLE_METADATA = { + 'version': '1.0', + 'supported_by': 'community', + 'status': ['stableinterface'] +} + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.six import iteritems + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(cursor): + cursor.execute("SAVE MYSQL USERS TO DISK") + return True + + +def load_config_to_runtime(cursor): + cursor.execute("LOAD MYSQL USERS TO RUNTIME") + return True + + +class ProxySQLUser(object): + + def __init__(self, module): + self.state = module.params["state"] + self.save_to_disk = module.params["save_to_disk"] + self.load_to_runtime = module.params["load_to_runtime"] + + self.username = module.params["username"] + self.backend = module.params["backend"] + self.frontend = module.params["frontend"] + + config_data_keys = ["password", + "active", + "use_ssl", + "default_hostgroup", + "default_schema", + "transaction_persistent", + "fast_forward", + "max_connections"] + + self.config_data = dict((k, module.params[k]) + for k in config_data_keys) + + def check_user_config_exists(self, cursor): + query_string = \ + """SELECT count(*) AS `user_count` + FROM mysql_users + WHERE username = %s + AND backend = %s + AND frontend = %s""" + + query_data = \ + [self.username, + self.backend, + self.frontend] + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['user_count']) > 0) + + def check_user_privs(self, cursor): + query_string = \ + """SELECT count(*) AS `user_count` + FROM mysql_users + WHERE username = %s + AND backend = %s + AND frontend = %s""" + + query_data = \ + [self.username, + self.backend, + self.frontend] + + for col, val in iteritems(self.config_data): + if val is not None: + query_data.append(val) + query_string += "\n AND " + col + " = %s" + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['user_count']) > 0) + + def get_user_config(self, cursor): + query_string = \ + """SELECT * + FROM mysql_users + WHERE username = %s + AND backend = %s + AND frontend = %s""" + + query_data = \ + [self.username, + self.backend, + self.frontend] + + cursor.execute(query_string, query_data) + user = cursor.fetchone() + return user + + def create_user_config(self, cursor): + query_string = \ + """INSERT INTO mysql_users ( + username, + backend, + frontend""" + + cols = 3 + query_data = \ + [self.username, + self.backend, + self.frontend] + + for col, val in iteritems(self.config_data): + if val is not None: + cols += 1 + query_data.append(val) + query_string += ",\n" + col + + query_string += \ + (")\n" + + "VALUES (" + + "%s ," * cols) + + query_string = query_string[:-2] + query_string += ")" + + cursor.execute(query_string, query_data) + return True + + def update_user_config(self, cursor): + query_string = """UPDATE mysql_users""" + + cols = 0 + query_data = [] + + for col, val in iteritems(self.config_data): + if val is not None: + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\nSET " + col + "= %s," + else: + query_string += "\n " + col + " = %s," + + query_string = query_string[:-1] + query_string += ("\nWHERE username = %s\n AND backend = %s" + + "\n AND frontend = %s") + + query_data.append(self.username) + query_data.append(self.backend) + query_data.append(self.frontend) + + cursor.execute(query_string, query_data) + return True + + def delete_user_config(self, cursor): + query_string = \ + """DELETE FROM mysql_users + WHERE username = %s + AND backend = %s + AND frontend = %s""" + + query_data = \ + [self.username, + self.backend, + self.frontend] + + cursor.execute(query_string, query_data) + return True + + def manage_config(self, cursor, state): + if state: + if self.save_to_disk: + save_config_to_disk(cursor) + if self.load_to_runtime: + load_config_to_runtime(cursor) + + def create_user(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.create_user_config(cursor) + result['msg'] = "Added user to mysql_users" + result['user'] = \ + self.get_user_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("User would have been added to" + + " mysql_users, however check_mode" + + " is enabled.") + + def update_user(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.update_user_config(cursor) + result['msg'] = "Updated user in mysql_users" + result['user'] = \ + self.get_user_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("User would have been updated in" + + " mysql_users, however check_mode" + + " is enabled.") + + def delete_user(self, check_mode, result, cursor): + if not check_mode: + result['user'] = \ + self.get_user_config(cursor) + result['changed'] = \ + self.delete_user_config(cursor) + result['msg'] = "Deleted user from mysql_users" + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("User would have been deleted from" + + " mysql_users, however check_mode is" + + " enabled.") + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default='', type='path'), + username=dict(required=True, type='str'), + password=dict(no_log=True, type='str'), + active=dict(type='bool'), + use_ssl=dict(type='bool'), + default_hostgroup=dict(type='int'), + default_schema=dict(type='str'), + transaction_persistent=dict(type='bool'), + fast_forward=dict(type='bool'), + backend=dict(default=True, type='bool'), + frontend=dict(default=True, type='bool'), + max_connections=dict(type='int'), + state=dict(default='present', choices=['present', + 'absent']), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % 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 MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to modify user.. %s" % 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 MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to remove user.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_query_rules.py b/lib/ansible/modules/database/proxysql/proxysql_query_rules.py new file mode 100644 index 00000000000..d85e4102256 --- /dev/null +++ b/lib/ansible/modules/database/proxysql/proxysql_query_rules.py @@ -0,0 +1,657 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_query_rules +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Modifies query rules using the proxysql admin interface. +description: + - The M(proxysql_query_rules) module modifies query rules using the + proxysql admin interface. +options: + rule_id: + description: + - The unique id of the rule. Rules are processed in rule_id order. + active: + description: + - A rule with I(active) set to C(False) will be tracked in the database, + but will be never loaded in the in-memory data structures. + username: + description: + - Filtering criteria matching username. If I(username) is non-NULL, a + query will match only if the connection is made with the correct + username. + schemaname: + description: + - Filtering criteria matching schemaname. If I(schemaname) is non-NULL, a + query will match only if the connection uses schemaname as its default + schema. + flagIN: + description: + - Used in combination with I(flagOUT) and I(apply) to create chains of + rules. + client_addr: + description: + - Match traffic from a specific source. + proxy_addr: + description: + - Match incoming traffic on a specific local IP. + proxy_port: + description: + - Match incoming traffic on a specific local port. + digest: + description: + - Match queries with a specific digest, as returned by + stats_mysql_query_digest.digest. + match_digest: + description: + - Regular expression that matches the query digest. The dialect of + regular expressions used is that of re2 - https://github.com/google/re2 + match_pattern: + description: + - Regular expression that matches the query text. The dialect of regular + expressions used is that of re2 - https://github.com/google/re2 + negate_match_pattern: + description: + - If I(negate_match_pattern) is set to C(True), only queries not matching + the query text will be considered as a match. This acts as a NOT + operator in front of the regular expression matching against + match_pattern. + flagOUT: + description: + - Used in combination with I(flagIN) and apply to create chains of rules. + When set, I(flagOUT) signifies the I(flagIN) to be used in the next + chain of rules. + replace_pattern: + description: + - This is the pattern with which to replace the matched pattern. Note + that this is optional, and when omitted, the query processor will only + cache, route, or set other parameters without rewriting. + destination_hostgroup: + description: + - Route matched queries to this hostgroup. This happens unless there is a + started transaction and the logged in user has + I(transaction_persistent) set to C(True) (see M(proxysql_mysql_users)). + cache_ttl: + description: + - The number of milliseconds for which to cache the result of the query. + Note in ProxySQL 1.1 I(cache_ttl) was in seconds. + timeout: + description: + - The maximum timeout in milliseconds with which the matched or rewritten + query should be executed. If a query run for longer than the specific + threshold, the query is automatically killed. If timeout is not + specified, the global variable mysql-default_query_timeout applies. + retries: + description: + - The maximum number of times a query needs to be re-executed in case of + detected failure during the execution of the query. If retries is not + specified, the global variable mysql-query_retries_on_failure applies. + delay: + description: + - Number of milliseconds to delay the execution of the query. This is + essentially a throttling mechanism and QoS, and allows a way to give + priority to queries over others. This value is added to the + mysql-default_query_delay global variable that applies to all queries. + mirror_flagOUT: + description: + - Enables query mirroring. If set I(mirror_flagOUT) can be used to + evaluates the mirrored query against the specified chain of rules. + mirror_hostgroup: + description: + - Enables query mirroring. If set I(mirror_hostgroup) can be used to + mirror queries to the same or different hostgroup. + error_msg: + description: + - Query will be blocked, and the specified error_msg will be returned to + the client. + log: + description: + - Query will be logged. + 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. + comment: + description: + - Free form text field, usable for a descriptive comment of the query + rule. + state: + description: + - When C(present) - adds the rule, when C(absent) - removes the rule. + choices: [ "present", "absent" ] + default: present + force_delete: + description: + - By default we avoid deleting more than one schedule in a single batch, + however if you need this behaviour and you're not concerned about the + schedules deleted, you can set I(force_delete) to C(True). + default: False + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface. + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface. + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface. + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface. + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read. + default: '' +''' + +EXAMPLES = ''' +--- +# This example adds a rule to redirect queries from a specific user to another +# hostgroup, it saves the mysql query rule config to disk, but avoids loading +# the mysql query config config to runtime (this might be because several +# rules are being added and the user wants to push the config to runtime in a +# single batch using the M(proxysql_manage_config) module). It uses supplied +# credentials to connect to the proxysql admin interface. + +- proxysql_backend_servers: + login_user: admin + login_password: admin + username: 'guest_ro' + destination_hostgroup: 1 + active: 1 + retries: 3 + state: present + load_to_runtime: False + +# This example removes all rules that use the username 'guest_ro', saves the +# mysql query rule config to disk, and dynamically loads the mysql query rule +# config to runtime. It uses credentials in a supplied config file to connect +# to the proxysql admin interface. + +- proxysql_backend_servers: + config_file: '~/proxysql.cnf' + username: 'guest_ro' + state: absent + force_delete: true +''' + +RETURN = ''' +stdout: + description: The mysql user modified or removed from proxysql + returned: On create/update will return the newly modified rule, in all + other cases will return a list of rules that match the supplied + criteria. + type: dict + "sample": { + "changed": true, + "msg": "Added rule to mysql_query_rules", + "rules": [ + { + "active": "0", + "apply": "0", + "cache_ttl": null, + "client_addr": null, + "comment": null, + "delay": null, + "destination_hostgroup": 1, + "digest": null, + "error_msg": null, + "flagIN": "0", + "flagOUT": null, + "log": null, + "match_digest": null, + "match_pattern": null, + "mirror_flagOUT": null, + "mirror_hostgroup": null, + "negate_match_pattern": "0", + "proxy_addr": null, + "proxy_port": null, + "reconnect": null, + "replace_pattern": null, + "retries": null, + "rule_id": "1", + "schemaname": null, + "timeout": null, + "username": "guest_ro" + } + ], + "state": "present" + } +''' + +ANSIBLE_METADATA = { + 'version': '1.0', + 'supported_by': 'community', + 'status': ['stableinterface'] +} + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.six import iteritems + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(cursor): + cursor.execute("SAVE MYSQL QUERY RULES TO DISK") + return True + + +def load_config_to_runtime(cursor): + cursor.execute("LOAD MYSQL QUERY RULES TO RUNTIME") + return True + + +class ProxyQueryRule(object): + + def __init__(self, module): + self.state = module.params["state"] + self.force_delete = module.params["force_delete"] + self.save_to_disk = module.params["save_to_disk"] + self.load_to_runtime = module.params["load_to_runtime"] + + config_data_keys = ["rule_id", + "active", + "username", + "schemaname", + "flagIN", + "client_addr", + "proxy_addr", + "proxy_port", + "digest", + "match_digest", + "match_pattern", + "negate_match_pattern", + "flagOUT", + "replace_pattern", + "destination_hostgroup", + "cache_ttl", + "timeout", + "retries", + "delay", + "mirror_flagOUT", + "mirror_hostgroup", + "error_msg", + "log", + "apply", + "comment"] + + self.config_data = dict((k, module.params[k]) + for k in config_data_keys) + + def check_rule_pk_exists(self, cursor): + query_string = \ + """SELECT count(*) AS `rule_count` + FROM mysql_query_rules + WHERE rule_id = %s""" + + query_data = \ + [self.config_data["rule_id"]] + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['rule_count']) > 0) + + def check_rule_cfg_exists(self, cursor): + query_string = \ + """SELECT count(*) AS `rule_count` + FROM mysql_query_rules""" + + cols = 0 + query_data = [] + + for col, val in iteritems(self.config_data): + if val is not None: + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\n WHERE " + col + " = %s" + else: + query_string += "\n AND " + col + " = %s" + + if cols > 0: + cursor.execute(query_string, query_data) + else: + cursor.execute(query_string) + check_count = cursor.fetchone() + return int(check_count['rule_count']) + + def get_rule_config(self, cursor, created_rule_id=None): + query_string = \ + """SELECT * + FROM mysql_query_rules""" + + if created_rule_id: + query_data = [created_rule_id, ] + query_string += "\nWHERE rule_id = %s" + + cursor.execute(query_string, query_data) + rule = cursor.fetchone() + else: + cols = 0 + query_data = [] + + for col, val in iteritems(self.config_data): + if val is not None: + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\n WHERE " + col + " = %s" + else: + query_string += "\n AND " + col + " = %s" + + if cols > 0: + cursor.execute(query_string, query_data) + else: + cursor.execute(query_string) + rule = cursor.fetchall() + + return rule + + def create_rule_config(self, cursor): + query_string = \ + """INSERT INTO mysql_query_rules (""" + + cols = 0 + query_data = [] + + for col, val in iteritems(self.config_data): + if val is not None: + cols += 1 + query_data.append(val) + query_string += "\n" + col + "," + + query_string = query_string[:-1] + + query_string += \ + (")\n" + + "VALUES (" + + "%s ," * cols) + + query_string = query_string[:-2] + query_string += ")" + + cursor.execute(query_string, query_data) + new_rule_id = cursor.lastrowid + return True, new_rule_id + + def update_rule_config(self, cursor): + query_string = """UPDATE mysql_query_rules""" + + cols = 0 + query_data = [] + + for col, val in iteritems(self.config_data): + if val is not None and col != "rule_id": + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\nSET " + col + "= %s," + else: + query_string += "\n " + col + " = %s," + + query_string = query_string[:-1] + query_string += "\nWHERE rule_id = %s" + + query_data.append(self.config_data["rule_id"]) + + cursor.execute(query_string, query_data) + return True + + def delete_rule_config(self, cursor): + query_string = \ + """DELETE FROM mysql_query_rules""" + + cols = 0 + query_data = [] + + for col, val in iteritems(self.config_data): + if val is not None: + cols += 1 + query_data.append(val) + if cols == 1: + query_string += "\n WHERE " + col + " = %s" + else: + query_string += "\n AND " + col + " = %s" + + if cols > 0: + cursor.execute(query_string, query_data) + else: + cursor.execute(query_string) + check_count = cursor.rowcount + return True, int(check_count) + + def manage_config(self, cursor, state): + if state: + if self.save_to_disk: + save_config_to_disk(cursor) + if self.load_to_runtime: + load_config_to_runtime(cursor) + + def create_rule(self, check_mode, result, cursor): + if not check_mode: + result['changed'], new_rule_id = \ + self.create_rule_config(cursor) + result['msg'] = "Added rule to mysql_query_rules" + self.manage_config(cursor, + result['changed']) + result['rules'] = \ + self.get_rule_config(cursor, new_rule_id) + else: + result['changed'] = True + result['msg'] = ("Rule would have been added to" + + " mysql_query_rules, however" + + " check_mode is enabled.") + + def update_rule(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.update_rule_config(cursor) + result['msg'] = "Updated rule in mysql_query_rules" + self.manage_config(cursor, + result['changed']) + result['rules'] = \ + self.get_rule_config(cursor) + else: + result['changed'] = True + result['msg'] = ("Rule would have been updated in" + + " mysql_query_rules, however" + + " check_mode is enabled.") + + def delete_rule(self, check_mode, result, cursor): + if not check_mode: + result['rules'] = \ + self.get_rule_config(cursor) + result['changed'], result['rows_affected'] = \ + self.delete_rule_config(cursor) + result['msg'] = "Deleted rule from mysql_query_rules" + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Rule would have been deleted from" + + " mysql_query_rules, however" + + " check_mode is enabled.") + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default="", type='path'), + rule_id=dict(type='int'), + active=dict(type='bool'), + username=dict(type='str'), + schemaname=dict(type='str'), + flagIN=dict(type='int'), + client_addr=dict(type='str'), + proxy_addr=dict(type='str'), + proxy_port=dict(type='int'), + digest=dict(type='str'), + match_digest=dict(type='str'), + match_pattern=dict(type='str'), + negate_match_pattern=dict(type='bool'), + flagOUT=dict(type='int'), + replace_pattern=dict(type='str'), + destination_hostgroup=dict(type='int'), + cache_ttl=dict(type='int'), + timeout=dict(type='int'), + retries=dict(type='int'), + delay=dict(type='int'), + mirror_flagOUT=dict(type='int'), + mirror_hostgroup=dict(type='int'), + error_msg=dict(type='str'), + log=dict(type='bool'), + apply=dict(type='bool'), + comment=dict(type='str'), + state=dict(default='present', choices=['present', + 'absent']), + force_delete=dict(default=False, type='bool'), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + proxysql_query_rule = ProxyQueryRule(module) + result = {} + + result['state'] = proxysql_query_rule.state + + if proxysql_query_rule.state == "present": + try: + if not proxysql_query_rule.check_rule_cfg_exists(cursor): + if proxysql_query_rule.config_data["rule_id"] and \ + proxysql_query_rule.check_rule_pk_exists(cursor): + proxysql_query_rule.update_rule(module.check_mode, + result, + cursor) + else: + proxysql_query_rule.create_rule(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The rule already exists in" + + " mysql_query_rules and doesn't need to be" + + " updated.") + result['rules'] = \ + proxysql_query_rule.get_rule_config(cursor) + + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to modify rule.. %s" % 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 MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to remove rule.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_replication_hostgroups.py b/lib/ansible/modules/database/proxysql/proxysql_replication_hostgroups.py new file mode 100644 index 00000000000..d32d6b98b2a --- /dev/null +++ b/lib/ansible/modules/database/proxysql/proxysql_replication_hostgroups.py @@ -0,0 +1,430 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_replication_hostgroups +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Manages replication hostgroups using the proxysql admin + interface. +description: + - Each row in mysql_replication_hostgroups represent a pair of + writer_hostgroup and reader_hostgroup. ProxySQL will monitor the value of + read_only for all the servers in specified hostgroups, and based on the + value of read_only will assign the server to the writer or reader + hostgroups. +options: + writer_hostgroup: + description: + - Id of the writer hostgroup. + required: True + reader_hostgroup: + description: + - Id of the reader hostgroup. + required: True + comment: + description: + - Text field that can be used for any purposed defined by the user. + state: + description: + - When C(present) - adds the replication hostgroup, when C(absent) - + removes the replication hostgroup. + choices: [ "present", "absent" ] + default: present + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface. + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface. + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface. + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface. + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read. + default: '' +''' + +EXAMPLES = ''' +--- +# This example adds a replication hostgroup, it saves the mysql server config +# to disk, but avoids loading the mysql server config to runtime (this might be +# because several replication hostgroup are being added and the user wants to +# push the config to runtime in a single batch using the +# M(proxysql_manage_config) module). It uses supplied credentials to connect +# to the proxysql admin interface. + +- proxysql_replication_hostgroups: + login_user: 'admin' + login_password: 'admin' + writer_hostgroup: 1 + reader_hostgroup: 2 + state: present + load_to_runtime: False + +# This example removes a replication hostgroup, saves the mysql server config +# to disk, and dynamically loads the mysql server config to runtime. It uses +# credentials in a supplied config file to connect to the proxysql admin +# interface. + +- proxysql_replication_hostgroups: + config_file: '~/proxysql.cnf' + writer_hostgroup: 3 + reader_hostgroup: 4 + state: absent +''' + +RETURN = ''' +stdout: + description: The replication hostgroup modified or removed from proxysql + returned: On create/update will return the newly modified group, on delete + it will return the deleted record. + type: dict + "sample": { + "changed": true, + "msg": "Added server to mysql_hosts", + "repl_group": { + "comment": "", + "reader_hostgroup": "1", + "writer_hostgroup": "2" + }, + "state": "present" + } +''' + +ANSIBLE_METADATA = { + 'version': '1.0', + 'supported_by': 'community', + 'status': ['stableinterface'] +} + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if not module.params["writer_hostgroup"] >= 0: + module.fail_json( + msg="writer_hostgroup must be a integer greater than or equal to 0" + ) + + if not module.params["reader_hostgroup"] == \ + module.params["writer_hostgroup"]: + if not module.params["reader_hostgroup"] > 0: + module.fail_json( + msg=("writer_hostgroup must be a integer greater than" + + " or equal to 0") + ) + else: + module.fail_json( + msg="reader_hostgroup cannot equal writer_hostgroup" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(cursor): + cursor.execute("SAVE MYSQL SERVERS TO DISK") + return True + + +def load_config_to_runtime(cursor): + cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") + return True + + +class ProxySQLReplicationHostgroup(object): + + def __init__(self, module): + self.state = module.params["state"] + self.save_to_disk = module.params["save_to_disk"] + self.load_to_runtime = module.params["load_to_runtime"] + self.writer_hostgroup = module.params["writer_hostgroup"] + self.reader_hostgroup = module.params["reader_hostgroup"] + self.comment = module.params["comment"] + + def check_repl_group_config(self, cursor, keys): + query_string = \ + """SELECT count(*) AS `repl_groups` + FROM mysql_replication_hostgroups + WHERE writer_hostgroup = %s + AND reader_hostgroup = %s""" + + query_data = \ + [self.writer_hostgroup, + self.reader_hostgroup] + + if self.comment and not keys: + query_string += "\n AND comment = %s" + query_data.append(self.comment) + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return (int(check_count['repl_groups']) > 0) + + def get_repl_group_config(self, cursor): + query_string = \ + """SELECT * + FROM mysql_replication_hostgroups + WHERE writer_hostgroup = %s + AND reader_hostgroup = %s""" + + query_data = \ + [self.writer_hostgroup, + self.reader_hostgroup] + + cursor.execute(query_string, query_data) + repl_group = cursor.fetchone() + return repl_group + + def create_repl_group_config(self, cursor): + query_string = \ + """INSERT INTO mysql_replication_hostgroups ( + writer_hostgroup, + reader_hostgroup, + comment) + VALUES (%s, %s, %s)""" + + query_data = \ + [self.writer_hostgroup, + self.reader_hostgroup, + self.comment or ''] + + cursor.execute(query_string, query_data) + return True + + def update_repl_group_config(self, cursor): + query_string = \ + """UPDATE mysql_replication_hostgroups + SET comment = %s + WHERE writer_hostgroup = %s + AND reader_hostgroup = %s""" + + query_data = \ + [self.comment, + self.writer_hostgroup, + self.reader_hostgroup] + + cursor.execute(query_string, query_data) + return True + + def delete_repl_group_config(self, cursor): + query_string = \ + """DELETE FROM mysql_replication_hostgroups + WHERE writer_hostgroup = %s + AND reader_hostgroup = %s""" + + query_data = \ + [self.writer_hostgroup, + self.reader_hostgroup] + + cursor.execute(query_string, query_data) + return True + + def manage_config(self, cursor, state): + if state: + if self.save_to_disk: + save_config_to_disk(cursor) + if self.load_to_runtime: + load_config_to_runtime(cursor) + + def create_repl_group(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.create_repl_group_config(cursor) + result['msg'] = "Added server to mysql_hosts" + result['repl_group'] = \ + self.get_repl_group_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Repl group would have been added to" + + " mysql_replication_hostgroups, however" + + " check_mode is enabled.") + + def update_repl_group(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.update_repl_group_config(cursor) + result['msg'] = "Updated server in mysql_hosts" + result['repl_group'] = \ + self.get_repl_group_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Repl group would have been updated in" + + " mysql_replication_hostgroups, however" + + " check_mode is enabled.") + + def delete_repl_group(self, check_mode, result, cursor): + if not check_mode: + result['repl_group'] = \ + self.get_repl_group_config(cursor) + result['changed'] = \ + self.delete_repl_group_config(cursor) + result['msg'] = "Deleted server from mysql_hosts" + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Repl group would have been deleted from" + + " mysql_replication_hostgroups, however" + + " check_mode is enabled.") + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default="", type='path'), + writer_hostgroup=dict(required=True, type='int'), + reader_hostgroup=dict(required=True, type='int'), + comment=dict(type='str'), + state=dict(default='present', choices=['present', + 'absent']), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + proxysql_repl_group = ProxySQLReplicationHostgroup(module) + result = {} + + result['state'] = proxysql_repl_group.state + + if proxysql_repl_group.state == "present": + try: + if not proxysql_repl_group.check_repl_group_config(cursor, + keys=True): + proxysql_repl_group.create_repl_group(module.check_mode, + result, + cursor) + else: + if not proxysql_repl_group.check_repl_group_config(cursor, + keys=False): + proxysql_repl_group.update_repl_group(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The repl group already exists in" + + " mysql_replication_hostgroups and" + + " doesn't need to be updated.") + result['repl_group'] = \ + proxysql_repl_group.get_repl_group_config(cursor) + + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to modify replication hostgroup.. %s" % e + ) + + elif proxysql_repl_group.state == "absent": + try: + if proxysql_repl_group.check_repl_group_config(cursor, + keys=True): + proxysql_repl_group.delete_repl_group(module.check_mode, + result, + cursor) + else: + result['changed'] = False + result['msg'] = ("The repl group is already absent from the" + + " mysql_replication_hostgroups memory" + + " configuration") + + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to delete replication hostgroup.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/database/proxysql/proxysql_scheduler.py b/lib/ansible/modules/database/proxysql/proxysql_scheduler.py new file mode 100644 index 00000000000..714d880c60b --- /dev/null +++ b/lib/ansible/modules/database/proxysql/proxysql_scheduler.py @@ -0,0 +1,465 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: proxysql_scheduler +version_added: "2.3" +author: "Ben Mildren (@bmildren)" +short_description: Adds or removes schedules from proxysql admin interface. +description: + - The M(proxysql_scheduler) module adds or removes schedules using the + proxysql admin interface. +options: + active: + description: + - A schedule with I(active) set to C(False) will be tracked in the + database, but will be never loaded in the in-memory data structures. + default: True + interval_ms: + description: + - How often (in millisecond) the job will be started. The minimum value + for I(interval_ms) is 100 milliseconds. + default: 10000 + filename: + description: + - Full path of the executable to be executed. + required: True + arg1: + description: + - Argument that can be passed to the job. + arg2: + description: + - Argument that can be passed to the job. + arg3: + description: + - Argument that can be passed to the job. + arg4: + description: + - Argument that can be passed to the job. + arg5: + description: + - Argument that can be passed to the job. + comment: + description: + - Text field that can be used for any purposed defined by the user. + state: + description: + - When C(present) - adds the schedule, when C(absent) - removes the + schedule. + choices: [ "present", "absent" ] + default: present + force_delete: + description: + - By default we avoid deleting more than one schedule in a single batch, + however if you need this behaviour and you're not concerned about the + schedules deleted, you can set I(force_delete) to C(True). + default: False + save_to_disk: + description: + - Save mysql host config to sqlite db on disk to persist the + configuration. + default: True + load_to_runtime: + description: + - Dynamically load mysql host config to runtime memory. + default: True + login_user: + description: + - The username used to authenticate to ProxySQL admin interface. + default: None + login_password: + description: + - The password used to authenticate to ProxySQL admin interface. + default: None + login_host: + description: + - The host used to connect to ProxySQL admin interface. + default: '127.0.0.1' + login_port: + description: + - The port used to connect to ProxySQL admin interface. + default: 6032 + config_file: + description: + - Specify a config file from which login_user and login_password are to + be read. + default: '' +''' + +EXAMPLES = ''' +--- +# This example adds a schedule, it saves the scheduler config to disk, but +# avoids loading the scheduler config to runtime (this might be because +# several servers are being added and the user wants to push the config to +# runtime in a single batch using the M(proxysql_manage_config) module). It +# uses supplied credentials to connect to the proxysql admin interface. + +- proxysql_scheduler: + login_user: 'admin' + login_password: 'admin' + interval_ms: 1000 + filename: "/opt/maintenance.py" + state: present + load_to_runtime: False + +# This example removes a schedule, saves the scheduler config to disk, and +# dynamically loads the scheduler config to runtime. It uses credentials +# in a supplied config file to connect to the proxysql admin interface. + +- proxysql_scheduler: + config_file: '~/proxysql.cnf' + filename: "/opt/old_script.py" + state: absent +''' + +RETURN = ''' +stdout: + description: The schedule modified or removed from proxysql + returned: On create/update will return the newly modified schedule, on + delete it will return the deleted record. + type: dict + "sample": { + "changed": true, + "filename": "/opt/test.py", + "msg": "Added schedule to scheduler", + "schedules": [ + { + "active": "1", + "arg1": null, + "arg2": null, + "arg3": null, + "arg4": null, + "arg5": null, + "comment": "", + "filename": "/opt/test.py", + "id": "1", + "interval_ms": "10000" + } + ], + "state": "present" + } +''' + +ANSIBLE_METADATA = { + 'version': '1.0', + 'supported_by': 'community', + 'status': ['stableinterface'] +} + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.mysql import mysql_connect +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.six import iteritems + +try: + import MySQLdb + import MySQLdb.cursors +except ImportError: + MYSQLDB_FOUND = False +else: + MYSQLDB_FOUND = True + +# =========================================== +# proxysql module specific support methods. +# + + +def perform_checks(module): + if module.params["login_port"] < 0 \ + or module.params["login_port"] > 65535: + module.fail_json( + msg="login_port must be a valid unix port number (0-65535)" + ) + + if module.params["interval_ms"] < 100 \ + or module.params["interval_ms"] > 100000000: + module.fail_json( + msg="interval_ms must between 100ms & 100000000ms" + ) + + if not MYSQLDB_FOUND: + module.fail_json( + msg="the python mysqldb module is required" + ) + + +def save_config_to_disk(cursor): + cursor.execute("SAVE SCHEDULER TO DISK") + return True + + +def load_config_to_runtime(cursor): + cursor.execute("LOAD SCHEDULER TO RUNTIME") + return True + + +class ProxySQLSchedule(object): + + def __init__(self, module): + self.state = module.params["state"] + self.force_delete = module.params["force_delete"] + self.save_to_disk = module.params["save_to_disk"] + self.load_to_runtime = module.params["load_to_runtime"] + self.active = module.params["active"] + self.interval_ms = module.params["interval_ms"] + self.filename = module.params["filename"] + + config_data_keys = ["arg1", + "arg2", + "arg3", + "arg4", + "arg5", + "comment"] + + self.config_data = dict((k, module.params[k]) + for k in config_data_keys) + + def check_schedule_config(self, cursor): + query_string = \ + """SELECT count(*) AS `schedule_count` + FROM scheduler + WHERE active = %s + AND interval_ms = %s + AND filename = %s""" + + query_data = \ + [self.active, + self.interval_ms, + self.filename] + + for col, val in iteritems(self.config_data): + if val is not None: + query_data.append(val) + query_string += "\n AND " + col + " = %s" + + cursor.execute(query_string, query_data) + check_count = cursor.fetchone() + return int(check_count['schedule_count']) + + def get_schedule_config(self, cursor): + query_string = \ + """SELECT * + FROM scheduler + WHERE active = %s + AND interval_ms = %s + AND filename = %s""" + + query_data = \ + [self.active, + self.interval_ms, + self.filename] + + for col, val in iteritems(self.config_data): + if val is not None: + query_data.append(val) + query_string += "\n AND " + col + " = %s" + + cursor.execute(query_string, query_data) + schedule = cursor.fetchall() + return schedule + + def create_schedule_config(self, cursor): + query_string = \ + """INSERT INTO scheduler ( + active, + interval_ms, + filename""" + + cols = 0 + query_data = \ + [self.active, + self.interval_ms, + self.filename] + + for col, val in iteritems(self.config_data): + if val is not None: + cols += 1 + query_data.append(val) + query_string += ",\n" + col + + query_string += \ + (")\n" + + "VALUES (%s, %s, %s" + + ", %s" * cols + + ")") + + cursor.execute(query_string, query_data) + return True + + def delete_schedule_config(self, cursor): + query_string = \ + """DELETE FROM scheduler + WHERE active = %s + AND interval_ms = %s + AND filename = %s""" + + query_data = \ + [self.active, + self.interval_ms, + self.filename] + + for col, val in iteritems(self.config_data): + if val is not None: + query_data.append(val) + query_string += "\n AND " + col + " = %s" + + cursor.execute(query_string, query_data) + check_count = cursor.rowcount + return True, int(check_count) + + def manage_config(self, cursor, state): + if state: + if self.save_to_disk: + save_config_to_disk(cursor) + if self.load_to_runtime: + load_config_to_runtime(cursor) + + def create_schedule(self, check_mode, result, cursor): + if not check_mode: + result['changed'] = \ + self.create_schedule_config(cursor) + result['msg'] = "Added schedule to scheduler" + result['schedules'] = \ + self.get_schedule_config(cursor) + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Schedule would have been added to" + + " scheduler, however check_mode" + + " is enabled.") + + def delete_schedule(self, check_mode, result, cursor): + if not check_mode: + result['schedules'] = \ + self.get_schedule_config(cursor) + result['changed'] = \ + self.delete_schedule_config(cursor) + result['msg'] = "Deleted schedule from scheduler" + self.manage_config(cursor, + result['changed']) + else: + result['changed'] = True + result['msg'] = ("Schedule would have been deleted from" + + " scheduler, however check_mode is" + + " enabled.") + +# =========================================== +# Module execution. +# + + +def main(): + module = AnsibleModule( + argument_spec=dict( + login_user=dict(default=None, type='str'), + login_password=dict(default=None, no_log=True, type='str'), + login_host=dict(default="127.0.0.1"), + login_unix_socket=dict(default=None), + login_port=dict(default=6032, type='int'), + config_file=dict(default="", type='path'), + active=dict(default=True, type='bool'), + interval_ms=dict(default=10000, type='int'), + filename=dict(required=True, type='str'), + arg1=dict(type='str'), + arg2=dict(type='str'), + arg3=dict(type='str'), + arg4=dict(type='str'), + arg5=dict(type='str'), + comment=dict(type='str'), + state=dict(default='present', choices=['present', + 'absent']), + force_delete=dict(default=False, type='bool'), + save_to_disk=dict(default=True, type='bool'), + load_to_runtime=dict(default=True, type='bool') + ), + supports_check_mode=True + ) + + perform_checks(module) + + login_user = module.params["login_user"] + login_password = module.params["login_password"] + config_file = module.params["config_file"] + + cursor = None + try: + cursor = mysql_connect(module, + login_user, + login_password, + config_file, + cursor_class=MySQLdb.cursors.DictCursor) + except MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to connect to ProxySQL Admin Module.. %s" % e + ) + + proxysql_schedule = ProxySQLSchedule(module) + result = {} + + result['state'] = proxysql_schedule.state + result['filename'] = proxysql_schedule.filename + + if proxysql_schedule.state == "present": + try: + if not 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 MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to modify schedule.. %s" % 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 MySQLdb.Error: + e = get_exception() + module.fail_json( + msg="unable to remove schedule.. %s" % e + ) + + module.exit_json(**result) + +if __name__ == '__main__': + main()