personal_infra/ansible/roles/monero/tasks/storage.yml
counterweight 71dbfbe5ff
monero: find the chain disk by its label, and a blank one by Proxmox slot
After the chain disk moved to the SSD, a reboot on 2026-10-04 swapped sda and
sdb, and monero_data_device: /dev/sdb then named the ROOT disk. The root-disk
guard caught it, but a routine run should never have been reading a device
name at all.

Every run now looks for the filesystem labelled monero-data first. If it is
there, monero_data_device is never consulted and the mount goes by that
filesystem's UUID. Only a blank disk is found by name, and the name is now
/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi1: Proxmox passes the slot
in as the serial, so it is fixed before boot and survives `qm disk move`.

The by-id path is resolved with readlink before the root-disk guard compares
it, since a symlink path is never a prefix of /dev/sdb1 and the guard would
otherwise always pass. The UUID assert now also fails on two disks carrying
the same label.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 17:30:57 +02:00

172 lines
7.5 KiB
YAML

---
# The 500 GB data disk.
#
# knots-box does this by hand: /mnt/knots_data holds ~875 GB of blockchain and
# appears nowhere in Ansible, so a rebuild of that host would come up with the
# chain silently missing. This role does it in code instead.
# Does the disk already carry our filesystem? Its label is the identity from
# here on. When it is found, the device name below is never read: everything
# that follows works from the labelled filesystem, so a stale or reordered
# device name cannot break a routine run. blkid exits 2 for "no such label".
- name: Look for an existing Monero filesystem by its label
ansible.builtin.command: "blkid -L {{ monero_data_label }}"
register: monero_labelled_fs
changed_when: false
failed_when: monero_labelled_fs.rc not in [0, 2]
check_mode: false
- name: Find the device backing the root filesystem
ansible.builtin.command: findmnt -no SOURCE /
register: monero_root_source
changed_when: false
check_mode: false
# First run only: the disk is blank and has to be found by name.
- name: Prepare a blank data disk
when: monero_labelled_fs.rc != 0
block:
# The configured name is a /dev/disk/by-id symlink. Resolve it before
# comparing it to anything: the root source is a kernel name like
# /dev/sdb1, and a symlink path is never a prefix of one, so comparing the
# unresolved name would make the root-disk guard below always pass.
- name: Resolve the configured data device to its kernel name
ansible.builtin.command: "readlink -e {{ monero_data_device }}"
register: monero_device_path
changed_when: false
failed_when: false
check_mode: false
- name: Check the Monero data device
ansible.builtin.stat:
path: "{{ monero_device_path.stdout | default('', true) or monero_data_device }}"
register: monero_device
- name: Find the disk the root filesystem lives on
ansible.builtin.command: "lsblk -no PKNAME {{ monero_root_source.stdout }}"
register: monero_root_disk
changed_when: false
check_mode: false
# A guard, not ceremony. mkfs on the wrong device is unrecoverable.
- name: Assert the data device is a real block device and is not the root disk
ansible.builtin.assert:
that:
- monero_device_path.rc == 0
- monero_device.stat.exists
- monero_device.stat.isblk | default(false)
- monero_device_path.stdout != '/dev/' ~ (monero_root_disk.stdout | trim)
- not monero_root_source.stdout.startswith(monero_device_path.stdout)
quiet: true
fail_msg: >-
{{ monero_data_device }} (-> {{ monero_device_path.stdout | default('nothing', true) }})
is not a usable data device, or it is the disk the root filesystem
lives on ({{ monero_root_source.stdout }}). Refusing to touch it.
Check `ls -l /dev/disk/by-id` on the host.
- name: Read any existing filesystem on the data device
ansible.builtin.command: "blkid -o value -s TYPE {{ monero_device_path.stdout }}"
register: monero_existing_fs
changed_when: false
failed_when: false
check_mode: false
# No monero-data label was found, so a filesystem here is something else.
# community.general.filesystem would leave an ext4 of any label alone but
# happily format over a disk it does not recognise. Say so out loud instead.
- name: Refuse to reformat a disk that already carries a different filesystem
ansible.builtin.assert:
that:
- monero_existing_fs.stdout | trim in ['', monero_data_fstype]
quiet: true
fail_msg: >-
{{ monero_data_device }} already holds a '{{ monero_existing_fs.stdout | trim }}'
filesystem, not '{{ monero_data_fstype }}'. Refusing to overwrite it.
- name: Create the filesystem on the data device
community.general.filesystem:
fstype: "{{ monero_data_fstype }}"
dev: "{{ monero_device_path.stdout }}"
opts: "{{ monero_data_mkfs_opts }}"
# By label: on the first run the filesystem was just created with it, on every
# other run it was found by it.
- name: Read the filesystem UUID
ansible.builtin.command: "blkid -o value -s UUID -t LABEL={{ monero_data_label }}"
register: monero_fs_uuid
changed_when: false
failed_when: false
check_mode: false
# Everything past this point mounts by UUID, so there is nothing sensible to do
# without one. On a --check run against a disk that has no filesystem yet this
# is expected and unavoidable: check mode did not create the filesystem, so
# there is no UUID to read. Say that, rather than leaving a bare blkid rc=2.
# Exactly one: two disks with this label (a cloned disk, say) would make the
# mount below ambiguous.
- name: Assert exactly one UUID was found
ansible.builtin.assert:
that:
- monero_fs_uuid.stdout | trim | length == 36
quiet: true
fail_msg: >-
Expected exactly one filesystem labelled {{ monero_data_label }}, found:
'{{ monero_fs_uuid.stdout | trim }}'.
{% if ansible_check_mode %}No UUID is expected under --check when the disk
has not been formatted yet - check mode cannot create the filesystem, so
it cannot dry-run the mount either. Run without --check once, then
--check is meaningful from then on.{% else %}If none: mkfs appears to have
failed. If several: more than one disk carries the label. Check `lsblk -f`
on the host.{% endif %}
# Same owner and mode as the "Own the mounted data directory" task below, and
# that is deliberate rather than duplication. Before the disk is mounted this
# creates the mount point; afterwards it targets the mounted filesystem and is a
# no-op. Giving it root:root 0755 here - the obvious thing to write - made the
# two tasks disagree, and each run flipped the directory back and forth.
- name: Create the Monero data directory as a mount point
ansible.builtin.file:
path: "{{ monero_data_dir }}"
state: directory
owner: "{{ monero_user }}"
group: "{{ monero_group }}"
mode: '0750'
# By UUID, never by a device name. nofail so a missing disk does not drop a headless
# VM into an emergency shell - monerod is stopped from starting by
# RequiresMountsFor= in its unit instead, which the systemd monitor then reports.
# noatime because LMDB touches a lot of pages and none of it wants an atime write.
- name: Mount the data disk by UUID
ansible.posix.mount:
path: "{{ monero_data_dir }}"
src: "UUID={{ monero_fs_uuid.stdout | trim }}"
fstype: "{{ monero_data_fstype }}"
opts: defaults,noatime,nofail
state: mounted
# AFTER the mount, never before: chowning the mount point first sets permissions
# on the directory the mount then hides, and the blockchain lands on a root-owned
# filesystem that monerod cannot write to.
- name: Own the mounted data directory
ansible.builtin.file:
path: "{{ monero_data_dir }}"
state: directory
owner: "{{ monero_user }}"
group: "{{ monero_group }}"
mode: '0750'
- name: Confirm the data directory is a mount point and not the root filesystem
ansible.builtin.command: "findmnt -no SOURCE --target {{ monero_data_dir }}"
register: monero_mounted_source
changed_when: false
check_mode: false
- name: Assert the blockchain will land on the data disk
ansible.builtin.assert:
that:
- monero_mounted_source.stdout | trim != monero_root_source.stdout | trim
quiet: true
fail_msg: >-
{{ monero_data_dir }} resolves to {{ monero_mounted_source.stdout | trim }},
which is the root filesystem. The 500 GB disk is not mounted and a full
sync would fill the 10 GB root disk.