ansible: add K3s datastore export/import, improve iSCSI handling, and migrate merlin to MariaDB primary

- Added tasks and playbooks for K3s datastore export/import using MariaDB Tools role, with associated tests and defaults.
- Enhanced iSCSI role to support mkfs-once logic and safer re-initialization of block storage.
- Migrated iSCSI-backed Rancher data from retropie to merlin.prole.org.
- Updated k3s roles/playbooks to relax Rancher storage preflight checks, supporting PARTUUID-based mounts.
- Adjusted Samba AD NetBIOS name derivation to use uppercase short hostname by default.
- Incremented prole DB version to 104, updated generated prole.cfg, inventory, and recovery manifest templates.
This commit is contained in:
chrisfu 2026-03-06 14:25:13 -08:00
parent cf6d79f505
commit d2295f4c2b
36 changed files with 613 additions and 81 deletions

View File

@ -19,3 +19,8 @@ become_ask_pass = False
[ssh_connection]
pipelining = True
# Use a repo-local known_hosts file and accept new keys non-interactively.
# This prevents unattended runs from failing when inventory uses IPs via `ansible_host`
# and the key is not yet present in the user's `~/.ssh/known_hosts`.
ssh_common_args = -o UserKnownHostsFile=.ansible/known_hosts -o StrictHostKeyChecking=accept-new

View File

