diff --git a/plugins/modules/mount.py b/plugins/modules/mount.py index 519bd631395..8b03ae902f0 100644 --- a/plugins/modules/mount.py +++ b/plugins/modules/mount.py @@ -2,8 +2,9 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2012, Red Hat, inc -# Written by Seth Vidal -# based on the mount modules from salt and puppet +# Copyright: (c) 2021, quidame +# Written by Seth Vidal, based on the mount modules from salt and puppet +# Enhanced by quidame (swapon/swapoff support) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function @@ -15,15 +16,17 @@ module: mount short_description: Control active and configured mount points description: - - This module controls active and configured mount points in C(/etc/fstab). + - This module controls active and configured mount points in C(/etc/fstab), + as well as active and configured swap spaces. author: - Ansible Core Team - Seth Vidal (@skvidal) + - quidame (@quidame) version_added: "1.0.0" options: path: description: - - Path to the mount point (e.g. C(/mnt/files)). + - Path to the mount point (e.g. C(/mnt/files)). Must be C(none) for swap spaces. - Before Ansible 2.3 this option was only usable as I(dest), I(destfile) and I(name). type: path required: true @@ -31,16 +34,18 @@ src: description: - Device (or NFS volume, or something else) to be mounted on I(path). - - Required when I(state) set to C(present) or C(mounted). + - Required when I(state) set to C(present) or C(mounted), or when I(fstype=swap). type: path fstype: description: - Filesystem type. - - Required when I(state) is C(present) or C(mounted). + - Required when I(state) is C(present) or C(mounted). Also required for + any I(state) to properly handle swap spaces (and then set to c(swap)). type: str opts: description: - - Mount options (see fstab(5), or vfstab(4) on Solaris). + - Mount options (see fstab(5), or vfstab(4) on Solaris) for mountable + filesystems, or swapon(8) options for C(swap) filesystems. type: str dump: description: @@ -64,7 +69,8 @@ description: - If C(mounted), the device will be actively mounted and appropriately configured in I(fstab). If the mount point is not present, the mount - point will be created. + point will be created (unless I(path=none), as expected for C(swap) + filesystems). - If C(unmounted), the device will be unmounted without changing I(fstab). - C(present) only specifies that the device is to be configured in I(fstab) and does not trigger or require a mount. @@ -84,6 +90,9 @@ fstab: description: - File to use instead of C(/etc/fstab). + - The filename must not start with a dot, and must end with C(.fstab), + otherwise it is silently ignored. It is also ignored by mount helpers + (for filesystems not natively supported by the C(mount) command). - You should not use this option unless you really know what you are doing. - This might be useful if you need to configure mountpoints in a chroot environment. - OpenBSD does not allow specifying alternate fstab files with mount so do not @@ -108,6 +117,11 @@ - Using C(remounted) with I(opts) set may create unexpected results based on the existing options already defined on mount, so care should be taken to ensure that conflicting options are not present before hand. + - Support for swap spaces activation/deactivation as been added in version + 1.2.0 of C(ansible.posix). + - Strictly speaking, swap filesystems can't be C(mounted), C(unmounted) or + C(remounted). The module internally calls C(swapon) and C(swapoff) commands + to enable or disable such filesystems and make them usable by the kernel. ''' EXAMPLES = r''' @@ -169,11 +183,27 @@ opts: rw,sync,hard,intr state: mounted fstype: nfs + +- name: Enable swap device with priority=1 + ansible.posix.mount: + src: /dev/mapper/vg0-swap + fstype: swap + path: none + opts: pri=1 + state: mounted + +- name: Disable swap file and keep its record in fstab + ansible.posix.mount: + src: /var/swapfile + fstype: swap + path: none + state: unmounted ''' import errno import os +import re import platform from ansible.module_utils.basic import AnsibleModule @@ -546,10 +576,10 @@ def is_bind_mounted(module, linux_mounts, dest, src=None, fstype=None): else: bin_path = module.get_bin_path('mount', required=True) cmd = '%s -l' % bin_path - rc, out, err = module.run_command(cmd) + _, out, _ = module.run_command(cmd) mounts = [] - if len(out): + if len(out) > 0: mounts = to_native(out).strip().split('\n') for mnt in mounts: @@ -639,6 +669,78 @@ def get_linux_mounts(module, mntinfo_file="/proc/self/mountinfo"): return mounts +def is_swap(module, args): + """Return True if the device/file is an active swap space, False otherwise.""" + + if module.params['fstype'] != 'swap' or args['name'] != 'none': + return False + + swapon_bin = module.get_bin_path('swapon', required=True) + cmd = [swapon_bin, '--noheadings', '--show=name'] + dev = os.path.realpath(args['src']) + + rc, out, err = module.run_command(cmd) + + if rc != 0: + module.fail_json(msg="Error while querying active swaps: %s" % err) + + return bool(dev in out.splitlines()) + + +def swapon(module, args): + """Activate a swap device/file with the proper options.""" + + swapon_bin = module.get_bin_path('swapon', required=True) + cmd = [swapon_bin] + + # Only 'swapon -a' applies options from fstab, otherwise they are ignored + # unless provided on command line with '-o opts'. But not all versions of + # swapon accept -o or --options. So we don't use it here, but at least we + # keep the 'priority' and 'discard' options. + if args['opts'] is not None: + for opt in args['opts'].split(','): + if re.match('pri=[0-9]', opt): + cmd += ['-p', opt.split('=')[1]] + if re.match('discard=', opt): + cmd += ['-d', opt.split('=')[1]] + + # src such as UUID=some_uuid and LABEL=some_label work as is. + cmd += [args['src']] + + rc, out, err = module.run_command(cmd) + + if rc: + return rc, out + err + return 0, '' + + +def swapoff(module, args): + """Deactivate a swap device/file.""" + + swapoff_bin = module.get_bin_path('swapoff', required=True) + cmd = [swapoff_bin, args['src']] + + rc, out, err = module.run_command(cmd) + + if rc: + return rc, out + err + return 0, '' + + +def reswap(module, args): + """Deactivate a swap device/file and reactivate it with new options.""" + + if is_swap(module, args): + rc, msg = swapoff(module, args) + if rc: + return rc, msg + + rc, msg = swapon(module, args) + if rc: + return rc, msg + return 0, '' + + def main(): module = AnsibleModule( argument_spec=dict( @@ -651,12 +753,14 @@ def main(): passno=dict(type='str'), src=dict(type='path'), backup=dict(type='bool', default=False), - state=dict(type='str', required=True, choices=['absent', 'mounted', 'present', 'unmounted', 'remounted']), + state=dict(type='str', required=True, + choices=['absent', 'mounted', 'present', 'unmounted', 'remounted']), ), supports_check_mode=True, required_if=( ['state', 'mounted', ['src', 'fstype']], ['state', 'present', ['src', 'fstype']], + ['fstype', 'swap', ['src']], ), ) @@ -744,6 +848,12 @@ def main(): if res: module.fail_json( msg="Error unmounting %s: %s" % (name, msg)) + elif is_swap(module, args): + res, msg = swapoff(module, args) + + if res: + module.fail_json( + msg="Error disabling swap space %s: %s" % (args['src'], msg)) if os.path.exists(name): try: @@ -759,10 +869,19 @@ def main(): module.fail_json( msg="Error unmounting %s: %s" % (name, msg)) + changed = True + elif is_swap(module, args): + if not module.check_mode: + res, msg = swapoff(module, args) + + if res: + module.fail_json( + msg="Error disabling swap space %s: %s" % (args['src'], msg)) + changed = True elif state == 'mounted': dirs_created = [] - if not os.path.exists(name) and not module.check_mode: + if name != 'none' and not os.path.exists(name) and not module.check_mode: try: # Something like mkdir -p but with the possibility to undo. # Based on some copy-paste from the "file" module. @@ -798,11 +917,18 @@ def main(): if changed and not module.check_mode: res, msg = remount(module, args) changed = True + elif is_swap(module, args): + if changed and not module.check_mode: + res, msg = reswap(module, args) + changed = True else: changed = True if not module.check_mode: - res, msg = mount(module, args) + if args['fstype'] == 'swap' and name == 'none': + res, msg = swapon(module, args) + else: + res, msg = mount(module, args) if res: # Not restoring fstab after a failed mount was reported as a bug, @@ -820,15 +946,26 @@ def main(): except Exception: pass - module.fail_json(msg="Error mounting %s: %s" % (name, msg)) + if args['fstype'] == 'swap' and name == 'none': + error_msg = "Error enabling swap space %s: %s" % (args['src'], msg) + else: + error_msg = "Error mounting %s: %s" % (name, msg) + + module.fail_json(msg=error_msg) elif state == 'present': name, changed = set_mount(module, args) elif state == 'remounted': if not module.check_mode: - res, msg = remount(module, args) + if module.params['fstype'] == 'swap' and name == 'none': + res, msg = reswap(module, args) + + if res: + module.fail_json(msg="Error re-enabling swap space %s: %s" % (args['src'], msg)) + else: + res, msg = remount(module, args) - if res: - module.fail_json(msg="Error remounting %s: %s" % (name, msg)) + if res: + module.fail_json(msg="Error remounting %s: %s" % (name, msg)) changed = True else: diff --git a/tests/integration/targets/mount/tasks/main.yml b/tests/integration/targets/mount/tasks/main.yml index 7eacc59f102..4ac7e855f23 100644 --- a/tests/integration/targets/mount/tasks/main.yml +++ b/tests/integration/targets/mount/tasks/main.yml @@ -1,11 +1,14 @@ +--- - name: Create the mount point file: state: directory path: '{{ output_dir }}/mount_dest' + - name: Create a directory to bind mount file: state: directory path: '{{ output_dir }}/mount_source' + - name: Put something in the directory so we see that it worked copy: content: 'Testing @@ -13,6 +16,7 @@ ' dest: '{{ output_dir }}/mount_source/test_file' register: orig_info + - name: Bind mount a filesystem (Linux) mount: src: '{{ output_dir }}/mount_source' @@ -22,6 +26,7 @@ opts: bind when: ansible_system == 'Linux' register: bind_result_linux + - name: Bind mount a filesystem (FreeBSD) mount: src: '{{ output_dir }}/mount_source' @@ -30,18 +35,22 @@ fstype: nullfs when: ansible_system == 'FreeBSD' register: bind_result_freebsd + - name: get checksum for bind mounted file stat: path: '{{ output_dir }}/mount_dest/test_file' when: ansible_system in ('FreeBSD', 'Linux') register: dest_stat + - name: assert the bind mount was successful assert: that: - - (ansible_system == 'Linux' and bind_result_linux['changed']) or (ansible_system == 'FreeBSD' and bind_result_freebsd['changed']) + - (ansible_system == 'Linux' and bind_result_linux['changed']) or + (ansible_system == 'FreeBSD' and bind_result_freebsd['changed']) - dest_stat['stat']['exists'] - orig_info['checksum'] == dest_stat['stat']['checksum'] when: ansible_system in ('FreeBSD', 'Linux') + - name: Bind mount a filesystem (Linux) mount: src: '{{ output_dir }}/mount_source' @@ -51,6 +60,7 @@ opts: bind when: ansible_system == 'Linux' register: bind_result_linux + - name: Bind mount a filesystem (FreeBSD) mount: src: '{{ output_dir }}/mount_source' @@ -59,11 +69,14 @@ fstype: nullfs when: ansible_system == 'FreeBSD' register: bind_result_freebsd + - name: Make sure we didn't mount a second time assert: that: - - (ansible_system == 'Linux' and not bind_result_linux['changed']) or (ansible_system == 'FreeBSD' and not bind_result_freebsd['changed']) + - (ansible_system == 'Linux' and not bind_result_linux['changed']) or + (ansible_system == 'FreeBSD' and not bind_result_freebsd['changed']) when: ansible_system in ('FreeBSD', 'Linux') + - name: Remount filesystem with different opts (Linux) mount: src: '{{ output_dir }}/mount_source' @@ -73,6 +86,7 @@ opts: bind,ro when: ansible_system == 'Linux' register: bind_result_linux + - name: Remount filesystem with different opts (FreeBSD) mount: src: '{{ output_dir }}/mount_source' @@ -82,121 +96,41 @@ opts: ro when: ansible_system == 'FreeBSD' register: bind_result_freebsd + - name: Get mount options shell: mount | grep mount_dest | grep -E -w '(ro|read-only)' | wc -l register: remount_options + - name: Make sure the filesystem now has the new opts assert: that: - - (ansible_system == 'Linux' and bind_result_linux['changed']) or (ansible_system == 'FreeBSD' and bind_result_freebsd['changed']) + - (ansible_system == 'Linux' and bind_result_linux['changed']) or + (ansible_system == 'FreeBSD' and bind_result_freebsd['changed']) - '''1'' in remount_options.stdout' - 1 == remount_options.stdout_lines | length when: ansible_system in ('FreeBSD', 'Linux') + - name: Unmount the bind mount mount: name: '{{ output_dir }}/mount_dest' state: absent when: ansible_system in ('Linux', 'FreeBSD') register: unmount_result + - name: Make sure the file no longer exists in dest stat: path: '{{ output_dir }}/mount_dest/test_file' when: ansible_system in ('FreeBSD', 'Linux') register: dest_stat + - name: Check that we unmounted assert: that: - unmount_result['changed'] - not dest_stat['stat']['exists'] when: ansible_system in ('FreeBSD', 'Linux') -- name: Create fstab record for the first swap file - mount: - name: none - src: /tmp/swap1 - opts: sw - fstype: swap - state: present - register: swap1_created - when: ansible_system in ('Linux') -- name: Try to create fstab record for the first swap file again - mount: - name: none - src: /tmp/swap1 - opts: sw - fstype: swap - state: present - register: swap1_created_again - when: ansible_system in ('Linux') -- name: Check that we created the swap1 record - assert: - that: - - swap1_created['changed'] - - not swap1_created_again['changed'] - when: ansible_system in ('Linux') -- name: Create fstab record for the second swap file - mount: - name: none - src: /tmp/swap2 - opts: sw - fstype: swap - state: present - register: swap2_created - when: ansible_system in ('Linux') -- name: Try to create fstab record for the second swap file again - mount: - name: none - src: /tmp/swap1 - opts: sw - fstype: swap - state: present - register: swap2_created_again - when: ansible_system in ('Linux') -- name: Check that we created the swap2 record - assert: - that: - - swap2_created['changed'] - - not swap2_created_again['changed'] - when: ansible_system in ('Linux') -- name: Remove the fstab record for the first swap file - mount: - name: none - src: /tmp/swap1 - state: absent - register: swap1_removed - when: ansible_system in ('Linux') -- name: Try to remove the fstab record for the first swap file again - mount: - name: none - src: /tmp/swap1 - state: absent - register: swap1_removed_again - when: ansible_system in ('Linux') -- name: Check that we removed the swap1 record - assert: - that: - - swap1_removed['changed'] - - not swap1_removed_again['changed'] - when: ansible_system in ('Linux') -- name: Remove the fstab record for the second swap file - mount: - name: none - src: /tmp/swap2 - state: absent - register: swap2_removed - when: ansible_system in ('Linux') -- name: Try to remove the fstab record for the second swap file again - mount: - name: none - src: /tmp/swap2 - state: absent - register: swap2_removed_again - when: ansible_system in ('Linux') -- name: Check that we removed the swap2 record - assert: - that: - - swap2_removed['changed'] - - not swap2_removed_again['changed'] - when: ansible_system in ('Linux') + + - name: Create fstab record with missing last two fields copy: dest: /etc/fstab @@ -204,6 +138,7 @@ ' when: ansible_system in ('Linux') + - name: Try to change the fstab record with the missing last two fields mount: src: //nas/photo @@ -213,10 +148,12 @@ state: present register: optional_fields_update when: ansible_system in ('Linux') + - name: Get the content of the fstab file shell: cat /etc/fstab register: optional_fields_content when: ansible_system in ('Linux') + - name: Check if the line containing the missing last two fields was changed assert: that: @@ -224,16 +161,20 @@ - ''' 0 0'' in optional_fields_content.stdout' - 1 == optional_fields_content.stdout_lines | length when: ansible_system in ('Linux') + + - name: Block to test remounted option block: - name: Create empty file command: dd if=/dev/zero of=/tmp/myfs.img bs=1048576 count=20 when: ansible_system in ('Linux') + - name: Format FS when: ansible_system in ('Linux') community.general.system.filesystem: fstype: ext3 dev: /tmp/myfs.img + - name: Mount the FS for the first time mount: path: /tmp/myfs @@ -241,43 +182,56 @@ fstype: ext2 state: mounted when: ansible_system in ('Linux') + - name: Get the last write time shell: 'dumpe2fs /tmp/myfs.img 2>/dev/null | grep -i last write time: |cut -d: -f2-' register: last_write_time when: ansible_system in ('Linux') + - name: Wait 2 second pause: seconds: 2 when: ansible_system in ('Linux') + - name: Test if the FS is remounted mount: path: /tmp/myfs state: remounted when: ansible_system in ('Linux') + - name: Get again the last write time shell: 'dumpe2fs /tmp/myfs.img 2>/dev/null | grep -i last write time: |cut -d: -f2-' register: last_write_time2 when: ansible_system in ('Linux') + - name: Fail if they are the same fail: msg: Filesytem was not remounted, testing of the module failed! - when: last_write is defined and last_write_time2 is defined and last_write_time.stdout == last_write_time2.stdout and ansible_system in ('Linux') + when: + - last_write is defined + - last_write_time2 is defined + - last_write_time.stdout == last_write_time2.stdout + - ansible_system in ('Linux') + - name: Remount filesystem with different opts using remounted option (Linux only) mount: path: /tmp/myfs state: remounted opts: rw,noexec when: ansible_system == 'Linux' + - name: Get remounted options (Linux only) shell: mount | grep myfs | grep -E -w 'noexec' | wc -l register: remounted_options when: ansible_system == 'Linux' + - name: Make sure the filesystem now has the new opts after using remounted (Linux only) assert: that: - "'1' in remounted_options.stdout" - "1 == remounted_options.stdout_lines | length" when: ansible_system == 'Linux' + always: - name: Umount the test FS mount: @@ -286,6 +240,7 @@ opts: loop state: absent when: ansible_system in ('Linux') + - name: Remove the test FS file: path: '{{ item }}' @@ -294,3 +249,8 @@ - /tmp/myfs.img - /tmp/myfs when: ansible_system in ('Linux') + + +- name: Include tests against swap filesystem + when: ansible_system in ('Linux') + include_tasks: swap.yml diff --git a/tests/integration/targets/mount/tasks/swap.yml b/tests/integration/targets/mount/tasks/swap.yml new file mode 100644 index 00000000000..625bccb547b --- /dev/null +++ b/tests/integration/targets/mount/tasks/swap.yml @@ -0,0 +1,202 @@ +--- +- name: Create fstab record for the first swap file + mount: + name: none + src: /tmp/swap1 + opts: sw + fstype: swap + state: present + register: swap1_created + +- name: Try to create fstab record for the first swap file again + mount: + name: none + src: /tmp/swap1 + opts: sw + fstype: swap + state: present + register: swap1_created_again + +- name: Check that we created the swap1 record + assert: + that: + - swap1_created['changed'] + - not swap1_created_again['changed'] + +- name: Create fstab record for the second swap file + mount: + name: none + src: /tmp/swap2 + opts: sw + fstype: swap + state: present + register: swap2_created + +- name: Try to create fstab record for the second swap file again + mount: + name: none + src: /tmp/swap1 + opts: sw + fstype: swap + state: present + register: swap2_created_again + +- name: Check that we created the swap2 record + assert: + that: + - swap2_created['changed'] + - not swap2_created_again['changed'] + +- name: Remove the fstab record for the first swap file + mount: + name: none + src: /tmp/swap1 + state: absent + register: swap1_removed + +- name: Try to remove the fstab record for the first swap file again + mount: + name: none + src: /tmp/swap1 + state: absent + register: swap1_removed_again + +- name: Check that we removed the swap1 record + assert: + that: + - swap1_removed['changed'] + - not swap1_removed_again['changed'] + +- name: Remove the fstab record for the second swap file + mount: + name: none + src: /tmp/swap2 + state: absent + register: swap2_removed + +- name: Try to remove the fstab record for the second swap file again + mount: + name: none + src: /tmp/swap2 + state: absent + register: swap2_removed_again + +- name: Check that we removed the swap2 record + assert: + that: + - swap2_removed['changed'] + - not swap2_removed_again['changed'] + + +- name: Try to activate swap spaces + block: + - name: create a file to be used for memory swapping + command: + cmd: dd if=/dev/zero of=/tmp/swap1 bs=1M count=64 + creates: /tmp/swap1 + + - name: create a swap filesystem into the dedicated file + community.general.filesystem: + dev: /tmp/swap1 + fstype: swap + + - name: setup permissions suitable for swap usage + file: + path: /tmp/swap1 + mode: u=rw,go= + state: file + + - name: Get swap info (0) + command: swapon --noheadings --show=name,prio + register: swap_info_0 + changed_when: false + + - name: Enable swap file with priority 1234 + mount: + path: none + src: /tmp/swap1 + fstype: swap + opts: pri=1234 + state: mounted + register: do_swap_1 + + - name: Get swap info (1) + command: swapon --noheadings --show=name,prio + register: swap_info_1 + changed_when: false + + - name: Update swap priority to 10 + mount: + path: none + src: /tmp/swap1 + fstype: swap + opts: pri=10 + state: mounted + register: do_swap_2 + + - name: Get swap info (2) + command: swapon --noheadings --show=name,prio + register: swap_info_2 + changed_when: false + + - name: Do it again (test idempotency) + mount: + path: none + src: /tmp/swap1 + fstype: swap + opts: pri=10 + state: mounted + register: do_swap_3 + + - name: Get swap info (3) + command: swapon --noheadings --show=name,prio + register: swap_info_3 + changed_when: false + + + always: + - name: Disable swap file totally + mount: + path: none + src: /tmp/swap1 + fstype: swap + state: absent + register: do_swap_4 + + - name: Remove swap file + file: + path: /tmp/swap1 + state: absent + + - name: Get swap info (4) + command: swapon --noheadings --show=name,prio + register: swap_info_4 + changed_when: false + + +- name: format results + set_fact: + list_prio: "{{ list_prio | d([]) + [swap_prio] }}" + loop: + - "{{ swap_info_0 }}" + - "{{ swap_info_1 }}" + - "{{ swap_info_2 }}" + - "{{ swap_info_3 }}" + - "{{ swap_info_4 }}" + vars: + swap_line: "{{ item.stdout_lines | select('match', '/tmp/swap1') | list | join }}" + swap_prio: "{{ swap_line.split()[1] if swap_line | length > 0 else '' }}" + +- name: check that results are as expected + assert: + that: + - do_swap_1 is changed + - do_swap_2 is changed + - do_swap_3 is not changed + - do_swap_4 is changed + + - list_prio[0] | length == 0 + - list_prio[1] | int == 1234 + - list_prio[2] | int == 10 + - list_prio[3] | int == 10 + - list_prio[4] | length == 0