--- # roles/storage_mounts/tasks/main.yml # # Idempotent NFS mount configuration for lab VMs. # Safe to run on already-mounted hosts — ansible.posix.mount checks current # kernel mount table and fstab before making any change. # -------------------------------------------------- # STEP 1: Install NFS client packages # WHY ansible.builtin.apt (not shell): idempotent; only installs when absent. # -------------------------------------------------- - name: Install NFS client package ansible.builtin.apt: name: nfs-common state: present update_cache: true tags: [storage, packages] # -------------------------------------------------- # STEP 2: Create mount point directories # WHY before fstab: mount points must pre-exist before systemd or mount(8) # can act on them. file module is idempotent on existing dirs. # -------------------------------------------------- - name: Ensure NFS mount point directories exist ansible.builtin.file: path: "{{ item.dest }}" state: directory mode: '0755' owner: root group: root loop: "{{ storage_nfs_mounts }}" tags: [storage, filesystem] # -------------------------------------------------- # STEP 3: Write fstab entries # WHY state: present (not mounted): separates "persist across reboots" # from "mount now". The next task handles the live mount separately so # each concern has a clear changed/ok signal. # -------------------------------------------------- - name: Write NFS entries to /etc/fstab ansible.posix.mount: src: "{{ storage_nfs_server }}:{{ item.src }}" path: "{{ item.dest }}" fstype: nfs opts: "{{ item.opts }}" state: present loop: "{{ storage_nfs_mounts }}" tags: [storage, fstab] # -------------------------------------------------- # STEP 4: Mount NFS shares in the live kernel # WHY state: mounted: issues mount(8) if not already in the mount table. # No-op when the share is already mounted; changed only on first run # or after an unmount. # -------------------------------------------------- - name: Mount NFS shares ansible.posix.mount: src: "{{ storage_nfs_server }}:{{ item.src }}" path: "{{ item.dest }}" fstype: nfs opts: "{{ item.opts }}" state: mounted loop: "{{ storage_nfs_mounts }}" tags: [storage, mount] # -------------------------------------------------- # STEP 5: Verify mounts are reachable # WHY stat (not ls): stat is a builtin module with no external dependency; # it checks that the path exists AND is a directory (not an empty # local dir masquerading as a successful mount point). # -------------------------------------------------- - name: Verify NFS mount points are accessible ansible.builtin.stat: path: "{{ item.dest }}" register: storage_mounts_stat loop: "{{ storage_nfs_mounts }}" tags: [storage, verify] - name: Assert NFS mount points are directories ansible.builtin.assert: that: - item.stat.exists - item.stat.isdir fail_msg: >- NFS mount point {{ item.item.dest }} is not accessible after mount attempt. Check NFS server ({{ storage_nfs_server }}) connectivity and export paths. loop: "{{ storage_mounts_stat.results }}" when: not ansible_check_mode tags: [storage, verify]