diff --git a/lib/ansible/modules/database/proxysql/__init__.py b/lib/ansible/modules/database/proxysql/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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 0000000000..969a78cc40
--- /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 0000000000..617ec18561
--- /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 0000000000..c440d29f40
--- /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 0000000000..e68e516fce
--- /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 0000000000..d85e410225
--- /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 0000000000..d32d6b98b2
--- /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 0000000000..714d880c60
--- /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()