--- - name: Preflight native MySQL delivery hosts: "{{ target_hosts }}" 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(3307) | int }}" mysql_memory_mb_value: "{{ memory_mb | default(2048) | int }}" mysql_storage_gb_value: "{{ storage_gb | default(20) | int }}" mysql_install_dir: "/opt/mysql-delivery/{{ mysql_instance }}" mysql_data_dir: "/data/mysql-delivery/{{ mysql_instance }}/data" mysql_log_dir: "/data/mysql-delivery/{{ mysql_instance }}/log" mysql_run_dir: "/run/mysql-delivery-{{ mysql_instance }}" mysql_config_file: "/etc/mysql/mysql-delivery/{{ mysql_instance }}.cnf" mysql_root_password_value: "{{ lookup('ansible.builtin.env', 'XINFRA_MYSQL_ROOT_PASSWORD') }}" mysql_admin_password_value: "{{ lookup('ansible.builtin.env', 'XINFRA_MYSQL_ADMIN_PASSWORD') }}" pre_tasks: - name: Validate prototype parameters ansible.builtin.assert: that: - topology == 'standalone' - mysql_instance is match('^[a-z0-9][a-z0-9-]{0,62}$') - mysql_version_value in mysql_package_map - (mysql_port_value | int) >= 1024 - (mysql_port_value | int) <= 65535 - (mysql_memory_mb_value | int) >= 1024 - (mysql_memory_mb_value | int) <= 4096 - (mysql_storage_gb_value | int) >= 10 - (mysql_storage_gb_value | int) <= 100 - mysql_root_password_value | length >= 16 - mysql_admin_password_value | length >= 16 fail_msg: The first machine prototype only supports safe standalone parameters 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', '/') | map(attribute='size_available') | first | int) >= (mysql_storage_gb_value | int) * 1073741824 fail_msg: Target host does not have enough available memory or disk tasks: - 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 /data/mysql-delivery/ r, /data/mysql-delivery/** rwk, /run/mysql-delivery-*/ rw, /run/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_data_dir }}", owner: mysql, group: mysql, mode: '0750' } - { path: "{{ mysql_log_dir }}", owner: mysql, group: mysql, mode: '0750' } - { path: "{{ mysql_run_dir }}", owner: mysql, group: mysql, mode: '0755' } - 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: 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 topology == 'standalone' else ('primary' if ansible_play_hosts_all.index(inventory_hostname) == 0 else ('replica' if topology == '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: 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: Reload systemd units ansible.builtin.systemd_service: daemon_reload: true when: mysql_systemd_unit.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_value | replace("'", "''") }}'; 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_value | replace("'", "''") }}'; ALTER USER 'xinfra_admin'@'%' IDENTIFIED BY '{{ mysql_admin_password_value | replace("'", "''") }}'; 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: Verify MySQL TCP health ansible.builtin.wait_for: host: "{{ ansible_host | default(inventory_hostname) }}" port: "{{ mysql_port_value }}" timeout: 30 handlers: - name: Restart MySQL delivery instance ansible.builtin.systemd_service: name: "mysql-delivery@{{ mysql_instance }}.service" state: restarted