diff --git a/ansible/mysql-deploy-callback.yml b/ansible/mysql-deploy-callback.yml index 945d3f9..01d2526 100644 --- a/ansible/mysql-deploy-callback.yml +++ b/ansible/mysql-deploy-callback.yml @@ -61,7 +61,13 @@ mgr_group_name: "{{ group_replication_group_name | default('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') }}" # --- 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) --- 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_role: >- {{ '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')) }} # host-wide unique: platform may pass explicit mysql_server_id; otherwise derive # port + node index (ports are unique per host in the pool model). @@ -479,6 +485,64 @@ failed_when: false 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 ansible.builtin.uri: url: "{{ delivery_callback_url_value }}" @@ -521,13 +585,10 @@ failed_when: false no_log: true - - name: Note pending HA runtime orchestration + - name: Note pending Group Replication orchestration ansible.builtin.debug: - msg: >- - topology={{ topology }} deployed with HA-ready config (GTID/binlog/relay/GR settings in place), - 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' + msg: "MGR remains unsupported; primary_replica GTID replication is configured and healthy." + when: topology == 'mgr_3' run_once: true - name: Notify platform registration handoff diff --git a/ansible/mysql-deploy.yml b/ansible/mysql-deploy.yml index ad5f69d..143c5f1 100644 --- a/ansible/mysql-deploy.yml +++ b/ansible/mysql-deploy.yml @@ -61,7 +61,13 @@ mgr_group_name: "{{ group_replication_group_name | default('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') }}" # --- 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) --- 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_role: >- {{ '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')) }} # host-wide unique: platform may pass explicit mysql_server_id; otherwise derive # port + node index (ports are unique per host in the pool model). @@ -364,19 +370,74 @@ changed_when: false 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: host: "{{ ansible_host | default(inventory_hostname) }}" port: "{{ mysql_port_value }}" timeout: 30 - - name: Note pending HA runtime orchestration + - name: Note pending Group Replication orchestration ansible.builtin.debug: - msg: >- - topology={{ topology }} deployed with HA-ready config (GTID/binlog/relay/GR settings in place), - 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' + msg: "MGR remains unsupported; primary_replica GTID replication is configured and healthy." + when: topology == 'mgr_3' run_once: true handlers: