feat(ansible): MySQL 交付剧本接入 GTID 异步主从复制
- mysql_expected_hosts 按 replica_count 推导预期节点数 - 主节点判定改为 mysql_primary_host 显式指定,IP 经 hostvars 回退解析 - 副本节点等待主节点端口就绪后执行 CHANGE REPLICATION SOURCE (defaults-extra-file 临时凭据文件 + SOURCE_AUTO_POSITION + GET_SOURCE_PUBLIC_KEY) - 复制线程验证含 IO/SQL Running 与错误字段空值检查 (锚点带 ^[[:space:]]* 适配 SHOW REPLICA STATUS\G 的字段名前导空格)
This commit is contained in:
@@ -61,7 +61,13 @@
|
|||||||
mgr_group_name: "{{ group_replication_group_name | default('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') }}"
|
mgr_group_name: "{{ group_replication_group_name | default('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') }}"
|
||||||
|
|
||||||
# --- expected node count per topology ---
|
# --- expected node count per topology ---
|
||||||
mysql_expected_hosts: "{{ {'standalone': 1, 'primary_replica': 2, 'mgr_3': 3}[topology | default('standalone')] }}"
|
mysql_expected_hosts: "{{ 1 if (topology | default('standalone')) == 'standalone' else ((replica_count | default(1) | int) + 1 if (topology | default('standalone')) == 'primary_replica' else 3) }}"
|
||||||
|
# 平台为 primary_replica 显式传入主从节点;standalone 仍按旧逻辑工作。
|
||||||
|
mysql_primary_host_value: "{{ mysql_primary_host | default(ansible_play_hosts_all[0]) }}"
|
||||||
|
mysql_primary_ip_value: "{{ mysql_primary_ip | default(hostvars[mysql_primary_host_value].ansible_host | default(mysql_primary_host_value)) }}"
|
||||||
|
# 交付只暴露 root 管理员;复制链路复用已配置好的 root@%。
|
||||||
|
mysql_replication_user: "root"
|
||||||
|
mysql_replication_password: "{{ mysql_root_password_value }}"
|
||||||
|
|
||||||
# --- secrets (prefer launch extra_vars; fall back to AWX credential-injected env) ---
|
# --- secrets (prefer launch extra_vars; fall back to AWX credential-injected env) ---
|
||||||
mysql_root_password_value: "{{ mysql_root_password | default(lookup('ansible.builtin.env', 'XINFRA_MYSQL_ROOT_PASSWORD'), true) }}"
|
mysql_root_password_value: "{{ mysql_root_password | default(lookup('ansible.builtin.env', 'XINFRA_MYSQL_ROOT_PASSWORD'), true) }}"
|
||||||
@@ -355,7 +361,7 @@
|
|||||||
mysql_node_index: "{{ ansible_play_hosts_all.index(inventory_hostname) }}"
|
mysql_node_index: "{{ ansible_play_hosts_all.index(inventory_hostname) }}"
|
||||||
mysql_node_role: >-
|
mysql_node_role: >-
|
||||||
{{ 'standalone' if topology == 'standalone' else
|
{{ 'standalone' if topology == 'standalone' else
|
||||||
('primary' if ansible_play_hosts_all.index(inventory_hostname) == 0 else
|
('primary' if inventory_hostname == mysql_primary_host_value else
|
||||||
('replica' if topology == 'primary_replica' else 'mgr')) }}
|
('replica' if topology == 'primary_replica' else 'mgr')) }}
|
||||||
# host-wide unique: platform may pass explicit mysql_server_id; otherwise derive
|
# host-wide unique: platform may pass explicit mysql_server_id; otherwise derive
|
||||||
# port + node index (ports are unique per host in the pool model).
|
# port + node index (ports are unique per host in the pool model).
|
||||||
@@ -479,6 +485,64 @@
|
|||||||
failed_when: false
|
failed_when: false
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
|
- name: Wait for the replica to reach the primary SQL port
|
||||||
|
ansible.builtin.wait_for:
|
||||||
|
host: "{{ mysql_primary_ip_value }}"
|
||||||
|
port: "{{ mysql_port_value }}"
|
||||||
|
timeout: 60
|
||||||
|
when: topology == 'primary_replica' and inventory_hostname != mysql_primary_host_value
|
||||||
|
|
||||||
|
- name: Configure and start GTID replication on the replica
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
set -euo pipefail
|
||||||
|
client_file="$(mktemp)"
|
||||||
|
sql_file="$(mktemp)"
|
||||||
|
trap 'rm -f "$client_file" "$sql_file"' EXIT
|
||||||
|
chmod 600 "$client_file" "$sql_file"
|
||||||
|
cat >"$client_file" <<'EOF'
|
||||||
|
[client]
|
||||||
|
user=root
|
||||||
|
password={{ mysql_root_password_value }}
|
||||||
|
socket={{ mysql_run_dir }}/mysql.sock
|
||||||
|
EOF
|
||||||
|
cat >"$sql_file" <<'EOF'
|
||||||
|
STOP REPLICA;
|
||||||
|
RESET REPLICA ALL;
|
||||||
|
CHANGE REPLICATION SOURCE TO SOURCE_HOST='{{ mysql_primary_ip_value }}', SOURCE_PORT={{ mysql_port_value }}, SOURCE_USER='{{ mysql_replication_user }}', SOURCE_PASSWORD='{{ mysql_replication_password | replace("'", "''") }}', SOURCE_AUTO_POSITION=1, GET_SOURCE_PUBLIC_KEY=1;
|
||||||
|
START REPLICA;
|
||||||
|
EOF
|
||||||
|
/usr/bin/mysql --defaults-extra-file="$client_file" <"$sql_file"
|
||||||
|
executable: /bin/bash
|
||||||
|
when: topology == 'primary_replica' and inventory_hostname != mysql_primary_host_value
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Verify GTID replication threads
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
set -euo pipefail
|
||||||
|
client_file="$(mktemp)"
|
||||||
|
trap 'rm -f "$client_file"' EXIT
|
||||||
|
chmod 600 "$client_file"
|
||||||
|
cat >"$client_file" <<'EOF'
|
||||||
|
[client]
|
||||||
|
user=root
|
||||||
|
password={{ mysql_root_password_value }}
|
||||||
|
socket={{ mysql_run_dir }}/mysql.sock
|
||||||
|
EOF
|
||||||
|
status="$(/usr/bin/mysql --defaults-extra-file="$client_file" -e 'SHOW REPLICA STATUS\G')"
|
||||||
|
grep -q 'Replica_IO_Running: Yes' <<<"$status"
|
||||||
|
grep -q 'Replica_SQL_Running: Yes' <<<"$status"
|
||||||
|
grep -qE '^[[:space:]]*Last_IO_Error:[[:space:]]*$' <<<"$status"
|
||||||
|
grep -qE '^[[:space:]]*Last_SQL_Error:[[:space:]]*$' <<<"$status"
|
||||||
|
executable: /bin/bash
|
||||||
|
register: mysql_replica_status
|
||||||
|
retries: 12
|
||||||
|
delay: 5
|
||||||
|
until: mysql_replica_status.rc == 0
|
||||||
|
when: topology == 'primary_replica' and inventory_hostname != mysql_primary_host_value
|
||||||
|
no_log: true
|
||||||
|
|
||||||
- name: Notify healthcheck started
|
- name: Notify healthcheck started
|
||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
url: "{{ delivery_callback_url_value }}"
|
url: "{{ delivery_callback_url_value }}"
|
||||||
@@ -521,13 +585,10 @@
|
|||||||
failed_when: false
|
failed_when: false
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Note pending HA runtime orchestration
|
- name: Note pending Group Replication orchestration
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: >-
|
msg: "MGR remains unsupported; primary_replica GTID replication is configured and healthy."
|
||||||
topology={{ topology }} deployed with HA-ready config (GTID/binlog/relay/GR settings in place),
|
when: topology == 'mgr_3'
|
||||||
but replication wiring (CHANGE REPLICATION SOURCE) and Group Replication bootstrap
|
|
||||||
(START GROUP_REPLICATION) are not yet automated — nodes start config-ready only.
|
|
||||||
when: topology != 'standalone'
|
|
||||||
run_once: true
|
run_once: true
|
||||||
|
|
||||||
- name: Notify platform registration handoff
|
- name: Notify platform registration handoff
|
||||||
|
|||||||
@@ -61,7 +61,13 @@
|
|||||||
mgr_group_name: "{{ group_replication_group_name | default('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') }}"
|
mgr_group_name: "{{ group_replication_group_name | default('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') }}"
|
||||||
|
|
||||||
# --- expected node count per topology ---
|
# --- expected node count per topology ---
|
||||||
mysql_expected_hosts: "{{ {'standalone': 1, 'primary_replica': 2, 'mgr_3': 3}[topology | default('standalone')] }}"
|
mysql_expected_hosts: "{{ 1 if (topology | default('standalone')) == 'standalone' else ((replica_count | default(1) | int) + 1 if (topology | default('standalone')) == 'primary_replica' else 3) }}"
|
||||||
|
# 平台为 primary_replica 显式传入主从节点;standalone 仍按旧逻辑工作。
|
||||||
|
mysql_primary_host_value: "{{ mysql_primary_host | default(ansible_play_hosts_all[0]) }}"
|
||||||
|
mysql_primary_ip_value: "{{ mysql_primary_ip | default(hostvars[mysql_primary_host_value].ansible_host | default(mysql_primary_host_value)) }}"
|
||||||
|
# 交付只暴露 root 管理员;复制链路复用已配置好的 root@%。
|
||||||
|
mysql_replication_user: "root"
|
||||||
|
mysql_replication_password: "{{ mysql_root_password_value }}"
|
||||||
|
|
||||||
# --- secrets (prefer launch extra_vars; fall back to AWX credential-injected env) ---
|
# --- secrets (prefer launch extra_vars; fall back to AWX credential-injected env) ---
|
||||||
mysql_root_password_value: "{{ mysql_root_password | default(lookup('ansible.builtin.env', 'XINFRA_MYSQL_ROOT_PASSWORD'), true) }}"
|
mysql_root_password_value: "{{ mysql_root_password | default(lookup('ansible.builtin.env', 'XINFRA_MYSQL_ROOT_PASSWORD'), true) }}"
|
||||||
@@ -258,7 +264,7 @@
|
|||||||
mysql_node_index: "{{ ansible_play_hosts_all.index(inventory_hostname) }}"
|
mysql_node_index: "{{ ansible_play_hosts_all.index(inventory_hostname) }}"
|
||||||
mysql_node_role: >-
|
mysql_node_role: >-
|
||||||
{{ 'standalone' if topology == 'standalone' else
|
{{ 'standalone' if topology == 'standalone' else
|
||||||
('primary' if ansible_play_hosts_all.index(inventory_hostname) == 0 else
|
('primary' if inventory_hostname == mysql_primary_host_value else
|
||||||
('replica' if topology == 'primary_replica' else 'mgr')) }}
|
('replica' if topology == 'primary_replica' else 'mgr')) }}
|
||||||
# host-wide unique: platform may pass explicit mysql_server_id; otherwise derive
|
# host-wide unique: platform may pass explicit mysql_server_id; otherwise derive
|
||||||
# port + node index (ports are unique per host in the pool model).
|
# port + node index (ports are unique per host in the pool model).
|
||||||
@@ -364,19 +370,74 @@
|
|||||||
changed_when: false
|
changed_when: false
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Verify MySQL TCP health
|
- name: Wait for the replica to reach the primary SQL port
|
||||||
|
ansible.builtin.wait_for:
|
||||||
|
host: "{{ mysql_primary_ip_value }}"
|
||||||
|
port: "{{ mysql_port_value }}"
|
||||||
|
timeout: 60
|
||||||
|
when: topology == 'primary_replica' and inventory_hostname != mysql_primary_host_value
|
||||||
|
|
||||||
|
- name: Configure and start GTID replication on the replica
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
set -euo pipefail
|
||||||
|
client_file="$(mktemp)"
|
||||||
|
sql_file="$(mktemp)"
|
||||||
|
trap 'rm -f "$client_file" "$sql_file"' EXIT
|
||||||
|
chmod 600 "$client_file" "$sql_file"
|
||||||
|
cat >"$client_file" <<'EOF'
|
||||||
|
[client]
|
||||||
|
user=root
|
||||||
|
password={{ mysql_root_password_value }}
|
||||||
|
socket={{ mysql_run_dir }}/mysql.sock
|
||||||
|
EOF
|
||||||
|
cat >"$sql_file" <<'EOF'
|
||||||
|
STOP REPLICA;
|
||||||
|
RESET REPLICA ALL;
|
||||||
|
CHANGE REPLICATION SOURCE TO SOURCE_HOST='{{ mysql_primary_ip_value }}', SOURCE_PORT={{ mysql_port_value }}, SOURCE_USER='{{ mysql_replication_user }}', SOURCE_PASSWORD='{{ mysql_replication_password | replace("'", "''") }}', SOURCE_AUTO_POSITION=1, GET_SOURCE_PUBLIC_KEY=1;
|
||||||
|
START REPLICA;
|
||||||
|
EOF
|
||||||
|
/usr/bin/mysql --defaults-extra-file="$client_file" <"$sql_file"
|
||||||
|
executable: /bin/bash
|
||||||
|
when: topology == 'primary_replica' and inventory_hostname != mysql_primary_host_value
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Verify GTID replication threads
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: |
|
||||||
|
set -euo pipefail
|
||||||
|
client_file="$(mktemp)"
|
||||||
|
trap 'rm -f "$client_file"' EXIT
|
||||||
|
chmod 600 "$client_file"
|
||||||
|
cat >"$client_file" <<'EOF'
|
||||||
|
[client]
|
||||||
|
user=root
|
||||||
|
password={{ mysql_root_password_value }}
|
||||||
|
socket={{ mysql_run_dir }}/mysql.sock
|
||||||
|
EOF
|
||||||
|
status="$(/usr/bin/mysql --defaults-extra-file="$client_file" -e 'SHOW REPLICA STATUS\G')"
|
||||||
|
grep -q 'Replica_IO_Running: Yes' <<<"$status"
|
||||||
|
grep -q 'Replica_SQL_Running: Yes' <<<"$status"
|
||||||
|
grep -qE '^[[:space:]]*Last_IO_Error:[[:space:]]*$' <<<"$status"
|
||||||
|
grep -qE '^[[:space:]]*Last_SQL_Error:[[:space:]]*$' <<<"$status"
|
||||||
|
executable: /bin/bash
|
||||||
|
register: mysql_replica_status
|
||||||
|
retries: 12
|
||||||
|
delay: 5
|
||||||
|
until: mysql_replica_status.rc == 0
|
||||||
|
when: topology == 'primary_replica' and inventory_hostname != mysql_primary_host_value
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Verify MySQL TCP health on every node
|
||||||
ansible.builtin.wait_for:
|
ansible.builtin.wait_for:
|
||||||
host: "{{ ansible_host | default(inventory_hostname) }}"
|
host: "{{ ansible_host | default(inventory_hostname) }}"
|
||||||
port: "{{ mysql_port_value }}"
|
port: "{{ mysql_port_value }}"
|
||||||
timeout: 30
|
timeout: 30
|
||||||
|
|
||||||
- name: Note pending HA runtime orchestration
|
- name: Note pending Group Replication orchestration
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: >-
|
msg: "MGR remains unsupported; primary_replica GTID replication is configured and healthy."
|
||||||
topology={{ topology }} deployed with HA-ready config (GTID/binlog/relay/GR settings in place),
|
when: topology == 'mgr_3'
|
||||||
but replication wiring (CHANGE REPLICATION SOURCE) and Group Replication bootstrap
|
|
||||||
(START GROUP_REPLICATION) are not yet automated — nodes start config-ready only.
|
|
||||||
when: topology != 'standalone'
|
|
||||||
run_once: true
|
run_once: true
|
||||||
|
|
||||||
handlers:
|
handlers:
|
||||||
|
|||||||
Reference in New Issue
Block a user