From eafd8f89355848548570f5bd80f7781b9fe56e06 Mon Sep 17 00:00:00 2001 From: quidame Date: Sat, 21 Nov 2020 19:17:34 +0100 Subject: [PATCH 1/2] feature: mount - implement swapon/swapoff (#106) * Declare functions swapon() & swapoff(), is_swap() & reswap(). * Apply swapon/swapoff for states mounted, unmounted, remounted and absent. * Override default opts and boot when fstype=swap. * Do not honor 'fstab' when fstype=swap (fail instead). * Also fail when fstype=swap and 'path' is not 'none' ('-' for Solaris). * Update module documentation accordingly. + Replace all platform.system() calls by a variable. refactor integration tests * Improve readability/understanding of what is tested, and what OS is targeted. * Move 'swap' related test cases into dedicated file (swap.yml). * Add new test cases about swap enabling/disabling. * Extend tests to FreeBSD when possible. --- plugins/modules/mount.py | 296 +++++++++++++--- .../targets/mount/tasks/bind_mount.yml | 95 ++++++ .../targets/mount/tasks/fstab_last_fields.yml | 32 ++ .../targets/mount/tasks/fstab_multi_swap.yml | 93 +++++ .../integration/targets/mount/tasks/main.yml | 321 ++---------------- .../targets/mount/tasks/remount.yml | 65 ++++ .../targets/mount/tasks/swapfile_freebsd.yml | 139 ++++++++ .../targets/mount/tasks/swapfile_linux.yml | 154 +++++++++ 8 files changed, 860 insertions(+), 335 deletions(-) create mode 100644 tests/integration/targets/mount/tasks/bind_mount.yml create mode 100644 tests/integration/targets/mount/tasks/fstab_last_fields.yml create mode 100644 tests/integration/targets/mount/tasks/fstab_multi_swap.yml create mode 100644 tests/integration/targets/mount/tasks/remount.yml create mode 100644 tests/integration/targets/mount/tasks/swapfile_freebsd.yml create mode 100644 tests/integration/targets/mount/tasks/swapfile_linux.yml diff --git a/plugins/modules/mount.py b/plugins/modules/mount.py index e7ce7eefec..98ea58f1d1 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,18 @@ 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) (or C(-) + on Solaris) 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 +35,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 +70,7 @@ 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(fstype=swap)). - 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 @@ -93,7 +102,7 @@ boot: description: - Determines if the filesystem should be mounted on boot. - - Only applies to Solaris systems. + - Only applies to Solaris systems. Defaults to C(true) unless I(fstype=swap). type: bool default: yes backup: @@ -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,6 +183,29 @@ opts: rw,sync,hard,intr state: mounted fstype: nfs + +- name: Enable swap device with priority=1 (Linux) + ansible.posix.mount: + src: /dev/mapper/vg0-swap + fstype: swap + path: none + opts: pri=1 + state: mounted + +- name: Enable a swapfile (Linux, Solaris) + ansible.posix.mount: + src: /var/swapfile + fstype: swap + path: none + state: mounted + +- name: Enable a swapfile (FreeBSD) + ansible.posix.mount: + src: md99 + fstype: swap + path: none + opts: file=/var/swapfile + state: mounted ''' @@ -182,6 +219,9 @@ from ansible.module_utils._text import to_bytes, to_native +SYSTEM = platform.system().lower() + + def write_fstab(module, lines, path): if module.params['backup']: module.backup_local(path) @@ -229,7 +269,7 @@ def _set_mount_save_old(module, args): escaped_args = dict([(k, _escape_fstab(v)) for k, v in iteritems(args)]) new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n' - if platform.system() == 'SunOS': + if SYSTEM == 'sunos': new_line = ( '%(src)s - %(name)s %(fstype)s %(passno)s %(boot)s %(opts)s\n') @@ -251,16 +291,16 @@ def _set_mount_save_old(module, args): # Check if we got a valid line for splitting # (on Linux the 5th and the 6th field is optional) if ( - platform.system() == 'SunOS' and len(fields) != 7 or - platform.system() == 'Linux' and len(fields) not in [4, 5, 6] or - platform.system() not in ['SunOS', 'Linux'] and len(fields) != 6): + SYSTEM == 'sunos' and len(fields) != 7 or + SYSTEM == 'linux' and len(fields) not in [4, 5, 6] or + SYSTEM not in ['sunos', 'linux'] and len(fields) != 6): to_write.append(line) continue ld = {} - if platform.system() == 'SunOS': + if SYSTEM == 'sunos': ( ld['src'], dash, @@ -285,8 +325,6 @@ def _set_mount_save_old(module, args): if ( ld['name'] != escaped_args['name'] or ( # In the case of swap, check the src instead - 'src' in args and - ld['name'] == 'none' and ld['fstype'] == 'swap' and ld['src'] != args['src'])): to_write.append(line) @@ -298,7 +336,7 @@ def _set_mount_save_old(module, args): exists = True args_to_check = ('src', 'fstype', 'opts', 'dump', 'passno') - if platform.system() == 'SunOS': + if SYSTEM == 'sunos': args_to_check = ('src', 'fstype', 'passno', 'boot', 'opts') for t in args_to_check: @@ -341,15 +379,15 @@ def unset_mount(module, args): # Check if we got a valid line for splitting if ( - platform.system() == 'SunOS' and len(line.split()) != 7 or - platform.system() != 'SunOS' and len(line.split()) != 6): + SYSTEM == 'sunos' and len(line.split()) != 7 or + SYSTEM != 'sunos' and len(line.split()) != 6): to_write.append(line) continue ld = {} - if platform.system() == 'SunOS': + if SYSTEM == 'sunos': ( ld['src'], dash, @@ -372,8 +410,6 @@ def unset_mount(module, args): if ( ld['name'] != escaped_name or ( # In the case of swap, check the src instead - 'src' in args and - ld['name'] == 'none' and ld['fstype'] == 'swap' and ld['src'] != args['src'])): to_write.append(line) @@ -395,8 +431,8 @@ def _set_fstab_args(fstab_file): if ( fstab_file and fstab_file != '/etc/fstab' and - platform.system().lower() != 'sunos'): - if platform.system().lower().endswith('bsd'): + SYSTEM != 'sunos'): + if SYSTEM.endswith('bsd'): result.append('-F') else: result.append('-T') @@ -413,7 +449,7 @@ def mount(module, args): name = args['name'] cmd = [mount_bin] - if platform.system().lower() == 'openbsd': + if SYSTEM == 'openbsd': # Use module.params['fstab'] here as args['fstab'] has been set to the # default value. if module.params['fstab'] is not None: @@ -454,7 +490,7 @@ def remount(module, args): cmd = [mount_bin] # Multiplatform remount opts - if platform.system().lower().endswith('bsd'): + if SYSTEM.endswith('bsd'): if module.params['state'] == 'remounted' and args['opts'] != 'defaults': cmd += ['-u', '-o', args['opts']] else: @@ -465,7 +501,7 @@ def remount(module, args): else: cmd += ['-o', 'remount'] - if platform.system().lower() == 'openbsd': + if SYSTEM == 'openbsd': # Use module.params['fstab'] here as args['fstab'] has been set to the # default value. if module.params['fstab'] is not None: @@ -480,7 +516,7 @@ def remount(module, args): out = err = '' try: - if platform.system().lower().endswith('bsd'): + if SYSTEM.endswith('bsd'): # Note: Forcing BSDs to do umount/mount due to BSD remount not # working as expected (suspect bug in the BSD mount command) # Interested contributor could rework this to use mount options on @@ -534,7 +570,7 @@ def is_bind_mounted(module, linux_mounts, dest, src=None, fstype=None): is_mounted = False - if platform.system() == 'Linux' and linux_mounts is not None: + if SYSTEM == 'linux' and linux_mounts is not None: if src is None: # That's for unmounted/absent if dest in linux_mounts: @@ -549,7 +585,7 @@ def is_bind_mounted(module, linux_mounts, dest, src=None, fstype=None): rc, out, err = 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 +675,123 @@ 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': + return False + + if SYSTEM == 'sunos': + swap_bin = module.get_bin_path('swap', required=True) + cmd = [swap_bin, '-l'] + elif SYSTEM.endswith('bsd'): + swapctl_bin = module.get_bin_path('swapctl', required=True) + cmd = [swapctl_bin, '-l'] + else: + # swapon is supposed to be the standard command + swapon_bin = module.get_bin_path('swapon', required=True) + cmd = [swapon_bin] + + rc, out, err = module.run_command(cmd) + + if rc: + module.fail_json(msg="Error while querying active swaps: %s" % err) + + # Get the first field of each line but the header + devices = [x.split()[0] for x in out.splitlines()[1:]] + dev = os.path.realpath(args['src']) + + if SYSTEM == 'linux': + if args['src'].startswith('UUID='): + uuid_path = os.path.join('/dev/disk/by-uuid', args['src'].split('=')[1]) + dev = os.path.realpath(uuid_path) + elif args['src'].startswith('LABEL='): + label_path = os.path.join('/dev/disk/by-label', args['src'].split('=')[1]) + dev = os.path.realpath(label_path) + elif SYSTEM == 'freebsd': + if args['src'].startswith('md'): + dev = os.path.join('/dev', args['src']) + + return bool(dev in devices) + + +def swapon(module, args): + """Activate a swap device/file with the proper options.""" + + if SYSTEM == 'sunos': + swap_bin = module.get_bin_path('swap', required=True) + cmd = [swap_bin, '-a'] + elif SYSTEM.endswith('bsd'): + swapctl_bin = module.get_bin_path('swapctl', required=True) + cmd = [swapctl_bin, '-a'] + else: + # swapon is supposed to be the standard command, isn't it ? + 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' flags available on Linux. + if SYSTEM == 'linux': + for opt in args['opts'].split(','): + if opt.startswith('pri='): + cmd += ['-p', opt.split('=')[1]] + elif opt.startswith('discard'): + cmd += ['--%s' % opt] + + if SYSTEM == 'freebsd' and args['src'].startswith('md'): + cmd += [os.path.join('/dev', args['src'])] + else: + 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.""" + + if SYSTEM == 'sunos': + swap_bin = module.get_bin_path('swap', required=True) + cmd = [swap_bin, '-d'] + elif SYSTEM.endswith('bsd'): + swapctl_bin = module.get_bin_path('swapctl', required=True) + cmd = [swapctl_bin, '-d'] + else: + # swapoff is supposed to be the standard command, isn't it ? + swapoff_bin = module.get_bin_path('swapoff', required=True) + cmd = [swapoff_bin] + + if SYSTEM == 'freebsd' and args['src'].startswith('md'): + cmd += [os.path.join('/dev', args['src'])] + else: + cmd += [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,22 +804,43 @@ def main(): passno=dict(type='str', no_log=False), 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']], ), ) + fstype = module.params['fstype'] + + # swapon/swapoff (and the likes) don't honor alternative fstab locations + # the same way the mount command does, that could make things very, very + # complicated... + if fstype == 'swap': + if SYSTEM == 'sunos': + swap_fstab_file = '/etc/vfstab' + swap_mountpoint = '-' + else: + swap_fstab_file = '/etc/fstab' + swap_mountpoint = 'none' + + if module.params['fstab'] not in (None, swap_fstab_file): + module.fail_json(msg="option 'fstype=swap' does not support alternative fstab locations") + if module.params['path'] != swap_mountpoint: + module.fail_json(msg="swap filesystems can't be mounted, please set path to '%s'" % + swap_mountpoint) + # solaris args: # name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab # linux args: # name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab # Note: Do not modify module.params['fstab'] as we need to know if the user # explicitly specified it in mount() and remount() - if platform.system().lower() == 'sunos': + if SYSTEM == 'sunos': args = dict( name=module.params['path'], opts='-', @@ -676,6 +850,10 @@ def main(): ) if args['fstab'] is None: args['fstab'] = '/etc/vfstab' + + # swap spaces are used internally by kernels and have no mountpoint + if fstype == 'swap': + args['boot'] = 'no' else: args = dict( name=module.params['path'], @@ -687,15 +865,18 @@ def main(): if args['fstab'] is None: args['fstab'] = '/etc/fstab' + # Override default value of options field for swap filesystems + if fstype == 'swap': + args['opts'] = 'sw' # FreeBSD doesn't have any 'default' so set 'rw' instead - if platform.system() == 'FreeBSD': + elif SYSTEM == 'freebsd': args['opts'] = 'rw' linux_mounts = [] # Cache all mounts here in order we have consistent results if we need to # call is_bind_mounted() multiple times - if platform.system() == 'Linux': + if SYSTEM == 'linux': linux_mounts = get_linux_mounts(module) if linux_mounts is None: @@ -731,7 +912,7 @@ def main(): # changed in fstab then remount it. state = module.params['state'] - name = module.params['path'] + name = args['name'] changed = False if state == 'absent': @@ -744,6 +925,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 +946,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 fstype != 'swap' 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 +994,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 fstype == 'swap': + 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 +1023,26 @@ def main(): except Exception: pass - module.fail_json(msg="Error mounting %s: %s" % (name, msg)) + if fstype == 'swap': + 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 fstype == 'swap': + res, msg = reswap(module, args) - if res: - module.fail_json(msg="Error remounting %s: %s" % (name, msg)) + 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)) changed = True else: diff --git a/tests/integration/targets/mount/tasks/bind_mount.yml b/tests/integration/targets/mount/tasks/bind_mount.yml new file mode 100644 index 0000000000..de641edb91 --- /dev/null +++ b/tests/integration/targets/mount/tasks/bind_mount.yml @@ -0,0 +1,95 @@ +--- +# Tasks to validate bind mounts (i.e. mount of a directory to another one). +# Linux and FreeBSD only. + +- name: Create the mount point (the target of the mount) + file: + state: directory + path: '{{ output_dir }}/mount_dest' + +- name: Create a directory to bind mount (the source of the mount) + file: + state: directory + path: '{{ output_dir }}/mount_source' + +- name: Put something in the directory so we see that it worked + copy: + content: 'Testing + + ' + dest: '{{ output_dir }}/mount_source/test_file' + register: orig_info + +- name: Bind mount a directory + mount: + src: '{{ output_dir }}/mount_source' + name: '{{ output_dir }}/mount_dest' + state: mounted + fstype: "{{ 'none' if ansible_system == 'Linux' else 'nullfs' }}" + opts: "{{ 'bind' if ansible_system == 'Linux' else omit }}" + register: bind_result + +- 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: + - bind_result is changed + - dest_stat.stat.exists + - orig_info.checksum == dest_stat.stat.checksum + +- name: Bind mount a directory again + mount: + src: '{{ output_dir }}/mount_source' + name: '{{ output_dir }}/mount_dest' + state: mounted + fstype: "{{ 'none' if ansible_system == 'Linux' else 'nullfs' }}" + opts: "{{ 'bind' if ansible_system == 'Linux' else omit }}" + register: bind_result + +- name: Make sure we didn't mount a second time + assert: + that: + - bind_result is not changed + +- name: Remount directory with different options + mount: + src: '{{ output_dir }}/mount_source' + name: '{{ output_dir }}/mount_dest' + state: mounted + fstype: "{{ 'none' if ansible_system == 'Linux' else 'nullfs' }}" + opts: "{{ 'bind,' if ansible_system == 'Linux' else '' }}ro" + register: bind_result + +- name: Get mount options + shell: + cmd: "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: + - bind_result is changed + - '''1'' in remount_options.stdout' + - 1 == remount_options.stdout_lines | length + +- name: Unmount the bind mount + mount: + name: '{{ output_dir }}/mount_dest' + state: absent + register: unmount_result + +- name: Check if the file still exists in dest + stat: + path: '{{ output_dir }}/mount_dest/test_file' + register: dest_stat + +- name: Assert that we unmounted + assert: + that: + - unmount_result is changed + - not dest_stat.stat.exists diff --git a/tests/integration/targets/mount/tasks/fstab_last_fields.yml b/tests/integration/targets/mount/tasks/fstab_last_fields.yml new file mode 100644 index 0000000000..3c9d673630 --- /dev/null +++ b/tests/integration/targets/mount/tasks/fstab_last_fields.yml @@ -0,0 +1,32 @@ +--- +# Tasks to validate the two last fields in fstab, that are optional on Linux, +# are added if missing. + +- name: Create fstab record with missing last two fields + copy: + dest: /etc/fstab + content: '//nas/photo /home/jik/pictures cifs defaults,credentials=/etc/security/nas.creds,uid=jik,gid=users,forceuid,forcegid,noserverino,_netdev + + ' + +- name: Try to change the fstab record with the missing last two fields + mount: + src: //nas/photo + path: /home/jik/pictures + fstype: cifs + opts: defaults,credentials=/etc/security/nas.creds,uid=jik,gid=users,forceuid,forcegid,noserverino,_netdev,x-systemd.mount-timeout=0 + state: present + register: optional_fields_update + +- name: Get the content of the fstab file + command: + cmd: cat /etc/fstab + changed_when: false + register: optional_fields_content + +- name: Check if the line containing the missing last two fields was changed + assert: + that: + - optional_fields_update is changed + - ''' 0 0'' in optional_fields_content.stdout' + - 1 == optional_fields_content.stdout_lines | length diff --git a/tests/integration/targets/mount/tasks/fstab_multi_swap.yml b/tests/integration/targets/mount/tasks/fstab_multi_swap.yml new file mode 100644 index 0000000000..c6a3668c5e --- /dev/null +++ b/tests/integration/targets/mount/tasks/fstab_multi_swap.yml @@ -0,0 +1,93 @@ +--- +# Tasks to validate that adding a swap record in fstab doesn't replace another +# one, unless the 'src' (not the 'path') is the same. For Linux and FreeBSD. + +- name: Create fstab record for the first swap device + mount: + name: none + src: /dev/swap1 + opts: sw + fstype: swap + state: present + register: swap1_created + +- name: Try to create fstab record for the first swap device again + mount: + name: none + src: /dev/swap1 + opts: sw + fstype: swap + state: present + register: swap1_created_again + +- name: Check that we created the swap1 record + assert: + that: + - swap1_created is changed + - swap1_created_again is not changed + +- name: Create fstab record for the second swap device + mount: + name: none + src: /dev/swap2 + opts: sw + fstype: swap + state: present + register: swap2_created + +- name: Try to create fstab record for the second swap device again + mount: + name: none + src: /dev/swap1 + opts: sw + fstype: swap + state: present + register: swap2_created_again + +- name: Check that we created the swap2 record + assert: + that: + - swap2_created is changed + - swap2_created_again is not changed + + + +- name: Remove the fstab record for the first swap device + mount: + name: none + src: /dev/swap1 + state: absent + register: swap1_removed + +- name: Try to remove the fstab record for the first swap device again + mount: + name: none + src: /dev/swap1 + state: absent + register: swap1_removed_again + +- name: Check that we removed the swap1 record + assert: + that: + - swap1_removed is changed + - swap1_removed_again is not changed + +- name: Remove the fstab record for the second swap device + mount: + name: none + src: /dev/swap2 + state: absent + register: swap2_removed + +- name: Try to remove the fstab record for the second swap device again + mount: + name: none + src: /dev/swap2 + state: absent + register: swap2_removed_again + +- name: Check that we removed the swap2 record + assert: + that: + - swap2_removed is changed + - swap2_removed_again is not changed diff --git a/tests/integration/targets/mount/tasks/main.yml b/tests/integration/targets/mount/tasks/main.yml index 7eacc59f10..373e170dbc 100644 --- a/tests/integration/targets/mount/tasks/main.yml +++ b/tests/integration/targets/mount/tasks/main.yml @@ -1,296 +1,29 @@ -- 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 +--- +- name: Include tasks to validate bind mount management (Linux, FreeBSD) + include_tasks: bind_mount.yml + when: + - ansible_system in ['Linux', 'FreeBSD'] - ' - dest: '{{ output_dir }}/mount_source/test_file' - register: orig_info -- name: Bind mount a filesystem (Linux) - mount: - src: '{{ output_dir }}/mount_source' - name: '{{ output_dir }}/mount_dest' - state: mounted - fstype: None - opts: bind - when: ansible_system == 'Linux' - register: bind_result_linux -- name: Bind mount a filesystem (FreeBSD) - mount: - src: '{{ output_dir }}/mount_source' - name: '{{ output_dir }}/mount_dest' - state: mounted - 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']) - - 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' - name: '{{ output_dir }}/mount_dest' - state: mounted - fstype: None - opts: bind - when: ansible_system == 'Linux' - register: bind_result_linux -- name: Bind mount a filesystem (FreeBSD) - mount: - src: '{{ output_dir }}/mount_source' - name: '{{ output_dir }}/mount_dest' - state: mounted - 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']) - when: ansible_system in ('FreeBSD', 'Linux') -- name: Remount filesystem with different opts (Linux) - mount: - src: '{{ output_dir }}/mount_source' - name: '{{ output_dir }}/mount_dest' - state: mounted - fstype: None - opts: bind,ro - when: ansible_system == 'Linux' - register: bind_result_linux -- name: Remount filesystem with different opts (FreeBSD) - mount: - src: '{{ output_dir }}/mount_source' - name: '{{ output_dir }}/mount_dest' - state: mounted - fstype: nullfs - 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']) - - '''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 - content: '//nas/photo /home/jik/pictures cifs defaults,credentials=/etc/security/nas.creds,uid=jik,gid=users,forceuid,forcegid,noserverino,_netdev - ' - when: ansible_system in ('Linux') -- name: Try to change the fstab record with the missing last two fields - mount: - src: //nas/photo - path: /home/jik/pictures - fstype: cifs - opts: defaults,credentials=/etc/security/nas.creds,uid=jik,gid=users,forceuid,forcegid,noserverino,_netdev,x-systemd.mount-timeout=0 - 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: - - optional_fields_update['changed'] - - ''' 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 - src: /tmp/myfs.img - 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') - - 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: - path: /tmp/myfs - src: /tmp/myfs.img - opts: loop - state: absent - when: ansible_system in ('Linux') - - name: Remove the test FS - file: - path: '{{ item }}' - state: absent - loop: - - /tmp/myfs.img - - /tmp/myfs - when: ansible_system in ('Linux') +- name: Include tasks to validate optional fields management in fstab (Linux) + include_tasks: fstab_last_fields.yml + when: + - ansible_system in ['Linux'] + + +- name: Include tasks to validate state=remounted behaviour (Linux) + include_tasks: remount.yml + when: + - ansible_system in ['Linux'] + + +- name: Include tasks to validate multi swap management in fstab (Linux) + include_tasks: fstab_multi_swap.yml + when: + - ansible_system in ['Linux', 'FreeBSD'] + + +- name: Include tasks to validate enabling/disabling a swapfile (Linux, FreeBSD) + include_tasks: "swapfile_{{ ansible_system | lower }}.yml" + when: + - ansible_system in ['Linux', 'FreeBSD'] diff --git a/tests/integration/targets/mount/tasks/remount.yml b/tests/integration/targets/mount/tasks/remount.yml new file mode 100644 index 0000000000..2567797202 --- /dev/null +++ b/tests/integration/targets/mount/tasks/remount.yml @@ -0,0 +1,65 @@ +--- +# Tasks to validate state=remounted behaves as expected. +# Linux only. + +- name: Create empty file + command: + cmd: "dd if=/dev/zero of=/tmp/myfs.img bs=1048576 count=20" + +- name: Format FS + community.general.filesystem: + fstype: ext3 + dev: /tmp/myfs.img + +- name: Mount the FS for the first time + mount: + path: /tmp/myfs + src: /tmp/myfs.img + fstype: ext3 + state: mounted + +- name: Get the mount counter value + shell: + cmd: "dumpe2fs /tmp/myfs.img 2>/dev/null | grep -i '^mount count:'" + changed_when: false + register: mount_count1 + +- name: Get the current mount as exposed in /proc/mounts + command: + cmd: "grep /tmp/myfs /proc/mounts" + changed_when: false + register: proc_mounts1 + +- name: Remount the filesystem + mount: + path: /tmp/myfs + opts: nosuid + state: remounted + +- name: Get again the mount counter value + shell: + cmd: "dumpe2fs /tmp/myfs.img 2>/dev/null | grep -i '^mount count:'" + changed_when: false + register: mount_count2 + +- name: Get again the current mount as exposed in /proc/mounts + command: + cmd: "grep /tmp/myfs /proc/mounts" + changed_when: false + register: proc_mounts2 + +- name: Assert that mount has changed despite its counter has not + assert: + that: + - mount_count1.stdout == mount_count2.stdout + - proc_mounts1.stdout != proc_mounts2.stdout + +- name: Unmount the FS + mount: + path: /tmp/myfs + state: absent + +- name: Remove disk image + file: + path: /tmp/myfs.img + state: absent diff --git a/tests/integration/targets/mount/tasks/swapfile_freebsd.yml b/tests/integration/targets/mount/tasks/swapfile_freebsd.yml new file mode 100644 index 0000000000..e2c7a8dd3a --- /dev/null +++ b/tests/integration/targets/mount/tasks/swapfile_freebsd.yml @@ -0,0 +1,139 @@ +--- +# Tasks to validate swapfile management (enabling/disabling its use) on FreeBSD. +# The swapfile MUST be associated to a memory disk, that becomes the 'src'. + +- name: Swap off all (try to cleanup previous erraneous tests) + command: + cmd: swapoff -a + + +- name: Try to activate swapfile + vars: + swapfile: /var/swapfile0 + md_unit: 99 + md_name: "md{{ md_unit }}" + md_path: "/dev/{{ md_name }}" + + block: + - name: Create a file to be used for memory swapping + command: + cmd: "dd if=/dev/zero of={{ swapfile }} bs=1M count=64" + creates: "{{ swapfile }}" + + - name: Setup permissions suitable for swap usage + file: + path: "{{ swapfile }}" + mode: u=rw,go= + state: file + + - name: Attach a memory disk to the swap file + command: + cmd: "mdconfig -a -t vnode -u {{ md_unit }} -f {{ swapfile }}" + creates: "{{ md_path }}" + + + - name: Get swap info (0) + command: swapctl -l + register: swap_info_0 + changed_when: false + + - name: Assert that memory disk {{ md_path }} is not used by swap + assert: + that: + - swap_info_0.stdout is not search(md_path) + + - name: Enable swap file + mount: + path: none + src: "{{ md_name }}" + fstype: swap + opts: "sw,file={{ swapfile }}" + state: mounted + register: swap_enabled_1 + + - name: Get swap info (1) + command: swapctl -l + register: swap_info_1 + changed_when: false + + - name: Assert that swap file is enabled + assert: + that: + - swap_enabled_1 is changed + - swap_info_1.stdout is search(md_path) + + + - name: Enable swap file, again + mount: + path: none + src: "{{ md_name }}" + fstype: swap + opts: "sw,file={{ swapfile }}" + state: mounted + register: swap_enabled_2 + + - name: Get swap info (2) + command: swapctl -l + register: swap_info_2 + changed_when: false + + - name: Assert that nothing changed + assert: + that: + - swap_enabled_2 is not changed + - swap_info_2.stdout == swap_info_1.stdout + + always: + - name: Disable swap file + mount: + path: none + src: "{{ md_name }}" + fstype: swap + state: absent + register: swap_disabled_1 + + - name: Get swap info (3) + command: swapctl -l + register: swap_info_3 + changed_when: false + + - name: Assert that swap file is disabled + assert: + that: + - swap_disabled_1 is changed + - swap_info_3.stdout == swap_info_0.stdout + + - name: Disable swap file, again + mount: + path: none + src: "{{ md_name }}" + fstype: swap + state: absent + register: swap_disabled_2 + + - name: Get swap info (4) + command: swapctl -l + register: swap_info_4 + changed_when: false + + - name: Assert that swap file is disabled and nothing changed + assert: + that: + - swap_disabled_2 is not changed + - swap_info_4.stdout == swap_info_3.stdout + + + - name: Detach memory disk + command: + cmd: "mdconfig -d -u {{ md_unit }}" + removes: "{{ md_path }}" + + - name: Remove swap file + file: + path: "{{ swapfile }}" + state: absent + + +- name: Swap on all (try to restore pre-test state) + command: + cmd: swapon -a diff --git a/tests/integration/targets/mount/tasks/swapfile_linux.yml b/tests/integration/targets/mount/tasks/swapfile_linux.yml new file mode 100644 index 0000000000..99b599d2c2 --- /dev/null +++ b/tests/integration/targets/mount/tasks/swapfile_linux.yml @@ -0,0 +1,154 @@ +--- +# Tasks to validate swapfile management (enabling/disabling its use) on Linux. +# The swapfile COULD be associated to a loop device, that would become the 'src'. + +- name: Swap off all (try to cleanup previous erraneous tests) + command: + cmd: swapoff -a + + +- name: Try to activate swapfile + vars: + swapfile: /var/swapfile0 + + block: + - name: Create a file to be used for memory swapping + command: + cmd: "dd if=/dev/zero of={{ swapfile }} bs=1M count=64" + creates: "{{ swapfile }}" + + - name: Setup permissions suitable for swap usage + file: + path: "{{ swapfile }}" + mode: u=rw,go= + state: file + + - name: Create a swap filesystem into the dedicated file + community.general.filesystem: + dev: "{{ swapfile }}" + fstype: swap + + + - name: Get swap info (0) + command: swapon --noheadings --show=name,prio + register: swap_info_0 + changed_when: false + + - name: Assert that swap file {{ swapfile }} is not used by swap + assert: + that: + - swap_info_0.stdout | length == 0 + + + - name: Enable swap file with priority 1234 + mount: + path: none + src: "{{ swapfile }}" + fstype: swap + opts: pri=1234 + state: mounted + register: swap_enabled_1 + + - name: Get swap info (1) + command: swapon --noheadings --show=name,prio + register: swap_info_1 + changed_when: false + + - name: Assert that swap file is enabled with priority 1234 + assert: + that: + - swap_enabled_1 is changed + - swap_info_1.stdout_lines[0].split()[1] | int == 1234 + + + - name: Update swap priority to 10 + mount: + path: none + src: "{{ swapfile }}" + fstype: swap + opts: pri=10 + state: mounted + register: swap_enabled_2 + + - name: Get swap info (2) + command: swapon --noheadings --show=name,prio + register: swap_info_2 + changed_when: false + + - name: Assert that swap file is enabled with priority 10 + assert: + that: + - swap_enabled_2 is changed + - swap_info_2.stdout_lines[0].split()[1] | int == 10 + + + - name: Update swap priority to 10, again + mount: + path: none + src: "{{ swapfile }}" + fstype: swap + opts: pri=10 + state: mounted + register: swap_enabled_3 + + - name: Get swap info (3) + command: swapon --noheadings --show=name,prio + register: swap_info_3 + changed_when: false + + - name: Assert that nothing changed + assert: + that: + - swap_enabled_3 is not changed + - swap_info_3.stdout_lines == swap_info_2.stdout_lines + + always: + - name: Disable swap file + mount: + path: none + src: "{{ swapfile }}" + fstype: swap + state: absent + register: swap_disabled_1 + + - name: Get swap info (4) + command: swapon --noheadings --show=name,prio + register: swap_info_4 + changed_when: false + + - name: Assert that swap file is disabled + assert: + that: + - swap_disabled_1 is changed + - swap_info_4.stdout_lines == swap_info_0.stdout_lines + + + - name: Disable swap file, again + mount: + path: none + src: "{{ swapfile }}" + fstype: swap + state: absent + register: swap_disabled_2 + + - name: Get swap info (5) + command: swapon --noheadings --show=name,prio + register: swap_info_5 + changed_when: false + + - name: Assert that swap file is disabled and nothing changed + assert: + that: + - swap_disabled_2 is not changed + - swap_info_5.stdout_lines == swap_info_0.stdout_lines + + + - name: Remove swap file + file: + path: "{{ swapfile }}" + state: absent + + +- name: Swap on all (try to restore pre-test state) + command: + cmd: swapon -a From e627afa8c0af9d0a0499d85793dc624c42c2bf44 Mon Sep 17 00:00:00 2001 From: quidame Date: Sun, 28 Mar 2021 01:37:20 +0100 Subject: [PATCH 2/2] add a changelog fragment (minor_changes) --- changelogs/fragments/135-mount-106-add-swap-management.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelogs/fragments/135-mount-106-add-swap-management.yml diff --git a/changelogs/fragments/135-mount-106-add-swap-management.yml b/changelogs/fragments/135-mount-106-add-swap-management.yml new file mode 100644 index 0000000000..0c76f7994b --- /dev/null +++ b/changelogs/fragments/135-mount-106-add-swap-management.yml @@ -0,0 +1,7 @@ +--- +minor_changes: + - mount - allow one to enable/disable swap spaces by mapping module's states + (``mounted``, ``unmounted`` ...) to ``swapon`` and ``swapoff`` commands + (https://github.com/ansible-collections/ansible.posix/issues/106). + - mount - refactor integration tests for better readability/understanding of + what is tested and what OS is targeted.