├── .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 | 
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 | 
400 | 
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 |
--------------------------------------------------------------------------------