Repository: ansible-collections/community.general

Issue:
module filesystem should have a state argument to make a fs absent
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
(initially filed in https://github.com/ansible/ansible/issues/65364)

I wanted to remove a type of FS absent based on the retrieved fact. I naively did something like
`filesystem: type=lvm state=absent dev={{ item.key }}`
looping over  `hostvars[inventory_hostname]['ansible_lvm']['pvs']`

but actually it does not support state argument
Ansible version: 2.9

##### ISSUE TYPE
support a keywork `state` with `present` and `absent` as a lot of other modules.
providing the argument `type` with `absent` could only delete if the type of the device match (I don't know if this is the spirit of other modules, or that rather be handled with a `when`) 

##### COMPONENT NAME
module filesystem

##### ADDITIONAL INFORMATION
The idea would to delete a filesystem.

(

module filesystem should have a state argument to make a fs absent
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
(initially filed in https://github.com/ansible/ansible/issues/65364)

I wanted to remove a type of FS absent based on the retrieved fact. I naively did something like
`filesystem: type=lvm state=absent dev={{ item.key }}`
looping over  `hostvars[inventory_hostname]['ansible_lvm']['pvs']`

but actually it does not support state argument
Ansible version: 2.9

##### ISSUE TYPE
support a keywork `state` with `present` and `absent` as a lot of other modules.
providing the argument `type` with `absent` could only delete if the type of the device match (I don't know if this is the spirit of other modules, or that rather be handled with a `when`) 

##### COMPONENT NAME
module filesystem

##### ADDITIONAL INFORMATION
The idea would to delete a filesystem.

(



Discussion:
Files identified in the description:
* [`plugins/modules/system/filesystem.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/system/filesystem.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
@bmillemathias: Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information.

Here are the items we could not find in your description:
- ansible version

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE/bug_report.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->
cc @MorrisA @abulimov @bcoca @d-little @flynn1973 @gforster @kairoaraujo @marvin-sinister @mator @molekuul @pilou- @ramooncamacho @wtcross
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
no one written "delete" functionality for filesystem module yet... does force create a new fs over existing one counts as "delete" ?!

on a technical side of the question, "delete" is probably `wipefs` for an underlaying device (and probably another ansible module)
Files identified in the description:
* [`plugins/modules/system/filesystem.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/system/filesystem.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
@bmillemathias: Greetings! Thanks for taking the time to open this issue. In order for the community to handle your issue effectively, we need a bit more information.

Here are the items we could not find in your description:
- ansible version

Please set the description of this issue with this template:
https://raw.githubusercontent.com/ansible/ansible/devel/.github/ISSUE_TEMPLATE/bug_report.md

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_missing_data --->
cc @MorrisA @abulimov @bcoca @d-little @flynn1973 @gforster @kairoaraujo @marvin-sinister @mator @molekuul @pilou- @ramooncamacho @wtcross
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
no one written "delete" functionality for filesystem module yet... does force create a new fs over existing one counts as "delete" ?!

on a technical side of the question, "delete" is probably `wipefs` for an underlaying device (and probably another ansible module)

Resolving patch:
diff --git a/plugins/modules/system/filesystem.py b/plugins/modules/system/filesystem.py
--- a/plugins/modules/system/filesystem.py
+++ b/plugins/modules/system/filesystem.py
@@ -16,10 +16,24 @@
 description:
   - This module creates a filesystem.
 options:
+  state:
+    description:
+    - If C(state=present), the filesystem is created if it doesn't already
+      exist, that is the default behaviour if I(state) is omitted.
+    - If C(state=absent), filesystem signatures on I(dev) are wiped if it
+      contains a filesystem (as known by C(blkid)).
+    - When C(state=absent), all other options but I(dev) are ignored, and the
+      module doesn't fail if the device I(dev) doesn't actually exist.
+    - C(state=absent) is not supported and will fail on FreeBSD systems.
+    type: str
+    choices: [ present, absent ]
+    default: present
+    version_added: 1.3.0
   fstype:
     choices: [ btrfs, ext2, ext3, ext4, ext4dev, f2fs, lvm, ocfs2, reiserfs, xfs, vfat, swap ]
     description:
-    - Filesystem type to be created.
+    - Filesystem type to be created. This option is required with
+      C(state=present) (or if I(state) is omitted).
     - reiserfs support was added in 2.2.
     - lvm support was added in 2.5.
     - since 2.5, I(dev) can be an image file.
@@ -27,11 +41,12 @@
     - ocfs2 support was added in 2.6
     - f2fs support was added in 2.7
     - swap support was added in 2.8
-    required: yes
+    type: str
     aliases: [type]
   dev:
     description:
     - Target path to device or image file.
+    type: path
     required: yes
     aliases: [device]
   force:
@@ -43,18 +58,22 @@
     description:
     - If C(yes), if the block device and filesystem size differ, grow the filesystem into the space.
     - Supported for C(ext2), C(ext3), C(ext4), C(ext4dev), C(f2fs), C(lvm), C(xfs), C(vfat), C(swap) filesystems.
-    - XFS Will only grow if mounted.
+    - XFS Will only grow if mounted. Currently, the module is based on commands
+      from C(util-linux) package to perform operations, so resizing of XFS is
+      not supported on FreeBSD systems.
     - vFAT will likely fail if fatresize < 1.04.
     type: bool
     default: 'no'
   opts:
     description:
     - List of options to be passed to mkfs command.
+    type: str
 requirements:
   - Uses tools related to the I(fstype) (C(mkfs)) and C(blkid) command. When I(resizefs) is enabled, C(blockdev) command is required too.
 notes:
   - Potential filesystem on I(dev) are checked using C(blkid), in case C(blkid) isn't able to detect an existing filesystem,
     this filesystem is overwritten even if I(force) is C(no).
+  - This module supports I(check_mode).
 '''
 
 EXAMPLES = '''
@@ -68,6 +87,11 @@
     fstype: ext4
     dev: /dev/sdb1
     opts: -cc
+
+- name: Blank filesystem signature on /dev/sdb1
+  community.general.filesystem:
+    dev: /dev/sdb1
+    state: absent
 '''
 
 from distutils.version import LooseVersion
@@ -144,6 +168,22 @@ def create(self, opts, dev):
             cmd = "%s %s %s '%s'" % (mkfs, self.MKFS_FORCE_FLAGS, opts, dev)
         self.module.run_command(cmd, check_rc=True)
 
+    def wipefs(self, dev):
+        if platform.system() == 'FreeBSD':
+            msg = "module param state=absent is currently not supported on this OS (FreeBSD)."
+            self.module.fail_json(msg=msg)
+
+        if self.module.check_mode:
+            return
+
+        # wipefs comes with util-linux package (as 'blockdev' & 'findmnt' above)
+        # so it is not supported on FreeBSD. Even the use of dd as a fallback is
+        # not doable here if it needs get_mountpoint() (to prevent corruption of
+        # a mounted filesystem), since 'findmnt' is not available on FreeBSD.
+        wipefs = self.module.get_bin_path('wipefs', required=True)
+        cmd = [wipefs, "--all", dev.__str__()]
+        self.module.run_command(cmd, check_rc=True)
+
     def grow_cmd(self, dev):
         cmd = self.module.get_bin_path(self.GROW, required=True)
         return [cmd, str(dev)]
@@ -369,34 +409,35 @@ def main():
     # There is no "single command" to manipulate filesystems, so we map them all out and their options
     module = AnsibleModule(
         argument_spec=dict(
-            fstype=dict(required=True, aliases=['type'],
-                        choices=list(fstypes)),
-            dev=dict(required=True, aliases=['device']),
-            opts=dict(),
+            state=dict(type='str', default='present', choices=['present', 'absent']),
+            fstype=dict(type='str', aliases=['type'], choices=list(fstypes)),
+            dev=dict(type='path', required=True, aliases=['device']),
+            opts=dict(type='str'),
             force=dict(type='bool', default=False),
             resizefs=dict(type='bool', default=False),
         ),
+        required_if=[
+            ('state', 'present', ['fstype'])
+        ],
         supports_check_mode=True,
     )
 
+    state = module.params['state']
     dev = module.params['dev']
     fstype = module.params['fstype']
     opts = module.params['opts']
     force = module.params['force']
     resizefs = module.params['resizefs']
 
-    if fstype in friendly_names:
-        fstype = friendly_names[fstype]
-
     changed = False
 
-    try:
-        klass = FILESYSTEMS[fstype]
-    except KeyError:
-        module.fail_json(changed=False, msg="module does not support this filesystem (%s) yet." % fstype)
-
     if not os.path.exists(dev):
-        module.fail_json(msg="Device %s not found." % dev)
+        msg = "Device %s not found." % dev
+        if state == "present":
+            module.fail_json(msg=msg)
+        else:
+            module.exit_json(msg=msg)
+
     dev = Device(module, dev)
 
     cmd = module.get_bin_path('blkid', required=True)
@@ -405,24 +446,39 @@ def main():
     # then this existing filesystem would be overwritten even if force isn't enabled.
     fs = raw_fs.strip()
 
-    filesystem = klass(module)
+    if state == "present":
+        if fstype in friendly_names:
+            fstype = friendly_names[fstype]
 
-    same_fs = fs and FILESYSTEMS.get(fs) == FILESYSTEMS[fstype]
-    if same_fs and not resizefs and not force:
-        module.exit_json(changed=False)
-    elif same_fs and resizefs:
-        if not filesystem.GROW:
-            module.fail_json(changed=False, msg="module does not support resizing %s filesystem yet." % fstype)
-
-        out = filesystem.grow(dev)
-
-        module.exit_json(changed=True, msg=out)
-    elif fs and not force:
-        module.fail_json(msg="'%s' is already used as %s, use force=yes to overwrite" % (dev, fs), rc=rc, err=err)
-
-    # create fs
-    filesystem.create(opts, dev)
-    changed = True
+        try:
+            klass = FILESYSTEMS[fstype]
+        except KeyError:
+            module.fail_json(changed=False, msg="module does not support this filesystem (%s) yet." % fstype)
+
+        filesystem = klass(module)
+
+        same_fs = fs and FILESYSTEMS.get(fs) == FILESYSTEMS[fstype]
+        if same_fs and not resizefs and not force:
+            module.exit_json(changed=False)
+        elif same_fs and resizefs:
+            if not filesystem.GROW:
+                module.fail_json(changed=False, msg="module does not support resizing %s filesystem yet." % fstype)
+
+            out = filesystem.grow(dev)
+
+            module.exit_json(changed=True, msg=out)
+        elif fs and not force:
+            module.fail_json(msg="'%s' is already used as %s, use force=yes to overwrite" % (dev, fs), rc=rc, err=err)
+
+        # create fs
+        filesystem.create(opts, dev)
+        changed = True
+
+    elif fs:
+        # wipe fs signatures
+        filesystem = Filesystem(module)
+        filesystem.wipefs(dev)
+        changed = True
 
     module.exit_json(changed=changed)
 


Test patch:
diff --git a/tests/integration/targets/filesystem/tasks/main.yml b/tests/integration/targets/filesystem/tasks/main.yml
--- a/tests/integration/targets/filesystem/tasks/main.yml
+++ b/tests/integration/targets/filesystem/tasks/main.yml
@@ -43,7 +43,12 @@
     - 'not (item.0.key == "f2fs" and ansible_distribution == "Ubuntu" and ansible_distribution_version is version("14.04", "<="))'
     - 'not (item.1 == "overwrite_another_fs" and ansible_system == "FreeBSD")'
 
+    - 'not (item.1 == "remove_fs" and ansible_system == "FreeBSD")' # util-linux not available on FreeBSD
+    # On CentOS 6 shippable containers, wipefs seems unable to remove vfat signatures
+    - 'not (item.1 == "remove_fs" and item.0.key == "vfat" and ansible_distribution == "CentOS" and
+            ansible_distribution_version is version("7.0", "<"))'
+
     # The xfsprogs package on newer versions of OpenSUSE (15+) require Python 3, we skip this on our Python 2 container
     # OpenSUSE 42.3 Python2 and the other py3 containers are not affected so we will continue to run that
     - 'not (item.0.key == "xfs" and ansible_os_family == "Suse" and ansible_python.version.major == 2 and ansible_distribution_major_version|int != 42)'
-  loop: "{{ query('dict', tested_filesystems)|product(['create_fs', 'overwrite_another_fs'])|list }}"
+  loop: "{{ query('dict', tested_filesystems)|product(['create_fs', 'overwrite_another_fs', 'remove_fs'])|list }}"
diff --git a/tests/integration/targets/filesystem/tasks/remove_fs.yml b/tests/integration/targets/filesystem/tasks/remove_fs.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/filesystem/tasks/remove_fs.yml
@@ -0,0 +1,98 @@
+---
+# We assume 'create_fs' tests have passed.
+
+- name: filesystem creation
+  filesystem:
+    dev: '{{ dev }}'
+    fstype: '{{ fstype }}'
+
+- name: get filesystem UUID with 'blkid'
+  command:
+    cmd: 'blkid -c /dev/null -o value -s UUID {{ dev }}'
+  changed_when: false
+  register: blkid_ref
+
+- name: Assert that a filesystem exists on top of the device
+  assert:
+    that:
+      - blkid_ref.stdout | length > 0
+
+
+# Test check_mode first
+- name: filesystem removal (check mode)
+  filesystem:
+    dev: '{{ dev }}'
+    state: absent
+  register: wipefs
+  check_mode: yes
+
+- name: get filesystem UUID with 'blkid' (should remain the same)
+  command:
+    cmd: 'blkid -c /dev/null -o value -s UUID {{ dev }}'
+  changed_when: false
+  register: blkid
+
+- name: Assert that the state changed but the filesystem still exists
+  assert:
+    that:
+      - wipefs is changed
+      - blkid.stdout == blkid_ref.stdout
+
+# Do it
+- name: filesystem removal
+  filesystem:
+    dev: '{{ dev }}'
+    state: absent
+  register: wipefs
+
+- name: get filesystem UUID with 'blkid' (should be empty)
+  command:
+    cmd: 'blkid -c /dev/null -o value -s UUID {{ dev }}'
+  changed_when: false
+  failed_when: false
+  register: blkid
+
+- name: Assert that the state changed and the device has no filesystem
+  assert:
+    that:
+      - wipefs is changed
+      - blkid.stdout | length == 0
+      - blkid.rc == 2
+
+# Do it again
+- name: filesystem removal (idempotency)
+  filesystem:
+    dev: '{{ dev }}'
+    state: absent
+  register: wipefs
+
+- name: Assert that the state did not change
+  assert:
+    that:
+      - wipefs is not changed
+
+# and again
+- name: filesystem removal (idempotency, check mode)
+  filesystem:
+    dev: '{{ dev }}'
+    state: absent
+  register: wipefs
+  check_mode: yes
+
+- name: Assert that the state did not change
+  assert:
+    that:
+      - wipefs is not changed
+
+
+# By the way, test removal of a filesystem on unexistent device
+- name: filesystem removal (unexistent device)
+  filesystem:
+    dev: '/dev/unexistent_device'
+    state: absent
+  register: wipefs
+
+- name: Assert that the state did not change
+  assert:
+    that:
+      - wipefs is not changed
diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt
--- a/tests/sanity/ignore-2.10.txt
+++ b/tests/sanity/ignore-2.10.txt
@@ -1169,7 +1169,6 @@ plugins/modules/system/dconf.py pylint:blacklisted-name
 plugins/modules/system/dconf.py validate-modules:doc-missing-type
 plugins/modules/system/dconf.py validate-modules:parameter-type-not-in-doc
 plugins/modules/system/filesystem.py pylint:blacklisted-name
-plugins/modules/system/filesystem.py validate-modules:doc-missing-type
 plugins/modules/system/gconftool2.py pylint:blacklisted-name
 plugins/modules/system/gconftool2.py validate-modules:parameter-state-invalid-choice
 plugins/modules/system/gconftool2.py validate-modules:parameter-type-not-in-doc
diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt
--- a/tests/sanity/ignore-2.11.txt
+++ b/tests/sanity/ignore-2.11.txt
@@ -1169,7 +1169,6 @@ plugins/modules/system/dconf.py pylint:blacklisted-name
 plugins/modules/system/dconf.py validate-modules:doc-missing-type
 plugins/modules/system/dconf.py validate-modules:parameter-type-not-in-doc
 plugins/modules/system/filesystem.py pylint:blacklisted-name
-plugins/modules/system/filesystem.py validate-modules:doc-missing-type
 plugins/modules/system/gconftool2.py pylint:blacklisted-name
 plugins/modules/system/gconftool2.py validate-modules:parameter-state-invalid-choice
 plugins/modules/system/gconftool2.py validate-modules:parameter-type-not-in-doc
diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt
--- a/tests/sanity/ignore-2.9.txt
+++ b/tests/sanity/ignore-2.9.txt
@@ -921,7 +921,6 @@ plugins/modules/system/dconf.py pylint:blacklisted-name
 plugins/modules/system/dconf.py validate-modules:doc-missing-type
 plugins/modules/system/dconf.py validate-modules:parameter-type-not-in-doc
 plugins/modules/system/filesystem.py pylint:blacklisted-name
-plugins/modules/system/filesystem.py validate-modules:doc-missing-type
 plugins/modules/system/gconftool2.py pylint:blacklisted-name
 plugins/modules/system/gconftool2.py validate-modules:parameter-type-not-in-doc
 plugins/modules/system/interfaces_file.py pylint:blacklisted-name