--- - name: Install MariaDB and tooling ansible.builtin.package: name: - mariadb-server - mariadb-client - rsync state: present - name: Temporary external storage for MariaDB (merlin) block: - name: Ensure exFAT support packages are installed (Debian/Ubuntu) block: - name: Install exfatprogs ansible.builtin.package: name: exfatprogs state: present rescue: - name: Install legacy exFAT packages ansible.builtin.package: name: - exfat-fuse - exfat-utils state: present when: ansible_facts['os_family'] | default('') == 'Debian' - name: Ensure external mountpoint directory exists ansible.builtin.file: path: "{{ mariadb_external_mountpoint | default('/external') }}" state: directory owner: root group: root mode: "0755" - name: Ensure external partition device exists ansible.builtin.stat: path: "{{ mariadb_external_device | default('/dev/sda1') }}" register: _mariadb_external_device_stat - name: Fail if external partition device is missing ansible.builtin.assert: that: - _mariadb_external_device_stat.stat.exists fail_msg: "External device {{ mariadb_external_device | default('/dev/sda1') }} does not exist on {{ inventory_hostname }}." - name: Discover filesystem UUID for external partition ansible.builtin.command: >- blkid -s UUID -o value {{ mariadb_external_device | default('/dev/sda1') }} register: _mariadb_external_uuid changed_when: false failed_when: _mariadb_external_uuid.rc != 0 or (_mariadb_external_uuid.stdout | trim | length) == 0 - name: Mount external filesystem at /external (persistent) ansible.posix.mount: src: "UUID={{ _mariadb_external_uuid.stdout | trim }}" path: "{{ mariadb_external_mountpoint | default('/external') }}" fstype: "{{ mariadb_external_fstype | default('exfat') }}" opts: "{{ mariadb_external_mount_opts | default('defaults,nofail') }}" state: mounted - name: Fail if /external is not mounted ansible.builtin.command: "mountpoint -q {{ mariadb_external_mountpoint | default('/external') }}" changed_when: false when: mariadb_external_enabled | default(false) | bool - name: Ensure USB mountpoint directory exists ansible.builtin.file: path: "{{ mariadb_usb_mountpoint | default('/srv/mariadb') }}" state: directory owner: root group: root mode: "0755" when: not (mariadb_external_enabled | default(false) | bool) - name: Mount MariaDB USB filesystem ansible.builtin.mount: src: "LABEL={{ mariadb_usb_label | default('MARIADB') }}" path: "{{ mariadb_usb_mountpoint | default('/srv/mariadb') }}" fstype: "{{ mariadb_usb_fstype | default('ext4') }}" opts: "{{ mariadb_usb_mount_opts | default('noatime,nofail') }}" state: mounted when: not (mariadb_external_enabled | default(false) | bool) - name: Stop MariaDB before datadir changes ansible.builtin.service: name: mariadb state: stopped failed_when: false - name: Migrate MariaDB datadir to /external (merlin temporary setup) vars: mariadb_external_src: "{{ mariadb_external_src_datadir | default('/srv/mariadb/mariadb') }}" mariadb_external_dst: "{{ mariadb_external_dst_datadir | default('/external/mariadb') }}" mariadb_external_backup: "{{ mariadb_external_backup_datadir | default('/srv/mariadb/mariadb.pre-external') }}" mariadb_external_marker: "{{ mariadb_external_migration_marker | default('/external/mariadb/.prole-mariadb-external-migrated') }}" block: - name: Fail if external mount is not present ansible.builtin.command: "mountpoint -q {{ mariadb_external_mountpoint | default('/external') }}" changed_when: false - name: Ensure destination directory exists on external disk ansible.builtin.file: path: "{{ mariadb_external_dst }}" state: directory - name: Check migration marker ansible.builtin.stat: path: "{{ mariadb_external_marker }}" register: _mariadb_external_marker_stat - name: Fail if source datadir does not exist ansible.builtin.stat: path: "{{ mariadb_external_src }}" register: _mariadb_external_src_stat - name: Assert source datadir exists ansible.builtin.assert: that: - _mariadb_external_src_stat.stat.exists - _mariadb_external_src_stat.stat.isdir fail_msg: "Source MariaDB datadir {{ mariadb_external_src }} is missing or not a directory on {{ inventory_hostname }}." - name: Fail clearly if rsync is not available ansible.builtin.command: rsync --version register: _mariadb_external_rsync_version changed_when: false - name: Check whether source datadir is already a mountpoint (bind-mounted) ansible.builtin.command: "mountpoint -q {{ mariadb_external_src }}" register: _mariadb_external_src_is_mountpoint changed_when: false failed_when: false - name: Rsync MariaDB data to external disk (copy; non-destructive) ansible.builtin.command: >- rsync -aH --numeric-ids {{ mariadb_external_src }}/ {{ mariadb_external_dst }}/ register: _mariadb_external_rsync changed_when: true when: not _mariadb_external_marker_stat.stat.exists - name: Write migration marker ansible.builtin.copy: dest: "{{ mariadb_external_marker }}" content: | migrated_from={{ mariadb_external_src }} migrated_to={{ mariadb_external_dst }} host={{ inventory_hostname }} when: not _mariadb_external_marker_stat.stat.exists - name: Check whether backup datadir already exists ansible.builtin.stat: path: "{{ mariadb_external_backup }}" register: _mariadb_external_backup_stat - name: Move original datadir aside (kept for rollback) ansible.builtin.command: >- mv {{ mariadb_external_src }} {{ mariadb_external_backup }} when: - _mariadb_external_marker_stat.stat.exists or (not _mariadb_external_marker_stat.stat.exists and (_mariadb_external_rsync is defined)) - _mariadb_external_src_is_mountpoint.rc != 0 - not _mariadb_external_backup_stat.stat.exists - name: Re-create source datadir path as a mountpoint directory ansible.builtin.file: path: "{{ mariadb_external_src }}" state: directory when: _mariadb_external_src_is_mountpoint.rc != 0 - name: Bind-mount external MariaDB directory to the expected datadir path ansible.posix.mount: src: "{{ mariadb_external_dst }}" path: "{{ mariadb_external_src }}" fstype: none opts: bind state: mounted when: _mariadb_external_src_is_mountpoint.rc != 0 - name: Fail if bind mount is not active ansible.builtin.command: "mountpoint -q {{ mariadb_external_src }}" changed_when: false when: mariadb_external_enabled | default(false) | bool - name: Ensure MariaDB datadir exists on USB ansible.builtin.file: path: "{{ mariadb_datadir | default((mariadb_usb_mountpoint | default('/srv/mariadb')) ~ '/mariadb') }}" state: directory owner: mysql group: mysql mode: "0700" when: not (mariadb_external_enabled | default(false) | bool) - name: Allow MariaDB datadir in AppArmor (Ubuntu/Debian) ansible.builtin.copy: dest: /etc/apparmor.d/local/usr.sbin.mysqld owner: root group: root mode: "0644" content: | # Managed by Ansible (mariadb_primary) {{ mariadb_datadir | default((mariadb_usb_mountpoint | default('/srv/mariadb')) ~ '/mariadb') }}/ r, {{ mariadb_datadir | default((mariadb_usb_mountpoint | default('/srv/mariadb')) ~ '/mariadb') }}/** rwk, register: _mariadb_apparmor_local when: ansible_facts['os_family'] | default('') == 'Debian' - name: Reload AppArmor profile for mysqld ansible.builtin.command: apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld changed_when: false when: - ansible_facts['os_family'] | default('') == 'Debian' - _mariadb_apparmor_local is changed failed_when: false - name: Configure MariaDB bind-address ansible.builtin.lineinfile: path: /etc/mysql/mariadb.conf.d/50-server.cnf regexp: '^bind-address\s*=' line: "bind-address = {{ mariadb_bind_address | default('0.0.0.0') }}" insertafter: '^\[mysqld\]' register: _mariadb_cfg_bind - name: Configure MariaDB datadir ansible.builtin.lineinfile: path: /etc/mysql/mariadb.conf.d/50-server.cnf regexp: '^datadir\s*=' line: "datadir = {{ mariadb_datadir | default((mariadb_usb_mountpoint | default('/srv/mariadb')) ~ '/mariadb') }}" insertafter: '^\[mysqld\]' register: _mariadb_cfg_datadir - name: Check if source MariaDB system tables exist ansible.builtin.stat: path: /var/lib/mysql/mysql register: _mariadb_src_tables - name: Check if destination MariaDB system tables exist ansible.builtin.stat: path: "{{ (mariadb_datadir | default((mariadb_usb_mountpoint | default('/srv/mariadb')) ~ '/mariadb')) ~ '/mysql' }}" register: _mariadb_dst_tables - name: Copy initial MariaDB data to USB datadir (first migration only) ansible.builtin.command: >- rsync -aHAX --numeric-ids /var/lib/mysql/ {{ mariadb_datadir | default((mariadb_usb_mountpoint | default('/srv/mariadb')) ~ '/mariadb') }}/ when: - _mariadb_src_tables.stat.exists - not _mariadb_dst_tables.stat.exists - not (mariadb_external_enabled | default(false) | bool) register: _mariadb_rsync changed_when: true - name: Ensure MariaDB datadir ownership after migration ansible.builtin.command: >- chown -R mysql:mysql {{ mariadb_datadir | default((mariadb_usb_mountpoint | default('/srv/mariadb')) ~ '/mariadb') }} when: - _mariadb_rsync is defined - _mariadb_rsync is changed - not (mariadb_external_enabled | default(false) | bool) changed_when: true - name: Ensure MariaDB service is started and enabled ansible.builtin.service: name: mariadb state: started enabled: true - name: Require MariaDB admin credentials ansible.builtin.assert: that: - k3s_mariadb_admin_user | default('') | length > 0 - k3s_mariadb_admin_password | default('') | length > 0 fail_msg: "Set k3s_mariadb_admin_user and vaulted k3s_mariadb_admin_password for MariaDB provisioning." when: not ansible_check_mode - name: Create/ensure MariaDB admin user for remote TCP management ansible.builtin.command: >- mysql --protocol=socket --user=root --execute=" CREATE USER IF NOT EXISTS '{{ k3s_mariadb_admin_user }}'@'%' IDENTIFIED BY '{{ k3s_mariadb_admin_password }}'; ALTER USER '{{ k3s_mariadb_admin_user }}'@'%' IDENTIFIED BY '{{ k3s_mariadb_admin_password }}'; GRANT ALL PRIVILEGES ON *.* TO '{{ k3s_mariadb_admin_user }}'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;" when: not ansible_check_mode no_log: true - name: Require k3s MariaDB user password (vaulted) ansible.builtin.assert: that: - k3s_datastore_mariadb_password | default('') | length > 0 fail_msg: "k3s_datastore_mariadb_password is empty; set it from Ansible Vault." when: not ansible_check_mode - name: Create k3s database/user/grants (local socket) ansible.builtin.command: >- mysql --protocol=socket --user=root --execute=" CREATE DATABASE IF NOT EXISTS `{{ k3s_datastore_mariadb_db | default('k3s') }}`; CREATE USER IF NOT EXISTS '{{ k3s_datastore_mariadb_user | default('prole_k3s') }}'@'%' IDENTIFIED BY '{{ k3s_datastore_mariadb_password }}'; ALTER USER '{{ k3s_datastore_mariadb_user | default('prole_k3s') }}'@'%' IDENTIFIED BY '{{ k3s_datastore_mariadb_password }}'; GRANT ALL PRIVILEGES ON `{{ k3s_datastore_mariadb_db | default('k3s') }}`.* TO '{{ k3s_datastore_mariadb_user | default('prole_k3s') }}'@'%'; FLUSH PRIVILEGES;" when: not ansible_check_mode no_log: true