feat(delivery): add AWX callback MySQL playbook
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
---
|
||||
- name: Preflight native MySQL delivery
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
any_errors_fatal: true
|
||||
vars:
|
||||
mysql_instance: "{{ instance_name }}"
|
||||
mysql_version_value: "{{ mysql_version | default('8.0') }}"
|
||||
mysql_package_map:
|
||||
"8.0": "mysql-server=8.0.46-0ubuntu0.24.04.3"
|
||||
mysql_package_name: "{{ mysql_package_map[mysql_version_value] }}"
|
||||
mysql_port_value: "{{ mysql_port | default(13306) | int }}"
|
||||
mysql_cpu_cores_value: "{{ cpu_cores | default(1) | int }}"
|
||||
mysql_memory_gb_value: "{{ memory_gb | default(2) | int }}"
|
||||
mysql_memory_mb_value: "{{ mysql_memory_gb_value | int * 1024 }}"
|
||||
mysql_storage_gb_value: "{{ storage_gb | default(20) | int }}"
|
||||
mysql_data_disk: "{{ data_disk | default('/data') }}"
|
||||
mysql_base_dir: "{{ mysql_data_disk }}/mysql-delivery/{{ mysql_instance }}"
|
||||
mysql_install_dir: "/opt/mysql-delivery/{{ mysql_instance }}"
|
||||
mysql_data_dir: "{{ mysql_base_dir }}/data"
|
||||
mysql_log_dir: "{{ mysql_base_dir }}/logs"
|
||||
mysql_binlog_dir: "{{ mysql_base_dir }}/logs/binlog"
|
||||
mysql_redo_dir: "{{ mysql_base_dir }}/logs/redo"
|
||||
mysql_run_dir: "{{ mysql_base_dir }}/run"
|
||||
mysql_tmp_dir: "{{ mysql_base_dir }}/tmp"
|
||||
mysql_conf_dir: "{{ mysql_base_dir }}/conf"
|
||||
mysql_config_file: "{{ mysql_conf_dir }}/my.cnf"
|
||||
mysql_system_config_file: "/etc/mysql/mysql-delivery/{{ mysql_instance }}.cnf"
|
||||
mysql_param_template_value: "{{ param_template | default('default') }}"
|
||||
mysql_timezone_value: "{{ timezone | default('+08:00') }}"
|
||||
mysql_lower_case_table_names_value: "{{ lower_case_table_names | default(1) | int }}"
|
||||
mysql_character_set_value: "{{ character_set | default('utf8mb4') }}"
|
||||
mysql_collation_value: "{{ collation | default('utf8mb4_general_ci') }}"
|
||||
mysql_max_connections_value: "{{ max_connections | default('auto') }}"
|
||||
mysql_redo_log_capacity_value: "{{ innodb_redo_log_capacity | default('auto') }}"
|
||||
mysql_flush_log_at_trx_commit_value: "{{ innodb_flush_log_at_trx_commit | default(1) | int }}"
|
||||
mysql_sync_binlog_value: "{{ sync_binlog | default(1) | int }}"
|
||||
mysql_io_capacity_value: "{{ innodb_io_capacity | default(2000) | int }}"
|
||||
mysql_long_query_time_value: "{{ long_query_time | default(1) }}"
|
||||
mysql_binlog_expire_logs_seconds_value: "{{ binlog_expire_logs_seconds | default(604800) | int }}"
|
||||
mysql_max_binlog_size_value: "{{ max_binlog_size | default('256M') }}"
|
||||
delivery_callback_url_value: "{{ delivery_callback_url | default('') }}"
|
||||
delivery_callback_token_value: "{{ delivery_callback_token | default('') }}"
|
||||
delivery_callback_enabled: "{{ (delivery_callback_url_value | length > 0) and (delivery_callback_token_value | length > 0) }}"
|
||||
delivery_awx_job_id: "{{ awx_job_id | default('') }}"
|
||||
mysql_root_password_value: "{{ lookup('ansible.builtin.env', 'XINFRA_MYSQL_ROOT_PASSWORD') }}"
|
||||
mysql_admin_password_value: "{{ lookup('ansible.builtin.env', 'XINFRA_MYSQL_ADMIN_PASSWORD') }}"
|
||||
mysql_single_quote: "'"
|
||||
mysql_double_single_quote: "''"
|
||||
mysql_root_password_sql: "{{ mysql_root_password_value | replace(mysql_single_quote, mysql_double_single_quote) }}"
|
||||
mysql_admin_password_sql: "{{ mysql_admin_password_value | replace(mysql_single_quote, mysql_double_single_quote) }}"
|
||||
mysql_topology_value: "{{ topology | default('standalone') }}"
|
||||
pre_tasks:
|
||||
- name: Notify precheck started
|
||||
ansible.builtin.uri:
|
||||
url: "{{ delivery_callback_url_value }}"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ delivery_callback_token_value }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
stage: precheck
|
||||
status: running
|
||||
message: resource lock and baseline checks started
|
||||
awx_job_id: "{{ delivery_awx_job_id }}"
|
||||
status_code: [200, 202]
|
||||
when: delivery_callback_enabled | bool
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Validate prototype parameters
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- mysql_topology_value == 'standalone'
|
||||
- mysql_instance is match('^[a-z0-9][a-z0-9-]{0,62}$')
|
||||
- mysql_version_value in mysql_package_map
|
||||
- (mysql_port_value | int) >= 13306
|
||||
- (mysql_port_value | int) <= 13999
|
||||
- (mysql_cpu_cores_value | int) in [1, 2, 4, 8, 16]
|
||||
- (mysql_memory_mb_value | int) >= 1024
|
||||
- (mysql_memory_mb_value | int) <= 65536
|
||||
- (mysql_storage_gb_value | int) >= 20
|
||||
- (mysql_storage_gb_value | int) <= 2000
|
||||
- mysql_data_disk in ['/data', '/disk1', '/mnt/vol-1']
|
||||
- mysql_param_template_value in ['default', 'high_performance', 'high_safety']
|
||||
- mysql_timezone_value in ['SYSTEM', '+08:00', '+00:00', 'Asia/Shanghai']
|
||||
- (mysql_lower_case_table_names_value | int) in [0, 1]
|
||||
- mysql_character_set_value in ['utf8mb4', 'utf8', 'gbk', 'latin1']
|
||||
- mysql_collation_value is match('^(utf8mb4_(general_ci|unicode_ci|0900_ai_ci)|utf8_general_ci|gbk_chinese_ci|latin1_swedish_ci)$')
|
||||
- mysql_max_connections_value == 'auto' or mysql_max_connections_value in ['200', '500', '1000', '2000', '4000', '8000', '16000']
|
||||
- mysql_redo_log_capacity_value in ['auto', '128M', '256M', '512M', '1G']
|
||||
- (mysql_flush_log_at_trx_commit_value | int) in [0, 1, 2]
|
||||
- (mysql_sync_binlog_value | int) in [0, 1]
|
||||
- (mysql_io_capacity_value | int) in [200, 2000, 5000]
|
||||
- (mysql_long_query_time_value | float) in [0.5, 1.0, 2.0, 5.0, 10.0]
|
||||
- (mysql_binlog_expire_logs_seconds_value | int) in [86400, 259200, 604800, 1209600]
|
||||
- mysql_max_binlog_size_value in ['128M', '256M', '512M', '1G']
|
||||
fail_msg: The first machine prototype only supports safe standalone parameters
|
||||
|
||||
- name: Validate MySQL delivery secrets
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- mysql_root_password_value | length >= 16
|
||||
- mysql_admin_password_value | length >= 16
|
||||
fail_msg: XINFRA_MYSQL_ROOT_PASSWORD and XINFRA_MYSQL_ADMIN_PASSWORD must be configured and at least 16 characters long
|
||||
no_log: true
|
||||
|
||||
- name: Validate topology host count
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_play_hosts_all | length == 1
|
||||
fail_msg: The selected topology does not match the target host count
|
||||
run_once: true
|
||||
|
||||
- name: Check port ownership
|
||||
ansible.builtin.shell: |
|
||||
set -o pipefail
|
||||
if ss -lntH "sport = :{{ mysql_port_value }}" | grep -q .; then
|
||||
systemctl is-active --quiet "mysql-delivery@{{ mysql_instance }}.service"
|
||||
fi
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
|
||||
- name: Read currently available memory
|
||||
ansible.builtin.shell: awk '/^MemAvailable:/ { print int($2 / 1024) }' /proc/meminfo
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: mysql_available_memory
|
||||
changed_when: false
|
||||
|
||||
- name: Check available memory and disk
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (mysql_available_memory.stdout | int) >= (mysql_memory_mb_value | int)
|
||||
- (ansible_mounts | selectattr('mount', 'equalto', mysql_data_disk) | map(attribute='size_available') | first | default(0) | int) >= (mysql_storage_gb_value | int) * 1073741824
|
||||
fail_msg: Target host does not have enough available memory or disk
|
||||
|
||||
- name: Notify precheck completed
|
||||
ansible.builtin.uri:
|
||||
url: "{{ delivery_callback_url_value }}"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ delivery_callback_token_value }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
stage: precheck
|
||||
status: success
|
||||
message: resource lock and baseline checks completed
|
||||
awx_job_id: "{{ delivery_awx_job_id }}"
|
||||
status_code: [200, 202]
|
||||
when: delivery_callback_enabled | bool
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
tasks:
|
||||
- name: Notify install started
|
||||
ansible.builtin.uri:
|
||||
url: "{{ delivery_callback_url_value }}"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ delivery_callback_token_value }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
stage: install
|
||||
status: running
|
||||
message: MySQL package and instance directories installation started
|
||||
awx_job_id: "{{ delivery_awx_job_id }}"
|
||||
status_code: [200, 202]
|
||||
when: delivery_callback_enabled | bool
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Detect an existing Ubuntu MySQL package
|
||||
ansible.builtin.command: dpkg-query -W mysql-server
|
||||
register: mysql_package_before
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Install Ubuntu MySQL package
|
||||
ansible.builtin.apt:
|
||||
name: "{{ mysql_package_name }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
|
||||
- name: Stop the automatically created default instance on a clean host
|
||||
ansible.builtin.systemd_service:
|
||||
name: mysql.service
|
||||
state: stopped
|
||||
enabled: false
|
||||
when: mysql_package_before.rc != 0
|
||||
|
||||
- name: Check for the bundled MySQL AppArmor profile
|
||||
ansible.builtin.stat:
|
||||
path: /etc/apparmor.d/usr.sbin.mysqld
|
||||
register: mysql_apparmor_profile
|
||||
|
||||
- name: Allow the delivery data and run paths in the MySQL AppArmor profile
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/apparmor.d/local/usr.sbin.mysqld
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
content: |
|
||||
# Managed by XINFRA MySQL delivery - grant per-instance native paths
|
||||
{{ mysql_data_disk }}/mysql-delivery/ r,
|
||||
{{ mysql_data_disk }}/mysql-delivery/** rwk,
|
||||
when: mysql_apparmor_profile.stat.exists
|
||||
register: mysql_apparmor_local
|
||||
|
||||
- name: Reload the MySQL AppArmor profile
|
||||
ansible.builtin.command: apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld
|
||||
when: mysql_apparmor_profile.stat.exists and mysql_apparmor_local.changed
|
||||
changed_when: true
|
||||
|
||||
- name: Create instance directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: directory
|
||||
owner: "{{ item.owner }}"
|
||||
group: "{{ item.group }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop:
|
||||
- { path: /etc/mysql/mysql-delivery, owner: root, group: mysql, mode: '0750' }
|
||||
- { path: "{{ mysql_install_dir }}", owner: root, group: root, mode: '0755' }
|
||||
- { path: "{{ mysql_base_dir }}", owner: mysql, group: mysql, mode: '0750' }
|
||||
- { path: "{{ mysql_data_dir }}", owner: mysql, group: mysql, mode: '0750' }
|
||||
- { path: "{{ mysql_log_dir }}", owner: mysql, group: mysql, mode: '0750' }
|
||||
- { path: "{{ mysql_binlog_dir }}", owner: mysql, group: mysql, mode: '0750' }
|
||||
- { path: "{{ mysql_redo_dir }}", owner: mysql, group: mysql, mode: '0750' }
|
||||
- { path: "{{ mysql_run_dir }}", owner: mysql, group: mysql, mode: '0755' }
|
||||
- { path: "{{ mysql_tmp_dir }}", owner: mysql, group: mysql, mode: '0750' }
|
||||
- { path: "{{ mysql_conf_dir }}", owner: root, group: mysql, mode: '0750' }
|
||||
|
||||
- name: Link native binaries into the instance directory
|
||||
ansible.builtin.file:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ mysql_install_dir }}/{{ item.dest }}"
|
||||
state: link
|
||||
loop:
|
||||
- { src: /usr/sbin/mysqld, dest: mysqld }
|
||||
- { src: /usr/bin/mysql, dest: mysql }
|
||||
- { src: /usr/bin/mysqladmin, dest: mysqladmin }
|
||||
|
||||
- name: Notify install completed
|
||||
ansible.builtin.uri:
|
||||
url: "{{ delivery_callback_url_value }}"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ delivery_callback_token_value }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
stage: install
|
||||
status: success
|
||||
message: MySQL package and instance directories installation completed
|
||||
awx_job_id: "{{ delivery_awx_job_id }}"
|
||||
status_code: [200, 202]
|
||||
when: delivery_callback_enabled | bool
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Notify configure started
|
||||
ansible.builtin.uri:
|
||||
url: "{{ delivery_callback_url_value }}"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ delivery_callback_token_value }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
stage: configure
|
||||
status: running
|
||||
message: MySQL configuration and initialization started
|
||||
awx_job_id: "{{ delivery_awx_job_id }}"
|
||||
status_code: [200, 202]
|
||||
when: delivery_callback_enabled | bool
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Calculate host role and server id
|
||||
ansible.builtin.set_fact:
|
||||
mysql_node_index: "{{ ansible_play_hosts_all.index(inventory_hostname) }}"
|
||||
mysql_node_role: >-
|
||||
{{ 'standalone' if mysql_topology_value == 'standalone' else
|
||||
('primary' if ansible_play_hosts_all.index(inventory_hostname) == 0 else
|
||||
('replica' if mysql_topology_value == 'primary_replica' else 'mgr')) }}
|
||||
|
||||
- name: Write instance configuration
|
||||
ansible.builtin.template:
|
||||
src: templates/mysql-instance.cnf.j2
|
||||
dest: "{{ mysql_config_file }}"
|
||||
owner: root
|
||||
group: mysql
|
||||
mode: '0640'
|
||||
notify: Restart MySQL delivery instance
|
||||
|
||||
- name: Link instance configuration into system path
|
||||
ansible.builtin.file:
|
||||
src: "{{ mysql_config_file }}"
|
||||
dest: "{{ mysql_system_config_file }}"
|
||||
state: link
|
||||
force: true
|
||||
|
||||
- name: Initialize the data directory once
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/mysqld
|
||||
- "--defaults-file={{ mysql_config_file }}"
|
||||
- --initialize-insecure
|
||||
- --user=mysql
|
||||
args:
|
||||
creates: "{{ mysql_data_dir }}/auto.cnf"
|
||||
no_log: true
|
||||
|
||||
- name: Install the delivery systemd template
|
||||
ansible.builtin.copy:
|
||||
src: files/mysql-delivery@.service
|
||||
dest: /etc/systemd/system/mysql-delivery@.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
register: mysql_systemd_unit
|
||||
|
||||
- name: Create per-instance systemd override directory
|
||||
ansible.builtin.file:
|
||||
path: "/etc/systemd/system/mysql-delivery@{{ mysql_instance }}.service.d"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0755'
|
||||
|
||||
- name: Write per-instance systemd resource limits
|
||||
ansible.builtin.copy:
|
||||
dest: "/etc/systemd/system/mysql-delivery@{{ mysql_instance }}.service.d/resources.conf"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
content: |
|
||||
[Service]
|
||||
CPUQuota={{ mysql_cpu_cores_value | int * 100 }}%
|
||||
register: mysql_systemd_resources
|
||||
notify: Restart MySQL delivery instance
|
||||
|
||||
- name: Reload systemd units
|
||||
ansible.builtin.systemd_service:
|
||||
daemon_reload: true
|
||||
when: mysql_systemd_unit.changed or mysql_systemd_resources.changed
|
||||
|
||||
- name: Start the MySQL delivery instance
|
||||
ansible.builtin.systemd_service:
|
||||
name: "mysql-delivery@{{ mysql_instance }}.service"
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: Wait for the local MySQL socket
|
||||
ansible.builtin.wait_for:
|
||||
path: "{{ mysql_run_dir }}/mysql.sock"
|
||||
timeout: 60
|
||||
|
||||
- name: Configure local administrative accounts
|
||||
ansible.builtin.shell: |
|
||||
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
|
||||
if ! /usr/bin/mysql --defaults-extra-file="$client_file" -e 'SELECT 1' >/dev/null 2>&1; then
|
||||
cat >"$sql_file" <<'EOF'
|
||||
ALTER USER 'root'@'localhost' IDENTIFIED BY '{{ mysql_root_password_sql }}';
|
||||
EOF
|
||||
/usr/bin/mysql --protocol=socket --socket={{ mysql_run_dir }}/mysql.sock -uroot <"$sql_file"
|
||||
fi
|
||||
cat >"$sql_file" <<'EOF'
|
||||
CREATE USER IF NOT EXISTS 'xinfra_admin'@'%' IDENTIFIED BY '{{ mysql_admin_password_sql }}';
|
||||
ALTER USER 'xinfra_admin'@'%' IDENTIFIED BY '{{ mysql_admin_password_sql }}';
|
||||
GRANT ALL PRIVILEGES ON *.* TO 'xinfra_admin'@'%' WITH GRANT OPTION;
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
/usr/bin/mysql --defaults-extra-file="$client_file" <"$sql_file"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Notify configure completed
|
||||
ansible.builtin.uri:
|
||||
url: "{{ delivery_callback_url_value }}"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ delivery_callback_token_value }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
stage: configure
|
||||
status: success
|
||||
message: MySQL configuration and initialization completed
|
||||
awx_job_id: "{{ delivery_awx_job_id }}"
|
||||
status_code: [200, 202]
|
||||
when: delivery_callback_enabled | bool
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Notify healthcheck started
|
||||
ansible.builtin.uri:
|
||||
url: "{{ delivery_callback_url_value }}"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ delivery_callback_token_value }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
stage: healthcheck
|
||||
status: running
|
||||
message: MySQL TCP health check started
|
||||
awx_job_id: "{{ delivery_awx_job_id }}"
|
||||
status_code: [200, 202]
|
||||
when: delivery_callback_enabled | bool
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Verify MySQL TCP health
|
||||
ansible.builtin.wait_for:
|
||||
host: "{{ ansible_host | default(inventory_hostname) }}"
|
||||
port: "{{ mysql_port_value }}"
|
||||
timeout: 30
|
||||
|
||||
- name: Notify healthcheck completed
|
||||
ansible.builtin.uri:
|
||||
url: "{{ delivery_callback_url_value }}"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ delivery_callback_token_value }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
stage: healthcheck
|
||||
status: success
|
||||
message: MySQL TCP health check completed
|
||||
awx_job_id: "{{ delivery_awx_job_id }}"
|
||||
status_code: [200, 202]
|
||||
when: delivery_callback_enabled | bool
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Notify platform registration handoff
|
||||
ansible.builtin.uri:
|
||||
url: "{{ delivery_callback_url_value }}"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "Bearer {{ delivery_callback_token_value }}"
|
||||
Content-Type: application/json
|
||||
body_format: json
|
||||
body:
|
||||
stage: register
|
||||
status: running
|
||||
message: MySQL deployment finished, waiting for platform asset registration
|
||||
awx_job_id: "{{ delivery_awx_job_id }}"
|
||||
status_code: [200, 202]
|
||||
when: delivery_callback_enabled | bool
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
handlers:
|
||||
- name: Restart MySQL delivery instance
|
||||
ansible.builtin.systemd_service:
|
||||
name: "mysql-delivery@{{ mysql_instance }}.service"
|
||||
state: restarted
|
||||
Reference in New Issue
Block a user