├── .gitignore ├── damp ├── bin │ └── run-playbook ├── roles │ ├── mha │ │ ├── tasks │ │ │ ├── main.yml │ │ │ ├── mha_node.yml │ │ │ ├── mha_config.yml │ │ │ └── mha_manager.yml │ │ ├── files │ │ │ └── mha │ │ │ │ ├── mha4mysql-node_0.56-0_all.deb │ │ │ │ ├── mha4mysql-manager_0.56-0_all.deb │ │ │ │ └── scripts │ │ │ │ └── master_ip_online_change │ │ └── templates │ │ │ └── mha_cluster.j2 │ ├── sysbench │ │ ├── files │ │ │ └── sysbench_0.5-3.xenial_amd64.deb │ │ └── tasks │ │ │ └── main.yml │ ├── orchestrator │ │ ├── templates │ │ │ ├── mysqld.cnf.j2 │ │ │ └── orchestrator-sample.conf.json.j2 │ │ └── tasks │ │ │ └── main.yml │ └── proxysql │ │ ├── templates │ │ ├── proxysql.conf.j2 │ │ └── proxysql_menu.sh.j2 │ │ └── tasks │ │ └── main.yml ├── setup.yml ├── group_vars │ └── all └── library │ ├── proxysql_manage_config.py │ ├── proxysql_global_variables.py │ ├── proxysql_replication_hostgroups.py │ ├── proxysql_scheduler.py │ ├── proxysql_mysql_users.py │ ├── proxysql_backend_servers.py │ └── proxysql_query_rules.py ├── proxysql_login_docker.sh ├── damp_start.sh ├── Dockerfile ├── damp_reset.sh ├── my.cnf ├── damp_create_cluster.sh ├── proxysql_menu.sh └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | mysql_hosts/* 2 | damp/hostfile 3 | -------------------------------------------------------------------------------- /damp/bin/run-playbook: -------------------------------------------------------------------------------- 1 | ansible-playbook -vvv -i hostfile setup.yml --limit localhost 2 | -------------------------------------------------------------------------------- /proxysql_login_docker.sh: -------------------------------------------------------------------------------- 1 | docker exec -it damp_proxysql bash -c 'cd /root/build/damp/;/bin/bash ' 2 | 3 | -------------------------------------------------------------------------------- /damp/roles/mha/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - include: mha_node.yml 2 | - include: mha_manager.yml 3 | - include: mha_config.yml 4 | -------------------------------------------------------------------------------- /damp/roles/mha/files/mha/mha4mysql-node_0.56-0_all.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miklos-szel/Ansible-MHA-ProxySQL-Docker/HEAD/damp/roles/mha/files/mha/mha4mysql-node_0.56-0_all.deb -------------------------------------------------------------------------------- /damp/roles/mha/files/mha/mha4mysql-manager_0.56-0_all.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miklos-szel/Ansible-MHA-ProxySQL-Docker/HEAD/damp/roles/mha/files/mha/mha4mysql-manager_0.56-0_all.deb -------------------------------------------------------------------------------- /damp/roles/sysbench/files/sysbench_0.5-3.xenial_amd64.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miklos-szel/Ansible-MHA-ProxySQL-Docker/HEAD/damp/roles/sysbench/files/sysbench_0.5-3.xenial_amd64.deb -------------------------------------------------------------------------------- /damp/setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: proxysql 3 | connection: local 4 | roles: 5 | - { role: proxysql, when: roles_enabled.proxysql } 6 | - { role: mha, when: roles_enabled.mha } 7 | - { role: sysbench, when: roles_enabled.sysbench } 8 | - { role: orchestrator, when: roles_enabled.orchestrator } 9 | -------------------------------------------------------------------------------- /damp_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | list=$(docker ps -a |grep 'damp_proxysql') 3 | if [[ "$list" != "" ]] 4 | then 5 | echo "Stopping and removing the existing damp_proxysql container" 6 | docker stop damp_proxysql 7 | docker rm -v damp_proxysql 8 | fi 9 | docker run -p 3000:3000 -p 6032:6032 -p 6033:6033 -v `pwd`:/root/build --name damp_proxysql -it damp 10 | 11 | -------------------------------------------------------------------------------- /damp/roles/mha/tasks/mha_node.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install dependencies for MHA Manager 3 | apt: name="{{ item }}" state=present 4 | with_items: 5 | - libdbd-mysql-perl 6 | 7 | 8 | - copy: 9 | src=mha/mha4mysql-node_0.56-0_all.deb 10 | dest=/tmp/ 11 | owner=root 12 | mode=0700 13 | 14 | - name: Install mha manager 15 | apt: 16 | deb=/tmp/mha4mysql-node_0.56-0_all.deb 17 | 18 | -------------------------------------------------------------------------------- /damp/roles/mha/tasks/mha_config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - set_fact: 3 | server_groups="{{ groups.keys() | map('regex_search','damp.*') | select('string') | list }}" 4 | 5 | - file: > 6 | path=/etc/mha/ 7 | owner=root group=root 8 | mode=0700 9 | state=directory 10 | 11 | - name: Generate MHA config file for cluster {{ item }} 12 | template: > 13 | src=mha_cluster.j2 14 | dest=/etc/mha/mha_{{ item }}.cnf 15 | owner=root 16 | group=root 17 | mode=600 18 | register: mha_conf_check 19 | with_items: 20 | - "{{ server_groups }}" 21 | 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | 4 | RUN apt-get update && apt-get install -y \ 5 | curl \ 6 | less \ 7 | ssh \ 8 | sudo \ 9 | vim 10 | 11 | #RUN ssh-keygen -q -t rsa -f /root/.ssh/id_rsa 12 | #RUN cp /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys 13 | 14 | RUN apt-get install -y software-properties-common 15 | RUN apt-add-repository ppa:ansible/ansible 16 | RUN apt-get update 17 | RUN apt-get install -y ansible 18 | 19 | ENV PATH $PATH:/root/ 20 | WORKDIR /root 21 | CMD cd build/damp/ ; ansible-playbook -i hostfile setup.yml --limit localhost ; /bin/bash 22 | 23 | -------------------------------------------------------------------------------- /damp_reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | currdir=`pwd` 3 | servers="$currdir/damp/hostfile" 4 | server_name=damp_server 5 | list=$(docker ps -a |grep ${server_name}) 6 | if [[ "$list" != "" ]] 7 | then 8 | echo "Stopping and removing the following containers:" 9 | docker ps -a |grep ${server_name} 10 | docker ps -a |grep ${server_name}|awk '{print $1 }'|xargs docker stop 11 | docker ps -a |grep ${server_name}|awk '{print $1 }'|xargs docker rm -f 12 | [ -d $currdir/mysql_hosts/ ] && rm -rf $currdir/mysql_hosts/ 13 | else 14 | echo "no $server_name containers are running" 15 | fi 16 | docker stop damp_proxysql 17 | docker rm damp_proxysql 18 | 19 | rm -f $servers 20 | -------------------------------------------------------------------------------- /damp/roles/sysbench/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Copy sysbench 3 | copy: 4 | src=sysbench_0.5-3.xenial_amd64.deb 5 | dest=/tmp/sysbench_0.5-3.xenial_amd64.deb 6 | owner=root 7 | mode=0644 8 | 9 | - name: Install sysbench 10 | apt: 11 | deb: /tmp/sysbench_0.5-3.xenial_amd64.deb 12 | 13 | - name: create database sbtest on all clusters 14 | mysql_db: > 15 | login_host={{ hostvars[item]['inventory_hostname'] }} 16 | login_user={{ mysql.login_user }} 17 | login_password={{ mysql.login_passwd }} 18 | name=sbtest 19 | state=present 20 | when: hostvars[item]['mysql_role'] == "master" 21 | with_inventory_hostnames: all:!proxysql 22 | 23 | 24 | -------------------------------------------------------------------------------- /damp/roles/mha/templates/mha_cluster.j2: -------------------------------------------------------------------------------- 1 | [server default] 2 | # mysql user and password 3 | user={{ mysql.login_user }} 4 | password={{ mysql.login_passwd }} 5 | # replication user password 6 | repl_user={{ mysql.login_user }} 7 | repl_password={{ mysql.login_passwd }} 8 | 9 | remote_workdir=/var/tmp 10 | # working directory on the manager 11 | manager_workdir=/var/log/mha/{{ item }} 12 | 13 | # manager log file 14 | manager_log=/var/log/mha/{{ item }}/{{ item }}.log 15 | ping_interval=15 16 | 17 | master_ip_online_change_script=/usr/local/bin/master_ip_online_change 18 | 19 | master_pid_file=/var/log/mysqld.pid 20 | ssh_user=root 21 | log_level=debug 22 | 23 | 24 | {% for instance in groups[item] %} 25 | [server_{{ instance }}] 26 | hostname={{ instance }} 27 | port=3306 28 | {% endfor -%} 29 | 30 | -------------------------------------------------------------------------------- /my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | binlog_format = MIXED 3 | server-id= 4 | log-bin 5 | read_only=1 6 | innodb_buffer_pool_size=32MB 7 | default-storage-engine=InnoDB 8 | innodb_log_buffer_size=256K 9 | query_cache_size=0 10 | max_connections=200 11 | key_buffer_size=8 12 | thread_cache_size=0 13 | host_cache_size=0 14 | innodb_ft_cache_size=1600000 15 | innodb_ft_total_cache_size=32000000 16 | log_slave_updates 17 | master_info_repository=TABLE 18 | 19 | # per thread or per operation settings 20 | thread_stack=131072 21 | sort_buffer_size=32K 22 | read_buffer_size=8200 23 | read_rnd_buffer_size=8200 24 | max_heap_table_size=16K 25 | tmp_table_size=1K 26 | bulk_insert_buffer_size=0 27 | join_buffer_size=128 28 | net_buffer_length=1K 29 | innodb_sort_buffer_size=64K 30 | 31 | #settings that relate to the binary log (if enabled) 32 | binlog_cache_size=4K 33 | binlog_stmt_cache_size=4K 34 | 35 | performance_schema=OFF 36 | 37 | #log=/var/log/mysql/mysql.log 38 | #log_error=/var/log/mysql/mysql.err 39 | #general_log_file=/var/log/mysql/mysql_general.log 40 | #slow_query_log_file= /var/log/mysql/mysql_slow.log 41 | 42 | 43 | -------------------------------------------------------------------------------- /damp/roles/orchestrator/templates/mysqld.cnf.j2: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | binlog_format = MIXED 3 | server-id=99999 4 | log-bin 5 | read_only=1 6 | innodb_buffer_pool_size=32MB 7 | default-storage-engine=InnoDB 8 | innodb_log_buffer_size=256K 9 | query_cache_size=0 10 | max_connections=200 11 | key_buffer_size=8 12 | thread_cache_size=0 13 | host_cache_size=0 14 | innodb_ft_cache_size=1600000 15 | innodb_ft_total_cache_size=32000000 16 | log_slave_updates 17 | master_info_repository=TABLE 18 | 19 | # per thread or per operation settings 20 | thread_stack=131072 21 | sort_buffer_size=32K 22 | read_buffer_size=8200 23 | read_rnd_buffer_size=8200 24 | max_heap_table_size=16K 25 | tmp_table_size=1K 26 | bulk_insert_buffer_size=0 27 | join_buffer_size=128 28 | net_buffer_length=1K 29 | innodb_sort_buffer_size=64K 30 | 31 | #settings that relate to the binary log (if enabled) 32 | binlog_cache_size=4K 33 | binlog_stmt_cache_size=4K 34 | 35 | performance_schema=OFF 36 | 37 | #log=/var/log/mysql/mysql.log 38 | #log_error=/var/log/mysql/mysql.err 39 | #general_log_file=/var/log/mysql/mysql_general.log 40 | #slow_query_log_file= /var/log/mysql/mysql_slow.log 41 | -------------------------------------------------------------------------------- /damp/roles/mha/tasks/mha_manager.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - set_fact: 3 | server_groups="{{ groups.keys() | map('regex_search','damp.*') | select('string') | list }}" 4 | 5 | - name: install dependencies for MHA Manager 6 | apt: name="{{ item }}" state=present update_cache=yes cache_valid_time=3600 7 | with_items: 8 | - libdbi-perl 9 | - libdbd-mysql-perl 10 | - libconfig-tiny-perl 11 | - liblog-dispatch-perl 12 | - libparallel-forkmanager-perl 13 | 14 | - name: create log directories for MHA 15 | file: > 16 | path=/var/log/mha/{{ item }} 17 | owner=root 18 | group=root 19 | mode=0700 20 | state=directory 21 | with_items: 22 | - "{{ server_groups }}" 23 | 24 | - copy: 25 | src=mha/mha4mysql-manager_0.56-0_all.deb 26 | dest=/tmp/ 27 | owner=root 28 | mode=0700 29 | 30 | - name: Install mha manager 31 | apt: 32 | deb=/tmp/mha4mysql-manager_0.56-0_all.deb 33 | 34 | - name: copy mha scripts 35 | copy: > 36 | src=mha/scripts/{{ item }} 37 | dest=/usr/local/bin/{{ item }} 38 | owner=root 39 | mode=0700 40 | with_items: 41 | - master_ip_online_change 42 | 43 | 44 | -------------------------------------------------------------------------------- /damp/group_vars/all: -------------------------------------------------------------------------------- 1 | roles_enabled: 2 | proxysql: true 3 | mha: true 4 | sysbench: true 5 | orchestrator: true 6 | 7 | proxysql_version: 1.3.3 8 | proxysql_filename: proxysql_{{ proxysql_version }}-ubuntu14_amd64.deb 9 | 10 | 11 | proxysql: 12 | admin: 13 | host: 127.0.0.1 14 | port: 6032 15 | user: admin 16 | passwd: admin 17 | interface: 0.0.0.0 18 | app: 19 | user: app 20 | passwd: gempa 21 | default_hostgroup: 1 22 | port: 6033 23 | priv: '*.*:CREATE,DELETE,DROP,EXECUTE,INSERT,SELECT,UPDATE,INDEX' 24 | host: '%' 25 | max_conn: 200 26 | transaction_persistent: false 27 | monitor: 28 | user: monitor 29 | passwd: monitor 30 | priv: '*.*:USAGE,REPLICATION CLIENT' 31 | host: '%' 32 | global_variables: 33 | mysql-default_query_timeout: 120000 34 | mysql-max_allowed_packet: 67108864 35 | mysql-monitor_read_only_timeout: 600 36 | mysql-monitor_ping_timeout: 600 37 | mysql-max_connections: 1024 38 | misc: 39 | max_replication_lag: 3 40 | mysql: 41 | login_user: root 42 | login_passwd: mysecretpass 43 | repl_user: repl 44 | repl_passwd: slavepass 45 | orchestrator: 46 | auto_failover: false 47 | -------------------------------------------------------------------------------- /damp/roles/proxysql/templates/proxysql.conf.j2: -------------------------------------------------------------------------------- 1 | #file proxysql.cfg 2 | 3 | # This config file is parsed using libconfig , and its grammar is described in: 4 | # http://www.hyperrealm.com/libconfig/libconfig_manual.html#Configuration-File-Grammar 5 | # Grammar is also copied at the end of this file 6 | 7 | 8 | 9 | datadir="/var/lib/proxysql" 10 | 11 | admin_variables= 12 | { 13 | admin_credentials="{{ proxysql.admin.user }}:{{ proxysql.admin.passwd }}" 14 | mysql_ifaces="{{ proxysql.admin.interface }}:{{ proxysql.admin.port }}" 15 | refresh_interval=2000 16 | } 17 | 18 | mysql_variables= 19 | { 20 | threads=4 21 | max_connections=2048 22 | default_query_delay=0 23 | default_query_timeout=36000000 24 | have_compress=true 25 | poll_timeout=2000 26 | interfaces="0.0.0.0:{{ proxysql.app.port }};/tmp/proxysql.sock" 27 | default_schema="information_schema" 28 | stacksize=1048576 29 | server_version="5.5.30" 30 | connect_timeout_server=3000 31 | monitor_history=600000 32 | monitor_connect_interval=60000 33 | monitor_ping_interval=10000 34 | monitor_read_only_interval=1500 35 | monitor_read_only_timeout=500 36 | ping_interval_server=120000 37 | ping_timeout_server=500 38 | commands_stats=true 39 | sessions_sort=true 40 | connect_retries_on_failure=10 41 | } 42 | 43 | 44 | # defines all the MySQL servers 45 | mysql_servers = 46 | ( 47 | ) 48 | 49 | 50 | # defines all the MySQL users 51 | mysql_users: 52 | ( 53 | ) 54 | 55 | 56 | 57 | #defines MySQL Query Rules 58 | mysql_query_rules: 59 | ( 60 | ) 61 | 62 | 63 | -------------------------------------------------------------------------------- /damp/roles/orchestrator/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - set_fact: 3 | server_groups="{{ groups.keys() | map('regex_search','damp.*') | select('string') | list }}" 4 | 5 | - name: Install MySQL-server for Orchestrator 6 | apt: name="{{ item }}" state=present update_cache=yes cache_valid_time=3600 7 | with_items: 8 | - mysql-server-5.7 9 | 10 | - name: Add a lightweight mysqld.conf 11 | template: 12 | src=mysqld.cnf.j2 13 | dest=/etc/mysql/mysql.conf.d/mysqld.cnf 14 | owner=root 15 | mode=0775 16 | 17 | - name: Start MySQL 18 | service: 19 | name=mysql 20 | state=running 21 | enabled=yes 22 | 23 | - name: Install Orchestrator 2.1.4 24 | apt: > 25 | deb=https://github.com/github/orchestrator/releases/download/v2.1.4/orchestrator_2.1.4_amd64.deb 26 | 27 | - name: crate symlink to the binary 28 | file: 29 | src=/usr/local/orchestrator/orchestrator 30 | dest=/usr/local/bin/orchestrator 31 | state=link 32 | 33 | - name: Generate Orchestrator config 34 | template: 35 | src=orchestrator-sample.conf.json.j2 36 | dest=/etc/orchestrator.conf.json 37 | owner=root 38 | mode=0700 39 | 40 | - name: Create a new database orchestrator 41 | mysql_db: 42 | name: orchestrator 43 | state: present 44 | 45 | - name: Start Orchestrator 46 | service: 47 | name=orchestrator 48 | state=restarted 49 | 50 | #- wait_for: host={{ proxysql.admin.host }} port={{ proxysql.admin.port }} delay=3 state=started 51 | 52 | - name: Add servers to orchestrator 53 | shell: orchestrator -c discover -i {{ hostvars[item]['inventory_hostname'] }}:3306 cli 54 | with_inventory_hostnames: all:!proxysql 55 | -------------------------------------------------------------------------------- /damp_create_cluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #brew install gnu-sed 3 | currdir=`pwd` 4 | servers="$currdir/damp/hostfile" 5 | touch $servers 6 | #server1 will be the initial master 7 | 8 | numargs=$# 9 | if [ $numargs -lt 2 ]; then 10 | echo "usage $0 clustername num_of_servers_including_master" 11 | exit 1 12 | fi 13 | 14 | server_name=damp_server_$1 15 | num_of_servers=$2 16 | replication_type=${3:-gtid} 17 | last_hostgroup=$(grep "hostgroup" $servers |tail -n 1 |cut -d":" -f 2 |sed 's/ //' ) 18 | if [[ -z "$last_hostgroup" ]] 19 | then 20 | hostgroup=1 21 | echo -e "[proxysql]\nlocalhost\n\n" >>$servers 22 | else 23 | hostgroup=$(( $last_hostgroup + 2 )) 24 | fi 25 | 26 | 27 | list=$(docker ps -a --format '{{.Names}}'|grep -E "^${server_name}[0-9]{1,2}$") 28 | if [[ "$list" != "" ]] 29 | then 30 | echo "Containers with name: $server_name are already running, quit!" 31 | exit 1 32 | fi 33 | 34 | docker-ip() { 35 | local ip=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "$@") 36 | echo $ip 37 | } 38 | 39 | 40 | 41 | echo "Starting the following containers in $server_name cluster:" 42 | for i in $(seq 1 $num_of_servers) 43 | do 44 | mkdir -p $currdir/mysql_hosts/${server_name}${i}/conf.d $currdir/mysql_hosts/${server_name}${i}/log_mysql 45 | if [ $i == "1" ] 46 | then 47 | sed -e "s/server-id=/server-id=1/" -e "s/read_only=1/read_only=0/" $currdir/my.cnf> $currdir/mysql_hosts/${server_name}${i}/conf.d/my.cnf 48 | else 49 | sed -e "s/server-id=/server-id=${i}/" $currdir/my.cnf> $currdir/mysql_hosts/${server_name}${i}/conf.d/my.cnf 50 | fi 51 | if [ "$replication_type" == "gtid" ] 52 | then 53 | echo "gtid-mode=ON" >>$currdir/mysql_hosts/${server_name}${i}/conf.d/my.cnf 54 | echo "enforce-gtid-consistency" >>$currdir/mysql_hosts/${server_name}${i}/conf.d/my.cnf 55 | fi 56 | 57 | cid=$(docker run --name ${server_name}${i} -h ${server_name}${i} -d -v $currdir/mysql_hosts/${server_name}${i}/conf.d:/etc/mysql/conf.d -v $currdir/mysql_hosts/${server_name}${i}/log_mysql:/var/log/mysql -e MYSQL_ROOT_PASSWORD=mysecretpass -d mysql:5.6) 58 | 59 | server_ip=$( docker-ip $cid ) 60 | echo "${server_name}${i} $cid($server_ip)" 61 | serverlist=("${serverlist[@]}" "$server_ip" ) 62 | 63 | if [ $i == "1" ] 64 | then 65 | master_ip=$server_ip 66 | fi 67 | 68 | done 69 | 70 | 71 | #waiting for the last server to be available 72 | isup=0 73 | 74 | until [ $isup -eq "1" ] 75 | 76 | do 77 | isup=$(docker exec -ti ${server_name}${num_of_servers} 'mysql' -NB -uroot -pmysecretpass -e"select(234);" |grep "234" |wc -l ) 78 | sleep 3 79 | echo "waiting for the ${server_name}${num_of_servers} to be available" 80 | done 81 | 82 | echo "add replication user to the master (${server_name})" 83 | docker exec -ti ${server_name}1 'mysql' -uroot -pmysecretpass -vvv -e "select @@version;" 84 | docker exec -ti ${server_name}1 'mysql' -uroot -pmysecretpass -vvv -e "GRANT REPLICATION SLAVE ON *.* TO repl@'%' IDENTIFIED BY 'slavepass'\G" 85 | 86 | #configure replication on all hosts 87 | for i in $(seq 2 $num_of_servers) 88 | do 89 | if [ "$replication_type" == "gtid" ] 90 | then 91 | docker exec -ti ${server_name}${i} 'mysql' -uroot -pmysecretpass -e"change master to master_host='$master_ip',master_user='repl',master_password='slavepass',master_auto_position = 1;" -vvv 92 | else 93 | docker exec -ti ${server_name}${i} 'mysql' -uroot -pmysecretpass -e"change master to master_host='$master_ip',master_user='repl',master_password='slavepass',master_log_file='mysqld-bin.000004',master_log_pos=120;" -vvv 94 | fi 95 | 96 | echo "start replication" 97 | docker exec -ti ${server_name}${i} 'mysql' -uroot -pmysecretpass -e"START SLAVE\G" -vvv 98 | 99 | echo "show slave status" 100 | docker exec -ti ${server_name}${i} 'mysql' -uroot -pmysecretpass -e"SHOW SLAVE STATUS\G" -vvv 101 | 102 | done 103 | 104 | #update the ansible yml file with this cluster's data 105 | echo -e "[${server_name}]" >>$servers 106 | for item in ${serverlist[*]} 107 | do 108 | if [ "$item" == "$master_ip" ] 109 | then 110 | echo "$item mysql_role=master" >>$servers 111 | else 112 | echo "$item mysql_role=slave" >>$servers 113 | fi 114 | done 115 | echo -e "\n[${server_name}:vars] 116 | cluster=${server_name} 117 | hostgroup=$hostgroup 118 | \n" >>$servers 119 | 120 | 121 | -------------------------------------------------------------------------------- /damp/roles/orchestrator/templates/orchestrator-sample.conf.json.j2: -------------------------------------------------------------------------------- 1 | { 2 | "Debug": true, 3 | "EnableSyslog": false, 4 | "ListenAddress": ":3000", 5 | "MySQLTopologyUser": "{{ mysql.login_user }}", 6 | "MySQLTopologyPassword": "{{ mysql.login_passwd }}", 7 | "MySQLTopologyCredentialsConfigFile": "", 8 | "MySQLTopologySSLPrivateKeyFile": "", 9 | "MySQLTopologySSLCertFile": "", 10 | "MySQLTopologySSLCAFile": "", 11 | "MySQLTopologySSLSkipVerify": true, 12 | "MySQLTopologyUseMutualTLS": false, 13 | "MySQLOrchestratorHost": "127.0.0.1", 14 | "MySQLOrchestratorPort": 3306, 15 | "MySQLOrchestratorDatabase": "orchestrator", 16 | "MySQLOrchestratorUser": "root", 17 | "MySQLOrchestratorPassword": "", 18 | "MySQLOrchestratorCredentialsConfigFile": "", 19 | "MySQLOrchestratorSSLPrivateKeyFile": "", 20 | "MySQLOrchestratorSSLCertFile": "", 21 | "MySQLOrchestratorSSLCAFile": "", 22 | "MySQLOrchestratorSSLSkipVerify": true, 23 | "MySQLOrchestratorUseMutualTLS": false, 24 | "MySQLConnectTimeoutSeconds": 1, 25 | "DefaultInstancePort": 3306, 26 | "DiscoverByShowSlaveHosts": false, 27 | "InstancePollSeconds": 5, 28 | "UnseenInstanceForgetHours": 240, 29 | "SnapshotTopologiesIntervalHours": 0, 30 | "InstanceBulkOperationsWaitTimeoutSeconds": 10, 31 | "HostnameResolveMethod": "none", 32 | "MySQLHostnameResolveMethod": "none", 33 | "SkipBinlogServerUnresolveCheck": true, 34 | "ExpiryHostnameResolvesMinutes": 60, 35 | "RejectHostnameResolvePattern": "", 36 | "ReasonableReplicationLagSeconds": 10, 37 | "ProblemIgnoreHostnameFilters": [], 38 | "VerifyReplicationFilters": false, 39 | "ReasonableMaintenanceReplicationLagSeconds": 20, 40 | "CandidateInstanceExpireMinutes": 60, 41 | "AuditLogFile": "", 42 | "AuditToSyslog": false, 43 | "RemoveTextFromHostnameDisplay": ".mydomain.com:3306", 44 | "ReadOnly": false, 45 | "AuthenticationMethod": "", 46 | "HTTPAuthUser": "", 47 | "HTTPAuthPassword": "", 48 | "AuthUserHeader": "", 49 | "PowerAuthUsers": [ 50 | "*" 51 | ], 52 | "ClusterNameToAlias": { 53 | "127.0.0.1": "test suite" 54 | }, 55 | "SlaveLagQuery": "", 56 | "DetectClusterAliasQuery": "SELECT SUBSTRING_INDEX(@@hostname, '.', 1)", 57 | "DetectClusterDomainQuery": "", 58 | "DetectInstanceAliasQuery": "", 59 | "DetectPromotionRuleQuery": "", 60 | "DataCenterPattern": "[.]([^.]+)[.][^.]+[.]mydomain[.]com", 61 | "PhysicalEnvironmentPattern": "[.]([^.]+[.][^.]+)[.]mydomain[.]com", 62 | "PromotionIgnoreHostnameFilters": [], 63 | "DetectSemiSyncEnforcedQuery": "", 64 | "ServeAgentsHttp": false, 65 | "AgentsServerPort": ":3001", 66 | "AgentsUseSSL": false, 67 | "AgentsUseMutualTLS": false, 68 | "AgentSSLSkipVerify": false, 69 | "AgentSSLPrivateKeyFile": "", 70 | "AgentSSLCertFile": "", 71 | "AgentSSLCAFile": "", 72 | "AgentSSLValidOUs": [], 73 | "UseSSL": false, 74 | "UseMutualTLS": false, 75 | "SSLSkipVerify": false, 76 | "SSLPrivateKeyFile": "", 77 | "SSLCertFile": "", 78 | "SSLCAFile": "", 79 | "SSLValidOUs": [], 80 | "URLPrefix": "", 81 | "StatusEndpoint": "/api/status", 82 | "StatusSimpleHealth": true, 83 | "StatusOUVerify": false, 84 | "AgentPollMinutes": 60, 85 | "UnseenAgentForgetHours": 6, 86 | "StaleSeedFailMinutes": 60, 87 | "SeedAcceptableBytesDiff": 8192, 88 | "PseudoGTIDPattern": "", 89 | "PseudoGTIDPatternIsFixedSubstring": false, 90 | "PseudoGTIDMonotonicHint": "asc:", 91 | "DetectPseudoGTIDQuery": "", 92 | "BinlogEventsChunkSize": 10000, 93 | "SkipBinlogEventsContaining": [], 94 | "ReduceReplicationAnalysisCount": true, 95 | "FailureDetectionPeriodBlockMinutes": 60, 96 | "RecoveryPollSeconds": 10, 97 | "RecoveryPeriodBlockSeconds": 3600, 98 | "RecoveryIgnoreHostnameFilters": [], 99 | "RecoverMasterClusterFilters": [ 100 | {% if orchestrator.auto_failover %} 101 | ".*" 102 | {% else %} 103 | "_master_pattern_" 104 | {% endif %} 105 | ], 106 | "RecoverIntermediateMasterClusterFilters": [ 107 | "_intermediate_master_pattern_" 108 | ], 109 | "OnFailureDetectionProcesses": [ 110 | "echo 'Detected {failureType} on {failureCluster}. Affected replicas: {countSlaves}' >> /tmp/recovery.log" 111 | ], 112 | "PreFailoverProcesses": [ 113 | "echo 'Will recover from {failureType} on {failureCluster}' >> /tmp/recovery.log" 114 | ], 115 | "PostFailoverProcesses": [ 116 | "echo '(for all types) Recovered from {failureType} on {failureCluster}. Failed: {failedHost}:{failedPort}; Successor: {successorHost}:{successorPort}' >> /tmp/recovery.log" 117 | ], 118 | "PostUnsuccessfulFailoverProcesses": [], 119 | "PostMasterFailoverProcesses": [ 120 | "echo 'Recovered from {failureType} on {failureCluster}. Failed: {failedHost}:{failedPort}; Promoted: {successorHost}:{successorPort}' >> /tmp/recovery.log" 121 | ], 122 | "PostIntermediateMasterFailoverProcesses": [ 123 | "echo 'Recovered from {failureType} on {failureCluster}. Failed: {failedHost}:{failedPort}; Successor: {successorHost}:{successorPort}' >> /tmp/recovery.log" 124 | ], 125 | "CoMasterRecoveryMustPromoteOtherCoMaster": true, 126 | "DetachLostSlavesAfterMasterFailover": true, 127 | "ApplyMySQLPromotionAfterMasterFailover": true, 128 | "MasterFailoverDetachSlaveMasterHost": false, 129 | "MasterFailoverLostInstancesDowntimeMinutes": 0, 130 | "PostponeSlaveRecoveryOnLagMinutes": 0, 131 | "OSCIgnoreHostnameFilters": [], 132 | "GraphiteAddr": "", 133 | "GraphitePath": "", 134 | "GraphiteConvertHostnameDotsToUnderscores": true 135 | } 136 | -------------------------------------------------------------------------------- /damp/roles/proxysql/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - set_fact: lsb_release={{ ansible_lsb.codename }} 3 | 4 | - name: add Percona repo 5 | apt_repository: repo="deb http://repo.percona.com/apt {{ lsb_release }} main" 6 | 7 | - name: Add signing key for Percona repo 8 | apt_key: 9 | id: 8507EFA5 10 | url: "https://percona.com/downloads/deb-percona-keyring.gpg" 11 | keyring: /etc/apt/trusted.gpg.d/debian.gpg 12 | 13 | - name: Install ProxySQL package and prerequisites 14 | apt: 15 | name={{ item }} 16 | update_cache=yes 17 | with_items: 18 | - mysql-client 19 | - python-mysqldb 20 | - proxysql 21 | 22 | 23 | #https://github.com/sysown/proxysql/blob/master/doc/configuration_system.md 24 | - name: generate proxysql.conf based on template 25 | template: 26 | src=proxysql.conf.j2 27 | dest=/etc/proxysql.cnf 28 | 29 | 30 | - service: > 31 | name=proxysql 32 | state=running 33 | enabled=yes 34 | 35 | - wait_for: host={{ proxysql.admin.host }} port={{ proxysql.admin.port }} delay=3 state=started 36 | 37 | - name: create 'app' user on the mysql masters 38 | mysql_user: > 39 | login_host={{ hostvars[item]['inventory_hostname'] }} 40 | login_user={{ mysql.login_user }} 41 | login_password={{ mysql.login_passwd }} 42 | name={{ proxysql.app.user }} 43 | password={{ proxysql.app.passwd }} 44 | priv={{ proxysql.app.priv }} 45 | host={{ proxysql.app.host }} 46 | state=present 47 | when: hostvars[item]['mysql_role'] == "master" 48 | with_inventory_hostnames: all:!proxysql 49 | 50 | 51 | - name: create 'app' user on the mysql masters 52 | mysql_user: > 53 | login_host={{ hostvars[item]['inventory_hostname'] }} 54 | login_user={{ mysql.login_user }} 55 | login_password={{ mysql.login_passwd }} 56 | name="app{{ hostvars[item]['hostgroup'] }}" 57 | password="app{{ hostvars[item]['hostgroup'] }}" 58 | priv={{ proxysql.app.priv }} 59 | host={{ proxysql.app.host }} 60 | state=present 61 | when: hostvars[item]['mysql_role'] == "master" 62 | with_inventory_hostnames: all:!proxysql 63 | 64 | - name: Create 'monitor' user on the mysql masters 65 | mysql_user: > 66 | login_host={{ hostvars[item]['inventory_hostname'] }} 67 | login_user={{ mysql.login_user }} 68 | login_password={{ mysql.login_passwd }} 69 | name={{ proxysql.monitor.user }} 70 | password={{ proxysql.monitor.passwd }} 71 | priv={{ proxysql.monitor.priv }} 72 | host={{ proxysql.monitor.host }} 73 | state=present 74 | when: hostvars[item]['mysql_role'] == "master" 75 | with_inventory_hostnames: all:!proxysql 76 | 77 | - name: proxysql | config | add ProxySQL app users 78 | proxysql_mysql_users: 79 | login_host: "{{ proxysql.admin.host }}" 80 | login_port: "{{ proxysql.admin.port }}" 81 | login_user: "{{ proxysql.admin.user }}" 82 | login_password: "{{ proxysql.admin.passwd }}" 83 | username: "app{{ hostvars[item]['hostgroup'] }}" 84 | password: "app{{ hostvars[item]['hostgroup'] }}" 85 | max_connections: "{{ proxysql.app.max_conn }}" 86 | default_hostgroup: "{{ hostvars[item]['hostgroup'] }}" 87 | transaction_persistent: "{{ proxysql.app.transaction_persistent }}" 88 | state: present 89 | load_to_runtime: True 90 | when: hostvars[item]['mysql_role'] == "master" 91 | with_inventory_hostnames: all:!proxysql 92 | 93 | - name: proxysql | config | manage monitor username 94 | proxysql_global_variables: 95 | login_host: "{{ proxysql.admin.host }}" 96 | login_port: "{{ proxysql.admin.port }}" 97 | login_user: "{{ proxysql.admin.user }}" 98 | login_password: "{{ proxysql.admin.passwd }}" 99 | variable: "mysql-monitor_username" 100 | value: "{{ proxysql.monitor.passwd }}" 101 | 102 | - name: proxysql | config | manage monitor password 103 | proxysql_global_variables: 104 | login_host: "{{ proxysql.admin.host }}" 105 | login_port: "{{ proxysql.admin.port }}" 106 | login_user: "{{ proxysql.admin.user }}" 107 | login_password: "{{ proxysql.admin.passwd }}" 108 | variable: "mysql-monitor_password" 109 | value: "{{ proxysql.monitor.passwd }}" 110 | 111 | - name: proxysql | config | set global_variables 112 | proxysql_global_variables: 113 | login_host: "{{ proxysql.admin.host }}" 114 | login_port: "{{ proxysql.admin.port }}" 115 | login_user: "{{ proxysql.admin.user }}" 116 | login_password: "{{ proxysql.admin.passwd }}" 117 | variable: "{{ item.key }}" 118 | value: "{{ item.value }}" 119 | with_dict: "{{ proxysql.global_variables }}" 120 | 121 | - name: proxysql | config | add replication hostgroups 122 | proxysql_replication_hostgroups: 123 | login_host: "{{ proxysql.admin.host }}" 124 | login_port: "{{ proxysql.admin.port }}" 125 | login_user: "{{ proxysql.admin.user }}" 126 | login_password: "{{ proxysql.admin.passwd }}" 127 | writer_hostgroup: "{{ hostvars[item]['hostgroup'] }}" 128 | reader_hostgroup: "{{ hostvars[item]['hostgroup'] | int + 1 }}" 129 | comment: "{{ hostvars[item]['cluster'] }}" 130 | load_to_runtime: True 131 | state: present 132 | when: hostvars[item]['mysql_role'] == "master" 133 | with_inventory_hostnames: all:!proxysql 134 | 135 | - name: Workaround - ProxySQL monitor runs DMLs on mysql_servers, disabling it while adding the servers 136 | shell: > 137 | mysql 138 | --user={{ proxysql.admin.user }} 139 | --password={{ proxysql.admin.passwd }} 140 | --host={{ proxysql.admin.host }} 141 | --port={{ proxysql.admin.port }} 142 | --execute "set mysql-monitor.enabled='false'; LOAD MYSQL VARIABLES TO RUNTIME;" 143 | 144 | 145 | - name: proxysql | config | add server 146 | proxysql_backend_servers: 147 | login_host: "{{ proxysql.admin.host }}" 148 | login_port: "{{ proxysql.admin.port }}" 149 | login_user: "{{ proxysql.admin.user }}" 150 | login_password: "{{ proxysql.admin.passwd }}" 151 | hostname: "{{ hostvars[item]['inventory_hostname'] }}" 152 | hostgroup_id: "{{ hostvars[item]['hostgroup'] }}" 153 | max_replication_lag: "{{ proxysql.misc.max_replication_lag }}" 154 | comment: "{{ hostvars[item]['cluster'] }}" 155 | port: "3306" 156 | load_to_runtime: False 157 | state: present 158 | with_inventory_hostnames: all:!proxysql 159 | register: servers 160 | 161 | - name: proxysql | config | load servers to runtime 162 | proxysql_manage_config: 163 | login_host: "{{ proxysql.admin.host }}" 164 | login_port: "{{ proxysql.admin.port }}" 165 | login_user: "{{ proxysql.admin.user }}" 166 | login_password: "{{ proxysql.admin.passwd }}" 167 | action: LOAD 168 | config_settings: "MYSQL SERVERS" 169 | direction: TO 170 | config_layer: RUNTIME 171 | when: servers.changed 172 | 173 | - name: Enable ProxySQL monitor 174 | shell: > 175 | mysql 176 | --user={{ proxysql.admin.user }} 177 | --password={{ proxysql.admin.passwd }} 178 | --host={{ proxysql.admin.host }} 179 | --port={{ proxysql.admin.port }} 180 | --execute " 181 | set mysql-monitor.enabled='true'; LOAD MYSQL VARIABLES TO RUNTIME;" 182 | 183 | - name: create dict from the clusters 184 | set_fact: 185 | clusters: "{{ clusters|default([]) + [ {'name': hostvars[item]['cluster'] , 'hostgroup': hostvars[item]['hostgroup'] , 'short_name': hostvars[item]['cluster']|regex_replace('^damp_server_(.*)$','\\1') } ] }}" 186 | when: hostvars[item]['mysql_role'] == "master" 187 | with_inventory_hostnames: all:!proxysql 188 | 189 | - name: template 190 | template: 191 | src=proxysql_menu.sh.j2 192 | dest=/usr/local/bin/proxysql_menu.sh 193 | owner=root 194 | group=root 195 | mode=0755 196 | -------------------------------------------------------------------------------- /damp/roles/proxysql/templates/proxysql_menu.sh.j2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | user={{ proxysql.admin.user }} 3 | passwd={{ proxysql.admin.passwd }} 4 | host={{ proxysql.admin.host }} 5 | port={{ proxysql.admin.port }} 6 | app_port={{ proxysql.app.port }} 7 | pcmd="mysql -h $host -u$user -p$passwd -P$port " 8 | while true 9 | do 10 | echo "ProxySQL admin" 11 | options=( 12 | "ProxySQL Admin Shell" 13 | "[runtime] Show servers" 14 | "[runtime] Show users" 15 | "[runtime] Show replication_hostgroups" 16 | "[runtime] Show query_rules" 17 | "[runtime] Show global_variables" 18 | "[stats] Show connection_pool" 19 | "[stats] Show command_counters" 20 | "[stats] Show query digest" 21 | "[stats] Show hostgroups" 22 | "[log] Show connect" 23 | "[log] Show ping" 24 | "[log] Show read_only" 25 | {% for item in clusters %} 26 | "[mysql][{{ item.short_name }}] Connect to cluster via ProxySQL" 27 | "[test][{{ item.short_name }}] sysbench prepare" 28 | "[test][{{ item.short_name }}] sysbench run - 15 sec, ro" 29 | "[test][{{ item.short_name }}] sysbench run - 60 sec, ro" 30 | "[test][{{ item.short_name }}] Split R/W" 31 | "[test][{{ item.short_name }}] Create 'world' sample db" 32 | "[HA][{{ item.short_name }}] MHA online failover (interactive)" 33 | "[HA][{{ item.short_name }}] MHA online failover (noninteractive)" 34 | {% endfor %} 35 | "Quit") 36 | PS3='Please enter your choice: ' 37 | 38 | exec_query () { 39 | query=$1 40 | echo "####" 41 | echo "Command: $pcmd -e '$query' " 42 | echo "####" 43 | $pcmd "-e $query" 44 | } 45 | 46 | exec_cmd () { 47 | cmd=$1 48 | echo "####" 49 | echo "Command: $cmd " 50 | echo "####" 51 | $cmd 52 | } 53 | 54 | select opt in "${options[@]}" 55 | do 56 | case $opt in 57 | "ProxySQL Admin Shell") 58 | $pcmd 59 | break 60 | ;; 61 | 62 | "[runtime] Show servers") 63 | query="SELECT hostgroup_id as hg, hostname,port,status,weight,max_replication_lag as max_repl_lag, max_connections as max_conn, comment FROM runtime_mysql_servers ORDER BY hostgroup_id,hostname ASC;" 64 | exec_query "$query" 65 | break 66 | ;; 67 | "[runtime] Show users") 68 | query="SELECT username,password,default_hostgroup as hg, active,max_connections FROM runtime_mysql_users;" 69 | exec_query "$query" 70 | break 71 | ;; 72 | 73 | "[runtime] Show replication_hostgroups") 74 | query="SELECT * FROM runtime_mysql_replication_hostgroups" 75 | exec_query "$query" 76 | break 77 | ;; 78 | 79 | "[runtime] Show global_variables") 80 | query="select * from global_variables;" 81 | exec_query "$query" 82 | break 83 | ;; 84 | 85 | "[runtime] Show query_rules") 86 | query="SELECT rule_id, match_digest, match_pattern, replace_pattern, cache_ttl, destination_hostgroup hg,apply FROM mysql_query_rules ORDER BY rule_id;" 87 | exec_query "$query" 88 | break 89 | ;; 90 | 91 | "[stats] Show connection_pool") 92 | query="SELECT * FROM stats.stats_mysql_connection_pool;" 93 | exec_query "$query" 94 | break 95 | ;; 96 | "[stats] Show command_counters") 97 | query="SELECT Command,Total_Time_us, Total_cnt FROM stats_mysql_commands_counters WHERE Total_cnt;" 98 | exec_query "$query" 99 | break 100 | ;; 101 | "[stats] Show query digest") 102 | query="SELECT hostgroup hg, sum_time, count_star, substr(digest_text,1,80) FROM stats_mysql_query_digest ORDER BY sum_time DESC LIMIT 15;" 103 | exec_query "$query" 104 | break 105 | ;; 106 | "[stats] Show hostgroups") 107 | query="SELECT hostgroup hg, SUM(sum_time), SUM(count_star) FROM stats_mysql_query_digest GROUP BY hostgroup;" 108 | exec_query "$query" 109 | break 110 | ;; 111 | 112 | "[log] Show connect") 113 | query="SELECT * FROM monitor.mysql_server_connect_log ORDER BY time_start_us DESC LIMIT 10;" 114 | exec_query "$query" 115 | break 116 | ;; 117 | "[log] Show ping") 118 | query="SELECT * FROM monitor.mysql_server_ping_log ORDER BY time_start_us DESC LIMIT 10;" 119 | exec_query "$query" 120 | break 121 | ;; 122 | "[log] Show read_only") 123 | query="SELECT * FROM monitor.mysql_server_read_only_log ORDER BY time_start_us DESC LIMIT 10;" 124 | exec_query "$query" 125 | break 126 | ;; 127 | {% for item in clusters %} 128 | "[mysql][{{ item.short_name }}] Connect to cluster via ProxySQL") 129 | cmd="mysql -h $host --user=app{{ item.hostgroup }} --password=app{{ item.hostgroup }} --port $app_port" 130 | exec_cmd "$cmd" 131 | break 132 | ;; 133 | "[test][{{ item.short_name }}] sysbench prepare") 134 | cmd="sysbench --report-interval=5 --num-threads=4 --num-requests=0 --max-time=20 --test=/usr/share/doc/sysbench/tests/db/oltp.lua --mysql-user=app{{ item.hostgroup }} --mysql-password=app{{ item.hostgroup }} --oltp-table-size=10000 --mysql-host=$host --mysql-port=$app_port prepare" 135 | exec_cmd "$cmd" 136 | break 137 | ;; 138 | "[test][{{ item.short_name }}] sysbench run - 15 sec, ro") 139 | cmd="sysbench --report-interval=1 --num-threads=4 --num-requests=0 --test=/usr/share/doc/sysbench/tests/db/oltp.lua --mysql-user=app{{ item.hostgroup }} --mysql-password=app{{ item.hostgroup }} --oltp-table-size=10000 --mysql-host=$host --mysql-port=$app_port --oltp-read-only=on --mysql-ignore-errors=all --max-time=15 run" 140 | exec_cmd "$cmd" 141 | break 142 | ;; 143 | "[test][{{ item.short_name }}] sysbench run - 60 sec, ro") 144 | cmd="sysbench --report-interval=1 --num-threads=4 --num-requests=0 --test=/usr/share/doc/sysbench/tests/db/oltp.lua --mysql-user=app{{ item.hostgroup }} --mysql-password=app{{ item.hostgroup }} --oltp-table-size=10000 --mysql-host=$host --mysql-port=$app_port --oltp-read-only=on --mysql-ignore-errors=all --max-time=60 run" 145 | exec_cmd "$cmd" 146 | break 147 | ;; 148 | 149 | "[HA][{{ item.short_name }}] MHA online failover (interactive)") 150 | cmd="masterha_master_switch --conf=/etc/mha/mha_{{ item.name }}.cnf --master_state=alive --orig_master_is_new_slave --interactive=1" 151 | exec_cmd "$cmd" 152 | break 153 | ;; 154 | "[HA][{{ item.short_name }}] MHA online failover (noninteractive)") 155 | cmd="masterha_master_switch --conf=/etc/mha/mha_{{ item.name }}.cnf --master_state=alive --orig_master_is_new_slave --interactive=0" 156 | exec_cmd "$cmd" 157 | break 158 | ;; 159 | "[test][{{ item.short_name }}] Split R/W") 160 | query="REPLACE INTO mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) VALUES(1000,1,'^select',{{ item.hostgroup | int + 1 }},0);LOAD MYSQL QUERY RULES TO RUNTIME;SAVE MYSQL QUERY RULES TO DISK;\G" 161 | exec_query "$query" 162 | break 163 | ;; 164 | "[test][{{ item.short_name }}] Create 'world' sample db") 165 | cmd="wget -O /tmp/world.sql.gz http://downloads.mysql.com/docs/world.sql.gz" 166 | exec_cmd "$cmd" 167 | zcat /tmp/world.sql.gz | mysql -h $host --user=app{{ item.hostgroup }} --password=app{{ item.hostgroup }} --port $app_port 168 | break 169 | ;; 170 | {% endfor %} 171 | "Quit") 172 | exit 173 | ;; 174 | *) echo invalid option;; 175 | esac 176 | done 177 | done 178 | -------------------------------------------------------------------------------- /damp/library/proxysql_manage_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of Ansible 5 | # 6 | # Ansible is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Ansible is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Ansible. If not, see . 18 | 19 | DOCUMENTATION = ''' 20 | --- 21 | module: proxysql_manage_config 22 | version_added: "2.2" 23 | author: "Ben Mildren (@bmildren)" 24 | short_description: Writes the proxysql configuration settings between layers. 25 | description: 26 | - The M(proxysql_global_variables) module writes the proxysql configuration 27 | settings between layers. Currently this module will always report a 28 | changed state, so should typically be used with WHEN however this will 29 | change in a future version when the CHECKSUM table commands are available 30 | for all tables in proxysql. 31 | options: 32 | action: 33 | description: 34 | - The supplied I(action) combines with the supplied I(direction) to 35 | provide the semantics of how we want to move the I(config_settings) 36 | between the I(config_layers). 37 | choices: [ "LOAD", "SAVE" ] 38 | required: True 39 | config_settings: 40 | description: 41 | - The I(config_settings) specifies which configuration we're writing. 42 | choices: [ "MYSQL USERS", "MYSQL SERVERS", "MYSQL QUERY RULES", 43 | "MYSQL VARIABLES", "ADMIN VARIABLES", "SCHEDULER" ] 44 | required: True 45 | direction: 46 | description: 47 | - FROM - denotes we're reading values FROM the supplied I(config_layer) 48 | and writing to the next layer 49 | TO - denotes we're reading from the previous layer and writing TO the 50 | supplied I(config_layer). 51 | choices: [ "FROM", "TO" ] 52 | required: True 53 | config_layer: 54 | description: 55 | - RUNTIME - represents the in-memory data structures of ProxySQL used by 56 | the threads that are handling the requests. 57 | MEMORY - (sometime also referred as main) represents the in-memory 58 | SQLite3 database. 59 | DISK - represents the on-disk SQLite3 database. 60 | CONFIG - is the classical config file. You can only LOAD FROM the 61 | config file. 62 | choices: [ "MEMORY", "DISK", "RUNTIME", "CONFIG" ] 63 | required: True 64 | login_user: 65 | description: 66 | - The username used to authenticate to ProxySQL admin interface 67 | default: None 68 | login_password: 69 | description: 70 | - The password used to authenticate to ProxySQL admin interface 71 | default: None 72 | login_host: 73 | description: 74 | - The host used to connect to ProxySQL admin interface 75 | default: '127.0.0.1' 76 | login_port: 77 | description: 78 | - The port used to connect to ProxySQL admin interface 79 | default: 6032 80 | config_file: 81 | description: 82 | - Specify a config file from which login_user and login_password are to 83 | be read 84 | default: '' 85 | ''' 86 | 87 | EXAMPLES = ''' 88 | --- 89 | # This example saves the mysql users config from memory to disk. It uses 90 | # supplied credentials to connect to the proxysql admin interface. 91 | 92 | - proxysql_global_variables: 93 | login_user: 'admin' 94 | login_password: 'admin' 95 | action: "SAVE" 96 | config_settings: "MYSQL USERS" 97 | direction: "FROM" 98 | config_layer: "MEMORY" 99 | 100 | # This example loads the mysql query rules config from memory to to runtime. It 101 | # uses supplied credentials to connect to the proxysql admin interface. 102 | 103 | - proxysql_global_variables: 104 | config_file: '~/proxysql.cnf' 105 | action: "LOAD" 106 | config_settings: "MYSQL QUERY RULES" 107 | direction: "TO" 108 | config_layer: "RUNTIME" 109 | ''' 110 | 111 | RETURN = ''' 112 | stdout: 113 | description: Simply reports whether the action reported a change. 114 | returned: Currently the returned value with always be changed=True. 115 | type: dict 116 | "sample": { 117 | "changed": true 118 | } 119 | ''' 120 | 121 | import sys 122 | 123 | try: 124 | import MySQLdb 125 | except ImportError: 126 | mysqldb_found = False 127 | else: 128 | mysqldb_found = True 129 | 130 | # =========================================== 131 | # proxysql module specific support methods. 132 | # 133 | 134 | 135 | def perform_checks(module): 136 | if module.params["login_port"] < 0 \ 137 | or module.params["login_port"] > 65535: 138 | module.fail_json( 139 | msg="login_port must be a valid unix port number (0-65535)" 140 | ) 141 | 142 | if module.params["config_layer"] == 'CONFIG' and \ 143 | (module.params["action"] != 'LOAD' or 144 | module.params["direction"] != 'FROM'): 145 | 146 | if (module.params["action"] != 'LOAD' and 147 | module.params["direction"] != 'FROM'): 148 | msg_string = ("Neither the action \"%s\" nor the direction" + 149 | " \"%s\" are valid combination with the CONFIG" + 150 | " config_layer") 151 | module.fail_json(msg=msg_string % (module.params["action"], 152 | module.params["direction"])) 153 | 154 | elif module.params["action"] != 'LOAD': 155 | msg_string = ("The action \"%s\" is not a valid combination" + 156 | " with the CONFIG config_layer") 157 | module.fail_json(msg=msg_string % module.params["action"]) 158 | 159 | else: 160 | msg_string = ("The direction \"%s\" is not a valid combination" + 161 | " with the CONFIG config_layer") 162 | module.fail_json(msg=msg_string % module.params["direction"]) 163 | 164 | if not mysqldb_found: 165 | module.fail_json( 166 | msg="the python mysqldb module is required" 167 | ) 168 | 169 | 170 | def manage_config(manage_config_settings, cursor): 171 | 172 | query_string = "%s" % ' '.join(manage_config_settings) 173 | 174 | cursor.execute(query_string) 175 | return True 176 | 177 | # =========================================== 178 | # Module execution. 179 | # 180 | 181 | 182 | def main(): 183 | module = AnsibleModule( 184 | argument_spec=dict( 185 | login_user=dict(default=None, type='str'), 186 | login_password=dict(default=None, no_log=True, type='str'), 187 | login_host=dict(default="127.0.0.1"), 188 | login_unix_socket=dict(default=None), 189 | login_port=dict(default=6032, type='int'), 190 | config_file=dict(default="", type='path'), 191 | action=dict(required=True, choices=['LOAD', 192 | 'SAVE']), 193 | config_settings=dict(requirerd=True, choices=['MYSQL USERS', 194 | 'MYSQL SERVERS', 195 | 'MYSQL QUERY RULES', 196 | 'MYSQL VARIABLES', 197 | 'ADMIN VARIABLES', 198 | 'SCHEDULER']), 199 | direction=dict(required=True, choices=['FROM', 200 | 'TO']), 201 | config_layer=dict(requirerd=True, choices=['MEMORY', 202 | 'DISK', 203 | 'RUNTIME', 204 | 'CONFIG']) 205 | ), 206 | supports_check_mode=True 207 | ) 208 | 209 | perform_checks(module) 210 | 211 | login_user = module.params["login_user"] 212 | login_password = module.params["login_password"] 213 | config_file = module.params["config_file"] 214 | action = module.params["action"] 215 | config_settings = module.params["config_settings"] 216 | direction = module.params["direction"] 217 | config_layer = module.params["config_layer"] 218 | 219 | cursor = None 220 | try: 221 | cursor = mysql_connect(module, 222 | login_user, 223 | login_password, 224 | config_file) 225 | except MySQLdb.Error: 226 | e = sys.exc_info()[1] 227 | module.fail_json( 228 | msg="unable to connect to ProxySQL Admin Module.. %s" % e 229 | ) 230 | 231 | result = {} 232 | 233 | manage_config_settings = \ 234 | [action, config_settings, direction, config_layer] 235 | 236 | try: 237 | result['changed'] = manage_config(manage_config_settings, 238 | cursor) 239 | except MySQLdb.Error: 240 | e = sys.exc_info()[1] 241 | module.fail_json( 242 | msg="unable to manage config.. %s" % e 243 | ) 244 | 245 | module.exit_json(**result) 246 | 247 | from ansible.module_utils.basic import * 248 | from ansible.module_utils.mysql import * 249 | if __name__ == '__main__': 250 | main() 251 | -------------------------------------------------------------------------------- /proxysql_menu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | user=admin 3 | passwd=admin 4 | host=127.0.0.1 5 | port=6032 6 | app_port=6033 7 | pcmd="mysql -h $host -u$user -p$passwd -P$port " 8 | while true 9 | do 10 | echo "ProxySQL admin" 11 | options=( 12 | "ProxySQL Admin Shell" 13 | "MySQL Connect to 'mukka' via ProxySQL" 14 | "MySQL Connect to 'rol' via ProxySQL" 15 | "[runtime] Show servers" 16 | "[runtime] Show users" 17 | "[runtime] Show repliation_hostgroups" 18 | "[runtime] Show query_rules" 19 | "[stats] Show connection_pool" 20 | "[stats] Show command_counters" 21 | "[stats] Show query digest" 22 | "[stats] Show hostgroups" 23 | "[log] Show connect" 24 | "[log] Show ping" 25 | "[log] Show read_only" 26 | "[test][mukka] sysbench prepare" 27 | "[test][mukka] sysbench run - 15 sec, ro" 28 | "[test][mukka] sysbench run - 60 sec, ro" 29 | "[test][mukka] Split R/W" 30 | "[test][mukka] Create 'world' sample db" 31 | "[HA][mukka] MHA online failover (interactive)" 32 | "[HA][mukka] MHA online failover (noninteractive)" 33 | "[test][rol] sysbench prepare" 34 | "[test][rol] sysbench run - 15 sec, ro" 35 | "[test][rol] sysbench run - 60 sec, ro" 36 | "[test][rol] Split R/W" 37 | "[test][rol] Create 'world' sample db" 38 | "[HA][rol] MHA online failover (interactive)" 39 | "[HA][rol] MHA online failover (noninteractive)" 40 | "Quit") 41 | PS3='Please enter your choice: ' 42 | 43 | exec_query () { 44 | query=$1 45 | echo "####" 46 | echo "Command: $pcmd -e '$query' " 47 | echo "####" 48 | $pcmd "-e $query" 49 | } 50 | 51 | exec_cmd () { 52 | cmd=$1 53 | echo "####" 54 | echo "Command: $cmd " 55 | echo "####" 56 | $cmd 57 | } 58 | 59 | select opt in "${options[@]}" 60 | do 61 | case $opt in 62 | "ProxySQL Admin Shell") 63 | $pcmd 64 | break 65 | ;; 66 | 67 | "MySQL Connect to 'mukka' via ProxySQL") 68 | cmd="mysql -h $host --user=app1 --password=app1 --port $app_port" 69 | exec_cmd "$cmd" 70 | break 71 | ;; 72 | 73 | "MySQL Connect to 'rol' via ProxySQL") 74 | cmd="mysql -h $host --user=app3 --password=app3 --port $app_port" 75 | exec_cmd "$cmd" 76 | break 77 | ;; 78 | 79 | "[runtime] Show servers") 80 | query="SELECT hostgroup_id as hg, hostname,port,status,weight,max_connections, comment FROM runtime_mysql_servers ORDER BY hostgroup_id,hostname ASC;" 81 | exec_query "$query" 82 | break 83 | ;; 84 | "[runtime] Show users") 85 | query="SELECT username,password,default_hostgroup as hg, active,max_connections FROM runtime_mysql_users;" 86 | exec_query "$query" 87 | break 88 | ;; 89 | 90 | "[runtime] Show repliation_hostgroups") 91 | query="SELECT * FROM runtime_mysql_replication_hostgroups" 92 | exec_query "$query" 93 | break 94 | ;; 95 | 96 | "[runtime] Show query_rules") 97 | query="SELECT rule_id, match_digest, match_pattern, replace_pattern, cache_ttl, destination_hostgroup hg,apply FROM mysql_query_rules ORDER BY rule_id;" 98 | exec_query "$query" 99 | break 100 | ;; 101 | 102 | "[stats] Show connection_pool") 103 | query="SELECT * FROM stats.stats_mysql_connection_pool;" 104 | exec_query "$query" 105 | break 106 | ;; 107 | "[stats] Show command_counters") 108 | query="SELECT Command,Total_Time_us, Total_cnt FROM stats_mysql_commands_counters WHERE Total_cnt;" 109 | exec_query "$query" 110 | break 111 | ;; 112 | "[stats] Show query digest") 113 | query="SELECT hostgroup hg, sum_time, count_star, substr(digest_text,1,80) FROM stats_mysql_query_digest ORDER BY sum_time DESC LIMIT 15;" 114 | exec_query "$query" 115 | break 116 | ;; 117 | "[stats] Show hostgroups") 118 | query="SELECT hostgroup hg, SUM(sum_time), SUM(count_star) FROM stats_mysql_query_digest GROUP BY hostgroup;" 119 | exec_query "$query" 120 | break 121 | ;; 122 | 123 | "[log] Show connect") 124 | query="SELECT * FROM monitor.mysql_server_connect_log ORDER BY time_start_us DESC LIMIT 10;" 125 | exec_query "$query" 126 | break 127 | ;; 128 | "[log] Show ping") 129 | query="SELECT * FROM monitor.mysql_server_ping_log ORDER BY time_start_us DESC LIMIT 10;" 130 | exec_query "$query" 131 | break 132 | ;; 133 | "[log] Show read_only") 134 | query="SELECT * FROM monitor.mysql_server_read_only_log ORDER BY time_start_us DESC LIMIT 10;" 135 | exec_query "$query" 136 | break 137 | ;; 138 | "[test][mukka] sysbench prepare") 139 | cmd="sysbench --report-interval=5 --num-threads=4 --num-requests=0 --max-time=20 --test=/usr/share/doc/sysbench/tests/db/oltp.lua --mysql-user=app1 --mysql-password=app1 --oltp-table-size=10000 --mysql-host=$host --mysql-port=$app_port prepare" 140 | exec_cmd "$cmd" 141 | break 142 | ;; 143 | "[test][mukka] sysbench run - 15 sec, ro") 144 | cmd="sysbench --report-interval=1 --num-threads=4 --num-requests=0 --test=/usr/share/doc/sysbench/tests/db/oltp.lua --mysql-user=app1 --mysql-password=app1 --oltp-table-size=10000 --mysql-host=$host --mysql-port=$app_port --oltp-read-only=on --mysql-ignore-errors=all --max-time=15 run" 145 | exec_cmd "$cmd" 146 | break 147 | ;; 148 | "[test][mukka] sysbench run - 60 sec, ro") 149 | cmd="sysbench --report-interval=1 --num-threads=4 --num-requests=0 --test=/usr/share/doc/sysbench/tests/db/oltp.lua --mysql-user=app1 --mysql-password=app1 --oltp-table-size=10000 --mysql-host=$host --mysql-port=$app_port --oltp-read-only=on --mysql-ignore-errors=all --max-time=60 run" 150 | exec_cmd "$cmd" 151 | break 152 | ;; 153 | 154 | "[HA][mukka] MHA online failover (interactive)") 155 | cmd="masterha_master_switch --conf=/etc/mha/mha_damp_server_mukka.cnf --master_state=alive --orig_master_is_new_slave --interactive=1" 156 | exec_cmd "$cmd" 157 | break 158 | ;; 159 | "[HA][mukka] MHA online failover (noninteractive)") 160 | cmd="masterha_master_switch --conf=/etc/mha/mha_damp_server_mukka.cnf --master_state=alive --orig_master_is_new_slave --interactive=0" 161 | exec_cmd "$cmd" 162 | break 163 | ;; 164 | "[test][mukka] Split R/W") 165 | query="REPLACE INTO mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) VALUES(1000,1,'^select',2,0);LOAD MYSQL QUERY RULES TO RUNTIME;SAVE MYSQL QUERY RULES TO DISK;\G" 166 | exec_query "$query" 167 | break 168 | ;; 169 | "[test][mukka] Create 'world' sample db") 170 | cmd="wget -O /tmp/world.sql.gz http://downloads.mysql.com/docs/world.sql.gz" 171 | exec_cmd "$cmd" 172 | zcat /tmp/world.sql.gz | mysql -h $host --user=app1 --password=app1 --port $app_port 173 | break 174 | ;; 175 | "[test][rol] sysbench prepare") 176 | cmd="sysbench --report-interval=5 --num-threads=4 --num-requests=0 --max-time=20 --test=/usr/share/doc/sysbench/tests/db/oltp.lua --mysql-user=app3 --mysql-password=app3 --oltp-table-size=10000 --mysql-host=$host --mysql-port=$app_port prepare" 177 | exec_cmd "$cmd" 178 | break 179 | ;; 180 | "[test][rol] sysbench run - 15 sec, ro") 181 | cmd="sysbench --report-interval=1 --num-threads=4 --num-requests=0 --test=/usr/share/doc/sysbench/tests/db/oltp.lua --mysql-user=app3 --mysql-password=app3 --oltp-table-size=10000 --mysql-host=$host --mysql-port=$app_port --oltp-read-only=on --mysql-ignore-errors=all --max-time=15 run" 182 | exec_cmd "$cmd" 183 | break 184 | ;; 185 | "[test][rol] sysbench run - 60 sec, ro") 186 | cmd="sysbench --report-interval=1 --num-threads=4 --num-requests=0 --test=/usr/share/doc/sysbench/tests/db/oltp.lua --mysql-user=app3 --mysql-password=app3 --oltp-table-size=10000 --mysql-host=$host --mysql-port=$app_port --oltp-read-only=on --mysql-ignore-errors=all --max-time=60 run" 187 | exec_cmd "$cmd" 188 | break 189 | ;; 190 | 191 | "[HA][rol] MHA online failover (interactive)") 192 | cmd="masterha_master_switch --conf=/etc/mha/mha_damp_server_rol.cnf --master_state=alive --orig_master_is_new_slave --interactive=1" 193 | exec_cmd "$cmd" 194 | break 195 | ;; 196 | "[HA][rol] MHA online failover (noninteractive)") 197 | cmd="masterha_master_switch --conf=/etc/mha/mha_damp_server_rol.cnf --master_state=alive --orig_master_is_new_slave --interactive=0" 198 | exec_cmd "$cmd" 199 | break 200 | ;; 201 | "[test][rol] Split R/W") 202 | query="REPLACE INTO mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) VALUES(1000,1,'^select',4,0);LOAD MYSQL QUERY RULES TO RUNTIME;SAVE MYSQL QUERY RULES TO DISK;\G" 203 | exec_query "$query" 204 | break 205 | ;; 206 | "[test][rol] Create 'world' sample db") 207 | cmd="wget -O /tmp/world.sql.gz http://downloads.mysql.com/docs/world.sql.gz" 208 | exec_cmd "$cmd" 209 | zcat /tmp/world.sql.gz | mysql -h $host --user=app3 --password=app3 --port $app_port 210 | break 211 | ;; 212 | "Quit") 213 | exit 214 | ;; 215 | *) echo invalid option;; 216 | esac 217 | done 218 | done 219 | -------------------------------------------------------------------------------- /damp/library/proxysql_global_variables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of Ansible 5 | # 6 | # Ansible is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Ansible is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Ansible. If not, see . 18 | 19 | DOCUMENTATION = ''' 20 | --- 21 | module: proxysql_global_variables 22 | version_added: "2.2" 23 | author: "Ben Mildren (@bmildren)" 24 | short_description: Gets or sets the proxysql global variables. 25 | description: 26 | - The M(proxysql_global_variables) module gets or sets the proxysql global 27 | variables. 28 | options: 29 | variable: 30 | description: 31 | - Defines which variable should be returned, or if I(value) is specified 32 | which variable should be updated. 33 | required: True 34 | value: 35 | description: 36 | - Defines a value the variable specified using I(variable) should be set 37 | to. 38 | save_to_disk: 39 | description: 40 | - Save mysql host config to sqlite db on disk to persist the 41 | configuration. 42 | default: True 43 | load_to_runtime: 44 | description: 45 | - Dynamically load mysql host config to runtime memory. 46 | default: True 47 | login_user: 48 | description: 49 | - The username used to authenticate to ProxySQL admin interface 50 | default: None 51 | login_password: 52 | description: 53 | - The password used to authenticate to ProxySQL admin interface 54 | default: None 55 | login_host: 56 | description: 57 | - The host used to connect to ProxySQL admin interface 58 | default: '127.0.0.1' 59 | login_port: 60 | description: 61 | - The port used to connect to ProxySQL admin interface 62 | default: 6032 63 | config_file: 64 | description: 65 | - Specify a config file from which login_user and login_password are to 66 | be read 67 | default: '' 68 | ''' 69 | 70 | EXAMPLES = ''' 71 | --- 72 | # This example sets the value of a variable, saves the mysql admin variables 73 | # config to disk, and dynamically loads the mysql admin variables config to 74 | # runtime. It uses supplied credentials to connect to the proxysql admin 75 | # interface. 76 | 77 | - proxysql_global_variables: 78 | login_user: 'admin' 79 | login_password: 'admin' 80 | variable: 'mysql-max_connections' 81 | value: 4096 82 | 83 | # This example gets the value of a variable. It uses credentials in a 84 | # supplied config file to connect to the proxysql admin interface. 85 | 86 | - proxysql_global_variables: 87 | config_file: '~/proxysql.cnf' 88 | variable: 'mysql-default_query_delay' 89 | ''' 90 | 91 | RETURN = ''' 92 | stdout: 93 | description: Returns the mysql variable supplied with it's associted value. 94 | returned: Returns the current variable and value, or the newly set value 95 | for the variable supplied.. 96 | type: dict 97 | "sample": { 98 | "changed": false, 99 | "msg": "The variable is already been set to the supplied value", 100 | "var": { 101 | "variable_name": "mysql-poll_timeout", 102 | "variable_value": "3000" 103 | } 104 | } 105 | ''' 106 | 107 | import sys 108 | 109 | try: 110 | import MySQLdb 111 | import MySQLdb.cursors 112 | except ImportError: 113 | mysqldb_found = False 114 | else: 115 | mysqldb_found = True 116 | 117 | # =========================================== 118 | # proxysql module specific support methods. 119 | # 120 | 121 | 122 | def perform_checks(module): 123 | if module.params["login_port"] < 0 \ 124 | or module.params["login_port"] > 65535: 125 | module.fail_json( 126 | msg="login_port must be a valid unix port number (0-65535)" 127 | ) 128 | 129 | if not mysqldb_found: 130 | module.fail_json( 131 | msg="the python mysqldb module is required" 132 | ) 133 | 134 | 135 | def save_config_to_disk(variable, cursor): 136 | if variable.startswith("admin"): 137 | cursor.execute("SAVE ADMIN VARIABLES TO DISK") 138 | else: 139 | cursor.execute("SAVE MYSQL VARIABLES TO DISK") 140 | return True 141 | 142 | 143 | def load_config_to_runtime(variable, cursor): 144 | if variable.startswith("admin"): 145 | cursor.execute("LOAD ADMIN VARIABLES TO RUNTIME") 146 | else: 147 | cursor.execute("LOAD MYSQL VARIABLES TO RUNTIME") 148 | return True 149 | 150 | 151 | def check_config(variable, value, cursor): 152 | query_string = \ 153 | """SELECT count(*) AS `variable_count` 154 | FROM global_variables 155 | WHERE variable_name = %s and variable_value = %s""" 156 | 157 | query_data = \ 158 | [variable, value] 159 | 160 | cursor.execute(query_string, query_data) 161 | check_count = cursor.fetchone() 162 | return (int(check_count['variable_count']) > 0) 163 | 164 | 165 | def get_config(variable, cursor): 166 | 167 | query_string = \ 168 | """SELECT * 169 | FROM global_variables 170 | WHERE variable_name = %s""" 171 | 172 | query_data = \ 173 | [variable, ] 174 | 175 | cursor.execute(query_string, query_data) 176 | row_count = cursor.rowcount 177 | resultset = cursor.fetchone() 178 | 179 | if row_count > 0: 180 | return resultset 181 | else: 182 | return False 183 | 184 | 185 | def set_config(variable, value, cursor): 186 | 187 | query_string = \ 188 | """UPDATE global_variables 189 | SET variable_value = %s 190 | WHERE variable_name = %s""" 191 | 192 | query_data = \ 193 | [value, variable] 194 | 195 | cursor.execute(query_string, query_data) 196 | return True 197 | 198 | 199 | def manage_config(variable, save_to_disk, load_to_runtime, cursor, state): 200 | if state: 201 | if save_to_disk: 202 | save_config_to_disk(variable, cursor) 203 | if load_to_runtime: 204 | load_config_to_runtime(variable, cursor) 205 | 206 | # =========================================== 207 | # Module execution. 208 | # 209 | 210 | 211 | def main(): 212 | module = AnsibleModule( 213 | argument_spec=dict( 214 | login_user=dict(default=None, type='str'), 215 | login_password=dict(default=None, no_log=True, type='str'), 216 | login_host=dict(default="127.0.0.1"), 217 | login_unix_socket=dict(default=None), 218 | login_port=dict(default=6032, type='int'), 219 | config_file=dict(default="", type='path'), 220 | variable=dict(required=True, type='str'), 221 | value=dict(), 222 | save_to_disk=dict(default=True, type='bool'), 223 | load_to_runtime=dict(default=True, type='bool') 224 | ), 225 | supports_check_mode=True 226 | ) 227 | 228 | perform_checks(module) 229 | 230 | login_user = module.params["login_user"] 231 | login_password = module.params["login_password"] 232 | config_file = module.params["config_file"] 233 | variable = module.params["variable"] 234 | value = module.params["value"] 235 | save_to_disk = module.params["save_to_disk"] 236 | load_to_runtime = module.params["load_to_runtime"] 237 | 238 | cursor = None 239 | try: 240 | cursor = mysql_connect(module, 241 | login_user, 242 | login_password, 243 | config_file, 244 | cursor_class=MySQLdb.cursors.DictCursor) 245 | except MySQLdb.Error: 246 | e = sys.exc_info()[1] 247 | module.fail_json( 248 | msg="unable to connect to ProxySQL Admin Module.. %s" % e 249 | ) 250 | 251 | result = {} 252 | 253 | if not value: 254 | try: 255 | if get_config(variable, cursor): 256 | result['changed'] = False 257 | result['msg'] = \ 258 | "Returned the variable and it's current value" 259 | result['var'] = get_config(variable, cursor) 260 | else: 261 | module.fail_json( 262 | msg="The variable \"%s\" was not found" % variable 263 | ) 264 | 265 | except MySQLdb.Error: 266 | e = sys.exc_info()[1] 267 | module.fail_json( 268 | msg="unable to get config.. %s" % e 269 | ) 270 | else: 271 | try: 272 | if get_config(variable, cursor): 273 | if not check_config(variable, value, cursor): 274 | if not module.check_mode: 275 | result['changed'] = set_config(variable, value, cursor) 276 | result['msg'] = \ 277 | "Set the variable to the supplied value" 278 | result['var'] = get_config(variable, cursor) 279 | manage_config(variable, 280 | save_to_disk, 281 | load_to_runtime, 282 | cursor, 283 | result['changed']) 284 | else: 285 | result['changed'] = True 286 | result['msg'] = ("Variable would have been set to" + 287 | " the supplied value, however" + 288 | " check_mode is enabled.") 289 | else: 290 | result['changed'] = False 291 | result['msg'] = ("The variable is already been set to" + 292 | " the supplied value") 293 | result['var'] = get_config(variable, cursor) 294 | else: 295 | module.fail_json( 296 | msg="The variable \"%s\" was not found" % variable 297 | ) 298 | 299 | except MySQLdb.Error: 300 | e = sys.exc_info()[1] 301 | module.fail_json( 302 | msg="unable to set config.. %s" % e 303 | ) 304 | 305 | module.exit_json(**result) 306 | 307 | from ansible.module_utils.basic import * 308 | from ansible.module_utils.mysql import * 309 | if __name__ == '__main__': 310 | main() 311 | -------------------------------------------------------------------------------- /damp/roles/mha/files/mha/scripts/master_ip_online_change: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | # Copyright (C) 2011 DeNA Co.,Ltd. 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 17 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 18 | 19 | ## Note: This is a sample script and is not complete. Modify the script based on your environment. 20 | 21 | use strict; 22 | use warnings FATAL => 'all'; 23 | 24 | use Getopt::Long; 25 | use MHA::DBHelper; 26 | use MHA::NodeUtil; 27 | use Time::HiRes qw( sleep gettimeofday tv_interval ); 28 | use Data::Dumper; 29 | my $_tstart; 30 | my $_running_interval = 0.1; 31 | my ( 32 | $command, $orig_master_host, $orig_master_ip, 33 | $orig_master_port, $orig_master_user, $orig_master_password, 34 | $new_master_host, $new_master_ip, $new_master_port, 35 | $new_master_user, $new_master_password, $orig_master_ssh_user, 36 | $new_master_ssh_user, $orig_master_is_new_slave 37 | ); 38 | GetOptions( 39 | 'command=s' => \$command, 40 | 'orig_master_host=s' => \$orig_master_host, 41 | 'orig_master_ip=s' => \$orig_master_ip, 42 | 'orig_master_port=i' => \$orig_master_port, 43 | 'orig_master_user=s' => \$orig_master_user, 44 | 'orig_master_password=s' => \$orig_master_password, 45 | 'new_master_host=s' => \$new_master_host, 46 | 'new_master_ip=s' => \$new_master_ip, 47 | 'new_master_port=i' => \$new_master_port, 48 | 'new_master_user=s' => \$new_master_user, 49 | 'new_master_password=s' => \$new_master_password, 50 | 'orig_master_ssh_user=s' => \$orig_master_ssh_user, 51 | 'new_master_ssh_user=s' => \$new_master_ssh_user, 52 | 'orig_master_is_new_slave=s' => \$orig_master_is_new_slave, 53 | ); 54 | #getopts escapes the chars like $ which can be present in the password (just as \) 55 | #$orig_master_password =~ s/\\//; 56 | #$new_master_password =~ s/\\//; 57 | 58 | exit &main(); 59 | 60 | sub current_time_us { 61 | my ( $sec, $microsec ) = gettimeofday(); 62 | my $curdate = localtime($sec); 63 | return $curdate . " " . sprintf( "%06d", $microsec ); 64 | } 65 | 66 | sub sleep_until { 67 | my $elapsed = tv_interval($_tstart); 68 | if ( $_running_interval > $elapsed ) { 69 | sleep( $_running_interval - $elapsed ); 70 | } 71 | } 72 | 73 | sub get_threads_util { 74 | my $dbh = shift; 75 | my $my_connection_id = shift; 76 | my $running_time_threshold = shift; 77 | my $type = shift; 78 | $running_time_threshold = 0 unless ($running_time_threshold); 79 | $type = 0 unless ($type); 80 | my @threads; 81 | 82 | my $sth = $dbh->prepare("SHOW PROCESSLIST"); 83 | $sth->execute(); 84 | 85 | while ( my $ref = $sth->fetchrow_hashref() ) { 86 | my $id = $ref->{Id}; 87 | my $user = $ref->{User}; 88 | my $host = $ref->{Host}; 89 | my $command = $ref->{Command}; 90 | my $state = $ref->{State}; 91 | my $query_time = $ref->{Time}; 92 | my $info = $ref->{Info}; 93 | $info =~ s/^\s*(.*?)\s*$/$1/ if defined($info); 94 | next if ( $my_connection_id == $id ); 95 | next if ( defined($query_time) && $query_time < $running_time_threshold ); 96 | next if ( defined($command) && $command eq "Binlog Dump" ); 97 | next if ( defined($user) && $user eq "system user" ); 98 | next 99 | if ( defined($command) 100 | && $command eq "Sleep" 101 | && defined($query_time) 102 | && $query_time >= 1 ); 103 | 104 | if ( $type >= 1 ) { 105 | next if ( defined($command) && $command eq "Sleep" ); 106 | next if ( defined($command) && $command eq "Connect" ); 107 | } 108 | 109 | if ( $type >= 2 ) { 110 | next if ( defined($info) && $info =~ m/^select/i ); 111 | next if ( defined($info) && $info =~ m/^show/i ); 112 | } 113 | 114 | push @threads, $ref; 115 | } 116 | return @threads; 117 | } 118 | 119 | sub main { 120 | if ( $command eq "stop" ) { 121 | ## Gracefully killing connections on the current master 122 | # 1. Set read_only= 1 on the new master 123 | # 2. DROP USER so that no app user can establish new connections 124 | # 3. Set read_only= 1 on the current master 125 | # 4. Kill current queries 126 | # * Any database access failure will result in script die. 127 | my $exit_code = 1; 128 | eval { 129 | ## Setting read_only=1 on the new master (to avoid accident) 130 | my $new_master_handler = new MHA::DBHelper(); 131 | 132 | # args: hostname, port, user, password, raise_error(die_on_error)_or_not 133 | $new_master_handler->connect( $new_master_ip, $new_master_port, 134 | $new_master_user, "$new_master_password", 1 ); 135 | print current_time_us() . " Set read_only on the new master.. "; 136 | $new_master_handler->enable_read_only(); 137 | if ( $new_master_handler->is_read_only() ) { 138 | print "ok.\n"; 139 | } 140 | else { 141 | die "Failed!\n"; 142 | } 143 | $new_master_handler->disconnect(); 144 | 145 | # Connecting to the orig master, die if any database error happens 146 | my $orig_master_handler = new MHA::DBHelper(); 147 | $orig_master_handler->connect( $orig_master_ip, $orig_master_port, 148 | $orig_master_user, "$new_master_password", 1 ); 149 | 150 | ## Drop application user so that nobody can connect. Disabling per-session binlog beforehand 151 | ##$orig_master_handler->disable_log_bin_local(); 152 | ##print current_time_us() . " Drpping app user on the orig master..\n"; 153 | ##FIXME_xxx_drop_app_user($orig_master_handler); 154 | 155 | ## Waiting for N * 100 milliseconds so that current connections can exit 156 | # my $time_until_read_only = 15; 157 | # $_tstart = [gettimeofday]; 158 | # my @threads = get_threads_util( $orig_master_handler->{dbh}, 159 | # $orig_master_handler->{connection_id} ); 160 | # while ( $time_until_read_only > 0 && $#threads >= 0 ) { 161 | # if ( $time_until_read_only % 5 == 0 ) { 162 | # printf 163 | #"%s Waiting all running %d threads are disconnected.. (max %d milliseconds)\n", 164 | # current_time_us(), $#threads + 1, $time_until_read_only * 100; 165 | # if ( $#threads < 5 ) { 166 | # print Data::Dumper->new( [$_] )->Indent(0)->Terse(1)->Dump . "\n" 167 | # foreach (@threads); 168 | # } 169 | # } 170 | # sleep_until(); 171 | # $_tstart = [gettimeofday]; 172 | # $time_until_read_only--; 173 | # @threads = get_threads_util( $orig_master_handler->{dbh}, 174 | # $orig_master_handler->{connection_id} ); 175 | # } 176 | 177 | ## Setting read_only=1 on the current master so that nobody(except SUPER) can write 178 | print current_time_us() . " Set read_only=1 on the orig master.. "; 179 | $orig_master_handler->enable_read_only(); 180 | if ( $orig_master_handler->is_read_only() ) { 181 | print "ok.\n"; 182 | } 183 | else { 184 | die "Failed!\n"; 185 | } 186 | 187 | ## Waiting for M * 100 milliseconds so that current update queries can complete 188 | # my $time_until_kill_threads = 5; 189 | my @threads = get_threads_util( $orig_master_handler->{dbh}, $orig_master_handler->{connection_id} ); 190 | # while ( $time_until_kill_threads > 0 && $#threads >= 0 ) { 191 | # if ( $time_until_kill_threads % 5 == 0 ) { 192 | # printf 193 | #"%s Waiting all running %d queries are disconnected.. (max %d milliseconds)\n", 194 | # current_time_us(), $#threads + 1, $time_until_kill_threads * 100; 195 | # if ( $#threads < 5 ) { 196 | # print Data::Dumper->new( [$_] )->Indent(0)->Terse(1)->Dump . "\n" 197 | # foreach (@threads); 198 | # } 199 | # } 200 | # sleep_until(); 201 | # $_tstart = [gettimeofday]; 202 | # $time_until_kill_threads--; 203 | # @threads = get_threads_util( $orig_master_handler->{dbh}, 204 | # $orig_master_handler->{connection_id} ); 205 | # } 206 | 207 | ## Terminating all threads 208 | print current_time_us() . " Killing all application threads..\n"; 209 | $orig_master_handler->kill_threads(@threads) if ( $#threads >= 0 ); 210 | print current_time_us() . " done.\n"; 211 | $orig_master_handler->enable_log_bin_local(); 212 | $orig_master_handler->disconnect(); 213 | 214 | ## After finishing the script, MHA executes FLUSH TABLES WITH READ LOCK 215 | $exit_code = 0; 216 | }; 217 | if ($@) { 218 | warn "Got Error: $@\n"; 219 | exit $exit_code; 220 | } 221 | exit $exit_code; 222 | } 223 | elsif ( $command eq "start" ) { 224 | ## Activating master ip on the new master 225 | # 1. Create app user with write privileges 226 | # 2. Moving backup script if needed 227 | # 3. Register new master's ip to the catalog database 228 | 229 | # We don't return error even though activating updatable accounts/ip failed so that we don't interrupt slaves' recovery. 230 | # If exit code is 0 or 10, MHA does not abort 231 | my $exit_code = 10; 232 | eval { 233 | my $new_master_handler = new MHA::DBHelper(); 234 | 235 | # args: hostname, port, user, password, raise_error_or_not 236 | $new_master_handler->connect( $new_master_ip, $new_master_port, 237 | $new_master_user, $new_master_password, 1 ); 238 | 239 | ## Set read_only=0 on the new master 240 | $new_master_handler->disable_log_bin_local(); 241 | print current_time_us() . " Set read_only=0 on the new master.\n"; 242 | $new_master_handler->disable_read_only(); 243 | 244 | ## Creating an app user on the new master 245 | print current_time_us() . " Creating app user on the new master..\n"; 246 | 247 | $new_master_handler->enable_log_bin_local(); 248 | $new_master_handler->disconnect(); 249 | 250 | ## Update ec2 tag MySQLRole 251 | # do nothing 252 | $exit_code = 0; 253 | }; 254 | if ($@) { 255 | warn "Got Error: $@\n"; 256 | exit $exit_code; 257 | } 258 | exit $exit_code; 259 | } 260 | elsif ( $command eq "status" ) { 261 | 262 | exit 0; 263 | } 264 | else { 265 | &usage(); 266 | exit 1; 267 | } 268 | } 269 | 270 | sub usage { 271 | print 272 | "Usage: master_ip_online_change --command=start|stop|status --orig_master_host=host --orig_master_ip=ip --orig_master_port=port --new_master_host=host --new_master_ip=ip --new_master_port=port\n"; 273 | die; 274 | } 275 | 276 | -------------------------------------------------------------------------------- /damp/library/proxysql_replication_hostgroups.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of Ansible 5 | # 6 | # Ansible is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Ansible is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Ansible. If not, see . 18 | 19 | DOCUMENTATION = ''' 20 | --- 21 | module: proxysql_replication_hostgroups 22 | version_added: "2.2" 23 | author: "Ben Mildren (@bmildren)" 24 | short_description: Manages replication hostgroups using the proxysql admin 25 | interface. 26 | description: 27 | - Each row in mysql_replication_hostgroups represent a pair of 28 | writer_hostgroup and reader_hostgroup . 29 | ProxySQL will monitor the value of read_only for all the servers in 30 | specified hostgroups, and based on the value of read_only will assign the 31 | server to the writer or reader hostgroups. 32 | options: 33 | writer_hostgroup: 34 | description: 35 | - Id of the writer hostgroup. 36 | required: True 37 | reader_hostgroup: 38 | description: 39 | - Id of the reader hostgroup. 40 | required: True 41 | comment: 42 | description: 43 | - Text field that can be used for any purposed defined by the user. 44 | state: 45 | description: 46 | - When C(present) - adds the replication hostgroup, when C(absent) - 47 | removes the replication hostgroup. 48 | choices: [ "present", "absent" ] 49 | default: present 50 | save_to_disk: 51 | description: 52 | - Save mysql host config to sqlite db on disk to persist the 53 | configuration. 54 | default: True 55 | load_to_runtime: 56 | description: 57 | - Dynamically load mysql host config to runtime memory. 58 | default: True 59 | login_user: 60 | description: 61 | - The username used to authenticate to ProxySQL admin interface 62 | default: None 63 | login_password: 64 | description: 65 | - The password used to authenticate to ProxySQL admin interface 66 | default: None 67 | login_host: 68 | description: 69 | - The host used to connect to ProxySQL admin interface 70 | default: '127.0.0.1' 71 | login_port: 72 | description: 73 | - The port used to connect to ProxySQL admin interface 74 | default: 6032 75 | config_file: 76 | description: 77 | - Specify a config file from which login_user and login_password are to 78 | be read 79 | default: '' 80 | ''' 81 | 82 | EXAMPLES = ''' 83 | --- 84 | # This example adds a replication hostgroup, it saves the mysql server config 85 | # to disk, but avoids loading the mysql server config to runtime (this might be 86 | # because several replication hostgroup are being added and the user wants to 87 | # push the config to runtime in a single batch using the 88 | # M(proxysql_manage_config) module). It uses supplied credentials to connect 89 | # to the proxysql admin interface. 90 | 91 | - proxysql_replication_hostgroups: 92 | login_user: 'admin' 93 | login_password: 'admin' 94 | writer_hostgroup: 1 95 | reader_hostgroup: 2 96 | state: present 97 | load_to_runtime: False 98 | 99 | # This example removes a replication hostgroup, saves the mysql server config 100 | # to disk, and dynamically loads the mysql server config to runtime. It uses 101 | # credentials in a supplied config file to connect to the proxysql admin 102 | # interface. 103 | 104 | - proxysql_replication_hostgroups: 105 | config_file: '~/proxysql.cnf' 106 | writer_hostgroup: 3 107 | reader_hostgroup: 4 108 | state: absent 109 | ''' 110 | 111 | RETURN = ''' 112 | stdout: 113 | description: The replication hostgroup modified or removed from proxysql 114 | returned: On create/update will return the newly modified group, on delete 115 | it will return the deleted record. 116 | type: dict 117 | "sample": { 118 | "changed": true, 119 | "msg": "Added server to mysql_hosts", 120 | "repl_group": { 121 | "comment": "", 122 | "reader_hostgroup": "1", 123 | "writer_hostgroup": "2" 124 | }, 125 | "state": "present" 126 | } 127 | ''' 128 | 129 | import sys 130 | 131 | try: 132 | import MySQLdb 133 | import MySQLdb.cursors 134 | except ImportError: 135 | mysqldb_found = False 136 | else: 137 | mysqldb_found = True 138 | 139 | # =========================================== 140 | # proxysql module specific support methods. 141 | # 142 | 143 | 144 | def perform_checks(module): 145 | if module.params["login_port"] < 0 \ 146 | or module.params["login_port"] > 65535: 147 | module.fail_json( 148 | msg="login_port must be a valid unix port number (0-65535)" 149 | ) 150 | 151 | if not module.params["writer_hostgroup"] >= 0: 152 | module.fail_json( 153 | msg="writer_hostgroup must be a integer greater than or equal to 0" 154 | ) 155 | 156 | if not module.params["reader_hostgroup"] == \ 157 | module.params["writer_hostgroup"]: 158 | if not module.params["reader_hostgroup"] > 0: 159 | module.fail_json( 160 | msg=("writer_hostgroup must be a integer greater than" + 161 | " or equal to 0") 162 | ) 163 | else: 164 | module.fail_json( 165 | msg="reader_hostgroup cannot equal writer_hostgroup" 166 | ) 167 | 168 | if not mysqldb_found: 169 | module.fail_json( 170 | msg="the python mysqldb module is required" 171 | ) 172 | 173 | 174 | def save_config_to_disk(cursor): 175 | cursor.execute("SAVE MYSQL SERVERS TO DISK") 176 | return True 177 | 178 | 179 | def load_config_to_runtime(cursor): 180 | cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") 181 | return True 182 | 183 | 184 | class ProxySQLReplicationHostgroup(object): 185 | 186 | def __init__(self, module): 187 | self.state = module.params["state"] 188 | self.save_to_disk = module.params["save_to_disk"] 189 | self.load_to_runtime = module.params["load_to_runtime"] 190 | self.writer_hostgroup = module.params["writer_hostgroup"] 191 | self.reader_hostgroup = module.params["reader_hostgroup"] 192 | self.comment = module.params["comment"] 193 | 194 | def check_repl_group_config(self, cursor, keys): 195 | query_string = \ 196 | """SELECT count(*) AS `repl_groups` 197 | FROM mysql_replication_hostgroups 198 | WHERE writer_hostgroup = %s 199 | AND reader_hostgroup = %s""" 200 | 201 | query_data = \ 202 | [self.writer_hostgroup, 203 | self.reader_hostgroup] 204 | 205 | if self.comment and not keys: 206 | query_string += "\n AND comment = %s" 207 | query_data.append(self.comment) 208 | 209 | cursor.execute(query_string, query_data) 210 | check_count = cursor.fetchone() 211 | return (int(check_count['repl_groups']) > 0) 212 | 213 | def get_repl_group_config(self, cursor): 214 | query_string = \ 215 | """SELECT * 216 | FROM mysql_replication_hostgroups 217 | WHERE writer_hostgroup = %s 218 | AND reader_hostgroup = %s""" 219 | 220 | query_data = \ 221 | [self.writer_hostgroup, 222 | self.reader_hostgroup] 223 | 224 | cursor.execute(query_string, query_data) 225 | repl_group = cursor.fetchone() 226 | return repl_group 227 | 228 | def create_repl_group_config(self, cursor): 229 | query_string = \ 230 | """INSERT INTO mysql_replication_hostgroups ( 231 | writer_hostgroup, 232 | reader_hostgroup, 233 | comment) 234 | VALUES (%s, %s, %s)""" 235 | 236 | query_data = \ 237 | [self.writer_hostgroup, 238 | self.reader_hostgroup, 239 | self.comment or ''] 240 | 241 | cursor.execute(query_string, query_data) 242 | return True 243 | 244 | def update_repl_group_config(self, cursor): 245 | query_string = \ 246 | """UPDATE mysql_replication_hostgroups 247 | SET comment = %s 248 | WHERE writer_hostgroup = %s 249 | AND reader_hostgroup = %s""" 250 | 251 | query_data = \ 252 | [self.comment, 253 | self.writer_hostgroup, 254 | self.reader_hostgroup] 255 | 256 | cursor.execute(query_string, query_data) 257 | return True 258 | 259 | def delete_repl_group_config(self, cursor): 260 | query_string = \ 261 | """DELETE FROM mysql_replication_hostgroups 262 | WHERE writer_hostgroup = %s 263 | AND reader_hostgroup = %s""" 264 | 265 | query_data = \ 266 | [self.writer_hostgroup, 267 | self.reader_hostgroup] 268 | 269 | cursor.execute(query_string, query_data) 270 | return True 271 | 272 | def manage_config(self, cursor, state): 273 | if state: 274 | if self.save_to_disk: 275 | save_config_to_disk(cursor) 276 | if self.load_to_runtime: 277 | load_config_to_runtime(cursor) 278 | 279 | def create_repl_group(self, check_mode, result, cursor): 280 | if not check_mode: 281 | result['changed'] = \ 282 | self.create_repl_group_config(cursor) 283 | result['msg'] = "Added server to mysql_hosts" 284 | result['repl_group'] = \ 285 | self.get_repl_group_config(cursor) 286 | self.manage_config(cursor, 287 | result['changed']) 288 | else: 289 | result['changed'] = True 290 | result['msg'] = ("Repl group would have been added to" + 291 | " mysql_replication_hostgroups, however" + 292 | " check_mode is enabled.") 293 | 294 | def update_repl_group(self, check_mode, result, cursor): 295 | if not check_mode: 296 | result['changed'] = \ 297 | self.update_repl_group_config(cursor) 298 | result['msg'] = "Updated server in mysql_hosts" 299 | result['repl_group'] = \ 300 | self.get_repl_group_config(cursor) 301 | self.manage_config(cursor, 302 | result['changed']) 303 | else: 304 | result['changed'] = True 305 | result['msg'] = ("Repl group would have been updated in" + 306 | " mysql_replication_hostgroups, however" + 307 | " check_mode is enabled.") 308 | 309 | def delete_repl_group(self, check_mode, result, cursor): 310 | if not check_mode: 311 | result['repl_group'] = \ 312 | self.get_repl_group_config(cursor) 313 | result['changed'] = \ 314 | self.delete_repl_group_config(cursor) 315 | result['msg'] = "Deleted server from mysql_hosts" 316 | self.manage_config(cursor, 317 | result['changed']) 318 | else: 319 | result['changed'] = True 320 | result['msg'] = ("Repl group would have been deleted from" + 321 | " mysql_replication_hostgroups, however" + 322 | " check_mode is enabled.") 323 | 324 | # =========================================== 325 | # Module execution. 326 | # 327 | 328 | 329 | def main(): 330 | module = AnsibleModule( 331 | argument_spec=dict( 332 | login_user=dict(default=None, type='str'), 333 | login_password=dict(default=None, no_log=True, type='str'), 334 | login_host=dict(default="127.0.0.1"), 335 | login_unix_socket=dict(default=None), 336 | login_port=dict(default=6032, type='int'), 337 | config_file=dict(default="", type='path'), 338 | writer_hostgroup=dict(required=True, type='int'), 339 | reader_hostgroup=dict(required=True, type='int'), 340 | comment=dict(type='str'), 341 | state=dict(default='present', choices=['present', 342 | 'absent']), 343 | save_to_disk=dict(default=True, type='bool'), 344 | load_to_runtime=dict(default=True, type='bool') 345 | ), 346 | supports_check_mode=True 347 | ) 348 | 349 | perform_checks(module) 350 | 351 | login_user = module.params["login_user"] 352 | login_password = module.params["login_password"] 353 | config_file = module.params["config_file"] 354 | 355 | cursor = None 356 | try: 357 | cursor = mysql_connect(module, 358 | login_user, 359 | login_password, 360 | config_file, 361 | cursor_class=MySQLdb.cursors.DictCursor) 362 | except MySQLdb.Error: 363 | e = sys.exc_info()[1] 364 | module.fail_json( 365 | msg="unable to connect to ProxySQL Admin Module.. %s" % e 366 | ) 367 | 368 | proxysql_repl_group = ProxySQLReplicationHostgroup(module) 369 | result = {} 370 | 371 | result['state'] = proxysql_repl_group.state 372 | 373 | if proxysql_repl_group.state == "present": 374 | try: 375 | if not proxysql_repl_group.check_repl_group_config(cursor, 376 | keys=True): 377 | proxysql_repl_group.create_repl_group(module.check_mode, 378 | result, 379 | cursor) 380 | else: 381 | if not proxysql_repl_group.check_repl_group_config(cursor, 382 | keys=False): 383 | proxysql_repl_group.update_repl_group(module.check_mode, 384 | result, 385 | cursor) 386 | else: 387 | result['changed'] = False 388 | result['msg'] = ("The repl group already exists in" + 389 | " mysql_replication_hostgroups and" + 390 | " doesn't need to be updated.") 391 | result['repl_group'] = \ 392 | proxysql_repl_group.get_repl_group_config(cursor) 393 | 394 | except MySQLdb.Error: 395 | e = sys.exc_info()[1] 396 | module.fail_json( 397 | msg="unable to modify replication hostgroup.. %s" % e 398 | ) 399 | 400 | elif proxysql_repl_group.state == "absent": 401 | try: 402 | if proxysql_repl_group.check_repl_group_config(cursor, 403 | keys=True): 404 | proxysql_repl_group.delete_repl_group(module.check_mode, 405 | result, 406 | cursor) 407 | else: 408 | result['changed'] = False 409 | result['msg'] = ("The repl group is already absent from the" + 410 | " mysql_replication_hostgroups memory" + 411 | " configuration") 412 | 413 | except MySQLdb.Error: 414 | e = sys.exc_info()[1] 415 | module.fail_json( 416 | msg="unable to delete replication hostgroup.. %s" % e 417 | ) 418 | 419 | module.exit_json(**result) 420 | 421 | from ansible.module_utils.basic import * 422 | from ansible.module_utils.mysql import * 423 | if __name__ == '__main__': 424 | main() 425 | -------------------------------------------------------------------------------- /damp/library/proxysql_scheduler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of Ansible 5 | # 6 | # Ansible is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Ansible is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Ansible. If not, see . 18 | 19 | DOCUMENTATION = ''' 20 | --- 21 | module: proxysql_scheduler 22 | version_added: "2.2" 23 | author: "Ben Mildren (@bmildren)" 24 | short_description: Adds or removes schedules from proxysql admin interface. 25 | description: 26 | - The M(proxysql_scheduler) module adds or removes schedules using the 27 | proxysql admin interface. 28 | options: 29 | active: 30 | description: 31 | - A schedule with I(active) set to C(False) will be tracked in the 32 | database, but will be never loaded in the in-memory data structures 33 | default: True 34 | interval_ms: 35 | description: 36 | - How often (in millisecond) the job will be started. The minimum value 37 | for I(interval_ms) is 100 milliseconds 38 | default: 10000 39 | filename: 40 | description: 41 | - Full path of the executable to be executed. 42 | required: True 43 | arg1: 44 | description: 45 | - Argument that can be passed to the job. 46 | arg2: 47 | description: 48 | - Argument that can be passed to the job. 49 | arg3: 50 | description: 51 | - Argument that can be passed to the job. 52 | arg4: 53 | description: 54 | - Argument that can be passed to the job. 55 | arg5: 56 | description: 57 | - Argument that can be passed to the job. 58 | comment: 59 | description: 60 | - Text field that can be used for any purposed defined by the user. 61 | state: 62 | description: 63 | - When C(present) - adds the schedule, when C(absent) - removes the 64 | schedule. 65 | choices: [ "present", "absent" ] 66 | default: present 67 | force_delete: 68 | description: 69 | - By default we avoid deleting more than one schedule in a single batch, 70 | however if you need this behaviour and you're not concerned about the 71 | schedules deleted, you can set I(force_delete) to C(True). 72 | default: False 73 | save_to_disk: 74 | description: 75 | - Save mysql host config to sqlite db on disk to persist the 76 | configuration. 77 | default: True 78 | load_to_runtime: 79 | description: 80 | - Dynamically load mysql host config to runtime memory. 81 | default: True 82 | login_user: 83 | description: 84 | - The username used to authenticate to ProxySQL admin interface 85 | default: None 86 | login_password: 87 | description: 88 | - The password used to authenticate to ProxySQL admin interface 89 | default: None 90 | login_host: 91 | description: 92 | - The host used to connect to ProxySQL admin interface 93 | default: '127.0.0.1' 94 | login_port: 95 | description: 96 | - The port used to connect to ProxySQL admin interface 97 | default: 6032 98 | config_file: 99 | description: 100 | - Specify a config file from which login_user and login_password are to 101 | be read 102 | default: '' 103 | ''' 104 | 105 | EXAMPLES = ''' 106 | --- 107 | # This example adds a schedule, it saves the scheduler config to disk, but 108 | # avoids loading the scheduler config to runtime (this might be because 109 | # several servers are being added and the user wants to push the config to 110 | # runtime in a single batch using the M(proxysql_manage_config) module). It 111 | # uses supplied credentials to connect to the proxysql admin interface. 112 | 113 | - proxysql_scheduler: 114 | login_user: 'admin' 115 | login_password: 'admin' 116 | interval_ms: 1000 117 | filename: "/opt/maintenance.py" 118 | state: present 119 | load_to_runtime: False 120 | 121 | # This example removes a schedule, saves the scheduler config to disk, and 122 | # dynamically loads the scheduler config to runtime. It uses credentials 123 | # in a supplied config file to connect to the proxysql admin interface. 124 | 125 | - proxysql_scheduler: 126 | config_file: '~/proxysql.cnf' 127 | filename: "/opt/old_script.py" 128 | state: absent 129 | ''' 130 | 131 | RETURN = ''' 132 | stdout: 133 | description: The schedule modified or removed from proxysql 134 | returned: On create/update will return the newly modified schedule, on 135 | delete it will return the deleted record. 136 | type: dict 137 | "sample": { 138 | "changed": true, 139 | "filename": "/opt/test.py", 140 | "msg": "Added schedule to scheduler", 141 | "schedules": [ 142 | { 143 | "active": "1", 144 | "arg1": null, 145 | "arg2": null, 146 | "arg3": null, 147 | "arg4": null, 148 | "arg5": null, 149 | "comment": "", 150 | "filename": "/opt/test.py", 151 | "id": "1", 152 | "interval_ms": "10000" 153 | } 154 | ], 155 | "state": "present" 156 | } 157 | ''' 158 | 159 | import sys 160 | 161 | try: 162 | import MySQLdb 163 | import MySQLdb.cursors 164 | except ImportError: 165 | mysqldb_found = False 166 | else: 167 | mysqldb_found = True 168 | 169 | # =========================================== 170 | # proxysql module specific support methods. 171 | # 172 | 173 | 174 | def perform_checks(module): 175 | if module.params["login_port"] < 0 \ 176 | or module.params["login_port"] > 65535: 177 | module.fail_json( 178 | msg="login_port must be a valid unix port number (0-65535)" 179 | ) 180 | 181 | if module.params["interval_ms"] < 100 \ 182 | or module.params["interval_ms"] > 100000000: 183 | module.fail_json( 184 | msg="interval_ms must between 100ms & 100000000ms" 185 | ) 186 | 187 | if not mysqldb_found: 188 | module.fail_json( 189 | msg="the python mysqldb module is required" 190 | ) 191 | 192 | 193 | def save_config_to_disk(cursor): 194 | cursor.execute("SAVE SCHEDULER TO DISK") 195 | return True 196 | 197 | 198 | def load_config_to_runtime(cursor): 199 | cursor.execute("LOAD SCHEDULER TO RUNTIME") 200 | return True 201 | 202 | 203 | class ProxySQLSchedule(object): 204 | 205 | def __init__(self, module): 206 | self.state = module.params["state"] 207 | self.force_delete = module.params["force_delete"] 208 | self.save_to_disk = module.params["save_to_disk"] 209 | self.load_to_runtime = module.params["load_to_runtime"] 210 | self.active = module.params["active"] 211 | self.interval_ms = module.params["interval_ms"] 212 | self.filename = module.params["filename"] 213 | 214 | config_data_keys = ["arg1", 215 | "arg2", 216 | "arg3", 217 | "arg4", 218 | "arg5", 219 | "comment"] 220 | 221 | self.config_data = dict((k, module.params[k]) 222 | for k in (config_data_keys)) 223 | 224 | def check_schedule_config(self, cursor): 225 | query_string = \ 226 | """SELECT count(*) AS `schedule_count` 227 | FROM scheduler 228 | WHERE active = %s 229 | AND interval_ms = %s 230 | AND filename = %s""" 231 | 232 | query_data = \ 233 | [self.active, 234 | self.interval_ms, 235 | self.filename] 236 | 237 | for col, val in self.config_data.iteritems(): 238 | if val is not None: 239 | query_data.append(val) 240 | query_string += "\n AND " + col + " = %s" 241 | 242 | cursor.execute(query_string, query_data) 243 | check_count = cursor.fetchone() 244 | return int(check_count['schedule_count']) 245 | 246 | def get_schedule_config(self, cursor): 247 | query_string = \ 248 | """SELECT * 249 | FROM scheduler 250 | WHERE active = %s 251 | AND interval_ms = %s 252 | AND filename = %s""" 253 | 254 | query_data = \ 255 | [self.active, 256 | self.interval_ms, 257 | self.filename] 258 | 259 | for col, val in self.config_data.iteritems(): 260 | if val is not None: 261 | query_data.append(val) 262 | query_string += "\n AND " + col + " = %s" 263 | 264 | cursor.execute(query_string, query_data) 265 | schedule = cursor.fetchall() 266 | return schedule 267 | 268 | def create_schedule_config(self, cursor): 269 | query_string = \ 270 | """INSERT INTO scheduler ( 271 | active, 272 | interval_ms, 273 | filename""" 274 | 275 | cols = 0 276 | query_data = \ 277 | [self.active, 278 | self.interval_ms, 279 | self.filename] 280 | 281 | for col, val in self.config_data.iteritems(): 282 | if val is not None: 283 | cols += 1 284 | query_data.append(val) 285 | query_string += ",\n" + col 286 | 287 | query_string += \ 288 | (")\n" + 289 | "VALUES (%s, %s, %s" + 290 | ", %s" * cols + 291 | ")") 292 | 293 | cursor.execute(query_string, query_data) 294 | return True 295 | 296 | def delete_schedule_config(self, cursor): 297 | query_string = \ 298 | """DELETE FROM scheduler 299 | WHERE active = %s 300 | AND interval_ms = %s 301 | AND filename = %s""" 302 | 303 | query_data = \ 304 | [self.active, 305 | self.interval_ms, 306 | self.filename] 307 | 308 | for col, val in self.config_data.iteritems(): 309 | if val is not None: 310 | query_data.append(val) 311 | query_string += "\n AND " + col + " = %s" 312 | 313 | cursor.execute(query_string, query_data) 314 | check_count = cursor.rowcount 315 | return True, int(check_count) 316 | 317 | def manage_config(self, cursor, state): 318 | if state: 319 | if self.save_to_disk: 320 | save_config_to_disk(cursor) 321 | if self.load_to_runtime: 322 | load_config_to_runtime(cursor) 323 | 324 | def create_schedule(self, check_mode, result, cursor): 325 | if not check_mode: 326 | result['changed'] = \ 327 | self.create_schedule_config(cursor) 328 | result['msg'] = "Added schedule to scheduler" 329 | result['schedules'] = \ 330 | self.get_schedule_config(cursor) 331 | self.manage_config(cursor, 332 | result['changed']) 333 | else: 334 | result['changed'] = True 335 | result['msg'] = ("Schedule would have been added to" + 336 | " scheduler, however check_mode" + 337 | " is enabled.") 338 | 339 | def delete_schedule(self, check_mode, result, cursor): 340 | if not check_mode: 341 | result['schedules'] = \ 342 | self.get_schedule_config(cursor) 343 | result['changed'] = \ 344 | self.delete_schedule_config(cursor) 345 | result['msg'] = "Deleted schedule from scheduler" 346 | self.manage_config(cursor, 347 | result['changed']) 348 | else: 349 | result['changed'] = True 350 | result['msg'] = ("Schedule would have been deleted from" + 351 | " scheduler, however check_mode is" + 352 | " enabled.") 353 | 354 | # =========================================== 355 | # Module execution. 356 | # 357 | 358 | 359 | def main(): 360 | module = AnsibleModule( 361 | argument_spec=dict( 362 | login_user=dict(default=None, type='str'), 363 | login_password=dict(default=None, no_log=True, type='str'), 364 | login_host=dict(default="127.0.0.1"), 365 | login_unix_socket=dict(default=None), 366 | login_port=dict(default=6032, type='int'), 367 | config_file=dict(default="", type='path'), 368 | active=dict(default=True, type='bool'), 369 | interval_ms=dict(default=10000, type='int'), 370 | filename=dict(required=True, type='str'), 371 | arg1=dict(type='str'), 372 | arg2=dict(type='str'), 373 | arg3=dict(type='str'), 374 | arg4=dict(type='str'), 375 | arg5=dict(type='str'), 376 | comment=dict(type='str'), 377 | state=dict(default='present', choices=['present', 378 | 'absent']), 379 | force_delete=dict(default=False, type='bool'), 380 | save_to_disk=dict(default=True, type='bool'), 381 | load_to_runtime=dict(default=True, type='bool') 382 | ), 383 | supports_check_mode=True 384 | ) 385 | 386 | perform_checks(module) 387 | 388 | login_user = module.params["login_user"] 389 | login_password = module.params["login_password"] 390 | config_file = module.params["config_file"] 391 | 392 | cursor = None 393 | try: 394 | cursor = mysql_connect(module, 395 | login_user, 396 | login_password, 397 | config_file, 398 | cursor_class=MySQLdb.cursors.DictCursor) 399 | except MySQLdb.Error: 400 | e = sys.exc_info()[1] 401 | module.fail_json( 402 | msg="unable to connect to ProxySQL Admin Module.. %s" % e 403 | ) 404 | 405 | proxysql_schedule = ProxySQLSchedule(module) 406 | result = {} 407 | 408 | result['state'] = proxysql_schedule.state 409 | result['filename'] = proxysql_schedule.filename 410 | 411 | if proxysql_schedule.state == "present": 412 | try: 413 | if not proxysql_schedule.check_schedule_config(cursor) > 0: 414 | proxysql_schedule.create_schedule(module.check_mode, 415 | result, 416 | cursor) 417 | else: 418 | result['changed'] = False 419 | result['msg'] = ("The schedule already exists and doesn't" + 420 | " need to be updated.") 421 | result['schedules'] = \ 422 | proxysql_schedule.get_schedule_config(cursor) 423 | except MySQLdb.Error: 424 | e = sys.exc_info()[1] 425 | module.fail_json( 426 | msg="unable to modify schedule.. %s" % e 427 | ) 428 | 429 | elif proxysql_schedule.state == "absent": 430 | try: 431 | existing_schedules = \ 432 | proxysql_schedule.check_schedule_config(cursor) 433 | if existing_schedules > 0: 434 | if existing_schedules == 1 or proxysql_schedule.force_delete: 435 | proxysql_schedule.delete_schedule(module.check_mode, 436 | result, 437 | cursor) 438 | else: 439 | module.fail_json( 440 | msg=("Operation would delete multiple records" + 441 | " use force_delete to override this") 442 | ) 443 | else: 444 | result['changed'] = False 445 | result['msg'] = ("The schedule is already absent from the" + 446 | " memory configuration") 447 | except MySQLdb.Error: 448 | e = sys.exc_info()[1] 449 | module.fail_json( 450 | msg="unable to remove schedule.. %s" % e 451 | ) 452 | 453 | module.exit_json(**result) 454 | 455 | from ansible.module_utils.basic import * 456 | from ansible.module_utils.mysql import * 457 | if __name__ == '__main__': 458 | main() 459 | -------------------------------------------------------------------------------- /damp/library/proxysql_mysql_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of Ansible 5 | # 6 | # Ansible is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Ansible is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Ansible. If not, see . 18 | 19 | DOCUMENTATION = ''' 20 | --- 21 | module: proxysql_mysql_users 22 | version_added: "2.2" 23 | author: "Ben Mildren (@bmildren)" 24 | short_description: Adds or removes mysql users from proxysql admin interface. 25 | description: 26 | - The M(proxysql_mysql_users) module adds or removes mysql users using the 27 | proxysql admin interface. 28 | options: 29 | username: 30 | description: 31 | - Name of the user connecting to the mysqld or ProxySQL instance. 32 | required: True 33 | password: 34 | description: 35 | - Password of the user connecting to the mysqld or ProxySQL instance. 36 | active: 37 | description: 38 | - A user with I(active) set to C(False) will be tracked in the database, 39 | but will be never loaded in the in-memory data structures. 40 | If ommitted the proxysql default for I(active) is C(True). 41 | use_ssl: 42 | description: 43 | - If I(use_ssl) is set to C(True), connections by this user will be 44 | made using SSL connections. 45 | If ommitted the proxysql default for I(use_ssl) is C(False). 46 | default_hostgroup: 47 | description: 48 | - If there is no matching rule for the queries sent by this user, the 49 | traffic it generates is sent to the specified hostgroup. 50 | If ommitted the proxysql default for I(use_ssl) is 0. 51 | default_schema: 52 | description: 53 | - The schema to which the connection should change to by default. 54 | transaction_persistent: 55 | description: 56 | - If this is set for the user with which the MySQL client is connecting 57 | to ProxySQL (thus a "frontend" user), transactions started within a 58 | hostgroup will remain within that hostgroup regardless of any other 59 | rules. 60 | If ommitted the proxysql default for I(transaction_persistent) is 61 | C(False). 62 | fast_forward: 63 | description: 64 | - If I(fast_forward) is set to C(True), I(fast_forward) will bypass the 65 | query processing layer (rewriting, caching) and pass through the query 66 | directly as is to the backend server. 67 | If ommitted the proxysql default for I(fast_forward) is C(False). 68 | backend: 69 | description: 70 | - If I(backend) is set to C(True), this (username, password) pair is 71 | used for authenticating to the ProxySQL instance. 72 | default: True 73 | frontend: 74 | description: 75 | - If I(frontend) is set to C(True), this (username, password) pair is 76 | used for authenticating to the mysqld servers against any hostgroup. 77 | default: True 78 | max_connections: 79 | description: 80 | - The maximum number of connections ProxySQL will open to the backend 81 | for this user. 82 | If ommitted the proxysql default for I(max_connections) is 10000. 83 | state: 84 | description: 85 | - When C(present) - adds the user, when C(absent) - removes the user. 86 | choices: [ "present", "absent" ] 87 | default: present 88 | save_to_disk: 89 | description: 90 | - Save mysql host config to sqlite db on disk to persist the 91 | configuration. 92 | default: True 93 | load_to_runtime: 94 | description: 95 | - Dynamically load mysql host config to runtime memory. 96 | default: True 97 | login_user: 98 | description: 99 | - The username used to authenticate to ProxySQL admin interface 100 | default: None 101 | login_password: 102 | description: 103 | - The password used to authenticate to ProxySQL admin interface 104 | default: None 105 | login_host: 106 | description: 107 | - The host used to connect to ProxySQL admin interface 108 | default: '127.0.0.1' 109 | login_port: 110 | description: 111 | - The port used to connect to ProxySQL admin interface 112 | default: 6032 113 | config_file: 114 | description: 115 | - Specify a config file from which login_user and login_password are to 116 | be read 117 | default: '' 118 | ''' 119 | 120 | EXAMPLES = ''' 121 | --- 122 | # This example adds a user, it saves the mysql user config to disk, but 123 | # avoids loading the mysql user config to runtime (this might be because 124 | # several users are being added and the user wants to push the config to 125 | # runtime in a single batch using the M(proxysql_manage_config) module). It 126 | # uses supplied credentials to connect to the proxysql admin interface. 127 | 128 | - proxysql_mysql_users: 129 | login_user: 'admin' 130 | login_password: 'admin' 131 | username: 'productiondba' 132 | state: present 133 | load_to_runtime: False 134 | 135 | # This example removes a user, saves the mysql user config to disk, and 136 | # dynamically loads the mysql user config to runtime. It uses credentials 137 | # in a supplied config file to connect to the proxysql admin interface. 138 | 139 | - proxysql_mysql_users: 140 | config_file: '~/proxysql.cnf' 141 | username: 'mysqlboy' 142 | state: absent 143 | ''' 144 | 145 | RETURN = ''' 146 | stdout: 147 | description: The mysql user modified or removed from proxysql 148 | returned: On create/update will return the newly modified user, on delete 149 | it will return the deleted record. 150 | type: dict 151 | sample": { 152 | "changed": true, 153 | "msg": "Added user to mysql_users", 154 | "state": "present", 155 | "user": { 156 | "active": "1", 157 | "backend": "1", 158 | "default_hostgroup": "1", 159 | "default_schema": null, 160 | "fast_forward": "0", 161 | "frontend": "1", 162 | "max_connections": "10000", 163 | "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 164 | "schema_locked": "0", 165 | "transaction_persistent": "0", 166 | "use_ssl": "0", 167 | "username": "guest_ro" 168 | }, 169 | "username": "guest_ro" 170 | } 171 | ''' 172 | 173 | import sys 174 | 175 | try: 176 | import MySQLdb 177 | import MySQLdb.cursors 178 | except ImportError: 179 | mysqldb_found = False 180 | else: 181 | mysqldb_found = True 182 | 183 | # =========================================== 184 | # proxysql module specific support methods. 185 | # 186 | 187 | 188 | def perform_checks(module): 189 | if module.params["login_port"] < 0 \ 190 | or module.params["login_port"] > 65535: 191 | module.fail_json( 192 | msg="login_port must be a valid unix port number (0-65535)" 193 | ) 194 | 195 | if not mysqldb_found: 196 | module.fail_json( 197 | msg="the python mysqldb module is required" 198 | ) 199 | 200 | 201 | def save_config_to_disk(cursor): 202 | cursor.execute("SAVE MYSQL USERS TO DISK") 203 | return True 204 | 205 | 206 | def load_config_to_runtime(cursor): 207 | cursor.execute("LOAD MYSQL USERS TO RUNTIME") 208 | return True 209 | 210 | 211 | class ProxySQLUser(object): 212 | 213 | def __init__(self, module): 214 | self.state = module.params["state"] 215 | self.save_to_disk = module.params["save_to_disk"] 216 | self.load_to_runtime = module.params["load_to_runtime"] 217 | 218 | self.username = module.params["username"] 219 | self.backend = module.params["backend"] 220 | self.frontend = module.params["frontend"] 221 | 222 | config_data_keys = ["password", 223 | "active", 224 | "use_ssl", 225 | "default_hostgroup", 226 | "default_schema", 227 | "transaction_persistent", 228 | "fast_forward", 229 | "max_connections"] 230 | 231 | self.config_data = dict((k, module.params[k]) 232 | for k in (config_data_keys)) 233 | 234 | def check_user_config_exists(self, cursor): 235 | query_string = \ 236 | """SELECT count(*) AS `user_count` 237 | FROM mysql_users 238 | WHERE username = %s 239 | AND backend = %s 240 | AND frontend = %s""" 241 | 242 | query_data = \ 243 | [self.username, 244 | self.backend, 245 | self.frontend] 246 | 247 | cursor.execute(query_string, query_data) 248 | check_count = cursor.fetchone() 249 | return (int(check_count['user_count']) > 0) 250 | 251 | def check_user_privs(self, cursor): 252 | query_string = \ 253 | """SELECT count(*) AS `user_count` 254 | FROM mysql_users 255 | WHERE username = %s 256 | AND backend = %s 257 | AND frontend = %s""" 258 | 259 | query_data = \ 260 | [self.username, 261 | self.backend, 262 | self.frontend] 263 | 264 | for col, val in self.config_data.iteritems(): 265 | if val is not None: 266 | query_data.append(val) 267 | query_string += "\n AND " + col + " = %s" 268 | 269 | cursor.execute(query_string, query_data) 270 | check_count = cursor.fetchone() 271 | return (int(check_count['user_count']) > 0) 272 | 273 | def get_user_config(self, cursor): 274 | query_string = \ 275 | """SELECT * 276 | FROM mysql_users 277 | WHERE username = %s 278 | AND backend = %s 279 | AND frontend = %s""" 280 | 281 | query_data = \ 282 | [self.username, 283 | self.backend, 284 | self.frontend] 285 | 286 | cursor.execute(query_string, query_data) 287 | user = cursor.fetchone() 288 | return user 289 | 290 | def create_user_config(self, cursor): 291 | query_string = \ 292 | """INSERT INTO mysql_users ( 293 | username, 294 | backend, 295 | frontend""" 296 | 297 | cols = 3 298 | query_data = \ 299 | [self.username, 300 | self.backend, 301 | self.frontend] 302 | 303 | for col, val in self.config_data.iteritems(): 304 | if val is not None: 305 | cols += 1 306 | query_data.append(val) 307 | query_string += ",\n" + col 308 | 309 | query_string += \ 310 | (")\n" + 311 | "VALUES (" + 312 | "%s ," * cols) 313 | 314 | query_string = query_string[:-2] 315 | query_string += ")" 316 | 317 | cursor.execute(query_string, query_data) 318 | return True 319 | 320 | def update_user_config(self, cursor): 321 | query_string = """UPDATE mysql_users""" 322 | 323 | cols = 0 324 | query_data = [] 325 | 326 | for col, val in self.config_data.iteritems(): 327 | if val is not None: 328 | cols += 1 329 | query_data.append(val) 330 | if cols == 1: 331 | query_string += "\nSET " + col + "= %s," 332 | else: 333 | query_string += "\n " + col + " = %s," 334 | 335 | query_string = query_string[:-1] 336 | query_string += ("\nWHERE username = %s\n AND backend = %s" + 337 | "\n AND frontend = %s") 338 | 339 | query_data.append(self.username) 340 | query_data.append(self.backend) 341 | query_data.append(self.frontend) 342 | 343 | cursor.execute(query_string, query_data) 344 | return True 345 | 346 | def delete_user_config(self, cursor): 347 | query_string = \ 348 | """DELETE FROM mysql_users 349 | WHERE username = %s 350 | AND backend = %s 351 | AND frontend = %s""" 352 | 353 | query_data = \ 354 | [self.username, 355 | self.backend, 356 | self.frontend] 357 | 358 | cursor.execute(query_string, query_data) 359 | return True 360 | 361 | def manage_config(self, cursor, state): 362 | if state: 363 | if self.save_to_disk: 364 | save_config_to_disk(cursor) 365 | if self.load_to_runtime: 366 | load_config_to_runtime(cursor) 367 | 368 | def create_user(self, check_mode, result, cursor): 369 | if not check_mode: 370 | result['changed'] = \ 371 | self.create_user_config(cursor) 372 | result['msg'] = "Added user to mysql_users" 373 | result['user'] = \ 374 | self.get_user_config(cursor) 375 | self.manage_config(cursor, 376 | result['changed']) 377 | else: 378 | result['changed'] = True 379 | result['msg'] = ("User would have been added to" + 380 | " mysql_users, however check_mode" + 381 | " is enabled.") 382 | 383 | def update_user(self, check_mode, result, cursor): 384 | if not check_mode: 385 | result['changed'] = \ 386 | self.update_user_config(cursor) 387 | result['msg'] = "Updated user in mysql_users" 388 | result['user'] = \ 389 | self.get_user_config(cursor) 390 | self.manage_config(cursor, 391 | result['changed']) 392 | else: 393 | result['changed'] = True 394 | result['msg'] = ("User would have been updated in" + 395 | " mysql_users, however check_mode" + 396 | " is enabled.") 397 | 398 | def delete_user(self, check_mode, result, cursor): 399 | if not check_mode: 400 | result['user'] = \ 401 | self.get_user_config(cursor) 402 | result['changed'] = \ 403 | self.delete_user_config(cursor) 404 | result['msg'] = "Deleted user from mysql_users" 405 | self.manage_config(cursor, 406 | result['changed']) 407 | else: 408 | result['changed'] = True 409 | result['msg'] = ("User would have been deleted from" + 410 | " mysql_users, however check_mode is" + 411 | " enabled.") 412 | 413 | # =========================================== 414 | # Module execution. 415 | # 416 | 417 | 418 | def main(): 419 | module = AnsibleModule( 420 | argument_spec=dict( 421 | login_user=dict(default=None, type='str'), 422 | login_password=dict(default=None, no_log=True, type='str'), 423 | login_host=dict(default="127.0.0.1"), 424 | login_unix_socket=dict(default=None), 425 | login_port=dict(default=6032, type='int'), 426 | config_file=dict(default='', type='path'), 427 | username=dict(required=True, type='str'), 428 | password=dict(no_log=True, type='str'), 429 | active=dict(type='bool'), 430 | use_ssl=dict(type='bool'), 431 | default_hostgroup=dict(type='int'), 432 | default_schema=dict(type='str'), 433 | transaction_persistent=dict(type='bool'), 434 | fast_forward=dict(type='bool'), 435 | backend=dict(default=True, type='bool'), 436 | frontend=dict(default=True, type='bool'), 437 | max_connections=dict(type='int'), 438 | state=dict(default='present', choices=['present', 439 | 'absent']), 440 | save_to_disk=dict(default=True, type='bool'), 441 | load_to_runtime=dict(default=True, type='bool') 442 | ), 443 | supports_check_mode=True 444 | ) 445 | 446 | perform_checks(module) 447 | 448 | login_user = module.params["login_user"] 449 | login_password = module.params["login_password"] 450 | config_file = module.params["config_file"] 451 | 452 | cursor = None 453 | try: 454 | cursor = mysql_connect(module, 455 | login_user, 456 | login_password, 457 | config_file, 458 | cursor_class=MySQLdb.cursors.DictCursor) 459 | except MySQLdb.Error: 460 | e = sys.exc_info()[1] 461 | module.fail_json( 462 | msg="unable to connect to ProxySQL Admin Module.. %s" % e 463 | ) 464 | 465 | proxysql_user = ProxySQLUser(module) 466 | result = {} 467 | 468 | result['state'] = proxysql_user.state 469 | if proxysql_user.username: 470 | result['username'] = proxysql_user.username 471 | 472 | if proxysql_user.state == "present": 473 | try: 474 | if not proxysql_user.check_user_privs(cursor): 475 | if not proxysql_user.check_user_config_exists(cursor): 476 | proxysql_user.create_user(module.check_mode, 477 | result, 478 | cursor) 479 | else: 480 | proxysql_user.update_user(module.check_mode, 481 | result, 482 | cursor) 483 | else: 484 | result['changed'] = False 485 | result['msg'] = ("The user already exists in mysql_users" + 486 | " and doesn't need to be updated.") 487 | result['user'] = \ 488 | proxysql_user.get_user_config(cursor) 489 | except MySQLdb.Error: 490 | e = sys.exc_info()[1] 491 | module.fail_json( 492 | msg="unable to modify user.. %s" % e 493 | ) 494 | 495 | elif proxysql_user.state == "absent": 496 | try: 497 | if proxysql_user.check_user_config_exists(cursor): 498 | proxysql_user.delete_user(module.check_mode, 499 | result, 500 | cursor) 501 | else: 502 | result['changed'] = False 503 | result['msg'] = ("The user is already absent from the" + 504 | " mysql_users memory configuration") 505 | except MySQLdb.Error: 506 | e = sys.exc_info()[1] 507 | module.fail_json( 508 | msg="unable to remove user.. %s" % e 509 | ) 510 | 511 | module.exit_json(**result) 512 | 513 | from ansible.module_utils.basic import * 514 | from ansible.module_utils.mysql import * 515 | if __name__ == '__main__': 516 | main() 517 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Anisble-MHA-Orchestrator-ProxySQL-Docker 2 | ============================================================ 3 | Teaching them to play together 4 | 5 | **Now with Orchestrator!** 6 | 7 | The big picture: 8 | ![img](http://i.imgur.com/sOKx0YL.png) 9 | 10 | 11 | Presentation about [DAMP](http://www.slideshare.net/MiklosSzel/painless-mysql-ha-scalability-and-flexibility-with-ansible-mha-and-proxysql) 12 | 13 | 14 | 15 | ## Install 16 | Prerequisities 17 | - Docker 18 | - GNU Bash 19 | 20 | Docker: 21 | ``` 22 | brew cask install docker 23 | ``` 24 | (you have to open docker from the applications and follow the steps, if you can execute 'docker ps' from a terminal, you are all set) 25 | 26 | 27 | ## Build the docker image 28 | ``` 29 | docker build -t damp . 30 | ```` 31 | 32 | ## Create some MySQL test clusters 33 | cluster of 3 machines (1 master -> 2 slaves) - GTID based replication 34 | ``` 35 | ./damp_create_cluster.sh zaphod 3 36 | ``` 37 | 38 | cluster of 2 machines (1 master -> 1 slaves) - Regular replication 39 | ``` 40 | ./damp_create_cluster.sh arthurdent 2 regular 41 | ``` 42 | The script generates the damp/hostfile Ansible inventory file. 43 | ``` 44 | [proxysql] 45 | localhost 46 | 47 | 48 | [damp_server_zaphod] 49 | 172.17.0.3 mysql_role=master 50 | 172.17.0.4 mysql_role=slave 51 | 172.17.0.5 mysql_role=slave 52 | 53 | [damp_server_zaphod:vars] 54 | cluster=damp_server_zaphod 55 | hostgroup=1 56 | 57 | 58 | [damp_server_arthurdent] 59 | 172.17.0.6 mysql_role=master 60 | 172.17.0.7 mysql_role=slave 61 | 62 | [damp_server_arthurdent:vars] 63 | cluster=damp_server_arthurdent 64 | hostgroup=3 65 | ``` 66 | 67 | 68 | ## start the Docker and install/setup ProxySQL(1.3.2)/MHA and sysbench   69 | ``` 70 | ./damp_start.sh 71 | ``` 72 | 73 | From inside the container run the following: 74 | ``` 75 | proxysql_menu.sh 76 | 77 | ProxySQL admin 78 | 1) ProxySQL Admin Shell 79 | 2) [runtime] Show servers 80 | 3) [runtime] Show users 81 | 4) [runtime] Show replication_hostgroups 82 | 5) [runtime] Show query_rules 83 | 6) [runtime] Show global_variables 84 | 7) [stats] Show connection_pool 85 | 8) [stats] Show command_counters 86 | 9) [stats] Show query digest 87 | 10) [stats] Show hostgroups 88 | 11) [log] Show connect 89 | 12) [log] Show ping 90 | 13) [log] Show read_only 91 | 14) [mysql][zaphod] Connect to cluster via ProxySQL 92 | 15) [test][zaphod] sysbench prepare 93 | 16) [test][zaphod] sysbench run - 15 sec, ro 94 | 17) [test][zaphod] sysbench run - 60 sec, ro 95 | 18) [test][zaphod] Split R/W 96 | 19) [test][zaphod] Create 'world' sample db 97 | 20) [HA][zaphod] MHA online failover (interactive) 98 | 21) [HA][zaphod] MHA online failover (noninteractive) 99 | 22) [mysql][arthurdent] Connect to cluster via ProxySQL 100 | 23) [test][arthurdent] sysbench prepare 101 | 24) [test][arthurdent] sysbench run - 15 sec, ro 102 | 25) [test][arthurdent] sysbench run - 60 sec, ro 103 | 26) [test][arthurdent] Split R/W 104 | 27) [test][arthurdent] Create 'world' sample db 105 | 28) [HA][arthurdent] MHA online failover (interactive) 106 | 29) [HA][arthurdent] MHA online failover (noninteractive) 107 | 30) Quit 108 | ``` 109 | 110 | This script can be also found outside of the container, but some options won't work from there (unless you have MHA/sysbench installed and set up:)). 111 | 112 | These menupoint are self explanatory shortcuts to Linux commands/sqls. All commands/queries will be printed before execution. 113 | 114 | Some expample outputs: 115 | 116 | 2) [runtime] Show servers 117 | ``` 118 | +----+------------+------+--------+--------+-----------------+------------------------+ 119 | | hg | hostname | port | status | weight | max_connections | comment | 120 | +----+------------+------+--------+--------+-----------------+------------------------+ 121 | | 1 | 172.17.0.3 | 3306 | ONLINE | 1 | 1000 | damp_server_zaphod | 122 | | 2 | 172.17.0.4 | 3306 | ONLINE | 1 | 1000 | damp_server_zaphod | 123 | | 2 | 172.17.0.5 | 3306 | ONLINE | 1 | 1000 | damp_server_zaphod | 124 | | 3 | 172.17.0.6 | 3306 | ONLINE | 1 | 1000 | damp_server_arthurdent | 125 | | 4 | 172.17.0.7 | 3306 | ONLINE | 1 | 1000 | damp_server_arthurdent | 126 | +----+------------+------+--------+--------+-----------------+------------------------+ 127 | 5 rows in set (0.01 sec) 128 | ``` 129 | 130 | 3) [runtime] Show users 131 | ``` 132 | +----------+-------------------------------------------+----+--------+-----------------+ 133 | | username | password | hg | active | max_connections | 134 | +----------+-------------------------------------------+----+--------+-----------------+ 135 | | app1 | *98E485B64DC03E6D8B4831D58E813F86025D7268 | 1 | 1 | 200 | 136 | | app3 | *944C03A73AF6A147B01A747C5D4EF0FF4A714D2D | 3 | 1 | 200 | 137 | | app1 | *98E485B64DC03E6D8B4831D58E813F86025D7268 | 1 | 1 | 200 | 138 | | app3 | *944C03A73AF6A147B01A747C5D4EF0FF4A714D2D | 3 | 1 | 200 | 139 | +----------+-------------------------------------------+----+--------+-----------------+ 140 | ``` 141 | connect to the MySQL cluster as an 'app' (mysql-client -> ProxySQL -> MySQL instanes) 142 | The username and the password will be the following 143 | ``` 144 | hostgroup=1 145 | username=app1 146 | password=app1 147 | 148 | hostgroup=3 149 | username=app3 150 | password=app3 151 | etc. 152 | 153 | host: 127.0.0.1 154 | user: app# 155 | passwd: app# 156 | port: 6033 157 | ``` 158 | 159 | 160 | 4) [runtime] Show replication_hostgroups 161 | ``` 162 | +------------------+------------------+------------------------+ 163 | | writer_hostgroup | reader_hostgroup | comment | 164 | +------------------+------------------+------------------------+ 165 | | 1 | 2 | damp_server_zaphod | 166 | | 3 | 4 | damp_server_arthurdent | 167 | +------------------+------------------+------------------------+ 168 | ``` 169 | App user (default hostgroup is the hostgroup in the inventory file for a given cluster, the traffic will go there unless told otherwise): 170 | 171 | 172 | ###Example test scenario #1: 173 | let's generate some traffic on the first cluster: 174 | execute these one after another 175 | ``` 176 | 15) [test][zaphod] sysbench prepare 177 | 16) [test][zaphod] sysbench run - 15 sec, ro 178 | ``` 179 | 180 | Then check the connection pool. We'll see that all traffic went to the master (reads and writes). By default ProxySQL sends all traffic to the writer_hostgroups 181 | ``` 182 | 7) [stats] Show connection_pool 183 | 184 | +-----------+------------+----------+--------+----------+----------+--------+---------+---------+-----------------+-----------------+------------+ 185 | | hostgroup | srv_host | srv_port | status | ConnUsed | ConnFree | ConnOK | ConnERR | Queries | Bytes_data_sent | Bytes_data_recv | Latency_ms | 186 | +-----------+------------+----------+--------+----------+----------+--------+---------+---------+-----------------+-----------------+------------+ 187 | | 1 | 172.17.0.3 | 3306 | ONLINE | 0 | 4 | 4 | 0 | 110150 | 6177839 | 264696684 | 175 | 188 | | 3 | 172.17.0.6 | 3306 | ONLINE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 222 | 189 | | 4 | 172.17.0.7 | 3306 | ONLINE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 279 | 190 | | 2 | 172.17.0.4 | 3306 | ONLINE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 238 | 191 | | 2 | 172.17.0.5 | 3306 | ONLINE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 159 | 192 | +-----------+------------+----------+--------+----------+----------+--------+---------+---------+-----------------+-----------------+------------+ 193 | ``` 194 | Tell ProxySQL to send all queries matching '^select' to the hostgroup 2 (readers) 195 | ``` 196 | 18) [test][zaphod] Split R/W 197 | 198 | Command: mysql -h 127.0.0.1 -uadmin -padmin -P6032 -e 'REPLACE INTO mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) VALUES(1000,1,'^select',2,0);LOAD MYSQL QUERY RULES TO RUNTIME;SAVE MYSQL QUERY RULES TO DISK;\G 199 | ``` 200 | re-run the sysbench and check the connection pool afterwards 201 | ``` 202 | 16) [test][zaphod] sysbench run - 15 sec, ro 203 | 7) [stats] Show connection_pool 204 | +-----------+------------+----------+--------+----------+----------+--------+---------+---------+-----------------+-----------------+------------+ 205 | | hostgroup | srv_host | srv_port | status | ConnUsed | ConnFree | ConnOK | ConnERR | Queries | Bytes_data_sent | Bytes_data_recv | Latency_ms | 206 | +-----------+------------+----------+--------+----------+----------+--------+---------+---------+-----------------+-----------------+------------+ 207 | | 1 | 172.17.0.3 | 3306 | ONLINE | 0 | 4 | 4 | 0 | 121530 | 6240429 | 264696684 | 185 | 208 | | 3 | 172.17.0.6 | 3306 | ONLINE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 249 | 209 | | 4 | 172.17.0.7 | 3306 | ONLINE | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 278 | 210 | | 2 | 172.17.0.4 | 3306 | ONLINE | 0 | 3 | 3 | 0 | 40173 | 1740087 | 110225431 | 271 | 211 | | 2 | 172.17.0.5 | 3306 | ONLINE | 0 | 3 | 3 | 0 | 39487 | 1708053 | 108560759 | 202 | 212 | +-----------+------------+----------+--------+----------+----------+--------+---------+---------+-----------------+-----------------+------------+ 213 | ``` 214 | We can see that a lot of traffic went to the hostgroup 2 (readers) 215 | 216 | Check the query digest too: 217 | ``` 218 | 11) [stats] Show query digest 219 | +----+----------+------------+----------------------------------------------------------------------------------+ 220 | | hg | sum_time | count_star | substr(digest_text,1,80) | 221 | +----+----------+------------+----------------------------------------------------------------------------------+ 222 | | 1 | 21055026 | 68840 | SELECT c FROM sbtest1 WHERE id=? | 223 | | 2 | 12534808 | 56900 | SELECT c FROM sbtest1 WHERE id=? | 224 | | 1 | 10226315 | 6884 | SELECT DISTINCT c FROM sbtest1 WHERE id BETWEEN ? AND ?+? ORDER BY c | 225 | | 1 | 5391754 | 6884 | SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ?+? ORDER BY c | 226 | | 1 | 4179020 | 12574 | COMMIT | 227 | | 1 | 3754569 | 6884 | SELECT SUM(K) FROM sbtest1 WHERE id BETWEEN ? AND ?+? | 228 | | 1 | 3214914 | 6884 | SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ?+? | 229 | | 1 | 2609316 | 12574 | BEGIN | 230 | | 2 | 2170878 | 5690 | SELECT DISTINCT c FROM sbtest1 WHERE id BETWEEN ? AND ?+? ORDER BY c | 231 | | 1 | 2111828 | 4 | INSERT INTO sbtest1(k, c, pad) VALUES(?, ?, ?),(?, ?, ?),(?, ?, ?),(?, ?, ?),(?, | 232 | | 2 | 1641139 | 5690 | SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ?+? ORDER BY c | 233 | | 2 | 1618228 | 5690 | SELECT SUM(K) FROM sbtest1 WHERE id BETWEEN ? AND ?+? | 234 | | 2 | 1336262 | 5690 | SELECT c FROM sbtest1 WHERE id BETWEEN ? AND ?+? | 235 | | 1 | 380320 | 1 | CREATE INDEX k_1 on sbtest1(k) | 236 | | 1 | 267295 | 1 | CREATE TABLE sbtest1 ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, k INTEGER UN | 237 | +----+----------+------------+----------------------------------------------------------------------------------+ 238 | ``` 239 | 240 | 241 | 242 | ###Example test scenario #2: 243 | testing online failover while reading from a cluster (all servers are up and running we only change the replication topology) 244 | login to the container in 2 terminals: 245 | ``` 246 | ./proxysql_login_docker.sh 247 | ``` 248 | and execute proxysql_menu.sh in both of them. 249 | check the serverlist: 250 | ``` 251 | 2) [runtime] Show servers 252 | +----+------------+------+--------+--------+-----------------+------------------------+ 253 | | hg | hostname | port | status | weight | max_connections | comment | 254 | +----+------------+------+--------+--------+-----------------+------------------------+ 255 | | 1 | 172.17.0.3 | 3306 | ONLINE | 1 | 1000 | damp_server_zaphod | 256 | | 2 | 172.17.0.4 | 3306 | ONLINE | 1 | 1000 | damp_server_zaphod | 257 | | 2 | 172.17.0.5 | 3306 | ONLINE | 1 | 1000 | damp_server_zaphod | 258 | | 3 | 172.17.0.6 | 3306 | ONLINE | 1 | 1000 | damp_server_arthurdent | 259 | | 4 | 172.17.0.7 | 3306 | ONLINE | 1 | 1000 | damp_server_arthurdent | 260 | +----+------------+------+--------+--------+-----------------+------------------------+ 261 | ``` 262 | The current masters are the 172.17.0.3 and 172.17.0.6 (even hostgroups) 263 | ``` 264 | 4) [runtime] Show replication_hostgroups 265 | +------------------+------------------+------------------------+ 266 | | writer_hostgroup | reader_hostgroup | comment | 267 | +------------------+------------------+------------------------+ 268 | | 1 | 2 | damp_server_zaphod | 269 | | 3 | 4 | damp_server_arthurdent | 270 | +------------------+------------------+------------------------+ 271 | ``` 272 | 273 | execute the following in one terminal: 274 | (skip 15) if you already ran it) 275 | ``` 276 | 15) [test][zaphod] sysbench prepare 277 | 278 | 17) [test][zaphod] sysbench run - 60 sec, ro 279 | ``` 280 | 281 | while the sysbench running, execute the online interactive failover in the other terminal: 282 | ``` 283 | 20) [HA][zaphod] MHA online failover (interactive. you have to answer YES twice) 284 | From: 285 | 172.17.0.3(172.17.0.3:3306) (current master) 286 | +--172.17.0.4(172.17.0.4:3306) 287 | +--172.17.0.5(172.17.0.5:3306) 288 | 289 | To: 290 | 172.17.0.4(172.17.0.4:3306) (new master) 291 | +--172.17.0.5(172.17.0.5:3306) 292 | +--172.17.0.3(172.17.0.3:3306) 293 | ``` 294 | 295 | The only things we noticed during the failover were some reconnects: 296 | ``` 297 | [ 13s] threads: 4, tps: 341.04, reads: 4746.60, writes: 0.00, response time: 17.56ms (95%), errors: 0.00, reconnects: 0.00 298 | [ 14s] threads: 4, tps: 337.03, reads: 4767.49, writes: 0.00, response time: 22.38ms (95%), errors: 0.00, reconnects: 3.00 299 | [ 15s] threads: 4, tps: 297.84, reads: 4236.67, writes: 0.00, response time: 26.13ms (95%), errors: 0.00, reconnects: 4.00 300 | [ 16s] threads: 4, tps: 294.14, reads: 4097.92, writes: 0.00, response time: 26.56ms (95%), errors: 0.00, reconnects: 0.00 301 | [ 17s] threads: 4, tps: 398.98, reads: 5590.68, writes: 0.00, response time: 16.87ms (95%), errors: 0.00, reconnects: 0.00 302 | ``` 303 | otherwise everything was seamless. 304 | 305 | ``` 306 | 2) [runtime] Show servers 307 | +----+------------+------+--------+--------+-----------------+------------------------+ 308 | | hg | hostname | port | status | weight | max_connections | comment | 309 | +----+------------+------+--------+--------+-----------------+------------------------+ 310 | | 1 | 172.17.0.4 | 3306 | ONLINE | 1 | 1000 | damp_server_zaphod | 311 | | 2 | 172.17.0.3 | 3306 | ONLINE | 1 | 1000 | damp_server_zaphod | 312 | | 2 | 172.17.0.4 | 3306 | ONLINE | 1 | 1000 | damp_server_zaphod | 313 | | 2 | 172.17.0.5 | 3306 | ONLINE | 1 | 1000 | damp_server_zaphod | 314 | | 3 | 172.17.0.6 | 3306 | ONLINE | 1 | 1000 | damp_server_arthurdent | 315 | | 4 | 172.17.0.7 | 3306 | ONLINE | 1 | 1000 | damp_server_arthurdent | 316 | +----+------------+------+--------+--------+-----------------+------------------------+ 317 | ``` 318 | hostgroup 1 -> 172.17.0.4 (master) 319 | hostgroup 2 -> 172.17.0.3,172.17.0.5 (slave) 320 | ProxySQL detected the changes and reassigned the servers to the proper replication_hostgroups 321 | 322 | 323 | 324 | 325 | ---- 326 | 327 | ####Edit the global configuration file if you want to change defaults, credentials, roles 328 | damp/group_vars/all 329 | the mysql sections shouldn't be modified 330 | roles_enabled: 331 | proxysql: true 332 | mha: true 333 | sysbench: true 334 | orchestrator: true 335 | ``` 336 | proxysql: 337 | admin: 338 | host: 127.0.0.1 339 | port: 6032 340 | user: admin 341 | passwd: admin 342 | interface: 0.0.0.0 343 | app: 344 | user: app 345 | passwd: gempa 346 | default_hostgroup: 1 347 | port: 6033 348 | priv: '*.*:CREATE,DELETE,DROP,EXECUTE,INSERT,SELECT,UPDATE,INDEX' 349 | host: '%' 350 | max_conn: 200 351 | monitor: 352 | user: monitor 353 | passwd: monitor 354 | priv: '*.*:USAGE,REPLICATION CLIENT' 355 | host: '%' 356 | global_variables: 357 | mysql-default_query_timeout: 120000 358 | mysql-max_allowed_packet: 67108864 359 | mysql-monitor_read_only_timeout: 600 360 | mysql-monitor_ping_timeout: 600 361 | mysql-max_connections: 1024 362 | 363 | mysql: 364 | login_user: root 365 | login_passwd: mysecretpass 366 | repl_user: repl 367 | repl_passwd: slavepass 368 | ``` 369 | 370 | ####Connect manually: 371 | ProxySQL admin interface (with any MySQL compatible client) 372 | ``` 373 | host: 127.0.0.1 374 | user: admin 375 | passwd: admin 376 | port: 6032 377 | ``` 378 | without having MySQL client installed: 379 | ``` 380 | docker exec -it damp_proxysql mysql -h 127.0.0.1 -u admin -padmin -P 6032 381 | ``` 382 | 383 | Run the following to reset the env and restart the test from scratch 384 | (this removes every MySQL containers(*damp_server*) and the inventory file) 385 | ``` 386 | ./dump_reset.sh 387 | ``` 388 | 389 | ## Orchestrator 390 | 391 | Orchestrator made part of the setup. 392 | Since both Orchestrator and MHA run with auto deadmaster failover disabled by default they can be tested independently. 393 | 394 | The playbook adds all MySQL clusters to the Orchestrator automagically: 395 | 396 | Once the playbook is done point your browser to 397 | http://localhost:3000 398 | 399 | ![img](http://i.imgur.com/qLcK6CA.png) 400 | ![img](http://i.imgur.com/wVZBZfE.png) 401 | 402 | Change this to true to enable automatic dead master failover with Orchestrator: 403 | groups_vars/all 404 | ``` 405 | orchestrator: 406 | auto_failover: false 407 | ``` 408 | 409 | 410 | notes: 411 | - the /etc/proxysql.cnf is configured via a template, but be aware that the ProxySQL only read it during the first start (when it create the sqlite database) - you can read more here https://github.com/sysown/proxysql/blob/master/doc/configuration_system.md 412 | - mha config files can be found under /etc/mha/mha_damp_server_${clustername}.cnf 413 | - ProxySQL log /var/lib/proxysql/proxysql.log 414 | 415 | Useful links, articles: 416 | 417 | https://github.com/sysown/proxysql/blob/master/doc/configuration_howto.md 418 | 419 | http://www.slideshare.net/DerekDowney/proxysql-tutorial-plam-2016 420 | 421 | http://www.slideshare.net/atezuysal/proxysql-use-case-scenarios-plam-2016 422 | 423 | Thanks 424 | - René Cannaò 425 | - Ben Mildren 426 | - Dave Turner 427 | - Derek Downey 428 | - Frédéric 'lefred' Descamps 429 | - Shlomi Noach 430 | 431 | -------------------------------------------------------------------------------- /damp/library/proxysql_backend_servers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of Ansible 5 | # 6 | # Ansible is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Ansible is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Ansible. If not, see . 18 | 19 | DOCUMENTATION = ''' 20 | --- 21 | module: proxysql_backend_servers 22 | version_added: "2.2" 23 | author: "Ben Mildren (@bmildren)" 24 | short_description: Adds or removes mysql hosts from proxysql admin interface. 25 | description: 26 | - The M(proxysql_backend_servers) module adds or removes mysql hosts using 27 | the proxysql admin interface. 28 | options: 29 | hostgroup_id: 30 | description: 31 | - The hostgroup in which this mysqld instance is included. An instance 32 | can be part of one or more hostgroups. 33 | default: 0 34 | hostname: 35 | description: 36 | - The ip address at which the mysqld instance can be contacted. 37 | required: True 38 | port: 39 | description: 40 | - The port at which the mysqld instance can be contacted. 41 | default: 3306 42 | status: 43 | description: 44 | - ONLINE - Backend server is fully operational. 45 | OFFLINE_SOFT - When a server is put into C(OFFLINE_SOFT) mode, 46 | connections are kept in use until the current 47 | transaction is completed. This allows to gracefully 48 | detach a backend. 49 | OFFLINE_HARD - When a server is put into C(OFFLINE_HARD) mode, the 50 | existing connections are dropped, while new incoming 51 | connections aren't accepted either. 52 | 53 | If ommitted the proxysql default for I(status) is C(ONLINE). 54 | choices: [ "ONLINE", "OFFLINE_SOFT", "OFFLINE_HARD"] 55 | weight: 56 | description: 57 | - The bigger the weight of a server relative to other weights, the higher 58 | the probability of the server being chosen from the hostgroup. 59 | If ommitted the proxysql default for I(weight) is 1. 60 | compression: 61 | description: 62 | - If the value of I(compression) is greater than 0, new connections to 63 | that server will use compression. 64 | If ommitted the proxysql default for I(compression) is 0. 65 | max_connections: 66 | description: 67 | - The maximum number of connections ProxySQL will open to this backend 68 | server. 69 | If ommitted the proxysql default for I(max_connections) is 1000. 70 | max_replication_lag: 71 | description: 72 | - If greater than 0, ProxySQL will reguarly monitor replication lag. If 73 | replication lag goes above I(max_replication_lag), proxysql will 74 | temporarily shun the server until replication catches up. 75 | If ommitted the proxysql default for I(max_replication_lag) is 0. 76 | use_ssl: 77 | description: 78 | - If I(use_ssl) is set to C(True), connections to this server will be 79 | made using SSL connections. 80 | If ommitted the proxysql default for I(use_ssl) is C(False). 81 | max_latency_ms: 82 | description: 83 | - Ping time is monitored regularly. If a host has a ping time greater 84 | than I(max_latency_ms) it is excluded from the connection pool 85 | (although the server stays ONLINE). 86 | If ommitted the proxysql default for I(max_latency_ms) is 0. 87 | comment: 88 | description: 89 | - Text field that can be used for any purposed defined by the user. Could 90 | be a description of what the host stores, a reminder of when the host 91 | was added or disabled, or a JSON processed by some checker script. 92 | default: '' 93 | state: 94 | description: 95 | - When C(present) - adds the host, when C(absent) - removes the host. 96 | choices: [ "present", "absent" ] 97 | default: present 98 | save_to_disk: 99 | description: 100 | - Save mysql host config to sqlite db on disk to persist the 101 | configuration. 102 | default: True 103 | load_to_runtime: 104 | description: 105 | - Dynamically load mysql host config to runtime memory. 106 | default: True 107 | login_user: 108 | description: 109 | - The username used to authenticate to ProxySQL admin interface 110 | default: None 111 | login_password: 112 | description: 113 | - The password used to authenticate to ProxySQL admin interface 114 | default: None 115 | login_host: 116 | description: 117 | - The host used to connect to ProxySQL admin interface 118 | default: '127.0.0.1' 119 | login_port: 120 | description: 121 | - The port used to connect to ProxySQL admin interface 122 | default: 6032 123 | config_file: 124 | description: 125 | - Specify a config file from which login_user and login_password are to 126 | be read 127 | default: '' 128 | ''' 129 | 130 | EXAMPLES = ''' 131 | --- 132 | # This example adds a server, it saves the mysql server config to disk, but 133 | # avoids loading the mysql server config to runtime (this might be because 134 | # several servers are being added and the user wants to push the config to 135 | # runtime in a single batch using the M(proxysql_manage_config) module). It 136 | # uses supplied credentials to connect to the proxysql admin interface. 137 | 138 | - proxysql_backend_servers: 139 | login_user: 'admin' 140 | login_password: 'admin' 141 | hostname: 'mysql01' 142 | state: present 143 | load_to_runtime: False 144 | 145 | # This example removes a server, saves the mysql server config to disk, and 146 | # dynamically loads the mysql server config to runtime. It uses credentials 147 | # in a supplied config file to connect to the proxysql admin interface. 148 | 149 | - proxysql_backend_servers: 150 | config_file: '~/proxysql.cnf' 151 | hostname: 'mysql02' 152 | state: absent 153 | ''' 154 | 155 | RETURN = ''' 156 | stdout: 157 | description: The mysql host modified or removed from proxysql 158 | returned: On create/update will return the newly modified host, on delete 159 | it will return the deleted record. 160 | type: dict 161 | "sample": { 162 | "changed": true, 163 | "hostname": "192.168.52.1", 164 | "msg": "Added server to mysql_hosts", 165 | "server": { 166 | "comment": "", 167 | "compression": "0", 168 | "hostgroup_id": "1", 169 | "hostname": "192.168.52.1", 170 | "max_connections": "1000", 171 | "max_latency_ms": "0", 172 | "max_replication_lag": "0", 173 | "port": "3306", 174 | "status": "ONLINE", 175 | "use_ssl": "0", 176 | "weight": "1" 177 | }, 178 | "state": "present" 179 | } 180 | ''' 181 | 182 | import sys 183 | 184 | try: 185 | import MySQLdb 186 | import MySQLdb.cursors 187 | except ImportError: 188 | mysqldb_found = False 189 | else: 190 | mysqldb_found = True 191 | 192 | # =========================================== 193 | # proxysql module specific support methods. 194 | # 195 | 196 | 197 | def perform_checks(module): 198 | if module.params["login_port"] < 0 \ 199 | or module.params["login_port"] > 65535: 200 | module.fail_json( 201 | msg="login_port must be a valid unix port number (0-65535)" 202 | ) 203 | 204 | if module.params["port"] < 0 \ 205 | or module.params["port"] > 65535: 206 | module.fail_json( 207 | msg="port must be a valid unix port number (0-65535)" 208 | ) 209 | 210 | if module.params["compression"]: 211 | if module.params["compression"] < 0 \ 212 | or module.params["compression"] > 102400: 213 | module.fail_json( 214 | msg="compression must be set between 0 and 102400" 215 | ) 216 | 217 | if module.params["max_replication_lag"]: 218 | if module.params["max_replication_lag"] < 0 \ 219 | or module.params["max_replication_lag"] > 126144000: 220 | module.fail_json( 221 | msg="max_replication_lag must be set between 0 and 102400" 222 | ) 223 | 224 | if not mysqldb_found: 225 | module.fail_json( 226 | msg="the python mysqldb module is required" 227 | ) 228 | 229 | 230 | def save_config_to_disk(cursor): 231 | cursor.execute("SAVE MYSQL SERVERS TO DISK") 232 | return True 233 | 234 | 235 | def load_config_to_runtime(cursor): 236 | cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") 237 | return True 238 | 239 | 240 | class ProxySQLServer(object): 241 | 242 | def __init__(self, module): 243 | self.state = module.params["state"] 244 | self.save_to_disk = module.params["save_to_disk"] 245 | self.load_to_runtime = module.params["load_to_runtime"] 246 | 247 | self.hostgroup_id = module.params["hostgroup_id"] 248 | self.hostname = module.params["hostname"] 249 | self.port = module.params["port"] 250 | 251 | config_data_keys = ["status", 252 | "weight", 253 | "compression", 254 | "max_connections", 255 | "max_replication_lag", 256 | "use_ssl", 257 | "max_latency_ms", 258 | "comment"] 259 | 260 | self.config_data = dict((k, module.params[k]) 261 | for k in (config_data_keys)) 262 | 263 | def check_server_config_exists(self, cursor): 264 | query_string = \ 265 | """SELECT count(*) AS `host_count` 266 | FROM mysql_servers 267 | WHERE hostgroup_id = %s 268 | AND hostname = %s 269 | AND port = %s""" 270 | 271 | query_data = \ 272 | [self.hostgroup_id, 273 | self.hostname, 274 | self.port] 275 | 276 | cursor.execute(query_string, query_data) 277 | check_count = cursor.fetchone() 278 | return (int(check_count['host_count']) > 0) 279 | 280 | def check_server_config(self, cursor): 281 | query_string = \ 282 | """SELECT count(*) AS `host_count` 283 | FROM mysql_servers 284 | WHERE hostgroup_id = %s 285 | AND hostname = %s 286 | AND port = %s""" 287 | 288 | query_data = \ 289 | [self.hostgroup_id, 290 | self.hostname, 291 | self.port] 292 | 293 | for col, val in self.config_data.iteritems(): 294 | if val is not None: 295 | query_data.append(val) 296 | query_string += "\n AND " + col + " = %s" 297 | 298 | cursor.execute(query_string, query_data) 299 | check_count = cursor.fetchone() 300 | return (int(check_count['host_count']) > 0) 301 | 302 | def get_server_config(self, cursor): 303 | query_string = \ 304 | """SELECT * 305 | FROM mysql_servers 306 | WHERE hostgroup_id = %s 307 | AND hostname = %s 308 | AND port = %s""" 309 | 310 | query_data = \ 311 | [self.hostgroup_id, 312 | self.hostname, 313 | self.port] 314 | 315 | cursor.execute(query_string, query_data) 316 | server = cursor.fetchone() 317 | return server 318 | 319 | def create_server_config(self, cursor): 320 | query_string = \ 321 | """INSERT INTO mysql_servers ( 322 | hostgroup_id, 323 | hostname, 324 | port""" 325 | 326 | cols = 3 327 | query_data = \ 328 | [self.hostgroup_id, 329 | self.hostname, 330 | self.port] 331 | 332 | for col, val in self.config_data.iteritems(): 333 | if val is not None: 334 | cols += 1 335 | query_data.append(val) 336 | query_string += ",\n" + col 337 | 338 | query_string += \ 339 | (")\n" + 340 | "VALUES (" + 341 | "%s ," * cols) 342 | 343 | query_string = query_string[:-2] 344 | query_string += ")" 345 | 346 | cursor.execute(query_string, query_data) 347 | return True 348 | 349 | def update_server_config(self, cursor): 350 | query_string = """UPDATE mysql_servers""" 351 | 352 | cols = 0 353 | query_data = [] 354 | 355 | for col, val in self.config_data.iteritems(): 356 | if val is not None: 357 | cols += 1 358 | query_data.append(val) 359 | if cols == 1: 360 | query_string += "\nSET " + col + "= %s," 361 | else: 362 | query_string += "\n " + col + " = %s," 363 | 364 | query_string = query_string[:-1] 365 | query_string += ("\nWHERE hostgroup_id = %s\n AND hostname = %s" + 366 | "\n AND port = %s") 367 | 368 | query_data.append(self.hostgroup_id) 369 | query_data.append(self.hostname) 370 | query_data.append(self.port) 371 | 372 | cursor.execute(query_string, query_data) 373 | return True 374 | 375 | def delete_server_config(self, cursor): 376 | query_string = \ 377 | """DELETE FROM mysql_servers 378 | WHERE hostgroup_id = %s 379 | AND hostname = %s 380 | AND port = %s""" 381 | 382 | query_data = \ 383 | [self.hostgroup_id, 384 | self.hostname, 385 | self.port] 386 | 387 | cursor.execute(query_string, query_data) 388 | return True 389 | 390 | def manage_config(self, cursor, state): 391 | if state: 392 | if self.save_to_disk: 393 | save_config_to_disk(cursor) 394 | if self.load_to_runtime: 395 | load_config_to_runtime(cursor) 396 | 397 | def create_server(self, check_mode, result, cursor): 398 | if not check_mode: 399 | result['changed'] = \ 400 | self.create_server_config(cursor) 401 | result['msg'] = "Added server to mysql_hosts" 402 | result['server'] = \ 403 | self.get_server_config(cursor) 404 | self.manage_config(cursor, 405 | result['changed']) 406 | else: 407 | result['changed'] = True 408 | result['msg'] = ("Server would have been added to" + 409 | " mysql_hosts, however check_mode" + 410 | " is enabled.") 411 | 412 | def update_server(self, check_mode, result, cursor): 413 | if not check_mode: 414 | result['changed'] = \ 415 | self.update_server_config(cursor) 416 | result['msg'] = "Updated server in mysql_hosts" 417 | result['server'] = \ 418 | self.get_server_config(cursor) 419 | self.manage_config(cursor, 420 | result['changed']) 421 | else: 422 | result['changed'] = True 423 | result['msg'] = ("Server would have been updated in" + 424 | " mysql_hosts, however check_mode" + 425 | " is enabled.") 426 | 427 | def delete_server(self, check_mode, result, cursor): 428 | if not check_mode: 429 | result['server'] = \ 430 | self.get_server_config(cursor) 431 | result['changed'] = \ 432 | self.delete_server_config(cursor) 433 | result['msg'] = "Deleted server from mysql_hosts" 434 | self.manage_config(cursor, 435 | result['changed']) 436 | else: 437 | result['changed'] = True 438 | result['msg'] = ("Server would have been deleted from" + 439 | " mysql_hosts, however check_mode is" + 440 | " enabled.") 441 | 442 | # =========================================== 443 | # Module execution. 444 | # 445 | 446 | 447 | def main(): 448 | module = AnsibleModule( 449 | argument_spec=dict( 450 | login_user=dict(default=None, type='str'), 451 | login_password=dict(default=None, no_log=True, type='str'), 452 | login_host=dict(default='127.0.0.1'), 453 | login_unix_socket=dict(default=None), 454 | login_port=dict(default=6032, type='int'), 455 | config_file=dict(default='', type='path'), 456 | hostgroup_id=dict(default=0, type='int'), 457 | hostname=dict(required=True, type='str'), 458 | port=dict(default=3306, type='int'), 459 | status=dict(choices=['ONLINE', 460 | 'OFFLINE_SOFT', 461 | 'OFFLINE_HARD']), 462 | weight=dict(type='int'), 463 | compression=dict(type='int'), 464 | max_connections=dict(type='int'), 465 | max_replication_lag=dict(type='int'), 466 | use_ssl=dict(type='bool'), 467 | max_latency_ms=dict(type='int'), 468 | comment=dict(default='', type='str'), 469 | state=dict(default='present', choices=['present', 470 | 'absent']), 471 | save_to_disk=dict(default=True, type='bool'), 472 | load_to_runtime=dict(default=True, type='bool') 473 | ), 474 | supports_check_mode=True 475 | ) 476 | 477 | perform_checks(module) 478 | 479 | login_user = module.params["login_user"] 480 | login_password = module.params["login_password"] 481 | config_file = module.params["config_file"] 482 | 483 | cursor = None 484 | try: 485 | cursor = mysql_connect(module, 486 | login_user, 487 | login_password, 488 | config_file, 489 | cursor_class=MySQLdb.cursors.DictCursor) 490 | except MySQLdb.Error: 491 | e = sys.exc_info()[1] 492 | module.fail_json( 493 | msg="unable to connect to ProxySQL Admin Module.. %s" % e 494 | ) 495 | 496 | proxysql_server = ProxySQLServer(module) 497 | result = {} 498 | 499 | result['state'] = proxysql_server.state 500 | if proxysql_server.hostname: 501 | result['hostname'] = proxysql_server.hostname 502 | 503 | if proxysql_server.state == "present": 504 | try: 505 | if not proxysql_server.check_server_config_exists(cursor): 506 | if not proxysql_server.check_server_config(cursor): 507 | proxysql_server.create_server(module.check_mode, 508 | result, 509 | cursor) 510 | else: 511 | proxysql_server.update_server(module.check_mode, 512 | result, 513 | cursor) 514 | else: 515 | result['changed'] = False 516 | result['msg'] = ("The server already exists in mysql_hosts" + 517 | " and doesn't need to be updated.") 518 | result['server'] = \ 519 | proxysql_server.get_server_config(cursor) 520 | except MySQLdb.Error: 521 | e = sys.exc_info()[1] 522 | module.fail_json( 523 | msg="unable to modify server.. %s" % e 524 | ) 525 | 526 | elif proxysql_server.state == "absent": 527 | try: 528 | if proxysql_server.check_server_config_exists(cursor): 529 | proxysql_server.delete_server(module.check_mode, 530 | result, 531 | cursor) 532 | else: 533 | result['changed'] = False 534 | result['msg'] = ("The server is already absent from the" + 535 | " mysql_hosts memory configuration") 536 | except MySQLdb.Error: 537 | e = sys.exc_info()[1] 538 | module.fail_json( 539 | msg="unable to remove server.. %s" % e 540 | ) 541 | 542 | module.exit_json(**result) 543 | 544 | from ansible.module_utils.basic import * 545 | from ansible.module_utils.mysql import * 546 | if __name__ == '__main__': 547 | main() 548 | -------------------------------------------------------------------------------- /damp/library/proxysql_query_rules.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # This file is part of Ansible 5 | # 6 | # Ansible is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Ansible is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with Ansible. If not, see . 18 | 19 | DOCUMENTATION = ''' 20 | --- 21 | module: proxysql_query_rules 22 | version_added: "2.2" 23 | author: "Ben Mildren (@bmildren)" 24 | short_description: Modifies query rules using the proxysql admin interface. 25 | description: 26 | - The M(proxysql_query_rules) module modifies query rules using the proxysql 27 | admin interface. 28 | options: 29 | rule_id: 30 | description: 31 | - The unique id of the rule. Rules are processed in rule_id order. 32 | [integer] 33 | active: 34 | description: 35 | - A rule with I(active) set to C(False) will be tracked in the database, 36 | but will be never loaded in the in-memory data structures [boolean] 37 | username: 38 | description: 39 | - Filtering criteria matching username. If I(username) is non-NULL, a 40 | query will match only if the connection is made with the correct 41 | username. [string] 42 | schemaname: 43 | description: 44 | - Filtering criteria matching schemaname. If I(schemaname) is 45 | non-NULL, a query will match only if the connection uses schemaname as 46 | its default schema. [string] 47 | flagIN: 48 | description: 49 | - Used in combination with I(flagOUT) and I(apply) to create chains of 50 | rules. [integer] 51 | client_addr: 52 | description: 53 | - Match traffic from a specific source. [string] 54 | proxy_addr: 55 | description: 56 | - Match incoming traffic on a specific local IP. [string] 57 | proxy_port: 58 | description: 59 | - Match incoming traffic on a specific local port. [integer] 60 | digest: 61 | description: 62 | - Match queries with a specific digest, as returned by 63 | stats_mysql_query_digest.digest. [string] 64 | match_digest: 65 | description: 66 | - Regular expression that matches the query digest. The dialect of 67 | regular expressions used is that of re2 - 68 | https://github.com/google/re2. [string] 69 | match_pattern: 70 | description: 71 | - Regular expression that matches the query text. The dialect of regular 72 | expressions used is that of re2 - https://github.com/google/re2. 73 | [string] 74 | negate_match_pattern: 75 | description: 76 | - If I(negate_match_pattern) is set to C(True), only queries not matching 77 | the query text will be considered as a match. This acts as a NOT 78 | operator in front of the regular expression matching against 79 | match_pattern. [boolean] 80 | flagOUT: 81 | description: 82 | - Used in combination with I(flagIN) and apply to create chains of rules. 83 | When set, I(flagOUT) signifies the I(flagIN) to be used in the next 84 | chain of rules. [integer] 85 | replace_pattern: 86 | description: 87 | - This is the pattern with which to replace the matched pattern. Note 88 | that this is optional, and when ommitted, the query processor will only 89 | cache, route, or set other parameters without rewriting. [string] 90 | destination_hostgroup: 91 | description: 92 | - Route matched queries to this hostgroup. This happens unless there is 93 | a started transaction and the logged in user has 94 | I(transaction_persistent) set to C(True) (see M(proxysql_mysql_users)). 95 | [integer] 96 | cache_ttl: 97 | description: 98 | - The number of milliseconds for which to cache the result of the query. 99 | Note in ProxySQL 1.1 I(cache_ttl) was in seconds. [integer] 100 | timeout: 101 | description: 102 | - The maximum timeout in milliseconds with which the matched or rewritten 103 | query should be executed. If a query run for longer than the specific 104 | threshold, the query is automatically killed. If timeout is not 105 | specified, the global variable mysql-default_query_timeout applies. 106 | [integer] 107 | retries: 108 | description: 109 | - The maximum number of times a query needs to be re-executed in case of 110 | detected failure during the execution of the query. If retries is not 111 | specified, the global variable mysql-query_retries_on_failure applies. 112 | [integer] 113 | delay: 114 | description: 115 | - Number of milliseconds to delay the execution of the query. This is 116 | essentially a throttling mechanism and QoS, and allows a way to give 117 | priority to queries over others. This value is added to the 118 | mysql-default_query_delay global variable that applies to all queries. 119 | [integer] 120 | mirror_flagOUT: 121 | description: 122 | - Enables query mirroring. If set I(mirror_flagOUT) can be used to 123 | evaluates the mirrored query against the specified chain of rules. 124 | [integer] 125 | mirror_hostgroup: 126 | description: 127 | - Enables query mirroring. If set I(mirror_hostgroup) can be used to 128 | mirror queries to the same or different hostgroup. [integer] 129 | error_msg: 130 | description: 131 | - Query will be blocked, and the specified error_msg will be returned to 132 | the client. [string] 133 | log: 134 | description: 135 | - Query will be logged. [boolean] 136 | apply: 137 | description: 138 | - Used in combination with I(flagIN) and I(flagOUT) to create chains of 139 | rules. Setting apply to True signifies the last rule to be applied. 140 | [boolean] 141 | comment: 142 | description: 143 | - Free form text field, usable for a descriptive comment of the query 144 | rule. [string] 145 | state: 146 | description: 147 | - When C(present) - adds the rule, when C(absent) - removes the rule. 148 | choices: [ "present", "absent" ] 149 | default: present 150 | force_delete: 151 | description: 152 | - By default we avoid deleting more than one schedule in a single batch, 153 | however if you need this behaviour and you're not concerned about the 154 | schedules deleted, you can set I(force_delete) to C(True). 155 | default: False 156 | save_to_disk: 157 | description: 158 | - Save mysql host config to sqlite db on disk to persist the 159 | configuration. 160 | default: True 161 | load_to_runtime: 162 | description: 163 | - Dynamically load mysql host config to runtime memory. 164 | default: True 165 | login_user: 166 | description: 167 | - The username used to authenticate to ProxySQL admin interface 168 | default: None 169 | login_password: 170 | description: 171 | - The password used to authenticate to ProxySQL admin interface 172 | default: None 173 | login_host: 174 | description: 175 | - The host used to connect to ProxySQL admin interface 176 | default: '127.0.0.1' 177 | login_port: 178 | description: 179 | - The port used to connect to ProxySQL admin interface 180 | default: 6032 181 | config_file: 182 | description: 183 | - Specify a config file from which login_user and login_password are to 184 | be read 185 | default: '' 186 | ''' 187 | 188 | EXAMPLES = ''' 189 | --- 190 | # This example adds a rule to redirect queries from a specific user to another 191 | # hostgroup, it saves the mysql query rule config to disk, but avoids loading 192 | # the mysql query config config to runtime (this might be because several 193 | # rules are being added and the user wants to push the config to runtime in a 194 | # single batch using the M(proxysql_manage_config) module). It uses supplied 195 | # credentials to connect to the proxysql admin interface. 196 | 197 | - proxysql_backend_servers: 198 | login_user: admin 199 | login_password: admin 200 | username: 'guest_ro' 201 | destination_hostgroup: 1 202 | active: 1 203 | retries: 3 204 | state: present 205 | load_to_runtime: False 206 | 207 | # This example removes all rules that use the username 'guest_ro', saves the 208 | # mysql query rule config to disk, and dynamically loads the mysql query rule 209 | # config to runtime. It uses credentials in a supplied config file to connect 210 | # to the proxysql admin interface. 211 | 212 | - proxysql_backend_servers: 213 | config_file: '~/proxysql.cnf' 214 | username: 'guest_ro' 215 | state: absent 216 | force_delete: true 217 | ''' 218 | 219 | RETURN = ''' 220 | stdout: 221 | description: The mysql user modified or removed from proxysql 222 | returned: On create/update will return the newly modified rule, in all 223 | other cases will return a list of rules that match the supplied 224 | criteria. 225 | type: dict 226 | "sample": { 227 | "changed": true, 228 | "msg": "Added rule to mysql_query_rules", 229 | "rules": [ 230 | { 231 | "active": "0", 232 | "apply": "0", 233 | "cache_ttl": null, 234 | "client_addr": null, 235 | "comment": null, 236 | "delay": null, 237 | "destination_hostgroup": 1, 238 | "digest": null, 239 | "error_msg": null, 240 | "flagIN": "0", 241 | "flagOUT": null, 242 | "log": null, 243 | "match_digest": null, 244 | "match_pattern": null, 245 | "mirror_flagOUT": null, 246 | "mirror_hostgroup": null, 247 | "negate_match_pattern": "0", 248 | "proxy_addr": null, 249 | "proxy_port": null, 250 | "reconnect": null, 251 | "replace_pattern": null, 252 | "retries": null, 253 | "rule_id": "1", 254 | "schemaname": null, 255 | "timeout": null, 256 | "username": "guest_ro" 257 | } 258 | ], 259 | "state": "present" 260 | } 261 | ''' 262 | 263 | import sys 264 | 265 | try: 266 | import MySQLdb 267 | import MySQLdb.cursors 268 | except ImportError: 269 | mysqldb_found = False 270 | else: 271 | mysqldb_found = True 272 | 273 | # =========================================== 274 | # proxysql module specific support methods. 275 | # 276 | 277 | 278 | def perform_checks(module): 279 | if module.params["login_port"] < 0 \ 280 | or module.params["login_port"] > 65535: 281 | module.fail_json( 282 | msg="login_port must be a valid unix port number (0-65535)" 283 | ) 284 | 285 | if not mysqldb_found: 286 | module.fail_json( 287 | msg="the python mysqldb module is required" 288 | ) 289 | 290 | 291 | def save_config_to_disk(cursor): 292 | cursor.execute("SAVE MYSQL QUERY RULES TO DISK") 293 | return True 294 | 295 | 296 | def load_config_to_runtime(cursor): 297 | cursor.execute("LOAD MYSQL QUERY RULES TO RUNTIME") 298 | return True 299 | 300 | 301 | class ProxyQueryRule(object): 302 | 303 | def __init__(self, module): 304 | self.state = module.params["state"] 305 | self.force_delete = module.params["force_delete"] 306 | self.save_to_disk = module.params["save_to_disk"] 307 | self.load_to_runtime = module.params["load_to_runtime"] 308 | 309 | config_data_keys = ["rule_id", 310 | "active", 311 | "username", 312 | "schemaname", 313 | "flagIN", 314 | "client_addr", 315 | "proxy_addr", 316 | "proxy_port", 317 | "digest", 318 | "match_digest", 319 | "match_pattern", 320 | "negate_match_pattern", 321 | "flagOUT", 322 | "replace_pattern", 323 | "destination_hostgroup", 324 | "cache_ttl", 325 | "timeout", 326 | "retries", 327 | "delay", 328 | "mirror_flagOUT", 329 | "mirror_hostgroup", 330 | "error_msg", 331 | "log", 332 | "apply", 333 | "comment"] 334 | 335 | self.config_data = dict((k, module.params[k]) 336 | for k in (config_data_keys)) 337 | 338 | def check_rule_pk_exists(self, cursor): 339 | query_string = \ 340 | """SELECT count(*) AS `rule_count` 341 | FROM mysql_query_rules 342 | WHERE rule_id = %s""" 343 | 344 | query_data = \ 345 | [self.config_data["rule_id"]] 346 | 347 | cursor.execute(query_string, query_data) 348 | check_count = cursor.fetchone() 349 | return (int(check_count['rule_count']) > 0) 350 | 351 | def check_rule_cfg_exists(self, cursor): 352 | query_string = \ 353 | """SELECT count(*) AS `rule_count` 354 | FROM mysql_query_rules""" 355 | 356 | cols = 0 357 | query_data = [] 358 | 359 | for col, val in self.config_data.iteritems(): 360 | if val is not None: 361 | cols += 1 362 | query_data.append(val) 363 | if cols == 1: 364 | query_string += "\n WHERE " + col + " = %s" 365 | else: 366 | query_string += "\n AND " + col + " = %s" 367 | 368 | if cols > 0: 369 | cursor.execute(query_string, query_data) 370 | else: 371 | cursor.execute(query_string) 372 | check_count = cursor.fetchone() 373 | return int(check_count['rule_count']) 374 | 375 | def get_rule_config(self, cursor, created_rule_id=None): 376 | query_string = \ 377 | """SELECT * 378 | FROM mysql_query_rules""" 379 | 380 | if created_rule_id: 381 | query_data = [created_rule_id, ] 382 | query_string += "\nWHERE rule_id = %s" 383 | 384 | cursor.execute(query_string, query_data) 385 | rule = cursor.fetchone() 386 | else: 387 | cols = 0 388 | query_data = [] 389 | 390 | for col, val in self.config_data.iteritems(): 391 | if val is not None: 392 | cols += 1 393 | query_data.append(val) 394 | if cols == 1: 395 | query_string += "\n WHERE " + col + " = %s" 396 | else: 397 | query_string += "\n AND " + col + " = %s" 398 | 399 | if cols > 0: 400 | cursor.execute(query_string, query_data) 401 | else: 402 | cursor.execute(query_string) 403 | rule = cursor.fetchall() 404 | 405 | return rule 406 | 407 | def create_rule_config(self, cursor): 408 | query_string = \ 409 | """INSERT INTO mysql_query_rules (""" 410 | 411 | cols = 0 412 | query_data = [] 413 | 414 | for col, val in self.config_data.iteritems(): 415 | if val is not None: 416 | cols += 1 417 | query_data.append(val) 418 | query_string += "\n" + col + "," 419 | 420 | query_string = query_string[:-1] 421 | 422 | query_string += \ 423 | (")\n" + 424 | "VALUES (" + 425 | "%s ," * cols) 426 | 427 | query_string = query_string[:-2] 428 | query_string += ")" 429 | 430 | cursor.execute(query_string, query_data) 431 | new_rule_id = cursor.lastrowid 432 | return True, new_rule_id 433 | 434 | def update_rule_config(self, cursor): 435 | query_string = """UPDATE mysql_query_rules""" 436 | 437 | cols = 0 438 | query_data = [] 439 | 440 | for col, val in self.config_data.iteritems(): 441 | if val is not None and col != "rule_id": 442 | cols += 1 443 | query_data.append(val) 444 | if cols == 1: 445 | query_string += "\nSET " + col + "= %s," 446 | else: 447 | query_string += "\n " + col + " = %s," 448 | 449 | query_string = query_string[:-1] 450 | query_string += "\nWHERE rule_id = %s" 451 | 452 | query_data.append(self.config_data["rule_id"]) 453 | 454 | cursor.execute(query_string, query_data) 455 | return True 456 | 457 | def delete_rule_config(self, cursor): 458 | query_string = \ 459 | """DELETE FROM mysql_query_rules""" 460 | 461 | cols = 0 462 | query_data = [] 463 | 464 | for col, val in self.config_data.iteritems(): 465 | if val is not None: 466 | cols += 1 467 | query_data.append(val) 468 | if cols == 1: 469 | query_string += "\n WHERE " + col + " = %s" 470 | else: 471 | query_string += "\n AND " + col + " = %s" 472 | 473 | if cols > 0: 474 | cursor.execute(query_string, query_data) 475 | else: 476 | cursor.execute(query_string) 477 | check_count = cursor.rowcount 478 | return True, int(check_count) 479 | 480 | def manage_config(self, cursor, state): 481 | if state: 482 | if self.save_to_disk: 483 | save_config_to_disk(cursor) 484 | if self.load_to_runtime: 485 | load_config_to_runtime(cursor) 486 | 487 | def create_rule(self, check_mode, result, cursor): 488 | if not check_mode: 489 | result['changed'], new_rule_id = \ 490 | self.create_rule_config(cursor) 491 | result['msg'] = "Added rule to mysql_query_rules" 492 | self.manage_config(cursor, 493 | result['changed']) 494 | result['rules'] = \ 495 | self.get_rule_config(cursor, new_rule_id) 496 | else: 497 | result['changed'] = True 498 | result['msg'] = ("Rule would have been added to" + 499 | " mysql_query_rules, however" + 500 | " check_mode is enabled.") 501 | 502 | def update_rule(self, check_mode, result, cursor): 503 | if not check_mode: 504 | result['changed'] = \ 505 | self.update_rule_config(cursor) 506 | result['msg'] = "Updated rule in mysql_query_rules" 507 | self.manage_config(cursor, 508 | result['changed']) 509 | result['rules'] = \ 510 | self.get_rule_config(cursor) 511 | else: 512 | result['changed'] = True 513 | result['msg'] = ("Rule would have been updated in" + 514 | " mysql_query_rules, however" + 515 | " check_mode is enabled.") 516 | 517 | def delete_rule(self, check_mode, result, cursor): 518 | if not check_mode: 519 | result['rules'] = \ 520 | self.get_rule_config(cursor) 521 | result['changed'], result['rows_affected'] = \ 522 | self.delete_rule_config(cursor) 523 | result['msg'] = "Deleted rule from mysql_query_rules" 524 | self.manage_config(cursor, 525 | result['changed']) 526 | else: 527 | result['changed'] = True 528 | result['msg'] = ("Rule would have been deleted from" + 529 | " mysql_query_rules, however" + 530 | " check_mode is enabled.") 531 | 532 | # =========================================== 533 | # Module execution. 534 | # 535 | 536 | 537 | def main(): 538 | module = AnsibleModule( 539 | argument_spec=dict( 540 | login_user=dict(default=None, type='str'), 541 | login_password=dict(default=None, no_log=True, type='str'), 542 | login_host=dict(default="127.0.0.1"), 543 | login_unix_socket=dict(default=None), 544 | login_port=dict(default=6032, type='int'), 545 | config_file=dict(default="", type='path'), 546 | rule_id=dict(type='int'), 547 | active=dict(type='bool'), 548 | username=dict(type='str'), 549 | schemaname=dict(type='str'), 550 | flagIN=dict(type='int'), 551 | client_addr=dict(type='str'), 552 | proxy_addr=dict(type='str'), 553 | proxy_port=dict(type='int'), 554 | digest=dict(type='str'), 555 | match_digest=dict(type='str'), 556 | match_pattern=dict(type='str'), 557 | negate_match_pattern=dict(type='bool'), 558 | flagOUT=dict(type='int'), 559 | replace_pattern=dict(type='str'), 560 | destination_hostgroup=dict(type='int'), 561 | cache_ttl=dict(type='int'), 562 | timeout=dict(type='int'), 563 | retries=dict(type='int'), 564 | delay=dict(type='int'), 565 | mirror_flagOUT=dict(type='int'), 566 | mirror_hostgroup=dict(type='int'), 567 | error_msg=dict(type='str'), 568 | log=dict(type='bool'), 569 | apply=dict(type='bool'), 570 | comment=dict(type='str'), 571 | state=dict(default='present', choices=['present', 572 | 'absent']), 573 | force_delete=dict(default=False, type='bool'), 574 | save_to_disk=dict(default=True, type='bool'), 575 | load_to_runtime=dict(default=True, type='bool') 576 | ), 577 | supports_check_mode=True 578 | ) 579 | 580 | perform_checks(module) 581 | 582 | login_user = module.params["login_user"] 583 | login_password = module.params["login_password"] 584 | config_file = module.params["config_file"] 585 | 586 | cursor = None 587 | try: 588 | cursor = mysql_connect(module, 589 | login_user, 590 | login_password, 591 | config_file, 592 | cursor_class=MySQLdb.cursors.DictCursor) 593 | except MySQLdb.Error: 594 | e = sys.exc_info()[1] 595 | module.fail_json( 596 | msg="unable to connect to ProxySQL Admin Module.. %s" % e 597 | ) 598 | 599 | proxysql_query_rule = ProxyQueryRule(module) 600 | result = {} 601 | 602 | result['state'] = proxysql_query_rule.state 603 | 604 | if proxysql_query_rule.state == "present": 605 | try: 606 | if not proxysql_query_rule.check_rule_cfg_exists(cursor): 607 | if proxysql_query_rule.config_data["rule_id"] and \ 608 | proxysql_query_rule.check_rule_pk_exists(cursor): 609 | proxysql_query_rule.update_rule(module.check_mode, 610 | result, 611 | cursor) 612 | else: 613 | proxysql_query_rule.create_rule(module.check_mode, 614 | result, 615 | cursor) 616 | else: 617 | result['changed'] = False 618 | result['msg'] = ("The rule already exists in" + 619 | " mysql_query_rules and doesn't need to be" + 620 | " updated.") 621 | result['rules'] = \ 622 | proxysql_query_rule.get_rule_config(cursor) 623 | 624 | except MySQLdb.Error: 625 | e = sys.exc_info()[1] 626 | module.fail_json( 627 | msg="unable to modify rule.. %s" % e 628 | ) 629 | 630 | elif proxysql_query_rule.state == "absent": 631 | try: 632 | existing_rules = proxysql_query_rule.check_rule_cfg_exists(cursor) 633 | if existing_rules > 0: 634 | if existing_rules == 1 or \ 635 | proxysql_query_rule.force_delete: 636 | proxysql_query_rule.delete_rule(module.check_mode, 637 | result, 638 | cursor) 639 | else: 640 | module.fail_json( 641 | msg=("Operation would delete multiple rules" + 642 | " use force_delete to override this") 643 | ) 644 | else: 645 | result['changed'] = False 646 | result['msg'] = ("The rule is already absent from the" + 647 | " mysql_query_rules memory configuration") 648 | except MySQLdb.Error: 649 | e = sys.exc_info()[1] 650 | module.fail_json( 651 | msg="unable to remove rule.. %s" % e 652 | ) 653 | 654 | module.exit_json(**result) 655 | 656 | from ansible.module_utils.basic import * 657 | from ansible.module_utils.mysql import * 658 | if __name__ == '__main__': 659 | main() 660 | --------------------------------------------------------------------------------