@ -1,5 +1,5 @@
; Prole Master Configuration File
; Generated by install.py on 2026-03-05 21:07:28
; Generated by install.py on 2026-03-06 00:36:32
; This file is used as input for Ansible deployment and k8s cluster creation.
[User]
@ -110,7 +110,7 @@ ANSIBLE_DOMAIN = prole.org
ANSIBLE_INFRASTRUCTURE = ${PROLE_HOME}/infrastructure
ANSIBLE_INVENTORY = ${PROLE_HOME}/infrastructure/inventory
ANSIBLE_REALM = PROLE.ORG
ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"https://myrddin.prole.org:6443","server_host":"myrddin.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_hosts":["myrddin.prole.org","pi.prole.org","retropie.prole.org","merlin.prole.org"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"mariadb_primary":["merlin.prole.org"],"mariadb_replica":["raspberry.prole.org"]},"hosts":{"myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":["merlin.prole.org"]}
ANSIBLE_TOPOLOGY = {"domain":"prole.org","realm":"PROLE.ORG","internal_records":{"aventage.prole.org":"10.0.0.206","fairyland.prole.org":"10.0.0.208","loghost.prole.org":"10.0.0.3","merlin.prole.org":"10.0.0.3","morana.prole.org":"10.0.0.66","morgoth.prole.org":"10.0.0.204","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207","synology.prole.org":"10.0.0.203","zinfandel.prole.org":"10.0.0.205"},"ad_dc":{"host":"myrddin.prole.org","ip":"10.0.0.3"},"k3s":{"server_url":"https://myrddin.prole.org:6443","server_host":"myrddin.prole.org","token_present":true},"groups":{"iscsi":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org"],"pihole":["pi.prole.org","raspberry.prole.org"],"ad_dc":["myrddin.prole.org"],"k3s_hosts":["myrddin.prole.org","pi.prole.org","retropie.prole.org","merlin.prole.org"],"linux_hosts":["pi.prole.org","raspberry.prole.org","myrddin.prole.org","retropie.prole.org","merlin.prole.org"],"mariadb_primary":["merlin.prole.org"],"mariadb_replica":["raspberry.prole.org"],"mariadb:children":["mariadb_primary","mariadb_replica"],"merlin_bootstrap":["merlin"]},"hosts":{"merlin":"10.0.0.3","merlin.prole.org":"10.0.0.3","myrddin.prole.org":"10.0.0.3","pi.prole.org":"10.0.0.5","raspberry.prole.org":"10.0.0.4","retropie.prole.org":"10.0.0.207"},"unmapped_hosts":["mariadb_primary","mariadb_replica"]}
KDC_ANSIBLE_DETECTED = 10.0.0.3
KDC_AUTO_DETECTED = 10.0.0.3
KERBEROS_AUTO_ENABLED = True
@ -177,7 +177,7 @@ LOCAL_REGISTRY = localhost:5000
LOCAL_REGISTRY_INTERNAL = k3d-prole-registry.localhost:5000
[Initialization Scripts]
STATUS = Attempted
; No configuration values captured yet for this section.
[Deployment]
MODE = k3s

View File

@ -36,6 +36,11 @@ prole_dns_records:
value: 73.15.20.166
ttl: 300
- name: merlin
type: A
value: 10.0.0.3
ttl: 300
- name: morana
type: A
value: 10.0.0.66
@ -134,6 +139,8 @@ prole_internal_a_records:
ipv4: 10.0.0.208
- fqdn: loghost.prole.org
ipv4: 10.0.0.3
- fqdn: merlin.prole.org
ipv4: 10.0.0.3
- fqdn: morana.prole.org
ipv4: 10.0.0.66
- fqdn: morgoth.prole.org

View File

@ -8,9 +8,4 @@ k3s_kubeconfig_group: kubeadm
k3s_kubeconfig_users:
- pi
# storage
k3s_rancher_mountpoint: /var/lib/rancher
k3s_rancher_fs_type: ext4
k3s_rancher_mount_opts: "_netdev,noatime"
k3s_require_rancher_mount: false
k3s_data_dir: /var/lib/rancher/k3s

View File

@ -31,8 +31,18 @@ k3s_node_labels:
- "prole.org/role=control"
# No iSCSI mounts on merlin
iscsi_targets: []
iscsi_absent_mounts: []
iscsi_portal: 10.0.0.203
# Do not require /var/lib/rancher to be a separate mount on this host.
k3s_require_rancher_mount: false
iscsi_targets:
# PROLE-K3S-1 (moved from retropie)
- iqn: "iqn.2000-01.com.synology:synology.Target-16.292d45194a1"
chap_user: "prole"
chap_password: "{{ vault_iscsi_prole_password }}"
mounts:
- path: /var/lib/rancher
fstype: ext4
opts: "_netdev,noatime"
# Use PARTUUID (stable across mkfs) because we intentionally re-initialize the filesystem.
src: "PARTUUID=29f70e08-5bf1-4803-b94c-ed82fa37f082"
iscsi_absent_mounts: []

View File

@ -1,30 +1,8 @@
iscsi_portal: 10.0.0.203
iscsi_targets: []
iscsi_absent_mounts:
- path: /var/lib/rancher
iscsi_targets:
# PROLE-K3S-1
- iqn: "iqn.2000-01.com.synology:synology.Target-16.292d45194a1"
chap_user: "prole"
chap_password: "{{ vault_iscsi_prole_password }}"
mounts:
- path: /var/lib/rancher
fstype: ext4
opts: "_netdev,noatime"
src: "UUID=73a886bb-3f01-4e8a-9c6d-ab431c671824"
k3s_enabled: true
k3s_role: agent
k3s_node_ip: "10.0.0.207"
k3s_flannel_iface: "wlan0"
k3s_server_url: "https://myrddin.prole.org:6443"
k3s_token: "{{ vault_k3s_token | default('') }}"
k3s_node_labels:
- "prole.org/role=spare-monitoring"
k3s_node_taints:
- "prole.org/exclude-cnpg=true:NoSchedule"
k3s_write_kubeconfig_mode: "0640"
k3s_kubeconfig_group: kubeadm
k3s_kubeconfig_users:
- pi
k3s_enabled: false
cgroups_cmdline_candidates:
- /boot/cmdline.txt

View File

@ -3,6 +3,7 @@ pi.prole.org
raspberry.prole.org
myrddin.prole.org
retropie.prole.org
merlin.prole.org
[pihole]
pi.prole.org
@ -11,12 +12,17 @@ raspberry.prole.org
[ad_dc]
myrddin.prole.org
[k3s_hosts]
[k3s_servers]
myrddin.prole.org
pi.prole.org
retropie.prole.org
merlin.prole.org
[k3s_agents]
[k3s_hosts:children]
k3s_servers
k3s_agents
[linux_hosts]
pi.prole.org
raspberry.prole.org

View File

@ -0,0 +1,11 @@
---
- name: iSCSI auth/login
hosts: iscsi
gather_facts: false
become: true
tasks:
- name: iSCSI auth/login
ansible.builtin.import_role:
name: iscsi
tasks_from: login

View File

@ -0,0 +1,11 @@
---
- name: iSCSI mounts
hosts: iscsi
gather_facts: false
become: true
tasks:
- name: iSCSI mounts
ansible.builtin.import_role:
name: iscsi
tasks_from: mount

View File

@ -0,0 +1,13 @@
---
- name: Cleanup / uninstall k3s (operator action)
# Intentionally broad targeting; use --limit to select the host(s) to clean.
hosts: all
gather_facts: false
become: true
serial: 1
tasks:
- name: Uninstall k3s and remove local state
ansible.builtin.import_role:
name: k3s
tasks_from: cleanup

View File

@ -0,0 +1,12 @@
---
- name: Export k3s datastore database (mysqldump)
hosts: mariadb_primary
gather_facts: false
become: true
serial: 1
tasks:
- name: Export k3s datastore database
ansible.builtin.import_role:
name: mariadb_tools
tasks_from: k3s_datastore_export

View File

@ -0,0 +1,12 @@
---
- name: Import k3s datastore database dump into Merlin MariaDB
hosts: mariadb_primary
gather_facts: false
become: true
serial: 1
tasks:
- name: Import k3s datastore database
ansible.builtin.import_role:
name: mariadb_tools
tasks_from: k3s_datastore_import

View File

@ -69,6 +69,8 @@
hosts: ad_dc
gather_facts: false
become: true
tags:
- samba
pre_tasks:
- name: Gather minimal facts
ansible.builtin.setup:

View File

@ -0,0 +1,12 @@
---
- name: Start k3s agents
hosts: k3s_agents
gather_facts: false
become: true
serial: 1
tasks:
- name: Start k3s-agent service
ansible.builtin.import_role:
name: k3s
tasks_from: start_k3s_agents

View File

@ -0,0 +1,12 @@
---
- name: Start k3s servers
hosts: k3s_servers
gather_facts: false
become: true
serial: 1
tasks:
- name: Start k3s service
ansible.builtin.import_role:
name: k3s
tasks_from: start_k3s_servers

View File

@ -0,0 +1,12 @@
---
- name: Stop k3s agents
hosts: k3s_agents
gather_facts: false
become: true
serial: 1
tasks:
- name: Stop k3s-agent service
ansible.builtin.import_role:
name: k3s
tasks_from: stop_k3s_agents

View File

@ -0,0 +1,12 @@
---
- name: Stop k3s servers
hosts: k3s_servers
gather_facts: false
become: true
serial: 1
tasks:
- name: Stop k3s service
ansible.builtin.import_role:
name: k3s
tasks_from: stop_k3s_servers

View File

@ -5,6 +5,88 @@
state: directory
mode: "0755"
- name: Ensure marker directory exists (mkfs_once)
ansible.builtin.file:
path: /var/lib/prole/iscsi
state: directory
owner: root
group: root
mode: "0755"
when: m.mkfs_once | default(false) | bool
- name: Compute mkfs marker path (mkfs_once)
ansible.builtin.set_fact:
_iscsi_mkfs_marker: >-
/var/lib/prole/iscsi/mkfs-
{{ (m.src | regex_replace('^UUID=', 'uuid-')
| regex_replace('^LABEL=', 'label-')
| regex_replace('[^A-Za-z0-9_.-]', '_')) }}
.done
when: m.mkfs_once | default(false) | bool
- name: Check mkfs marker presence (mkfs_once)
ansible.builtin.stat:
path: "{{ _iscsi_mkfs_marker }}"
register: _iscsi_mkfs_marker_stat
when: m.mkfs_once | default(false) | bool
- name: Fail if mountpoint is already mounted but mkfs_once requested
ansible.builtin.command: "findmnt -n {{ m.path }}"
register: _iscsi_findmnt_before_mkfs
changed_when: false
failed_when: false
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
- name: Refuse to mkfs when mountpoint is mounted
ansible.builtin.fail:
msg: "Refusing to mkfs {{ m.src }} because {{ m.path }} is already mounted ({{ _iscsi_findmnt_before_mkfs.stdout | default('') }})."
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
- _iscsi_findmnt_before_mkfs.rc == 0
- name: Resolve block device for mkfs (mkfs_once)
ansible.builtin.command: >-
{{ (m.src | regex_search('^UUID=') )
| ternary('blkid -U ' ~ (m.src | regex_replace('^UUID=', '')),
(m.src | regex_search('^LABEL=') )
| ternary('blkid -L ' ~ (m.src | regex_replace('^LABEL=', '')),
'echo ' ~ (m.src))) }}
register: _iscsi_mkfs_dev
changed_when: false
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
- name: Require resolved device to look like /dev/* (mkfs_once)
ansible.builtin.assert:
that:
- (_iscsi_mkfs_dev.stdout | default('') | trim) is match('^/dev/')
fail_msg: "Unable to resolve a /dev/* block device for mkfs from src={{ m.src }} (got: {{ _iscsi_mkfs_dev.stdout | default('') | trim }})."
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
- name: Re-initialize filesystem (mkfs.ext4) (mkfs_once)
ansible.builtin.command: "mkfs.ext4 -F {{ _iscsi_mkfs_dev.stdout | trim }}"
changed_when: true
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
- name: Write mkfs marker (mkfs_once)
ansible.builtin.file:
path: "{{ _iscsi_mkfs_marker }}"
state: touch
owner: root
group: root
mode: "0644"
when:
- m.mkfs_once | default(false) | bool
- _iscsi_mkfs_marker_stat.stat.exists is not defined or not _iscsi_mkfs_marker_stat.stat.exists
- name: Mount {{ m.path }}
ansible.builtin.mount:
path: "{{ m.path }}"

View File

@ -17,3 +17,4 @@
loop: "{{ iscsi_targets | default([]) }}"
loop_control:
loop_var: t
label: "{{ t.iqn | default('<unknown-iqn>') }}"

View File

@ -13,7 +13,7 @@
ansible.builtin.lineinfile:
path: /etc/fstab
state: absent
regexp: "^\\\\S+\\\\s+{{ (item.path | default(item)) | regex_escape }}\\\\s+"
regexp: "^\\s*\\S+\\s+{{ (item.path | default(item)) | regex_escape }}\\s+"
loop: "{{ iscsi_absent_mounts | default([]) }}"
loop_control:
label: "{{ item.path | default(item) }}"

View File

@ -170,39 +170,6 @@
- not ansible_check_mode
- (['cgroup_memory=1', 'cgroup_enable=memory'] | reject('in', k3s_proc_cmdline.stdout | default('')) | list) | length > 0
- name: Preflight - detect rancher backing device
ansible.builtin.shell: |
set -euo pipefail
mp="{{ k3s_rancher_mountpoint | default('/var/lib/rancher') }}"
# If the mountpoint is a separate mount, use its source; otherwise fall back to the root filesystem source.
src="$(awk -v mp="$mp" '$5==mp {for(i=1;i<=NF;i++) if($i=="-") {print $(i+2); exit}}' /proc/self/mountinfo)"
if [[ -n "$src" ]]; then
echo "mount:${src}"
exit 0
fi
root_src="$(awk '$5=="/" {for(i=1;i<=NF;i++) if($i=="-") {print $(i+2); exit}}' /proc/self/mountinfo)"
echo "root:${root_src}"
register: rancher_source
changed_when: false
check_mode: no
- name: Fail if rancher must be a separate mount but is on rootfs
ansible.builtin.fail:
msg: "{{ k3s_rancher_mountpoint | default('/var/lib/rancher') }} is not a separate mount ({{ rancher_source.stdout | default('') }}). Refusing to proceed (k3s_require_rancher_mount=true)."
when:
- not ansible_check_mode
- k3s_require_rancher_mount | default(false) | bool
- (rancher_source.stdout | default('')) is search('^root:')
- name: Fail if rancher storage is on SD
ansible.builtin.fail:
msg: "{{ k3s_rancher_mountpoint | default('/var/lib/rancher') }} is on SD/rootfs ({{ rancher_source.stdout | default('') }}). Refusing to proceed."
when:
- not ansible_check_mode
- (rancher_source.stdout | default('')) is search("mmcblk0") or (rancher_source.stdout | default('')) is search("/dev/mmc")
- name: Preflight - ensure required mountpoints are mounted
ansible.builtin.shell: |
awk -v mp="{{ item }}" '$5==mp {found=1} END {exit(found?0:1)}' /proc/self/mountinfo
@ -457,7 +424,7 @@
- /etc/rancher
- /etc/rancher/k3s
- /etc/rancher/k3s/config.yaml.d
- "{{ k3s_rancher_mountpoint | default('/var/lib/rancher') }}"
- "{{ k3s_effective_data_dir | dirname }}"
when: k3s_state != "absent"
- name: Detect active interface for k3s_node_ip (Retropie/Pi only)

View File

@ -0,0 +1,21 @@
---
- name: Gather service facts
ansible.builtin.service_facts:
- name: Require k3s-agent service to exist
ansible.builtin.assert:
that:
- ('k3s-agent' + '.service') in ansible_facts.services
- ansible_facts.services['k3s-agent' + '.service'].status != 'not-found'
fail_msg: "k3s-agent service is not installed on {{ inventory_hostname }}."
when: not ansible_check_mode
- name: Start k3s-agent
ansible.builtin.systemd:
name: k3s-agent
state: started
enabled: true
no_block: true
when:
- ('k3s-agent' + '.service') in ansible_facts.services
- ansible_facts.services['k3s-agent' + '.service'].status != 'not-found'

View File

@ -0,0 +1,21 @@
---
- name: Gather service facts
ansible.builtin.service_facts:
- name: Require k3s service to exist
ansible.builtin.assert:
that:
- ('k3s' + '.service') in ansible_facts.services
- ansible_facts.services['k3s' + '.service'].status != 'not-found'
fail_msg: "k3s service is not installed on {{ inventory_hostname }}."
when: not ansible_check_mode
- name: Start k3s
ansible.builtin.systemd:
name: k3s
state: started
enabled: true
no_block: true
when:
- ('k3s' + '.service') in ansible_facts.services
- ansible_facts.services['k3s' + '.service'].status != 'not-found'

View File

@ -0,0 +1,21 @@
---
- name: Gather service facts
ansible.builtin.service_facts:
# Agents can sometimes be in a Restart=always loop; disabling stops the loop.
- name: Stop and disable k3s-agent service when present
ansible.builtin.systemd:
name: k3s-agent
state: stopped
enabled: false
when:
- ('k3s-agent' + '.service') in ansible_facts.services
- ansible_facts.services['k3s-agent' + '.service'].status != 'not-found'
- name: Clear failed state for k3s-agent (if any)
ansible.builtin.command: systemctl reset-failed k3s-agent
changed_when: false
failed_when: false
when:
- ('k3s-agent' + '.service') in ansible_facts.services
- ansible_facts.services['k3s-agent' + '.service'].status != 'not-found'

View File

@ -0,0 +1,19 @@
---
- name: Gather service facts
ansible.builtin.service_facts:
- name: Stop k3s service when present
ansible.builtin.systemd:
name: k3s
state: stopped
when:
- ('k3s' + '.service') in ansible_facts.services
- ansible_facts.services['k3s' + '.service'].status != 'not-found'
- name: Clear failed state for k3s (if any)
ansible.builtin.command: systemctl reset-failed k3s
changed_when: false
failed_when: false
when:
- ('k3s' + '.service') in ansible_facts.services
- ansible_facts.services['k3s' + '.service'].status != 'not-found'

View File

@ -0,0 +1,18 @@
---
- name: Ensure k3s install preflight does not enforce rancher-on-SD guardrail
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Read k3s install tasks
ansible.builtin.set_fact:
_k3s_install_tasks: "{{ lookup('ansible.builtin.file', playbook_dir ~ '/../tasks/install.yml') }}"
- name: Assert rancher-on-SD guardrail is absent
ansible.builtin.assert:
that:
- (_k3s_install_tasks is search('Fail if rancher storage is on SD')) == false
- (_k3s_install_tasks is search('Preflight - detect rancher backing device')) == false
fail_msg: >-
k3s install preflight should not hard-fail based on rancher storage device.

View File

@ -0,0 +1,3 @@
---
# Predictable dump directory for MariaDB exports created by this role.
mariadb_dump_dir: /var/backups/mariadb/k3s-datastore

View File

@ -0,0 +1,72 @@
---
- name: Require k3s datastore MariaDB connection variables
ansible.builtin.assert:
that:
- (k3s_datastore_mariadb_host | default('')) | length > 0
- (k3s_datastore_mariadb_port | default('') | string) | length > 0
- (k3s_datastore_mariadb_db | default('')) | length > 0
- (k3s_datastore_mariadb_user | default('')) | length > 0
- (k3s_datastore_mariadb_password | default('')) | length > 0
fail_msg: >-
Missing one or more required vars: k3s_datastore_mariadb_host/port/db/user/password.
Ensure the k3s datastore credentials are sourced from Ansible Vault.
when: not ansible_check_mode
- name: Ensure dump directory exists
ansible.builtin.file:
path: "{{ mariadb_dump_dir }}"
state: directory
owner: root
group: root
mode: "0700"
- name: Verify mysqldump is available
ansible.builtin.command: mysqldump --version
changed_when: false
- name: Get timestamp for dump filename
ansible.builtin.command: date +%Y%m%d-%H%M%S
register: _mariadb_dump_ts
changed_when: false
- name: Set dumpfile path
ansible.builtin.set_fact:
mariadb_dumpfile: >-
{{ mariadb_dump_dir }}/k3s-datastore-{{ k3s_datastore_mariadb_db }}-{{ _mariadb_dump_ts.stdout }}.sql
- name: Export k3s datastore database with mysqldump
ansible.builtin.command: >-
mysqldump
--protocol=tcp
--host={{ k3s_datastore_mariadb_host }}
--port={{ k3s_datastore_mariadb_port }}
--user={{ k3s_datastore_mariadb_user }}
--default-character-set=utf8mb4
--single-transaction
--quick
--routines
--events
--triggers
--skip-lock-tables
--set-gtid-purged=OFF
--result-file={{ mariadb_dumpfile }}
{{ k3s_datastore_mariadb_db }}
environment:
MYSQL_PWD: "{{ k3s_datastore_mariadb_password }}"
changed_when: true
no_log: true
when: not ansible_check_mode
- name: Tighten dumpfile permissions
ansible.builtin.file:
path: "{{ mariadb_dumpfile }}"
owner: root
group: root
mode: "0600"
when:
- mariadb_dumpfile is defined
- not ansible_check_mode
- name: Print resulting dumpfile path
ansible.builtin.debug:
msg: "Dump created: {{ mariadb_dumpfile }}"

View File

@ -0,0 +1,65 @@
---
- name: Require mariadb_import_dumpfile
ansible.builtin.assert:
that:
- mariadb_import_dumpfile is defined
- (mariadb_import_dumpfile | string) | length > 0
fail_msg: "Set -e mariadb_import_dumpfile=/path/to/dump.sql (path must exist on the target host)."
- name: Require k3s datastore database name
ansible.builtin.assert:
that:
- (k3s_datastore_mariadb_db | default('')) | length > 0
fail_msg: "k3s_datastore_mariadb_db is empty."
- name: Check dumpfile exists on target host
ansible.builtin.stat:
path: "{{ mariadb_import_dumpfile }}"
register: _mariadb_import_dump_stat
- name: Fail when dumpfile is missing
ansible.builtin.fail:
msg: "Dumpfile does not exist on {{ inventory_hostname }}: {{ mariadb_import_dumpfile }}"
when: not _mariadb_import_dump_stat.stat.exists
- name: Ensure MariaDB service is running
ansible.builtin.service:
name: mariadb
state: started
- name: Ensure target k3s datastore database exists
ansible.builtin.command: >-
mysql --protocol=socket --user=root
--execute="CREATE DATABASE IF NOT EXISTS `{{ k3s_datastore_mariadb_db }}`"
changed_when: false
- name: Refuse dumpfiles that appear to reference other databases
ansible.builtin.command: >-
{{ ansible_playbook_python }} -c
"import re,sys
dump=sys.argv[1]
target_db=sys.argv[2]
bad=set()
with open(dump,'r',encoding='utf-8',errors='ignore') as f:
for ln in f:
if ln.startswith('CREATE DATABASE'):
bad.add('CREATE DATABASE')
if ln.startswith('USE '):
m=re.match(r'^USE\\s+`?([^` ;]+)`?;?', ln.strip())
if m and m.group(1) != target_db:
bad.add(m.group(1))
if bad:
print('Dumpfile references non-target DB(s) or contains CREATE DATABASE:', ','.join(sorted(bad)))
sys.exit(2)
print('Dumpfile scope OK')"
{{ mariadb_import_dumpfile }}
{{ k3s_datastore_mariadb_db }}
changed_when: false
- name: Import dumpfile into k3s datastore database (deliberate operator action)
ansible.builtin.shell: >-
set -euo pipefail;
mysql --protocol=socket --user=root {{ k3s_datastore_mariadb_db }} < {{ mariadb_import_dumpfile }}
args:
executable: /bin/bash
changed_when: true

View File

@ -0,0 +1,30 @@
---
- name: Sanity check mariadb_tools task files
hosts: localhost
connection: local
gather_facts: false
become: false
tasks:
- name: Read export task file
ansible.builtin.set_fact:
_export_tasks: "{{ lookup('ansible.builtin.file', playbook_dir ~ '/../tasks/k3s_datastore_export.yml') }}"
- name: Assert export task file contains expected primitives
ansible.builtin.assert:
that:
- _export_tasks is search('mysqldump')
- _export_tasks is search('--single-transaction')
- _export_tasks is search('mariadb_dumpfile')
fail_msg: "Export task file missing expected mysqldump/flags/output handling."
- name: Read import task file
ansible.builtin.set_fact:
_import_tasks: "{{ lookup('ansible.builtin.file', playbook_dir ~ '/../tasks/k3s_datastore_import.yml') }}"
- name: Assert import task requires mariadb_import_dumpfile
ansible.builtin.assert:
that:
- _import_tasks is search('mariadb_import_dumpfile')
- _import_tasks is search('mysql --protocol=socket')
fail_msg: "Import task file missing required var checks and/or socket import path."

View File

@ -71,7 +71,16 @@
changed_when: false
when: _netplan_cfg is changed
- name: Apply netplan config
ansible.builtin.command: netplan apply
changed_when: false
- name: Apply netplan config (async; may disrupt SSH)
block:
- name: Apply netplan
ansible.builtin.command: netplan apply
async: 60
poll: 0
changed_when: false
- name: Wait for host to come back after netplan apply
ansible.builtin.wait_for_connection:
delay: 3
timeout: 180
when: _netplan_cfg is changed

View File

@ -18,7 +18,7 @@ samba_ad_dc_realm: >-
samba_ad_dc_workgroup: >-
{{ (samba_ad_dc_child_id | length > 0)
| ternary((samba_ad_dc_child_id | upper), "PROLE") }}
samba_ad_dc_netbios_name: "{{ samba_ad_dc_workgroup }}"
samba_ad_dc_netbios_name: "{{ inventory_hostname_short | upper }}"
samba_ad_dc_server_string: "{{ samba_ad_dc_realm }} AD DC"
# Role/services

View File

@ -2,7 +2,7 @@
[global]
dns forwarder = {{ samba_ad_dc_dns_forwarders | join(' ') }}
netbios name = {{ samba_ad_dc_netbios_name }}
netbios name = {{ (samba_ad_dc_netbios_name | default(inventory_hostname_short)) | upper }}
realm = {{ samba_ad_dc_realm }}
server string = {{ samba_ad_dc_server_string }}
server role = {{ samba_ad_dc_server_role }}

View File

@ -0,0 +1,45 @@
---
- name: Add a representative AD DC host for template rendering
hosts: localhost
connection: local
gather_facts: false
become: false
tasks:
- name: Add myrddin.prole.org as a local-connection host
ansible.builtin.add_host:
name: myrddin.prole.org
groups: samba_ad_dc_test_hosts
ansible_connection: local
- name: Render smb.conf and assert netbios name derives from short hostname
hosts: samba_ad_dc_test_hosts
connection: local
gather_facts: false
become: false
vars:
_rendered_path: /tmp/samba_ad_dc_smb.conf
tasks:
- name: Load samba_ad_dc role defaults
ansible.builtin.include_vars:
file: "{{ playbook_dir }}/../defaults/main.yml"
- name: Render smb.conf template
ansible.builtin.template:
src: "{{ playbook_dir }}/../templates/smb.conf.j2"
dest: "{{ _rendered_path }}"
mode: "0600"
- name: Read rendered smb.conf
ansible.builtin.slurp:
src: "{{ _rendered_path }}"
register: _smb_conf
- name: Assert netbios name is the uppercase short hostname
ansible.builtin.assert:
that:
- (_smb_conf.content | b64decode) is search('(?m)^\\s*netbios name\\s*=\\s*MYRDDIN\\s*$')
fail_msg: >-
Expected smb.conf to contain: netbios name = MYRDDIN

View File

@ -1 +1 @@
103
104

View File

@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
SRC_HOST="synology.prole.org"
SRC_PORT="3306"
SRC_DB="k3s"
SRC_USER="root"
DB_PASSWORD='7EAs3!kF2!7eQypP'
OUT_DIR="${1:-$PWD}"
TS="$(date +%Y%m%d-%H%M%S)"
OUT_FILE="${OUT_DIR%/}/k3s-datastore-${SRC_HOST}-${TS}.sql"
DUMP_BIN="${DUMP_BIN:-mysqldump}" # or mariadb-dump if thats what you have
extra=()
# MySQL 8 client compat (harmless if absent)
if "${DUMP_BIN}" --help 2>/dev/null | grep -q -- '--column-statistics'; then
extra+=(--column-statistics=0)
fi
# Some clients support it; some MariaDB builds dont. Only add when supported.
if "${DUMP_BIN}" --help 2>/dev/null | grep -q -- '--set-gtid-purged'; then
extra+=(--set-gtid-purged=OFF)
fi
# IMPORTANT for your Ansible import roles safety check:
# - do NOT use --databases / --all-databases (those emit CREATE DATABASE)
# - dump exactly one DB (k3s), so any USE statement matches the target DB
MYSQL_PWD="${DB_PASSWORD}" "${DUMP_BIN}" \
--protocol=tcp \
--host="${SRC_HOST}" \
--port="${SRC_PORT}" \
--user="${SRC_USER}" \
--default-character-set=utf8mb4 \
--single-transaction \
--quick \
--routines \
--events \
--triggers \
--skip-lock-tables \
--skip-add-locks \
--hex-blob \
"${extra[@]}" \
"${SRC_DB}" \
>"${OUT_FILE}"
chmod 0600 "${OUT_FILE}"
echo "Wrote: ${OUT_FILE}"